Create NotificationListController

Move a few things from NotificationEntryManager into it.

This class will (hopefully) eventually replace *NotificationPresenter
as the main "controller" class for the notification shade. This will
allow us to centralize all of our universal controller code into one
place. The controller defer certain method calls to platform-specific
objects (what remains of the NotificationPresenters).

Test: atest
Change-Id: Ic729014f1bef825b635ced86ffb825b5734ad562
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java
index eea4490..839b06c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java
@@ -33,6 +33,13 @@
     default void onPendingEntryAdded(NotificationEntry entry) {
     }
 
+    // TODO: Combine this with onPreEntryUpdated into "onBeforeEntryFiltered" or similar
+    /**
+     * Called when a new entry is created but before it has been filtered or displayed to the user.
+     */
+    default void onBeforeNotificationAdded(NotificationEntry entry) {
+    }
+
     /**
      * Called when a new entry is created.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 45db002..989e781 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -18,11 +18,9 @@
 import android.annotation.Nullable;
 import android.app.Notification;
 import android.content.Context;
-import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -41,7 +39,6 @@
 import com.android.systemui.statusbar.notification.row.NotificationInflater;
 import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.util.leak.LeakDetector;
 
@@ -68,8 +65,6 @@
     @VisibleForTesting
     protected final HashMap<String, NotificationEntry> mPendingNotifications = new HashMap<>();
 
-    private final DeviceProvisionedController mDeviceProvisionedController =
-            Dependency.get(DeviceProvisionedController.class);
     private final ForegroundServiceController mForegroundServiceController =
             Dependency.get(ForegroundServiceController.class);
 
@@ -81,25 +76,12 @@
     private NotificationListenerService.RankingMap mLatestRankingMap;
     @VisibleForTesting
     protected NotificationData mNotificationData;
-    private NotificationListContainer mListContainer;
+
     @VisibleForTesting
     final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
             = new ArrayList<>();
     private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>();
 
-    private final DeviceProvisionedController.DeviceProvisionedListener
-            mDeviceProvisionedListener =
-            new DeviceProvisionedController.DeviceProvisionedListener() {
-                @Override
-                public void onDeviceProvisionedChanged() {
-                    updateNotifications();
-                }
-            };
-
-    public void destroy() {
-        mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
-    }
-
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("NotificationEntryManager state:");
@@ -151,9 +133,6 @@
             HeadsUpManager headsUpManager) {
         mPresenter = presenter;
         mNotificationData.setHeadsUpManager(headsUpManager);
-        mListContainer = listContainer;
-
-        mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
     }
 
     /** Adds multiple {@link NotificationLifetimeExtender}s. */
@@ -227,7 +206,9 @@
                     listener.onEntryInflated(entry, inflatedFlags);
                 }
                 mNotificationData.add(entry);
-                tagForeground(entry.notification);
+                for (NotificationEntryListener listener : mNotificationEntryListeners) {
+                    listener.onBeforeNotificationAdded(entry);
+                }
                 updateNotifications();
                 for (NotificationEntryListener listener : mNotificationEntryListeners) {
                     listener.onNotificationAdded(entry);
@@ -283,7 +264,6 @@
 
                 if (entry.rowExists()) {
                     entry.removeRow();
-                    mListContainer.cleanUpViewStateForEntry(entry);
                 }
 
                 // Let's remove the children if this was a summary
@@ -368,19 +348,6 @@
         }
     }
 
-    @VisibleForTesting
-    void tagForeground(StatusBarNotification notification) {
-        ArraySet<Integer> activeOps = mForegroundServiceController.getAppOps(
-                notification.getUserId(), notification.getPackageName());
-        if (activeOps != null) {
-            int N = activeOps.size();
-            for (int i = 0; i < N; i++) {
-                updateNotificationsForAppOp(activeOps.valueAt(i), notification.getUid(),
-                        notification.getPackageName(), true);
-            }
-        }
-    }
-
     @Override
     public void addNotification(StatusBarNotification notification,
             NotificationListenerService.RankingMap ranking) {
@@ -391,15 +358,6 @@
         }
     }
 
-    public void updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon) {
-        String foregroundKey = mForegroundServiceController.getStandardLayoutKey(
-                UserHandle.getUserId(uid), pkg);
-        if (foregroundKey != null) {
-            mNotificationData.updateAppOp(appOp, uid, pkg, foregroundKey, showIcon);
-            updateNotifications();
-        }
-    }
-
     private void updateNotificationInternal(StatusBarNotification notification,
             NotificationListenerService.RankingMap ranking) throws InflationException {
         if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
@@ -452,8 +410,9 @@
 
     public void updateNotifications() {
         mNotificationData.filterAndSort();
-
-        mPresenter.updateNotificationViews();
+        if (mPresenter != null) {
+            mPresenter.updateNotificationViews();
+        }
     }
 
     public void updateNotificationRanking(NotificationListenerService.RankingMap rankingMap) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationListController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationListController.java
new file mode 100644
index 0000000..88f4ca2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationListController.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
+
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+
+/**
+ * Root controller for the list of notifications in the shade.
+ *
+ * TODO: Much of the code in NotificationPresenter should eventually move in here. It will proxy
+ * domain-specific behavior (ARC, etc) to subcontrollers.
+ */
+public class NotificationListController {
+    private final NotificationEntryManager mEntryManager;
+    private final NotificationListContainer mListContainer;
+    private final ForegroundServiceController mForegroundServiceController;
+    private final DeviceProvisionedController mDeviceProvisionedController;
+
+    public NotificationListController(
+            NotificationEntryManager entryManager,
+            NotificationListContainer listContainer,
+            ForegroundServiceController foregroundServiceController,
+            DeviceProvisionedController deviceProvisionedController) {
+        mEntryManager = checkNotNull(entryManager);
+        mListContainer = checkNotNull(listContainer);
+        mForegroundServiceController = checkNotNull(foregroundServiceController);
+        mDeviceProvisionedController = checkNotNull(deviceProvisionedController);
+    }
+
+    /**
+     * Causes the controller to register listeners on its dependencies. This method must be called
+     * before the controller is ready to perform its duties.
+     */
+    public void bind() {
+        mEntryManager.addNotificationEntryListener(mEntryListener);
+        mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
+    }
+
+    /** Should be called when the list controller is being destroyed. */
+    public void destroy() {
+        mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
+    }
+
+    @SuppressWarnings("FieldCanBeLocal")
+    private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
+        @Override
+        public void onEntryRemoved(
+                NotificationEntry entry,
+                NotificationVisibility visibility,
+                boolean removedByUser) {
+            mListContainer.cleanUpViewStateForEntry(entry);
+        }
+
+        @Override
+        public void onBeforeNotificationAdded(NotificationEntry entry) {
+            tagForeground(entry.notification);
+        }
+    };
+
+    private final DeviceProvisionedListener mDeviceProvisionedListener =
+            new DeviceProvisionedListener() {
+                @Override
+                public void onDeviceProvisionedChanged() {
+                    mEntryManager.updateNotifications();
+                }
+            };
+
+    // TODO: This method is horrifically inefficient
+    private void tagForeground(StatusBarNotification notification) {
+        ArraySet<Integer> activeOps =
+                mForegroundServiceController.getAppOps(
+                        notification.getUserId(), notification.getPackageName());
+        if (activeOps != null) {
+            int len = activeOps.size();
+            for (int i = 0; i < len; i++) {
+                updateNotificationsForAppOp(activeOps.valueAt(i), notification.getUid(),
+                        notification.getPackageName(), true);
+            }
+        }
+    }
+
+    /** When an app op changes, propagate that change to notifications. */
+    public void updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon) {
+        String foregroundKey =
+                mForegroundServiceController.getStandardLayoutKey(UserHandle.getUserId(uid), pkg);
+        if (foregroundKey != null) {
+            mEntryManager
+                    .getNotificationData().updateAppOp(appOp, uid, pkg, foregroundKey, showIcon);
+            mEntryManager.updateNotifications();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 9abd86d..ac507ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -196,6 +196,7 @@
 import com.android.systemui.statusbar.notification.NotificationClicker;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
+import com.android.systemui.statusbar.notification.NotificationListController;
 import com.android.systemui.statusbar.notification.NotificationRowBinder;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -387,6 +388,7 @@
     private NotificationGutsManager mGutsManager;
     protected NotificationLogger mNotificationLogger;
     protected NotificationEntryManager mEntryManager;
+    private NotificationListController mNotificationListController;
     private NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
     private NotificationRowBinder mNotificationRowBinder;
     protected NotificationViewHierarchyManager mViewHierarchyManager;
@@ -594,7 +596,7 @@
     public void onActiveStateChanged(int code, int uid, String packageName, boolean active) {
         mForegroundServiceController.onAppOpChanged(code, uid, packageName, active);
         Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
-            mEntryManager.updateNotificationsForAppOp(code, uid, packageName, active);
+            mNotificationListController.updateNotificationsForAppOp(code, uid, packageName, active);
         });
     }
 
@@ -1045,6 +1047,13 @@
                 mScrimController, mActivityLaunchAnimator, mStatusBarKeyguardViewManager,
                 mNotificationAlertingManager);
 
+        mNotificationListController =
+                new NotificationListController(
+                        mEntryManager,
+                        (NotificationListContainer) mStackScroller,
+                        mForegroundServiceController,
+                        mDeviceProvisionedController);
+
         mAppOpsController.addCallback(APP_OPS, this);
         mNotificationListener.setUpWithPresenter(mPresenter);
         mNotificationShelf.setOnActivatedListener(mPresenter);
@@ -1057,6 +1066,7 @@
                 this, Dependency.get(BubbleController.class), mNotificationActivityStarter));
 
         mGroupAlertTransferHelper.bind(mEntryManager, mGroupManager);
+        mNotificationListController.bind();
     }
 
     /**
@@ -2832,7 +2842,7 @@
         } catch (RemoteException e) {
             // Ignore.
         }
-        mEntryManager.destroy();
+        mNotificationListController.destroy();
         // End old BaseStatusBar.destroy().
         if (mStatusBarWindow != null) {
             mWindowManager.removeViewImmediate(mStatusBarWindow);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index d937f93..9ce6ae1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -22,19 +22,15 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
-import android.app.AppOpsManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -49,7 +45,6 @@
 import android.support.test.filters.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.util.ArraySet;
 import android.widget.FrameLayout;
 
 import com.android.internal.logging.MetricsLogger;
@@ -79,8 +74,6 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 
-import junit.framework.Assert;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -346,7 +339,6 @@
 
         verify(mEntryListener, never()).onInflationError(any(), any());
 
-        verify(mListContainer).cleanUpViewStateForEntry(mEntry);
         verify(mPresenter).updateNotificationViews();
         verify(mEntryListener).onEntryRemoved(
                 mEntry, null, false /* removedByUser */);
@@ -401,90 +393,6 @@
     }
 
     @Test
-    public void testUpdateAppOps_foregroundNoti() {
-        com.android.systemui.util.Assert.isNotMainThread();
-
-        when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString()))
-                .thenReturn(mEntry.key);
-        mEntry.setRow(mRow);
-        mEntryManager.getNotificationData().add(mEntry);
-
-        mEntryManager.updateNotificationsForAppOp(
-                AppOpsManager.OP_CAMERA, mEntry.notification.getUid(),
-                mEntry.notification.getPackageName(), true);
-
-        verify(mPresenter, times(1)).updateNotificationViews();
-        assertTrue(mEntryManager.getNotificationData().get(mEntry.key).mActiveAppOps.contains(
-                AppOpsManager.OP_CAMERA));
-    }
-
-    @Test
-    public void testUpdateAppOps_otherNoti() {
-        com.android.systemui.util.Assert.isNotMainThread();
-
-        when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString()))
-                .thenReturn(null);
-        mEntryManager.updateNotificationsForAppOp(AppOpsManager.OP_CAMERA, 1000, "pkg", true);
-
-        verify(mPresenter, never()).updateNotificationViews();
-    }
-
-    @Test
-    public void testAddNotificationExistingAppOps() {
-        mEntry.setRow(mRow);
-        mEntryManager.getNotificationData().add(mEntry);
-        ArraySet<Integer> expected = new ArraySet<>();
-        expected.add(3);
-        expected.add(235);
-        expected.add(1);
-
-        when(mForegroundServiceController.getAppOps(mEntry.notification.getUserId(),
-                mEntry.notification.getPackageName())).thenReturn(expected);
-        when(mForegroundServiceController.getStandardLayoutKey(
-                mEntry.notification.getUserId(),
-                mEntry.notification.getPackageName())).thenReturn(mEntry.key);
-
-        mEntryManager.tagForeground(mEntry.notification);
-
-        Assert.assertEquals(expected.size(), mEntry.mActiveAppOps.size());
-        for (int op : expected) {
-            assertTrue("Entry missing op " + op, mEntry.mActiveAppOps.contains(op));
-        }
-    }
-
-    @Test
-    public void testAdd_noExistingAppOps() {
-        mEntry.setRow(mRow);
-        mEntryManager.getNotificationData().add(mEntry);
-        when(mForegroundServiceController.getStandardLayoutKey(
-                mEntry.notification.getUserId(),
-                mEntry.notification.getPackageName())).thenReturn(mEntry.key);
-        when(mForegroundServiceController.getAppOps(mEntry.notification.getUserId(),
-                mEntry.notification.getPackageName())).thenReturn(null);
-
-        mEntryManager.tagForeground(mEntry.notification);
-        Assert.assertEquals(0, mEntry.mActiveAppOps.size());
-    }
-
-    @Test
-    public void testAdd_existingAppOpsNotForegroundNoti() {
-        mEntry.setRow(mRow);
-        mEntryManager.getNotificationData().add(mEntry);
-        ArraySet<Integer> ops = new ArraySet<>();
-        ops.add(3);
-        ops.add(235);
-        ops.add(1);
-        when(mForegroundServiceController.getAppOps(mEntry.notification.getUserId(),
-                mEntry.notification.getPackageName())).thenReturn(ops);
-        when(mForegroundServiceController.getStandardLayoutKey(
-                mEntry.notification.getUserId(),
-                mEntry.notification.getPackageName())).thenReturn("something else");
-
-        mEntryManager.tagForeground(mEntry.notification);
-        Assert.assertEquals(0, mEntry.mActiveAppOps.size());
-    }
-
-    @Test
     public void testUpdateNotificationRanking() {
         when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
         when(mEnvironment.isDeviceProvisioned()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationListControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationListControllerTest.java
new file mode 100644
index 0000000..4b5037b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationListControllerTest.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.Notification;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.ArraySet;
+
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationData;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class NotificationListControllerTest extends SysuiTestCase {
+    private NotificationListController mController;
+
+    @Mock private NotificationEntryManager mEntryManager;
+    @Mock private NotificationListContainer mListContainer;
+    @Mock private ForegroundServiceController mForegroundServiceController;
+    @Mock private DeviceProvisionedController mDeviceProvisionedController;
+
+    @Captor private ArgumentCaptor<NotificationEntryListener> mEntryListenerCaptor;
+    @Captor private ArgumentCaptor<DeviceProvisionedListener> mProvisionedCaptor;
+
+    private NotificationEntryListener mEntryListener;
+    private DeviceProvisionedListener mProvisionedListener;
+
+    // TODO: Remove this once EntryManager no longer needs to be mocked
+    private NotificationData mNotificationData = new NotificationData();
+
+    private int mNextNotifId = 0;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mEntryManager.getNotificationData()).thenReturn(mNotificationData);
+
+        mController = new NotificationListController(
+                mEntryManager,
+                mListContainer,
+                mForegroundServiceController,
+                mDeviceProvisionedController);
+        mController.bind();
+
+        // Capture callbacks passed to mocks
+        verify(mEntryManager).addNotificationEntryListener(mEntryListenerCaptor.capture());
+        mEntryListener = mEntryListenerCaptor.getValue();
+        verify(mDeviceProvisionedController).addCallback(mProvisionedCaptor.capture());
+        mProvisionedListener = mProvisionedCaptor.getValue();
+    }
+
+    @Test
+    public void testCleanUpViewStateOnEntryRemoved() {
+        final NotificationEntry entry = buildEntry();
+        mEntryListener.onEntryRemoved(
+                entry,
+                NotificationVisibility.obtain(entry.key, 0, 0, true),
+                false);
+        verify(mListContainer).cleanUpViewStateForEntry(entry);
+    }
+
+    @Test
+    public void testCallUpdateNotificationsOnDeviceProvisionedChange() {
+        mProvisionedListener.onDeviceProvisionedChanged();
+        verify(mEntryManager).updateNotifications();
+    }
+
+    @Test
+    public void testAppOps_appOpAddedToForegroundNotif() {
+        // GIVEN a notification associated with a foreground service
+        final NotificationEntry entry = buildEntry();
+        mNotificationData.add(entry);
+        when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString()))
+                .thenReturn(entry.key);
+
+        // WHEN we are notified of a new app op
+        mController.updateNotificationsForAppOp(
+                AppOpsManager.OP_CAMERA,
+                entry.notification.getUid(),
+                entry.notification.getPackageName(),
+                true);
+
+        // THEN the app op is added to the entry
+        assertTrue(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA));
+        // THEN updateNotifications() is called
+        verify(mEntryManager, times(1)).updateNotifications();
+    }
+
+    @Test
+    public void testAppOps_appOpAddedToUnrelatedNotif() {
+        // GIVEN No current foreground notifs
+        when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString()))
+                .thenReturn(null);
+
+        // WHEN An unrelated notification gets a new app op
+        mController.updateNotificationsForAppOp(AppOpsManager.OP_CAMERA, 1000, "pkg", true);
+
+        // THEN We never call updateNotifications()
+        verify(mEntryManager, never()).updateNotifications();
+    }
+
+    @Test
+    public void testAppOps_addNotificationWithExistingAppOps() {
+        // GIVEN a notification with three associated app ops that is associated with a foreground
+        // service
+        final NotificationEntry entry = buildEntry();
+        mNotificationData.add(entry);
+        ArraySet<Integer> expected = new ArraySet<>();
+        expected.add(3);
+        expected.add(235);
+        expected.add(1);
+        when(mForegroundServiceController.getStandardLayoutKey(
+                entry.notification.getUserId(),
+                entry.notification.getPackageName())).thenReturn(entry.key);
+        when(mForegroundServiceController.getAppOps(entry.notification.getUserId(),
+                entry.notification.getPackageName())).thenReturn(expected);
+
+        // WHEN the notification is added
+        mEntryListener.onBeforeNotificationAdded(entry);
+
+        // THEN the entry is tagged with all three app ops
+        assertEquals(expected.size(), entry.mActiveAppOps.size());
+        for (int op : expected) {
+            assertTrue("Entry missing op " + op, entry.mActiveAppOps.contains(op));
+        }
+    }
+
+    @Test
+    public void testAdd_addNotificationWithNoExistingAppOps() {
+        // GIVEN a notification with NO associated app ops
+        final NotificationEntry entry = buildEntry();
+
+        mNotificationData.add(entry);
+        when(mForegroundServiceController.getStandardLayoutKey(
+                entry.notification.getUserId(),
+                entry.notification.getPackageName())).thenReturn(entry.key);
+        when(mForegroundServiceController.getAppOps(entry.notification.getUserId(),
+                entry.notification.getPackageName())).thenReturn(null);
+
+        // WHEN the notification is added
+        mEntryListener.onBeforeNotificationAdded(entry);
+
+        // THEN the entry doesn't have any app ops associated with it
+        assertEquals(0, entry.mActiveAppOps.size());
+    }
+
+    @Test
+    public void testAdd_addNonForegroundNotificationWithExistingAppOps() {
+        // GIVEN a notification with app ops that isn't associated with a foreground service
+        final NotificationEntry entry = buildEntry();
+        mNotificationData.add(entry);
+        ArraySet<Integer> ops = new ArraySet<>();
+        ops.add(3);
+        ops.add(235);
+        ops.add(1);
+        when(mForegroundServiceController.getAppOps(entry.notification.getUserId(),
+                entry.notification.getPackageName())).thenReturn(ops);
+        when(mForegroundServiceController.getStandardLayoutKey(
+                entry.notification.getUserId(),
+                entry.notification.getPackageName())).thenReturn("something else");
+
+        // WHEN the notification is added
+        mEntryListener.onBeforeNotificationAdded(entry);
+
+        // THEN the entry doesn't have any app ops associated with it
+        assertEquals(0, entry.mActiveAppOps.size());
+    }
+
+    private NotificationEntry buildEntry() {
+        mNextNotifId++;
+
+        Notification.Builder n = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setContentTitle("Title")
+                .setContentText("Text");
+
+        StatusBarNotification notification =
+                new StatusBarNotification(
+                        TEST_PACKAGE_NAME,
+                        TEST_PACKAGE_NAME,
+                        mNextNotifId,
+                        null,
+                        TEST_UID,
+                        0,
+                        n.build(),
+                        new UserHandle(ActivityManager.getCurrentUser()),
+                        null,
+                        0);
+        return new NotificationEntry(notification);
+    }
+
+    private static final String TEST_PACKAGE_NAME = "test";
+    private static final int TEST_UID = 0;
+}