Test for background bypass via notifications

Test that apps can't retrieve their own notification and use it to
bypass background restrictions (BAL & BG-FGS).

Bug: 185388103
Bug: 169821287
Test: atest -d android.app.cts.NotificationManagerTest#testActivityStartFromRetrievedNotification_isBlocked
Change-Id: I2ca81ac0101dd579b16864759e57fec00d20b7c7
diff --git a/tests/app/NotificationTrampolineBase/src/com/android/test/notificationtrampoline/NotificationTrampolineTestService.java b/tests/app/NotificationTrampolineBase/src/com/android/test/notificationtrampoline/NotificationTrampolineTestService.java
index ded29be..e230eb6 100644
--- a/tests/app/NotificationTrampolineBase/src/com/android/test/notificationtrampoline/NotificationTrampolineTestService.java
+++ b/tests/app/NotificationTrampolineBase/src/com/android/test/notificationtrampoline/NotificationTrampolineTestService.java
@@ -40,6 +40,7 @@
 
 import java.lang.ref.WeakReference;
 import java.util.Set;
+import java.util.stream.Stream;
 
 /**
  * This is a bound service used in conjunction with trampoline tests in NotificationManagerTest.
@@ -52,9 +53,13 @@
     private static final String RECEIVER_ACTION = ".TRAMPOLINE";
     private static final int MESSAGE_BROADCAST_NOTIFICATION = 1;
     private static final int MESSAGE_SERVICE_NOTIFICATION = 2;
+    private static final int MESSAGE_CLICK_NOTIFICATION = 3;
     private static final int TEST_MESSAGE_BROADCAST_RECEIVED = 1;
     private static final int TEST_MESSAGE_SERVICE_STARTED = 2;
     private static final int TEST_MESSAGE_ACTIVITY_STARTED = 3;
+    private static final int TEST_MESSAGE_NOTIFICATION_CLICKED = 4;
+    private static final int PI_FLAGS =
+            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
 
     private final Handler mHandler = new ServiceHandler();
     private final ActivityReference mActivityRef = new ActivityReference();
@@ -107,14 +112,14 @@
                     mReceiver = new BroadcastReceiver() {
                         @Override
                         public void onReceive(Context context, Intent broadcastIntent) {
-                            sendMessageToTest(mCallback, TEST_MESSAGE_BROADCAST_RECEIVED);
+                            sendMessageToTest(mCallback, TEST_MESSAGE_BROADCAST_RECEIVED, true);
                             startTargetActivity();
                         }
                     };
                     registerReceiver(mReceiver, new IntentFilter(mReceiverAction));
                     Intent intent = new Intent(mReceiverAction);
                     postNotification(notificationId,
-                            PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
+                            PendingIntent.getBroadcast(context, 0, intent, PI_FLAGS));
                     break;
                 }
                 case MESSAGE_SERVICE_NOTIFICATION: {
@@ -123,7 +128,24 @@
                     // trampoline) in this case.
                     Intent intent = new Intent(context, NotificationTrampolineTestService.class);
                     postNotification(notificationId,
-                            PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_MUTABLE_UNAUDITED));
+                            PendingIntent.getService(context, 0, intent, PI_FLAGS));
+                    break;
+                }
+                case MESSAGE_CLICK_NOTIFICATION: {
+                    PendingIntent intent = Stream
+                            .of(mNotificationManager.getActiveNotifications())
+                            .filter(sb -> sb.getId() == notificationId)
+                            .map(sb -> sb.getNotification().contentIntent)
+                            .findFirst()
+                            .orElse(null);
+                    if (intent != null) {
+                        try {
+                            intent.send();
+                        } catch (PendingIntent.CanceledException e) {
+                            throw new IllegalStateException("Notification PI cancelled", e);
+                        }
+                    }
+                    sendMessageToTest(mCallback, TEST_MESSAGE_NOTIFICATION_CLICKED, intent != null);
                     break;
                 }
                 default:
@@ -134,7 +156,7 @@
 
     @Override
     public int onStartCommand(Intent serviceIntent, int flags, int startId) {
-        sendMessageToTest(mCallback, TEST_MESSAGE_SERVICE_STARTED);
+        sendMessageToTest(mCallback, TEST_MESSAGE_SERVICE_STARTED, true);
         startTargetActivity();
         stopSelf(startId);
         return START_REDELIVER_INTENT;
@@ -163,9 +185,9 @@
         startActivity(intent);
     }
 
-    private static void sendMessageToTest(Messenger callback, int message) {
+    private static void sendMessageToTest(Messenger callback, int message, boolean success) {
         try {
-            callback.send(Message.obtain(null, message));
+            callback.send(Message.obtain(null, message, success ? 0 : 1, 0));
         } catch (RemoteException e) {
             throw new IllegalStateException(
                     "Couldn't send message " + message + " to test process", e);
@@ -185,10 +207,11 @@
         protected void onResume() {
             super.onResume();
             Messenger callback = getIntent().getParcelableExtra(EXTRA_CALLBACK);
-            ActivityReference activityRef =
-                    (ActivityReference) getIntent().getExtras().getBinder(EXTRA_ACTIVITY_REF);
-            activityRef.activity = new WeakReference<>(this);
-            sendMessageToTest(callback, TEST_MESSAGE_ACTIVITY_STARTED);
+            IBinder activityRef = getIntent().getExtras().getBinder(EXTRA_ACTIVITY_REF);
+            if (activityRef instanceof ActivityReference) {
+                ((ActivityReference) activityRef).activity = new WeakReference<>(this);
+            }
+            sendMessageToTest(callback, TEST_MESSAGE_ACTIVITY_STARTED, true);
         }
     }
 }
diff --git a/tests/app/src/android/app/cts/NotificationManagerTest.java b/tests/app/src/android/app/cts/NotificationManagerTest.java
index 769e191..f018351 100644
--- a/tests/app/src/android/app/cts/NotificationManagerTest.java
+++ b/tests/app/src/android/app/cts/NotificationManagerTest.java
@@ -110,7 +110,6 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -143,7 +142,9 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.FeatureUtil;
+import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.ThrowingSupplier;
 import com.android.test.notificationlistener.INotificationUriAccessService;
 
 import com.google.common.base.Preconditions;
@@ -163,10 +164,13 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /* This tests NotificationListenerService together with NotificationManager, as you need to have
  * notifications to manipulate in order to test the listener service. */
@@ -203,6 +207,7 @@
     private static final long TIMEOUT_MS = 4000;
     private static final int MESSAGE_BROADCAST_NOTIFICATION = 1;
     private static final int MESSAGE_SERVICE_NOTIFICATION = 2;
+    private static final int MESSAGE_CLICK_NOTIFICATION = 3;
 
     private PackageManager mPackageManager;
     private AudioManager mAudioManager;
@@ -3095,6 +3100,14 @@
         return Uri.parse(imageUriString);
     }
 
+    private <T> T uncheck(ThrowingSupplier<T> supplier) {
+        try {
+            return supplier.get();
+        } catch (Exception e) {
+            throw new CompletionException(e);
+        }
+    }
+
     public void testNotificationListener_setNotificationsShown() throws Exception {
         toggleListenerAccess(true);
         Thread.sleep(500); // wait for listener to be allowed
@@ -4225,6 +4238,29 @@
 
     }
 
+    /**
+     * This method verifies that an app can't bypass background restrictions by retrieving their own
+     * notification and triggering it.
+     */
+    public void testActivityStartFromRetrievedNotification_isBlocked() throws Exception {
+        deactivateGracePeriod();
+        EventCallback callback = new EventCallback();
+        int notificationId = 6007;
+
+        // Post notification and fire its pending intent
+        sendTrampolineMessage(TRAMPOLINE_SERVICE_API_30, MESSAGE_SERVICE_NOTIFICATION,
+                notificationId, callback);
+        PollingCheck.waitFor(TIMEOUT_MS,  () -> uncheck(() -> {
+            sendTrampolineMessage(TRAMPOLINE_SERVICE, MESSAGE_CLICK_NOTIFICATION, notificationId,
+                    callback);
+            // timeoutMs = 1ms below because surrounding waitFor already handles retry & timeout.
+            return callback.waitFor(EventCallback.NOTIFICATION_CLICKED, /* timeoutMs */ 1);
+        }));
+
+        assertFalse("Activity start should have been blocked",
+                callback.waitFor(EventCallback.ACTIVITY_STARTED, TIMEOUT_MS));
+    }
+
     public void testActivityStartOnBroadcastTrampoline_isBlocked() throws Exception {
         deactivateGracePeriod();
         setUpNotifListener();
@@ -4407,8 +4443,9 @@
         private static final int BROADCAST_RECEIVED = 1;
         private static final int SERVICE_STARTED = 2;
         private static final int ACTIVITY_STARTED = 3;
+        private static final int NOTIFICATION_CLICKED = 4;
 
-        private final Map<Integer, ConditionVariable> mEvents =
+        private final Map<Integer, CompletableFuture<Integer>> mEvents =
                 Collections.synchronizedMap(new ArrayMap<>());
 
         private EventCallback() {
@@ -4417,11 +4454,17 @@
 
         @Override
         public void handleMessage(Message message) {
-            mEvents.computeIfAbsent(message.what, e -> new ConditionVariable()).open();
+            mEvents.computeIfAbsent(message.what, e -> new CompletableFuture<>()).complete(
+                    message.arg1);
         }
 
         public boolean waitFor(int event, long timeoutMs) {
-            return mEvents.computeIfAbsent(event, e -> new ConditionVariable()).block(timeoutMs);
+            try {
+                return mEvents.computeIfAbsent(event, e -> new CompletableFuture<>()).get(timeoutMs,
+                        TimeUnit.MILLISECONDS) == 0;
+            } catch (InterruptedException | ExecutionException | TimeoutException e) {
+                return false;
+            }
         }
     }
 }