Added test for whitelisting pending intent for Doze Mode.

BUG: 28818704

Change-Id: I927364e78cd73133899d67be23e0b274829686af
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
index c7978f8..0598a3b 100644
--- a/tests/cts/hostside/app/AndroidManifest.xml
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -31,6 +31,14 @@
                 <action android:name="android.net.VpnService"/>
             </intent-filter>
         </service>
+        <service
+            android:name=".MyNotificationListenerService"
+            android:label="MyNotificationListenerService"
+            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
+            <intent-filter>
+                <action android:name="android.service.notification.NotificationListenerService" />
+            </intent-filter>
+        </service>
     </application>
 
     <instrumentation
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
index f3c4935..e0ba76b 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import android.os.SystemClock;
+
 /**
  * Base class for metered and non-metered Doze Mode tests.
  */
@@ -99,6 +101,24 @@
         assertBackgroundNetworkAccess(true);
     }
 
+    public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
+            throws Exception {
+        setPendingIntentWhitelistDuration(NETWORK_TIMEOUT_MS);
+        try {
+            registerNotificationListenerService();
+            setDozeMode(true);
+            assertBackgroundNetworkAccess(false);
+
+            sendNotification(42);
+            assertBackgroundNetworkAccess(true);
+            // Make sure access is disabled after it expires
+            SystemClock.sleep(NETWORK_TIMEOUT_MS);
+            assertBackgroundNetworkAccess(false);
+        } finally {
+            resetDeviceIdleSettings();
+        }
+    }
+
     // Must override so it only tests foreground service - once an app goes to foreground, device
     // leaves Doze Mode.
     @Override
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 82ef752..ba383a8 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -34,6 +34,8 @@
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkInfo.State;
 import android.net.wifi.WifiManager;
+import android.os.SystemClock;
+import android.service.notification.NotificationListenerService;
 import android.test.InstrumentationTestCase;
 import android.util.Log;
 
@@ -60,12 +62,16 @@
             "com.android.cts.net.hostside.app2.action.CHECK_NETWORK";
     private static final String ACTION_RECEIVER_READY =
             "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
+    static final String ACTION_SEND_NOTIFICATION =
+            "com.android.cts.net.hostside.app2.action.SEND_NOTIFICATION";
     private static final String EXTRA_ACTION = "com.android.cts.net.hostside.app2.extra.ACTION";
     private static final String EXTRA_RECEIVER_NAME =
             "com.android.cts.net.hostside.app2.extra.RECEIVER_NAME";
+    private static final String EXTRA_NOTIFICATION_ID =
+            "com.android.cts.net.hostside.app2.extra.NOTIFICATION_ID";
     private static final String NETWORK_STATUS_SEPARATOR = "\\|";
     private static final int SECOND_IN_MS = 1000;
-    private static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
+    static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
     private static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
 
 
@@ -118,7 +124,7 @@
             Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
                     + attempts + " attempts; sleeping "
                     + SLEEP_TIME_SEC + " seconds before trying again");
-            Thread.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+            SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
         } while (attempts <= maxAttempts);
         assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
                 + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
@@ -211,7 +217,7 @@
             }
             Log.d(TAG, "App not on background state on attempt #" + i
                     + "; sleeping 1s before trying again");
-            Thread.sleep(SECOND_IN_MS);
+            SystemClock.sleep(SECOND_IN_MS);
         }
         fail("App2 is not on background state after " + maxTries + " attempts: " + state );
     }
@@ -228,7 +234,7 @@
             }
             Log.d(TAG, "App not on foreground state on attempt #" + i
                     + "; sleeping 1s before trying again");
-            Thread.sleep(SECOND_IN_MS);
+            SystemClock.sleep(SECOND_IN_MS);
         }
         fail("App2 is not on foreground state after " + maxTries + " attempts: " + state );
     }
@@ -245,7 +251,7 @@
             }
             Log.d(TAG, "App not on foreground service state on attempt #" + i
                     + "; sleeping 1s before trying again");
-            Thread.sleep(SECOND_IN_MS);
+            SystemClock.sleep(SECOND_IN_MS);
         }
         fail("App2 is not on foreground service state after " + maxTries + " attempts: " + state );
     }
@@ -286,7 +292,7 @@
                 if (state != State.CONNECTED) {
                     Log.d(TAG, "State (" + state + ") not set to CONNECTED on attempt #" + i
                             + "; sleeping 1s before trying again");
-                    Thread.sleep(SECOND_IN_MS);
+                    SystemClock.sleep(SECOND_IN_MS);
                 } else {
                     assertEquals("wrong detailed state for " + networkInfo,
                             DetailedState.CONNECTED, detailedState);
@@ -299,7 +305,7 @@
                 if (state != State.DISCONNECTED) {
                     Log.d(TAG, "State (" + state + ") not set to DISCONNECTED on attempt #" + i
                             + "; sleeping 1s before trying again");
-                    Thread.sleep(SECOND_IN_MS);
+                    SystemClock.sleep(SECOND_IN_MS);
                 } else {
                     assertEquals("wrong detailed state for " + networkInfo,
                             DetailedState.BLOCKED, detailedState);
@@ -361,7 +367,7 @@
             Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
                     + checker.getExpected() + "' on attempt #" + i
                     + "; sleeping " + napTimeSeconds + "s before trying again");
-            Thread.sleep(napTimeSeconds * SECOND_IN_MS);
+            SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
         }
         fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
                 + maxTries
@@ -526,7 +532,7 @@
             }
             Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
                     + expected + ", got " + actual + "); sleeping 1s before polling again");
-            Thread.sleep(SECOND_IN_MS);
+            SystemClock.sleep(SECOND_IN_MS);
         }
         fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
                 + ". Full list: " + uids);
@@ -632,11 +638,42 @@
                 return;
             }
             Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
-            Thread.sleep(SECOND_IN_MS);
+            SystemClock.sleep(SECOND_IN_MS);
         }
         fail("app2 receiver is not ready");
     }
 
+    /**
+     * Registers a {@link NotificationListenerService} implementation that will execute the
+     * notification actions right after the notification is sent.
+     */
+    protected void registerNotificationListenerService() throws Exception {
+        final StringBuilder listeners = new StringBuilder(getNotificationListenerServices());
+        if (listeners.length() > 0) {
+            listeners.append(":");
+        }
+        listeners.append(MyNotificationListenerService.getId());
+        executeShellCommand("settings put secure enabled_notification_listeners " + listeners);
+        final String newListeners = getNotificationListenerServices();
+        assertEquals("Failed to set 'enabled_notification_listeners'",
+                listeners.toString(), newListeners);
+    }
+
+    private String getNotificationListenerServices() throws Exception {
+        return executeShellCommand("settings get secure enabled_notification_listeners");
+    }
+
+    protected void setPendingIntentWhitelistDuration(int durationMs) throws Exception {
+        final String command = String.format(
+                "settings put global device_idle_constants %s=%d",
+                "notification_whitelist_duration", durationMs);
+        executeSilentShellCommand(command);
+    }
+
+    protected void resetDeviceIdleSettings() throws Exception {
+        executeShellCommand("settings delete global device_idle_constants");
+    }
+
     protected void startForegroundService() throws Exception {
         executeShellCommand(
                 "am startservice -f 1 com.android.cts.net.hostside.app2/.MyForegroundService");
@@ -667,6 +704,13 @@
                 + "--receiver-foreground --receiver-registered-only");
     }
 
+    protected void sendNotification(int notificationId) {
+        final Intent intent = new Intent(ACTION_SEND_NOTIFICATION);
+        intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
+        Log.d(TAG, "Sending broadcast: " + intent);
+        mContext.sendBroadcast(intent);
+    }
+
     private String toString(int status) {
         switch (status) {
             case RESTRICT_BACKGROUND_STATUS_DISABLED:
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
new file mode 100644
index 0000000..b9c3031
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+import android.app.Notification;
+import android.app.PendingIntent.CanceledException;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+/**
+ * NotificationListenerService implementation that executes the notification actions once they're
+ * created.
+ */
+public class MyNotificationListenerService extends NotificationListenerService {
+    private static final String TAG = "MyNotificationListenerService";
+
+    @Override
+    public void onListenerConnected() {
+        Log.d(TAG, "onListenerConnected()");
+    }
+
+    @Override
+    public void onNotificationPosted(StatusBarNotification sbn) {
+        Log.d(TAG, "onNotificationPosted(): "  + sbn);
+        if (!sbn.getPackageName().startsWith(getPackageName())) {
+            Log.v(TAG, "ignoring notification from a different package");
+            return;
+        }
+        final Notification notification = sbn.getNotification();
+        if (notification.actions == null) {
+            Log.w(TAG, "ignoring notification without an action");
+        }
+        for (Notification.Action action : notification.actions) {
+            Log.i(TAG, "Sending pending intent " + action.actionIntent);
+            try {
+                action.actionIntent.send();
+            } catch (CanceledException e) {
+                Log.w(TAG, "Pending Intent canceled");
+            }
+        }
+    }
+
+    static String getId() {
+        return String.format("%s/%s", MyNotificationListenerService.class.getPackage().getName(),
+                MyNotificationListenerService.class.getName());
+    }
+}
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index 80b669d..1fa49ba 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -45,7 +45,8 @@
                 <action android:name="com.android.cts.net.hostside.app2.action.GET_COUNTERS" />
                 <action android:name="com.android.cts.net.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS" />
                 <action android:name="com.android.cts.net.hostside.app2.action.CHECK_NETWORK" />
-            </intent-filter>
+                <action android:name="com.android.cts.net.hostside.app2.action.SEND_NOTIFICATION" />
+                </intent-filter>
         </receiver>
     </application>
 
diff --git a/tests/cts/hostside/app2/res/drawable/ic_notification.png b/tests/cts/hostside/app2/res/drawable/ic_notification.png
new file mode 100644
index 0000000..6ae570b
--- /dev/null
+++ b/tests/cts/hostside/app2/res/drawable/ic_notification.png
Binary files differ
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
index d827921..f02f651 100644
--- 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
@@ -36,9 +36,13 @@
             "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
     static final String ACTION_FINISH_ACTIVITY =
             "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY";
+    static final String ACTION_SEND_NOTIFICATION =
+            "com.android.cts.net.hostside.app2.action.SEND_NOTIFICATION";
     static final String EXTRA_ACTION = "com.android.cts.net.hostside.app2.extra.ACTION";
     static final String EXTRA_RECEIVER_NAME =
             "com.android.cts.net.hostside.app2.extra.RECEIVER_NAME";
+    static final String EXTRA_NOTIFICATION_ID =
+            "com.android.cts.net.hostside.app2.extra.NOTIFICATION_ID";
 
     static int getUid(Context context) {
         final String packageName = context.getPackageName();
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
index 114d5c1..96e9d2b 100644
--- 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
@@ -22,17 +22,17 @@
 import static com.android.cts.net.hostside.app2.Common.ACTION_GET_COUNTERS;
 import static com.android.cts.net.hostside.app2.Common.ACTION_GET_RESTRICT_BACKGROUND_STATUS;
 import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY;
+import static com.android.cts.net.hostside.app2.Common.ACTION_SEND_NOTIFICATION;
 import static com.android.cts.net.hostside.app2.Common.EXTRA_ACTION;
+import static com.android.cts.net.hostside.app2.Common.EXTRA_NOTIFICATION_ID;
 import static com.android.cts.net.hostside.app2.Common.EXTRA_RECEIVER_NAME;
 import static com.android.cts.net.hostside.app2.Common.MANIFEST_RECEIVER;
 import static com.android.cts.net.hostside.app2.Common.TAG;
 import static com.android.cts.net.hostside.app2.Common.getUid;
 
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -41,6 +41,11 @@
 import android.net.NetworkInfo;
 import android.util.Log;
 
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Receiver used to:
  * <ol>
@@ -85,6 +90,9 @@
                 Log.d(TAG, message);
                 setResultData(message);
                 break;
+            case ACTION_SEND_NOTIFICATION:
+                sendNotification(context, intent);
+                break;
             default:
                 Log.e(TAG, "received unexpected action: " + action);
         }
@@ -213,4 +221,23 @@
         final int counter = getCounter(context, action, receiverName);
         setResultData(String.valueOf(counter));
     }
+
+    /**
+     * Sends a system notification containing actions with pending intents to launch the app's
+     * main activitiy or service.
+     */
+    private void sendNotification(Context context, Intent intent) {
+        final int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
+        final Intent serviceIntent = new Intent(context, MyService.class);
+        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent, 0);
+
+        final Notification notification = new Notification.Builder(context)
+                .setSmallIcon(R.drawable.ic_notification)
+                .setContentTitle("Light, Cameras...")
+                .setContentIntent(pendingIntent)
+                .addAction(R.drawable.ic_notification, "ACTION", pendingIntent)
+                .build();
+        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+            .notify(notificationId, notification);
+    }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
index 8152e55..c741b12 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -184,6 +184,11 @@
                 "testBackgroundNetworkAccess_enabled");
     }
 
+    public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
+    }
+
     // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
     // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
     //    public void testDozeMode_reinstall() throws Exception {
@@ -204,6 +209,12 @@
                 "testBackgroundNetworkAccess_enabled");
     }
 
+    public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
+            throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
+    }
+
     /**********************
      * Mixed modes tests. *
      **********************/