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;
+ }
}
}
}