Add a debug mode that allows disabling notifications for all packages that aren't allowed.

Bug: 209080636
Test: manual
Change-Id: Ifc9e348c5d4c8075a62ac95bd88596dbb33b22e1
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java
index 1940cb2..54f1380 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java
@@ -34,6 +34,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.provider.DebugModeFilterProvider;
 
 import javax.inject.Inject;
 
@@ -44,6 +45,7 @@
 @SysUISingleton
 public class NotificationFilter {
 
+    private final DebugModeFilterProvider mDebugNotificationFilter;
     private final StatusBarStateController mStatusBarStateController;
     private final KeyguardEnvironment mKeyguardEnvironment;
     private final ForegroundServiceController mForegroundServiceController;
@@ -52,11 +54,13 @@
 
     @Inject
     public NotificationFilter(
+            DebugModeFilterProvider debugNotificationFilter,
             StatusBarStateController statusBarStateController,
             KeyguardEnvironment keyguardEnvironment,
             ForegroundServiceController foregroundServiceController,
             NotificationLockscreenUserManager userManager,
             MediaFeatureFlag mediaFeatureFlag) {
+        mDebugNotificationFilter = debugNotificationFilter;
         mStatusBarStateController = statusBarStateController;
         mKeyguardEnvironment = keyguardEnvironment;
         mForegroundServiceController = foregroundServiceController;
@@ -69,6 +73,10 @@
      */
     public boolean shouldFilterOut(NotificationEntry entry) {
         final StatusBarNotification sbn = entry.getSbn();
+        if (mDebugNotificationFilter.shouldFilterOut(entry)) {
+            return true;
+        }
+
         if (!(mKeyguardEnvironment.isDeviceProvisioned()
                 || showNotificationEvenIfUnprovisioned(sbn))) {
             return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DebugModeCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DebugModeCoordinator.kt
new file mode 100644
index 0000000..df54ccd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DebugModeCoordinator.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.provider.DebugModeFilterProvider
+import javax.inject.Inject
+
+/** A small coordinator which filters out notifications from non-allowed apps. */
+@CoordinatorScope
+class DebugModeCoordinator @Inject constructor(
+    private val debugModeFilterProvider: DebugModeFilterProvider
+) : Coordinator {
+
+    override fun attach(pipeline: NotifPipeline) {
+        pipeline.addPreGroupFilter(preGroupFilter)
+        debugModeFilterProvider.registerInvalidationListener(preGroupFilter::invalidateList)
+    }
+
+    private val preGroupFilter = object : NotifFilter("DebugModeCoordinator") {
+        override fun shouldFilterOut(entry: NotificationEntry, now: Long) =
+            debugModeFilterProvider.shouldFilterOut(entry)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 02649ba..d21d5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -47,6 +47,7 @@
     gutsCoordinator: GutsCoordinator,
     communalCoordinator: CommunalCoordinator,
     conversationCoordinator: ConversationCoordinator,
+    debugModeCoordinator: DebugModeCoordinator,
     groupCountCoordinator: GroupCountCoordinator,
     mediaCoordinator: MediaCoordinator,
     preparationCoordinator: PreparationCoordinator,
@@ -86,6 +87,7 @@
         mCoordinators.add(deviceProvisionedCoordinator)
         mCoordinators.add(bubbleCoordinator)
         mCoordinators.add(communalCoordinator)
+        mCoordinators.add(debugModeCoordinator)
         mCoordinators.add(conversationCoordinator)
         mCoordinators.add(groupCountCoordinator)
         mCoordinators.add(mediaCoordinator)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
new file mode 100644
index 0000000..d16d76a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021 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.systemui.statusbar.notification.collection.provider
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import android.util.Log
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.Assert
+import com.android.systemui.util.ListenerSet
+import com.android.systemui.util.isNotEmpty
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * A debug mode provider which is used by both the legacy and new notification pipelines to
+ * block unwanted notifications from appearing to the user, primarily for integration testing.
+ *
+ * The only configuration is a list of allowed packages.  When this list is empty, the feature is
+ * disabled.  When SystemUI starts up, this feature is disabled.
+ *
+ * To enabled filtering, provide the list of packages in a comma-separated list using the command:
+ *
+ * `$ adb shell am broadcast -a com.android.systemui.action.SET_NOTIF_DEBUG_MODE
+ *          --esal allowed_packages <comma-separated-packages>`
+ *
+ * To disable filtering, send the action without a list:
+ *
+ * `$ adb shell am broadcast -a com.android.systemui.action.SET_NOTIF_DEBUG_MODE`
+ *
+ * NOTE: this feature only works on debug builds, and when the broadcaster is root.
+ */
+@SysUISingleton
+class DebugModeFilterProvider @Inject constructor(
+    private val context: Context,
+    dumpManager: DumpManager
+) : Dumpable {
+    private var allowedPackages: List<String> = emptyList()
+    private val listeners = ListenerSet<Runnable>()
+
+    init {
+        dumpManager.registerDumpable(this)
+    }
+
+    /**
+     * Register a runnable to be invoked when the allowed packages changes, which would mean the
+     * result of [shouldFilterOut] may have changed for some entries.
+     */
+    fun registerInvalidationListener(listener: Runnable) {
+        Assert.isMainThread()
+        if (!Build.isDebuggable()) {
+            return
+        }
+        val needsInitialization = listeners.isEmpty()
+        listeners.addIfAbsent(listener)
+        if (needsInitialization) {
+            val filter = IntentFilter().apply { addAction(ACTION_SET_NOTIF_DEBUG_MODE) }
+            val permission = NOTIF_DEBUG_MODE_PERMISSION
+            context.registerReceiver(mReceiver, filter, permission, null)
+            Log.d(TAG, "Registered: $mReceiver")
+        }
+    }
+
+    /**
+     * Determine if the given entry should be hidden from the user in debug mode.
+     * Will always return false in release.
+     */
+    fun shouldFilterOut(entry: NotificationEntry): Boolean {
+        if (allowedPackages.isEmpty()) {
+            return false
+        }
+        return entry.sbn.packageName !in allowedPackages
+    }
+
+    override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+        pw.println("initialized: ${listeners.isNotEmpty()}")
+        pw.println("allowedPackages: ${allowedPackages.size}")
+        allowedPackages.forEachIndexed { i, pkg ->
+            pw.println("  [$i]: $pkg")
+        }
+    }
+
+    private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent?) {
+            val action = intent?.action
+            if (ACTION_SET_NOTIF_DEBUG_MODE == action) {
+                allowedPackages = intent.extras?.getStringArrayList(EXTRA_ALLOWED_PACKAGES)
+                    ?: emptyList()
+                Log.d(TAG, "Updated allowedPackages: $allowedPackages")
+                listeners.forEach(Runnable::run)
+            } else {
+                Log.d(TAG, "Malformed intent: $intent")
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "DebugModeFilterProvider"
+        private const val ACTION_SET_NOTIF_DEBUG_MODE =
+            "com.android.systemui.action.SET_NOTIF_DEBUG_MODE"
+        private const val NOTIF_DEBUG_MODE_PERMISSION =
+            "com.android.systemui.permission.NOTIF_DEBUG_MODE"
+        private const val EXTRA_ALLOWED_PACKAGES = "allowed_packages"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index 84f0955..38f3c39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer
 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
+import com.android.systemui.statusbar.notification.collection.provider.DebugModeFilterProvider
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
@@ -65,6 +66,7 @@
     private val notifPipelineFlags: NotifPipelineFlags,
     private val notificationListener: NotificationListener,
     private val entryManager: NotificationEntryManager,
+    private val debugModeFilterProvider: DebugModeFilterProvider,
     private val legacyRanker: NotificationRankingManager,
     private val commonNotifCollection: Lazy<CommonNotifCollection>,
     private val notifPipeline: Lazy<NotifPipeline>,
@@ -134,6 +136,9 @@
             headsUpController.attach(entryManager, headsUpManager)
             groupManagerLegacy.get().setHeadsUpManager(headsUpManager)
             groupAlertTransferHelper.setHeadsUpManager(headsUpManager)
+            debugModeFilterProvider.registerInvalidationListener {
+                entryManager.updateNotifications("debug mode filter changed")
+            }
 
             entryManager.initialize(notificationListener, legacyRanker)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
index b02a336..ed8b532 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
@@ -37,6 +37,7 @@
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 
+import androidx.annotation.NonNull;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
 
@@ -50,6 +51,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
+import com.android.systemui.statusbar.notification.collection.provider.DebugModeFilterProvider;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -78,6 +80,8 @@
             mock(StatusBarNotification.class);
 
     @Mock
+    DebugModeFilterProvider mDebugModeFilterProvider;
+    @Mock
     StatusBarStateController mStatusBarStateController;
     @Mock
     KeyguardEnvironment mEnvironment;
@@ -132,7 +136,13 @@
                 mDependency,
                 TestableLooper.get(this));
         mRow = testHelper.createRow();
-        mNotificationFilter = new NotificationFilter(
+        mNotificationFilter = newNotificationFilter();
+    }
+
+    @NonNull
+    private NotificationFilter newNotificationFilter() {
+        return new NotificationFilter(
+                mDebugModeFilterProvider,
                 mStatusBarStateController,
                 mEnvironment,
                 mFsc,
@@ -205,12 +215,7 @@
     public void shouldFilterOtherNotificationWhenDisabled() {
         // GIVEN that the media feature is disabled
         when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
-        NotificationFilter filter = new NotificationFilter(
-                mStatusBarStateController,
-                mEnvironment,
-                mFsc,
-                mUserManager,
-                mMediaFeatureFlag);
+        NotificationFilter filter = newNotificationFilter();
         // WHEN the media filter is asked about an entry
         NotificationEntry otherEntry = new NotificationEntryBuilder().build();
         final boolean shouldFilter = filter.shouldFilterOut(otherEntry);
@@ -222,12 +227,7 @@
     public void shouldFilterOtherNotificationWhenEnabled() {
         // GIVEN that the media feature is enabled
         when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
-        NotificationFilter filter = new NotificationFilter(
-                mStatusBarStateController,
-                mEnvironment,
-                mFsc,
-                mUserManager,
-                mMediaFeatureFlag);
+        NotificationFilter filter = newNotificationFilter();
         // WHEN the media filter is asked about an entry
         NotificationEntry otherEntry = new NotificationEntryBuilder().build();
         final boolean shouldFilter = filter.shouldFilterOut(otherEntry);
@@ -239,12 +239,7 @@
     public void shouldFilterMediaNotificationWhenDisabled() {
         // GIVEN that the media feature is disabled
         when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
-        NotificationFilter filter = new NotificationFilter(
-                mStatusBarStateController,
-                mEnvironment,
-                mFsc,
-                mUserManager,
-                mMediaFeatureFlag);
+        NotificationFilter filter = newNotificationFilter();
         // WHEN the media filter is asked about a media entry
         final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry);
         // THEN it shouldn't be filtered
@@ -255,12 +250,7 @@
     public void shouldFilterMediaNotificationWhenEnabled() {
         // GIVEN that the media feature is enabled
         when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
-        NotificationFilter filter = new NotificationFilter(
-                mStatusBarStateController,
-                mEnvironment,
-                mFsc,
-                mUserManager,
-                mMediaFeatureFlag);
+        NotificationFilter filter = newNotificationFilter();
         // WHEN the media filter is asked about a media entry
         final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry);
         // THEN it should be filtered