Adds a AppOpsController that can be suscribed to.

This is a dependency that replaces AppOpsListener asuming all its
responsibilities and functions. Additionally, it can handle arbitrary
callbacks for an activeChanged notification indicating the uid that had
a change.

In the case of location updates, they are removed if they haven't been
updated in 5 sec.

Test: atest
Change-Id: I647e86418e552721f1a1098d611538ef09654243
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 494880e..26fb486 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -33,34 +33,35 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.Preconditions;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.systemui.appops.AppOpsController;
+import com.android.systemui.appops.AppOpsControllerImpl;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.PluginInitializerImpl;
 import com.android.systemui.plugins.PluginDependencyProvider;
-import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.shared.plugins.PluginManagerImpl;
+import com.android.systemui.plugins.PluginInitializerImpl;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.power.EnhancedEstimates;
 import com.android.systemui.power.EnhancedEstimatesImpl;
 import com.android.systemui.power.PowerNotificationWarnings;
 import com.android.systemui.power.PowerUI;
+import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.shared.plugins.PluginManagerImpl;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.notification.AppOpsListener;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.notification.NotificationData.KeyguardEnvironment;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
+import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
@@ -336,8 +337,6 @@
 
         mProviders.put(EnhancedEstimates.class, () -> new EnhancedEstimatesImpl());
 
-        mProviders.put(AppOpsListener.class, () -> new AppOpsListener(mContext));
-
         mProviders.put(VibratorHelper.class, () -> new VibratorHelper(mContext));
 
         mProviders.put(IStatusBarService.class, () -> IStatusBarService.Stub.asInterface(
@@ -357,6 +356,9 @@
 
         mProviders.put(InitController.class, InitController::new);
 
+        mProviders.put(AppOpsController.class, () ->
+                new AppOpsControllerImpl(mContext, getDependency(BG_LOOPER)));
+
         // Put all dependencies above here so the factory can override them if it wants.
         SystemUIFactory.getInstance().injectDependencies(mProviders, mContext);
 
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java
new file mode 100644
index 0000000..9f363f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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.appops;
+
+/**
+ * Item to store information of active applications using different APP OPS
+ */
+public class AppOpItem {
+
+    private int mCode;
+    private int mUid;
+    private String mPackageName;
+    private long mTimeStarted;
+
+    public AppOpItem(int code, int uid, String packageName, long timeStarted) {
+        this.mCode = code;
+        this.mUid = uid;
+        this.mPackageName = packageName;
+        this.mTimeStarted = timeStarted;
+    }
+
+    public int getCode() {
+        return mCode;
+    }
+
+    public int getUid() {
+        return mUid;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public long getTimeStarted() {
+        return mTimeStarted;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsController.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsController.java
new file mode 100644
index 0000000..4966fc6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsController.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 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.appops;
+
+import java.util.List;
+
+/**
+ * Controller to keep track of applications that have requested access to given App Ops.
+ *
+ * It can be subscribed to with callbacks. Additionally, it passes on the information to
+ * NotificationPresenter to be displayed to the user.
+ */
+public interface AppOpsController {
+
+    /**
+     * Callback to notify when the state of active AppOps tracked by the controller has changed
+     */
+    interface Callback {
+        void onActiveStateChanged(int code, int uid, String packageName, boolean active);
+    }
+
+    /**
+     * Adds a callback that will get notified when an AppOp of the type the controller tracks
+     * changes
+     *
+     * @param opsCodes App Ops the callback was interested in checking
+     * @param cb Callback to report changes
+     *
+     * @see #removeCallback(int[], Callback)
+     */
+    void addCallback(int[] opsCodes, Callback cb);
+
+    /**
+     * Removes a callback from those notifified when an AppOp of the type the controller tracks
+     * changes
+     *
+     * @param opsCodes App Ops the callback is interested in checking
+     * @param cb Callback to stop reporting changes
+     *
+     * @see #addCallback(int[], Callback)
+     */
+    void removeCallback(int[] opsCodes, Callback cb);
+
+    /**
+     * Returns a copy of the list containing all the active AppOps that the controller tracks.
+     *
+     * @return List of active AppOps information
+     */
+    List<AppOpItem> getActiveAppOps();
+
+    /**
+     * Returns a copy of the list containing all the active AppOps that the controller tracks, for
+     * a given user id.
+     *
+     * @param userId User id to track
+     *
+     * @return List of active AppOps information for that user id
+     */
+    List<AppOpItem> getActiveAppOpsForUser(int userId);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
new file mode 100644
index 0000000..906a210
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2018 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.appops;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Controller to keep track of applications that have requested access to given App Ops
+ *
+ * It can be subscribed to with callbacks. Additionally, it passes on the information to
+ * NotificationPresenter to be displayed to the user.
+ */
+public class AppOpsControllerImpl implements AppOpsController,
+        AppOpsManager.OnOpActiveChangedListener {
+
+    private static final long LOCATION_TIME_DELAY_MS = 5000;
+    private static final String TAG = "AppOpsControllerImpl";
+    private static final boolean DEBUG = false;
+    private final Context mContext;
+
+    protected final AppOpsManager mAppOps;
+    private final H mBGHandler;
+    private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>();
+    private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>();
+    @GuardedBy("mActiveItems")
+    private final List<AppOpItem> mActiveItems = new ArrayList<>();
+
+    protected static final int[] OPS = new int[] {AppOpsManager.OP_CAMERA,
+            AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
+            AppOpsManager.OP_RECORD_AUDIO,
+            AppOpsManager.OP_COARSE_LOCATION,
+            AppOpsManager.OP_FINE_LOCATION};
+
+    public AppOpsControllerImpl(Context context, Looper bgLooper) {
+        mContext = context;
+        mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+        mBGHandler = new H(bgLooper);
+        final int numOps = OPS.length;
+        for (int i = 0; i < numOps; i++) {
+            mCallbacksByCode.put(OPS[i], new ArraySet<>());
+        }
+    }
+
+    @VisibleForTesting
+    protected void setListening(boolean listening) {
+        if (listening) {
+            mAppOps.startWatchingActive(OPS, this);
+        } else {
+            mAppOps.stopWatchingActive(this);
+        }
+    }
+
+    /**
+     * Adds a callback that will get notifified when an AppOp of the type the controller tracks
+     * changes
+     *
+     * @param callback Callback to report changes
+     * @param opsCodes App Ops the callback is interested in checking
+     *
+     * @see #removeCallback(int[], Callback)
+     */
+    @Override
+    public void addCallback(int[] opsCodes, AppOpsController.Callback callback) {
+        boolean added = false;
+        final int numCodes = opsCodes.length;
+        for (int i = 0; i < numCodes; i++) {
+            if (mCallbacksByCode.containsKey(opsCodes[i])) {
+                mCallbacksByCode.get(opsCodes[i]).add(callback);
+                added = true;
+            } else {
+                if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported");
+            }
+        }
+        if (added) mCallbacks.add(callback);
+        if (!mCallbacks.isEmpty()) setListening(true);
+    }
+
+    /**
+     * Removes a callback from those notified when an AppOp of the type the controller tracks
+     * changes
+     *
+     * @param callback Callback to stop reporting changes
+     * @param opsCodes App Ops the callback was interested in checking
+     *
+     * @see #addCallback(int[], Callback)
+     */
+    @Override
+    public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) {
+        final int numCodes = opsCodes.length;
+        for (int i = 0; i < numCodes; i++) {
+            if (mCallbacksByCode.containsKey(opsCodes[i])) {
+                mCallbacksByCode.get(opsCodes[i]).remove(callback);
+            }
+        }
+        mCallbacks.remove(callback);
+        if (mCallbacks.isEmpty()) setListening(false);
+    }
+
+    private AppOpItem getAppOpItem(int code, int uid, String packageName) {
+        final int itemsQ = mActiveItems.size();
+        for (int i = 0; i < itemsQ; i++) {
+            AppOpItem item = mActiveItems.get(i);
+            if (item.getCode() == code && item.getUid() == uid
+                    && item.getPackageName().equals(packageName)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    private boolean updateActives(int code, int uid, String packageName, boolean active) {
+        synchronized (mActiveItems) {
+            AppOpItem item = getAppOpItem(code, uid, packageName);
+            if (item == null && active) {
+                item = new AppOpItem(code, uid, packageName, System.currentTimeMillis());
+                mActiveItems.add(item);
+                if (code == AppOpsManager.OP_COARSE_LOCATION
+                        || code == AppOpsManager.OP_FINE_LOCATION) {
+                    mBGHandler.scheduleRemoval(item, LOCATION_TIME_DELAY_MS);
+                }
+                if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
+                return true;
+            } else if (item != null && !active) {
+                mActiveItems.remove(item);
+                if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
+                return true;
+            } else if (item != null && active
+                    && (code == AppOpsManager.OP_COARSE_LOCATION
+                            || code == AppOpsManager.OP_FINE_LOCATION)) {
+                mBGHandler.scheduleRemoval(item, LOCATION_TIME_DELAY_MS);
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Returns a copy of the list containing all the active AppOps that the controller tracks.
+     *
+     * @return List of active AppOps information
+     */
+    public List<AppOpItem> getActiveAppOps() {
+        synchronized (mActiveItems) {
+            return new ArrayList<>(mActiveItems);
+        }
+    }
+
+    /**
+     * Returns a copy of the list containing all the active AppOps that the controller tracks, for
+     * a given user id.
+     *
+     * @param userId User id to track
+     *
+     * @return List of active AppOps information for that user id
+     */
+    public List<AppOpItem> getActiveAppOpsForUser(int userId) {
+        List<AppOpItem> list = new ArrayList<>();
+        synchronized (mActiveItems) {
+            final int numActiveItems = mActiveItems.size();
+            for (int i = 0; i < numActiveItems; i++) {
+                AppOpItem item = mActiveItems.get(i);
+                if (UserHandle.getUserId(item.getUid()) == userId) {
+                    list.add(item);
+                }
+            }
+        }
+        return list;
+    }
+
+    @Override
+    public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
+        if (updateActives(code, uid, packageName, active)) {
+            for (Callback cb: mCallbacksByCode.get(code)) {
+                cb.onActiveStateChanged(code, uid, packageName, active);
+            }
+        }
+    }
+
+    private final class H extends Handler {
+        H(Looper looper) {
+            super(looper);
+        }
+
+        public void scheduleRemoval(AppOpItem item, long timeToRemoval) {
+            removeCallbacksAndMessages(item);
+            postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    onOpActiveChanged(item.getCode(), item.getUid(),
+                            item.getPackageName(), false);
+                }
+            }, item, timeToRemoval);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AppOpsListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AppOpsListener.java
deleted file mode 100644
index 9e99fbb..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AppOpsListener.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2017 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 android.app.AppOpsManager;
-import android.content.Context;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.ForegroundServiceController;
-import com.android.systemui.statusbar.NotificationPresenter;
-
-/**
- * This class handles listening to notification updates and passing them along to
- * NotificationPresenter to be displayed to the user.
- */
-public class AppOpsListener implements AppOpsManager.OnOpActiveChangedListener {
-    private static final String TAG = "NotificationListener";
-
-    // Dependencies:
-    private final ForegroundServiceController mFsc =
-            Dependency.get(ForegroundServiceController.class);
-    private final NotificationEntryManager mEntryManager =
-            Dependency.get(NotificationEntryManager.class);
-
-    private final Context mContext;
-    protected NotificationPresenter mPresenter;
-    protected final AppOpsManager mAppOps;
-
-    protected static final int[] OPS = new int[] {AppOpsManager.OP_CAMERA,
-            AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
-            AppOpsManager.OP_RECORD_AUDIO};
-
-    public AppOpsListener(Context context) {
-        mContext = context;
-        mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-    }
-
-    public void setUpWithPresenter(NotificationPresenter presenter) {
-        mPresenter = presenter;
-        mAppOps.startWatchingActive(OPS, this);
-    }
-
-    public void destroy() {
-        mAppOps.stopWatchingActive(this);
-    }
-
-    @Override
-    public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
-        mFsc.onAppOpChanged(code, uid, packageName, active);
-        Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
-          mEntryManager.updateNotificationsForAppOp(code, uid, packageName, active);
-        });
-    }
-}
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 b9ca949..37eccb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -29,7 +29,6 @@
 import static com.android.systemui.shared.system.WindowManagerWrapper.NAV_BAR_POS_INVALID;
 import static com.android.systemui.shared.system.WindowManagerWrapper.NAV_BAR_POS_LEFT;
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
-import static com.android.systemui.statusbar.NotificationMediaManager.DEBUG_MEDIA;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
 import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
@@ -46,6 +45,7 @@
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.AlarmManager;
+import android.app.AppOpsManager;
 import android.app.IWallpaperManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
@@ -130,6 +130,7 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.EventLogTags;
+import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.InitController;
 import com.android.systemui.Interpolators;
 import com.android.systemui.Prefs;
@@ -138,6 +139,7 @@
 import com.android.systemui.SystemUI;
 import com.android.systemui.SystemUIFactory;
 import com.android.systemui.UiOffloadThread;
+import com.android.systemui.appops.AppOpsController;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.charging.WirelessChargingAnimation;
 import com.android.systemui.classifier.FalsingLog;
@@ -183,7 +185,6 @@
 import com.android.systemui.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
-import com.android.systemui.statusbar.notification.AppOpsListener;
 import com.android.systemui.statusbar.notification.NotificationData;
 import com.android.systemui.statusbar.notification.NotificationData.Entry;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -228,7 +229,8 @@
         OnHeadsUpChangedListener, CommandQueue.Callbacks, ZenModeController.Callback,
         ColorExtractor.OnColorsChangedListener, ConfigurationListener,
         StatusBarStateController.StateListener, ShadeController,
-        ActivityLaunchAnimator.Callback, AmbientPulseManager.OnAmbientChangedListener {
+        ActivityLaunchAnimator.Callback, AmbientPulseManager.OnAmbientChangedListener,
+        AppOpsController.Callback {
     public static final boolean MULTIUSER_DEBUG = false;
 
     public static final boolean ENABLE_CHILD_NOTIFICATIONS
@@ -372,7 +374,8 @@
     protected NotificationLogger mNotificationLogger;
     protected NotificationEntryManager mEntryManager;
     protected NotificationViewHierarchyManager mViewHierarchyManager;
-    protected AppOpsListener mAppOpsListener;
+    protected ForegroundServiceController mForegroundServiceController;
+    protected AppOpsController mAppOpsController;
     protected KeyguardViewMediator mKeyguardViewMediator;
     private ZenModeController mZenController;
 
@@ -564,6 +567,20 @@
     protected NotificationPresenter mPresenter;
 
     @Override
+    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);
+        });
+    }
+
+    protected static final int[] APP_OPS = new int[] {AppOpsManager.OP_CAMERA,
+            AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
+            AppOpsManager.OP_RECORD_AUDIO,
+            AppOpsManager.OP_COARSE_LOCATION,
+            AppOpsManager.OP_FINE_LOCATION};
+
+    @Override
     public void start() {
         mGroupManager = Dependency.get(NotificationGroupManager.class);
         mVisualStabilityManager = Dependency.get(VisualStabilityManager.class);
@@ -585,7 +602,8 @@
         mMediaManager = Dependency.get(NotificationMediaManager.class);
         mEntryManager = Dependency.get(NotificationEntryManager.class);
         mViewHierarchyManager = Dependency.get(NotificationViewHierarchyManager.class);
-        mAppOpsListener = Dependency.get(AppOpsListener.class);
+        mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
+        mAppOpsController = Dependency.get(AppOpsController.class);
         mZenController = Dependency.get(ZenModeController.class);
         mKeyguardViewMediator = getComponent(KeyguardViewMediator.class);
         mColorExtractor = Dependency.get(SysuiColorExtractor.class);
@@ -984,7 +1002,7 @@
         mPresenter = new StatusBarNotificationPresenter(mContext, mNotificationPanel,
                 mHeadsUpManager, mStatusBarWindow, mStackScroller, mDozeScrimController,
                 mScrimController, this);
-        mAppOpsListener.setUpWithPresenter(mPresenter);
+        mAppOpsController.addCallback(APP_OPS, this);
         mNotificationListener.setUpWithPresenter(mPresenter);
         mNotificationShelf.setOnActivatedListener(mPresenter);
         mRemoteInputManager.getController().addCallback(mStatusBarWindowController);
@@ -2853,7 +2871,7 @@
         mDeviceProvisionedController.removeCallback(mUserSetupObserver);
         Dependency.get(ConfigurationController.class).removeCallback(this);
         mZenController.removeCallback(this);
-        mAppOpsListener.destroy();
+        mAppOpsController.removeCallback(APP_OPS, this);
     }
 
     private boolean mDemoModeAllowed;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
new file mode 100644
index 0000000..b699163
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2018 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.appops;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+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 android.app.AppOpsManager;
+import android.os.UserHandle;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.NotificationPresenter;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class AppOpsControllerTest extends SysuiTestCase {
+    private static final String TEST_PACKAGE_NAME = "test";
+    private static final int TEST_UID = 0;
+    private static final int TEST_UID_OTHER = 500000;
+
+    @Mock private NotificationPresenter mPresenter;
+    @Mock private AppOpsManager mAppOpsManager;
+    @Mock private AppOpsController.Callback mCallback;
+
+    private AppOpsControllerImpl mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager);
+
+        mController = new AppOpsControllerImpl(mContext, Dependency.get(Dependency.BG_LOOPER));
+    }
+
+    @Test
+    public void testOnlyListenForFewOps() {
+        mController.setListening(true);
+        verify(mAppOpsManager, times(1)).startWatchingActive(AppOpsControllerImpl.OPS, mController);
+    }
+
+    @Test
+    public void testStopListening() {
+        mController.setListening(false);
+        verify(mAppOpsManager, times(1)).stopWatchingActive(mController);
+    }
+
+    @Test
+    public void addCallback_includedCode() {
+        mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
+        mController.onOpActiveChanged(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+        verify(mCallback).onActiveStateChanged(AppOpsManager.OP_RECORD_AUDIO,
+                TEST_UID, TEST_PACKAGE_NAME, true);
+    }
+
+    @Test
+    public void addCallback_notIncludedCode() {
+        mController.addCallback(new int[]{AppOpsManager.OP_FINE_LOCATION}, mCallback);
+        mController.onOpActiveChanged(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+        verify(mCallback, never()).onActiveStateChanged(
+                anyInt(), anyInt(), anyString(), anyBoolean());
+    }
+
+    @Test
+    public void removeCallback_sameCode() {
+        mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
+        mController.removeCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
+        mController.onOpActiveChanged(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+        verify(mCallback, never()).onActiveStateChanged(
+                anyInt(), anyInt(), anyString(), anyBoolean());
+    }
+
+    @Test
+    public void addCallback_notSameCode() {
+        mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
+        mController.removeCallback(new int[]{AppOpsManager.OP_FINE_LOCATION}, mCallback);
+        mController.onOpActiveChanged(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+        verify(mCallback).onActiveStateChanged(AppOpsManager.OP_RECORD_AUDIO,
+                TEST_UID, TEST_PACKAGE_NAME, true);
+    }
+
+    @Test
+    public void getActiveItems_sameDetails() {
+        mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
+                TEST_UID, TEST_PACKAGE_NAME, true);
+        mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
+                TEST_UID, TEST_PACKAGE_NAME, true);
+        assertEquals(1, mController.getActiveAppOps().size());
+    }
+
+    @Test
+    public void getActiveItems_differentDetails() {
+        mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
+                TEST_UID, TEST_PACKAGE_NAME, true);
+        mController.onOpActiveChanged(AppOpsManager.OP_CAMERA,
+                TEST_UID, TEST_PACKAGE_NAME, true);
+        assertEquals(2, mController.getActiveAppOps().size());
+    }
+
+    @Test public void getActiveItemsForUser() {
+        mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
+                TEST_UID, TEST_PACKAGE_NAME, true);
+        mController.onOpActiveChanged(AppOpsManager.OP_CAMERA,
+                TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
+        assertEquals(1,
+                mController.getActiveAppOpsForUser(UserHandle.getUserId(TEST_UID)).size());
+        assertEquals(1,
+                mController.getActiveAppOpsForUser(UserHandle.getUserId(TEST_UID)).size());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AppOpsListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AppOpsListenerTest.java
deleted file mode 100644
index b405a5c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AppOpsListenerTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2018 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 org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.app.AppOpsManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.test.filters.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.ForegroundServiceController;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.NotificationPresenter;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class AppOpsListenerTest extends SysuiTestCase {
-    private static final String TEST_PACKAGE_NAME = "test";
-    private static final int TEST_UID = 0;
-
-    @Mock private NotificationPresenter mPresenter;
-    @Mock private AppOpsManager mAppOpsManager;
-
-    // Dependency mocks:
-    @Mock private NotificationEntryManager mEntryManager;
-    @Mock private ForegroundServiceController mFsc;
-
-    private AppOpsListener mListener;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
-        mDependency.injectTestDependency(ForegroundServiceController.class, mFsc);
-        getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager);
-        mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
-                Handler.createAsync(Looper.myLooper()));
-
-        mListener = new AppOpsListener(mContext);
-    }
-
-    @Test
-    public void testOnlyListenForFewOps() {
-        mListener.setUpWithPresenter(mPresenter);
-
-        verify(mAppOpsManager, times(1)).startWatchingActive(AppOpsListener.OPS, mListener);
-    }
-
-    @Test
-    public void testStopListening() {
-        mListener.destroy();
-        verify(mAppOpsManager, times(1)).stopWatchingActive(mListener);
-    }
-
-    @Test
-    public void testInformEntryMgrOnAppOpsChange() {
-        mListener.setUpWithPresenter(mPresenter);
-        mListener.onOpActiveChanged(
-                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
-        TestableLooper.get(this).processAllMessages();
-        verify(mEntryManager, times(1)).updateNotificationsForAppOp(
-                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
-    }
-
-    @Test
-    public void testInformFscOnAppOpsChange() {
-        mListener.setUpWithPresenter(mPresenter);
-        mListener.onOpActiveChanged(
-                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
-        TestableLooper.get(this).processAllMessages();
-        verify(mFsc, times(1)).onAppOpChanged(
-                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 939245f..882f261 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -67,6 +67,8 @@
 import com.android.systemui.InitController;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.appops.AppOpsController;
+import com.android.systemui.appops.AppOpsControllerImpl;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.classifier.FalsingManager;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -84,7 +86,6 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.notification.AppOpsListener;
 import com.android.systemui.statusbar.notification.NotificationData;
 import com.android.systemui.statusbar.notification.NotificationData.Entry;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -157,7 +158,7 @@
         mDependency.injectTestDependency(VisualStabilityManager.class, mVisualStabilityManager);
         mDependency.injectTestDependency(NotificationListener.class, mNotificationListener);
         mDependency.injectTestDependency(KeyguardMonitor.class, mock(KeyguardMonitorImpl.class));
-        mDependency.injectTestDependency(AppOpsListener.class, mock(AppOpsListener.class));
+        mDependency.injectTestDependency(AppOpsController.class, mock(AppOpsControllerImpl.class));
         mDependency.injectTestDependency(StatusBarStateController.class, mStatusBarStateController);
         mDependency.injectTestDependency(DeviceProvisionedController.class,
                 mDeviceProvisionedController);