Added CTS tests for RESTRICT_BACKGROUND_CHANGED.

These tests require a second app (besides the test app) that defines a
service; the host-side test then launches the service whose only purpose
is to define a broadcast receiver, which in turn will count the number
of intents received in a shared preferences file. Then the test app will
read the shared preferences and assert the proper number of intents have
been received.

BUG: 26451391
Change-Id: I4c5d5e57c09a0bd57a7f6581820cc9115318dd47
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
index cdde7dc..f44fdd1 100644
--- a/tests/cts/hostside/app/AndroidManifest.xml
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -15,7 +15,8 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.cts.net.hostside">
+        package="com.android.cts.net.hostside"
+        android:sharedUserId="com.android.cts.net.hostside.apps">
 
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnectivityManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnectivityManagerTest.java
index 5d3812c..300e39d 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnectivityManagerTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnectivityManagerTest.java
@@ -16,10 +16,14 @@
 
 package com.android.cts.net.hostside;
 
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
+
 import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
 import android.net.ConnectivityManager;
 import android.test.InstrumentationTestCase;
 import android.util.Log;
@@ -34,6 +38,9 @@
 public class ConnectivityManagerTest extends InstrumentationTestCase {
     private static final String TAG = "ConnectivityManagerTest";
 
+    static final String MANIFEST_RECEIVER = "ManifestReceiver";
+    static final String DYNAMIC_RECEIVER = "DynamicReceiver";
+
     private ConnectivityManager mCM;
 
     @Override
@@ -41,7 +48,7 @@
         super.setUp();
         mCM = (ConnectivityManager) getInstrumentation().getContext().getSystemService(
                 Activity.CONNECTIVITY_SERVICE);
-    }
+   }
 
     public void testGetRestrictBackgroundStatus_disabled() {
         assertRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
@@ -55,6 +62,46 @@
         assertRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_ENABLED);
     }
 
+    public void testRestrictBackgroundChangedReceivedOnce() throws Exception {
+        assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, 1);
+        assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0);
+    }
+
+    public void testRestrictBackgroundChangedReceivedTwice() throws Exception {
+        assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, 2);
+        assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0);
+    }
+
+    private void assertRestrictBackgroundChangedReceived(String receiverName, int expectedCount)
+            throws Exception {
+        int attempts = 0;
+        int count = 0;
+        final int maxAttempts = 5;
+        final int sleepTime = 10;
+        do {
+            attempts++;
+            count = getNumberBroadcastsReceived(getInstrumentation().getContext(), receiverName,
+                    ACTION_RESTRICT_BACKGROUND_CHANGED);
+            if (count == expectedCount) {
+                break;
+            }
+            Log.d(TAG, "Count is " + count + " after " + attempts + " attempts; sleeping "
+                    + sleepTime + " seconds before trying again");
+            Thread.sleep(sleepTime * 1000);
+        } while (attempts <= maxAttempts);
+        assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
+                + maxAttempts * sleepTime + " seconds", expectedCount, count);
+    }
+
+    static int getNumberBroadcastsReceived(Context context, String receiverName, String action)
+            throws Exception {
+        final Context sharedContext = context.createPackageContext(
+                "com.android.cts.net.hostside.app2", Context.CONTEXT_IGNORE_SECURITY);
+        final SharedPreferences prefs = sharedContext.getSharedPreferences(receiverName,
+                Context.MODE_PRIVATE);
+        return prefs.getInt(action, 0);
+    }
+
     private void assertRestrictBackgroundStatus(int expectedStatus) {
         final String expected = toString(expectedStatus);
         Log.d(TAG, getName() + " (expecting " + expected + ")");
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
index 375c852..0d0bc58 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
@@ -18,13 +18,9 @@
 
 import android.app.Activity;
 import android.content.Intent;
-import android.net.VpnService;
 import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
 import android.view.WindowManager;
 
-import java.util.Arrays;
-import java.util.ArrayList;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
index a3f400c..90a3ce4 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyVpnService.java
@@ -26,7 +26,6 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.ArrayList;
 
 public class MyVpnService extends VpnService {
 
diff --git a/tests/cts/hostside/app2/Android.mk b/tests/cts/hostside/app2/Android.mk
new file mode 100644
index 0000000..e330bf7
--- /dev/null
+++ b/tests/cts/hostside/app2/Android.mk
@@ -0,0 +1,31 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsHostsideNetworkTestsApp2
+
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
new file mode 100644
index 0000000..d69bf56
--- /dev/null
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.net.hostside.app2"
+    android:sharedUserId="com.android.cts.net.hostside.apps" >
+
+    <!-- This application is used to listen to RESTRICT_BACKGROUND_CHANGED intents and store
+         them in a shared preferences which is then read by the test app.
+         It defines 2 listeners, one in the manifest and another dynamically registered by
+         a service.
+    -->
+    <application>
+        <service android:name=".MyService" />
+
+        <receiver android:name=".MyBroadcastReceiver" >
+            <intent-filter>
+                <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
new file mode 100644
index 0000000..91caeda
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.net.hostside.app2;
+
+public final class Common {
+
+    static final String TAG = "CtsNetApp2";
+    static final String MANIFEST_RECEIVER = "ManifestReceiver";
+    static final String DYNAMIC_RECEIVER = "DynamicReceiver";
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
new file mode 100644
index 0000000..0cbf360
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.net.hostside.app2;
+
+import static com.android.cts.net.hostside.app2.Common.MANIFEST_RECEIVER;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+/**
+ * Receiver that stores received broadcasts in a shared preference.
+ */
+public class MyBroadcastReceiver extends BroadcastReceiver {
+
+    private final String mName;
+
+    public MyBroadcastReceiver() {
+        this(MANIFEST_RECEIVER);
+    }
+
+    MyBroadcastReceiver(String name) {
+        Log.d(TAG, "Constructing MyBroadcastReceiver named " + name);
+        mName = name;
+   }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.d(TAG, "onReceive() for " + mName + ": " + intent);
+        final SharedPreferences prefs = context.getSharedPreferences(mName, Context.MODE_PRIVATE);
+        final String pref = intent.getAction();
+        final int value = prefs.getInt(pref, 0) + 1;
+        Log.d(TAG, "Setting " + pref + " = " + value);
+        prefs.edit().putInt(pref, value).apply();
+    }
+}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
new file mode 100644
index 0000000..882bb62
--- /dev/null
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.net.hostside.app2;
+
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER;
+import static com.android.cts.net.hostside.app2.Common.TAG;
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * Service used to dynamically register a broadcast receiver.
+ */
+public class MyService extends Service {
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.d(TAG, "onStartCommand: " + intent);
+        getApplicationContext().registerReceiver(new MyBroadcastReceiver(DYNAMIC_RECEIVER),
+                new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED));
+        return START_STICKY;
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTests.java
index dd2424c..bedfad6 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTests.java
@@ -42,6 +42,9 @@
     private static final String TEST_PKG = "com.android.cts.net.hostside";
     private static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
 
+    private static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
+    private static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
+
     private IAbi mAbi;
     private IBuildInfo mCtsBuild;
 
@@ -63,15 +66,22 @@
         assertNotNull(mCtsBuild);
 
         setRestrictBackground(false);
-        uninstallTestPackage(false);
-        installTestPackage();
+
+        uninstallPackage(TEST_PKG, false);
+        installPackage(TEST_APK);
+        // TODO: split this class into HostsideVpnTests and HostsideConnectivityManagerTests so
+        // the former don't need to unnecessarily install app2.
+        uninstallPackage(TEST_APP2_PKG, false);
+        installPackage(TEST_APP2_APK);
     }
 
     @Override
     protected void tearDown() throws Exception {
         super.tearDown();
 
-        uninstallTestPackage(false);
+        uninstallPackage(TEST_PKG, true);
+        uninstallPackage(TEST_APP2_PKG, true);
+
         setRestrictBackground(false);
     }
 
@@ -80,26 +90,41 @@
     }
 
     public void testConnectivityManager_getRestrictBackgroundStatus_disabled() throws Exception {
+        startBroadcastReceiverService();
         final int uid = getUid(TEST_PKG);
+
         removeRestrictBackgroundWhitelist(uid);
         assertRestrictBackgroundStatusDisabled();
+        assertRestrictBackgroundChangedReceivedOnce();
+
         // Sanity check: make sure status is always disabled, never whitelisted
         addRestrictBackgroundWhitelist(uid);
         assertRestrictBackgroundStatusDisabled();
+        assertRestrictBackgroundChangedReceivedTwice();
     }
 
     public void testConnectivityManager_getRestrictBackgroundStatus_whitelisted() throws Exception {
+        startBroadcastReceiverService();
         final int uid = getUid(TEST_PKG);
+
         setRestrictBackground(true);
+        assertRestrictBackgroundChangedReceivedOnce();
+
         addRestrictBackgroundWhitelist(uid);
         assertRestrictBackgroundStatusWhitelisted();
+        assertRestrictBackgroundChangedReceivedTwice();
     }
 
     public void testConnectivityManager_getRestrictBackgroundStatus_enabled() throws Exception {
+        startBroadcastReceiverService();
         final int uid = getUid(TEST_PKG);
+
         setRestrictBackground(true);
+        assertRestrictBackgroundChangedReceivedOnce();
+
         removeRestrictBackgroundWhitelist(uid);
         assertRestrictBackgroundStatusEnabled();
+        assertRestrictBackgroundChangedReceivedTwice();
     }
 
     public void testConnectivityManager_getRestrictBackgroundStatus_uninstall() throws Exception {
@@ -108,44 +133,66 @@
         addRestrictBackgroundWhitelist(uid);
         assertRestrictBackgroundWhitelist(uid, true);
 
-        uninstallTestPackage(true);
+        uninstallPackage(TEST_PKG, true);
         assertPackageUninstalled(TEST_PKG);
         assertRestrictBackgroundWhitelist(uid, false);
 
-        installTestPackage();
+        installPackage(TEST_APK);
         final int newUid = getUid(TEST_PKG);
         assertRestrictBackgroundWhitelist(uid, false);
         assertRestrictBackgroundWhitelist(newUid, false);
     }
 
-    private void installTestPackage() throws DeviceNotAvailableException, FileNotFoundException {
+    private void installPackage(String apk) throws DeviceNotAvailableException,
+            FileNotFoundException {
         assertNull(getDevice().installPackage(
-                MigrationHelper.getTestFile(mCtsBuild, TEST_APK), false));
+                MigrationHelper.getTestFile(mCtsBuild, apk), false));
     }
 
-    private void uninstallTestPackage(boolean shouldSucceed) throws DeviceNotAvailableException {
-        final String result = getDevice().uninstallPackage(TEST_PKG);
+    private void uninstallPackage(String packageName, boolean shouldSucceed)
+            throws DeviceNotAvailableException {
+        final String result = getDevice().uninstallPackage(packageName);
         if (shouldSucceed) {
-            assertNull("uninstallPackage failed: " + result, result);
+            assertNull("uninstallPackage(" + packageName + ") failed: " + result, result);
         }
     }
 
-    private void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException {
-        final String command = "cmd package list packages -f " + packageName;
+    /**
+     * Starts a service that will register a broadcast receiver to receive
+     * {@code RESTRICT_BACKGROUND_CHANGE} intents.
+     * <p>
+     * The service must run in a separate app because otherwise it would be killed every time
+     * {@link #runDeviceTests(String, String)} is executed.
+     */
+    private void startBroadcastReceiverService() throws DeviceNotAvailableException {
+        runCommand("am startservice " + TEST_APP2_PKG + "/.MyService");
+    }
+
+    private void assertPackageUninstalled(String packageName) throws Exception {
+        final String command = "cmd package list packages " + packageName;
         final int max_tries = 5;
         for (int i = 1; i <= max_tries; i++) {
             final String result = runCommand(command);
             if (result.trim().isEmpty()) {
                 return;
             }
+            // 'list packages' filters by substring, so we need to iterate with the results
+            // and check one by one, otherwise 'com.android.cts.net.hostside' could return
+            // 'com.android.cts.net.hostside.app2'
+            boolean found = false;
+            for (String line : result.split("[\\r\\n]+")) {
+                if (line.endsWith(packageName)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                return;
+            }
             i++;
             Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
                     + "); sleeping 1s before polling again");
-            try {
-                Thread.sleep(1000);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
+            Thread.sleep(1000);
         }
         fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
     }
@@ -165,6 +212,16 @@
                 "testGetRestrictBackgroundStatus_enabled");
     }
 
+    private void assertRestrictBackgroundChangedReceivedOnce() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".ConnectivityManagerTest",
+                "testRestrictBackgroundChangedReceivedOnce");
+    }
+
+    private void assertRestrictBackgroundChangedReceivedTwice() throws DeviceNotAvailableException {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".ConnectivityManagerTest",
+                "testRestrictBackgroundChangedReceivedTwice");
+    }
+
     public void runDeviceTests(String packageName, String testClassName)
             throws DeviceNotAvailableException {
         runDeviceTests(packageName, testClassName, null);
@@ -176,14 +233,11 @@
                 "android.support.test.runner.AndroidJUnitRunner", getDevice().getIDevice());
 
         if (testClassName != null) {
-            // TODO: figure out why testRunner.setMethodName() / testRunner.setClassName() doesn't
-            // work
-            final StringBuilder runOptions = new StringBuilder("-e class ").append(testClassName);
             if (methodName != null) {
-                runOptions.append('#').append(methodName);
+                testRunner.setMethodName(testClassName, methodName);
+            } else {
+                testRunner.setClassName(testClassName);
             }
-            Log.i(TAG, "Setting runOptions() as " + runOptions);
-            testRunner.setRunOptions(runOptions.toString());
         }
 
         final CollectingTestListener listener = new CollectingTestListener();
@@ -224,27 +278,31 @@
                 + output);
     }
 
-    private void addRestrictBackgroundWhitelist(int uid) throws DeviceNotAvailableException {
+    private void addRestrictBackgroundWhitelist(int uid) throws Exception {
         runCommand("cmd netpolicy add restrict-background-whitelist " + uid);
         assertRestrictBackgroundWhitelist(uid, true);
     }
 
-    private void removeRestrictBackgroundWhitelist(int uid) throws DeviceNotAvailableException {
+    private void removeRestrictBackgroundWhitelist(int uid) throws Exception {
         runCommand("cmd netpolicy remove restrict-background-whitelist " + uid);
         assertRestrictBackgroundWhitelist(uid, false);
     }
 
-    private void assertRestrictBackgroundWhitelist(int uid, boolean expected)
-            throws DeviceNotAvailableException {
-        final String output = runCommand("cmd netpolicy list restrict-background-whitelist ");
-        // TODO: use MoreAsserts
-        if (expected) {
-            assertTrue("Did not find uid '" + uid + "' on '" + output + "'",
-                    output.contains(Integer.toString(uid)));
-        } else {
-            assertFalse("Found uid '" + uid + "' on '" + output + "'",
-                    output.contains(Integer.toString(uid)));
+    private void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
+        final int max_tries = 5;
+        boolean actual = false;
+        for (int i = 1; i <= max_tries; i++) {
+            final String output = runCommand("cmd netpolicy list restrict-background-whitelist ");
+            actual = output.contains(Integer.toString(uid));
+            if (expected == actual) {
+                return;
+            }
+            Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected "
+                    + expected + ", got " + actual + "); sleeping 1s before polling again");
+            Thread.sleep(1000);
         }
+        fail("whitelist check for uid " + uid + " failed: expected "
+                + expected + ", got " + actual);
     }
 
     private void setRestrictBackground(boolean enabled) throws DeviceNotAvailableException {