Merge changes from topic "privacy-type-media-projection" into tm-dev

* changes:
  Add MediaProjection privacy item type
  Extract privacy item monitoring interface, add AppOpsPrivacyItemMonitor
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index d72073d..777104d8 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -158,6 +158,12 @@
     public static final String PROPERTY_LOCATION_INDICATORS_ENABLED = "location_indicators_enabled";
 
     /**
+     * Whether to show privacy chip for media projection.
+     */
+    public static final String PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED =
+            "media_projection_indicators_enabled";
+
+    /**
      * Whether to show old location indicator on all location accesses.
      */
     public static final String PROPERTY_LOCATION_INDICATORS_SMALL_ENABLED =
diff --git a/packages/SystemUI/res/drawable/privacy_item_circle_media_projection.xml b/packages/SystemUI/res/drawable/privacy_item_circle_media_projection.xml
new file mode 100644
index 0000000..ac563de
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_item_circle_media_projection.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@id/background"
+          android:gravity="center"
+          >
+        <shape android:shape="oval">
+            <size
+                android:height="@dimen/ongoing_appops_dialog_circle_size"
+                android:width="@dimen/ongoing_appops_dialog_circle_size"
+            />
+            <solid android:color="@color/privacy_chip_background" />
+        </shape>
+    </item>
+    <item android:id="@id/icon"
+          android:gravity="center"
+          android:width="@dimen/ongoing_appops_dialog_icon_size"
+          android:height="@dimen/ongoing_appops_dialog_icon_size"
+          android:drawable="@drawable/stat_sys_cast"
+    />
+</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c1e485b..1dd41a3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2046,6 +2046,9 @@
     <!-- Text for microphone app op [CHAR LIMIT=20]-->
     <string name="privacy_type_microphone">microphone</string>
 
+    <!-- Text for media projection privacy type [CHAR LIMIT=20]-->
+    <string name="privacy_type_media_projection">screen recording</string>
+
     <!-- What to show on the ambient display player when song doesn't have a title. [CHAR LIMIT=20] -->
     <string name="music_controls_no_title">No title</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 5b6ddd8..aeda20f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -54,6 +54,7 @@
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.media.MediaRouter2Manager;
+import android.media.projection.MediaProjectionManager;
 import android.media.session.MediaSessionManager;
 import android.net.ConnectivityManager;
 import android.net.NetworkScoreManager;
@@ -321,6 +322,11 @@
     }
 
     @Provides
+    static MediaProjectionManager provideMediaProjectionManager(Context context) {
+        return context.getSystemService(MediaProjectionManager.class);
+    }
+
+    @Provides
     static MediaRouter2Manager provideMediaRouter2Manager(Context context) {
         return MediaRouter2Manager.getInstance(context);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 535eff8..366ef26 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -46,6 +46,7 @@
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationBarComponent;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
+import com.android.systemui.privacy.PrivacyModule;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.screenshot.dagger.ScreenshotModule;
 import com.android.systemui.settings.dagger.SettingsModule;
@@ -122,6 +123,7 @@
             LogModule.class,
             PeopleHubModule.class,
             PluginModule.class,
+            PrivacyModule.class,
             QsFrameTranslateModule.class,
             ScreenshotModule.class,
             SensorModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
new file mode 100644
index 0000000..de34cd6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2022 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.privacy
+
+import android.app.AppOpsManager
+import android.content.Context
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import com.android.internal.annotations.GuardedBy
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.withIncreasedIndent
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Monitors privacy items backed by app ops:
+ * - Mic & Camera
+ * - Location
+ *
+ * If [PrivacyConfig.micCameraAvailable] / [PrivacyConfig.locationAvailable] are disabled,
+ * the corresponding PrivacyItems will not be reported.
+ */
+@SysUISingleton
+class AppOpsPrivacyItemMonitor @Inject constructor(
+    private val appOpsController: AppOpsController,
+    private val userTracker: UserTracker,
+    private val privacyConfig: PrivacyConfig,
+    @Background private val bgExecutor: DelayableExecutor,
+    private val logger: PrivacyLogger
+) : PrivacyItemMonitor {
+
+    @VisibleForTesting
+    companion object {
+        val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA,
+                AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO,
+                AppOpsManager.OP_PHONE_CALL_MICROPHONE,
+                AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO)
+        val OPS_LOCATION = intArrayOf(
+                AppOpsManager.OP_COARSE_LOCATION,
+                AppOpsManager.OP_FINE_LOCATION)
+        val OPS = OPS_MIC_CAMERA + OPS_LOCATION
+        val USER_INDEPENDENT_OPS = intArrayOf(AppOpsManager.OP_PHONE_CALL_CAMERA,
+                AppOpsManager.OP_PHONE_CALL_MICROPHONE)
+    }
+
+    private val lock = Any()
+
+    @GuardedBy("lock")
+    private var callback: PrivacyItemMonitor.Callback? = null
+    @GuardedBy("lock")
+    private var micCameraAvailable = privacyConfig.micCameraAvailable
+    @GuardedBy("lock")
+    private var locationAvailable = privacyConfig.locationAvailable
+    @GuardedBy("lock")
+    private var listening = false
+
+    private val appOpsCallback = object : AppOpsController.Callback {
+        override fun onActiveStateChanged(
+            code: Int,
+            uid: Int,
+            packageName: String,
+            active: Boolean
+        ) {
+            synchronized(lock) {
+                // Check if we care about this code right now
+                if (code in OPS_MIC_CAMERA && !micCameraAvailable) {
+                    return
+                }
+                if (code in OPS_LOCATION && !locationAvailable) {
+                    return
+                }
+                if (userTracker.userProfiles.any { it.id == UserHandle.getUserId(uid) } ||
+                        code in USER_INDEPENDENT_OPS) {
+                    logger.logUpdatedItemFromAppOps(code, uid, packageName, active)
+                    dispatchOnPrivacyItemsChanged()
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal val userTrackerCallback = object : UserTracker.Callback {
+        override fun onUserChanged(newUser: Int, userContext: Context) {
+            onCurrentProfilesChanged()
+        }
+
+        override fun onProfilesChanged(profiles: List<UserInfo>) {
+            onCurrentProfilesChanged()
+        }
+    }
+
+    private val configCallback = object : PrivacyConfig.Callback {
+        override fun onFlagLocationChanged(flag: Boolean) {
+            onFlagChanged()
+        }
+
+        override fun onFlagMicCameraChanged(flag: Boolean) {
+            onFlagChanged()
+        }
+
+        private fun onFlagChanged() {
+            synchronized(lock) {
+                micCameraAvailable = privacyConfig.micCameraAvailable
+                locationAvailable = privacyConfig.locationAvailable
+                setListeningStateLocked()
+            }
+            dispatchOnPrivacyItemsChanged()
+        }
+    }
+
+    init {
+        privacyConfig.addCallback(configCallback)
+    }
+
+    override fun startListening(callback: PrivacyItemMonitor.Callback) {
+        synchronized(lock) {
+            this.callback = callback
+            setListeningStateLocked()
+        }
+    }
+
+    override fun stopListening() {
+        synchronized(lock) {
+            this.callback = null
+            setListeningStateLocked()
+        }
+    }
+
+    /**
+     * Updates listening status based on whether there are callbacks and the indicators are enabled.
+     *
+     * Always listen to all OPS so we don't have to figure out what we should be listening to. We
+     * still have to filter anyway. Updates are filtered in the callback.
+     *
+     * This is only called from private (add/remove)Callback and from the config listener, all in
+     * main thread.
+     */
+    @GuardedBy("lock")
+    private fun setListeningStateLocked() {
+        val shouldListen = callback != null && (micCameraAvailable || locationAvailable)
+        if (listening == shouldListen) {
+            return
+        }
+
+        listening = shouldListen
+        if (shouldListen) {
+            appOpsController.addCallback(OPS, appOpsCallback)
+            userTracker.addCallback(userTrackerCallback, bgExecutor)
+            onCurrentProfilesChanged()
+        } else {
+            appOpsController.removeCallback(OPS, appOpsCallback)
+            userTracker.removeCallback(userTrackerCallback)
+        }
+    }
+
+    override fun getActivePrivacyItems(): List<PrivacyItem> {
+        val activeAppOps = appOpsController.getActiveAppOps(true)
+        val currentUserProfiles = userTracker.userProfiles
+
+        return synchronized(lock) {
+            activeAppOps.filter {
+                currentUserProfiles.any { user -> user.id == UserHandle.getUserId(it.uid) } ||
+                        it.code in USER_INDEPENDENT_OPS
+            }.mapNotNull { toPrivacyItemLocked(it) }
+        }.distinct()
+    }
+
+    @GuardedBy("lock")
+    private fun privacyItemForAppOpEnabledLocked(code: Int): Boolean {
+        if (code in OPS_LOCATION) {
+            return locationAvailable
+        } else if (code in OPS_MIC_CAMERA) {
+            return micCameraAvailable
+        } else {
+            return false
+        }
+    }
+
+    @GuardedBy("lock")
+    private fun toPrivacyItemLocked(appOpItem: AppOpItem): PrivacyItem? {
+        if (!privacyItemForAppOpEnabledLocked(appOpItem.code)) {
+            return null
+        }
+        val type: PrivacyType = when (appOpItem.code) {
+            AppOpsManager.OP_PHONE_CALL_CAMERA,
+            AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
+            AppOpsManager.OP_COARSE_LOCATION,
+            AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
+            AppOpsManager.OP_PHONE_CALL_MICROPHONE,
+            AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+            AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
+            else -> return null
+        }
+        val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid)
+        return PrivacyItem(type, app, appOpItem.timeStartedElapsed, appOpItem.isDisabled)
+    }
+
+    private fun onCurrentProfilesChanged() {
+        val currentUserIds = userTracker.userProfiles.map { it.id }
+        logger.logCurrentProfilesChanged(currentUserIds)
+        dispatchOnPrivacyItemsChanged()
+    }
+
+    private fun dispatchOnPrivacyItemsChanged() {
+        val cb = synchronized(lock) { callback }
+        if (cb != null) {
+            bgExecutor.execute {
+                cb.onPrivacyItemsChanged()
+            }
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        val ipw = pw.asIndenting()
+        ipw.println("AppOpsPrivacyItemMonitor:")
+        ipw.withIncreasedIndent {
+            synchronized(lock) {
+                ipw.println("Listening: $listening")
+                ipw.println("micCameraAvailable: $micCameraAvailable")
+                ipw.println("locationAvailable: $locationAvailable")
+                ipw.println("Callback: $callback")
+            }
+            ipw.println("Current user ids: ${userTracker.userProfiles.map { it.id }}")
+        }
+        ipw.flush()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/MediaProjectionPrivacyItemMonitor.kt b/packages/SystemUI/src/com/android/systemui/privacy/MediaProjectionPrivacyItemMonitor.kt
new file mode 100644
index 0000000..9b5a675
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/MediaProjectionPrivacyItemMonitor.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2022 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.privacy
+
+import android.content.pm.PackageManager
+import android.media.projection.MediaProjectionInfo
+import android.media.projection.MediaProjectionManager
+import android.os.Handler
+import android.util.Log
+import androidx.annotation.WorkerThread
+import com.android.internal.annotations.GuardedBy
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.withIncreasedIndent
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Monitors the active media projection to update privacy items.
+ */
+@SysUISingleton
+class MediaProjectionPrivacyItemMonitor @Inject constructor(
+    private val mediaProjectionManager: MediaProjectionManager,
+    private val packageManager: PackageManager,
+    private val privacyConfig: PrivacyConfig,
+    @Background private val bgHandler: Handler,
+    private val systemClock: SystemClock,
+    private val logger: PrivacyLogger
+) : PrivacyItemMonitor {
+
+    companion object {
+        const val TAG = "MediaProjectionPrivacyItemMonitor"
+        const val DEBUG = false
+    }
+
+    private val lock = Any()
+
+    @GuardedBy("lock")
+    private var callback: PrivacyItemMonitor.Callback? = null
+
+    @GuardedBy("lock")
+    private var mediaProjectionAvailable = privacyConfig.mediaProjectionAvailable
+    @GuardedBy("lock")
+    private var listening = false
+
+    @GuardedBy("lock")
+    private val privacyItems = mutableListOf<PrivacyItem>()
+
+    private val optionsCallback = object : PrivacyConfig.Callback {
+        override fun onFlagMediaProjectionChanged(flag: Boolean) {
+            synchronized(lock) {
+                mediaProjectionAvailable = privacyConfig.mediaProjectionAvailable
+                setListeningStateLocked()
+            }
+            dispatchOnPrivacyItemsChanged()
+        }
+    }
+
+    private val mediaProjectionCallback = object : MediaProjectionManager.Callback() {
+        @WorkerThread
+        override fun onStart(info: MediaProjectionInfo) {
+            synchronized(lock) { onMediaProjectionStartedLocked(info) }
+            dispatchOnPrivacyItemsChanged()
+        }
+
+        @WorkerThread
+        override fun onStop(info: MediaProjectionInfo) {
+            synchronized(lock) { onMediaProjectionStoppedLocked(info) }
+            dispatchOnPrivacyItemsChanged()
+        }
+    }
+
+    init {
+        privacyConfig.addCallback(optionsCallback)
+        setListeningStateLocked()
+    }
+
+    override fun startListening(callback: PrivacyItemMonitor.Callback) {
+        synchronized(lock) {
+            this.callback = callback
+        }
+    }
+
+    override fun stopListening() {
+        synchronized(lock) {
+            this.callback = null
+        }
+    }
+
+    @GuardedBy("lock")
+    @WorkerThread
+    private fun onMediaProjectionStartedLocked(info: MediaProjectionInfo) {
+        if (DEBUG) Log.d(TAG, "onMediaProjectionStartedLocked: info=$info")
+        val item = makePrivacyItem(info)
+        privacyItems.add(item)
+        logItemActive(item, true)
+    }
+
+    @GuardedBy("lock")
+    @WorkerThread
+    private fun onMediaProjectionStoppedLocked(info: MediaProjectionInfo) {
+        if (DEBUG) Log.d(TAG, "onMediaProjectionStoppedLocked: info=$info")
+        val item = makePrivacyItem(info)
+        privacyItems.removeAt(privacyItems.indexOfFirst { it.application == item.application })
+        logItemActive(item, false)
+    }
+
+    @WorkerThread
+    private fun makePrivacyItem(info: MediaProjectionInfo): PrivacyItem {
+        val userId = info.userHandle.identifier
+        val uid = packageManager.getPackageUidAsUser(info.packageName, userId)
+        val app = PrivacyApplication(info.packageName, uid)
+        val now = systemClock.elapsedRealtime()
+        return PrivacyItem(PrivacyType.TYPE_MEDIA_PROJECTION, app, now)
+    }
+
+    private fun logItemActive(item: PrivacyItem, active: Boolean) {
+        logger.logUpdatedItemFromMediaProjection(
+                item.application.uid, item.application.packageName, active)
+    }
+
+    /**
+     * Updates listening status based on whether there are callbacks and the indicator is enabled.
+     */
+    @GuardedBy("lock")
+    private fun setListeningStateLocked() {
+        val shouldListen = mediaProjectionAvailable
+        if (DEBUG) {
+            Log.d(TAG, "shouldListen=$shouldListen, " +
+                    "mediaProjectionAvailable=$mediaProjectionAvailable")
+        }
+        if (listening == shouldListen) {
+            return
+        }
+
+        listening = shouldListen
+        if (shouldListen) {
+            if (DEBUG) Log.d(TAG, "Registering MediaProjectionManager callback")
+            mediaProjectionManager.addCallback(mediaProjectionCallback, bgHandler)
+
+            val activeProjection = mediaProjectionManager.activeProjectionInfo
+            if (activeProjection != null) {
+                onMediaProjectionStartedLocked(activeProjection)
+                dispatchOnPrivacyItemsChanged()
+            }
+        } else {
+            if (DEBUG) Log.d(TAG, "Unregistering MediaProjectionManager callback")
+            mediaProjectionManager.removeCallback(mediaProjectionCallback)
+            privacyItems.forEach { logItemActive(it, false) }
+            privacyItems.clear()
+            dispatchOnPrivacyItemsChanged()
+        }
+    }
+
+    override fun getActivePrivacyItems(): List<PrivacyItem> {
+        synchronized(lock) {
+            if (DEBUG) Log.d(TAG, "getActivePrivacyItems: privacyItems=$privacyItems")
+            return privacyItems.toList()
+        }
+    }
+
+    private fun dispatchOnPrivacyItemsChanged() {
+        if (DEBUG) Log.d(TAG, "dispatchOnPrivacyItemsChanged")
+        val cb = synchronized(lock) { callback }
+        if (cb != null) {
+            bgHandler.post {
+                cb.onPrivacyItemsChanged()
+            }
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        val ipw = pw.asIndenting()
+        ipw.println("MediaProjectionPrivacyItemMonitor:")
+        ipw.withIncreasedIndent {
+            synchronized(lock) {
+                ipw.println("Listening: $listening")
+                ipw.println("mediaProjectionAvailable: $mediaProjectionAvailable")
+                ipw.println("Callback: $callback")
+                ipw.println("Privacy Items: $privacyItems")
+            }
+        }
+        ipw.flush()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt
new file mode 100644
index 0000000..d652889
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2022 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.privacy
+
+import android.provider.DeviceConfig
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.DeviceConfigProxy
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.withIncreasedIndent
+import java.io.PrintWriter
+import java.lang.ref.WeakReference
+import javax.inject.Inject
+
+@SysUISingleton
+class PrivacyConfig @Inject constructor(
+    @Main private val uiExecutor: DelayableExecutor,
+    private val deviceConfigProxy: DeviceConfigProxy,
+    dumpManager: DumpManager
+) : Dumpable {
+
+    @VisibleForTesting
+    internal companion object {
+        const val TAG = "PrivacyConfig"
+        private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
+        private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
+        private const val MEDIA_PROJECTION =
+                SystemUiDeviceConfigFlags.PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED
+        private const val DEFAULT_MIC_CAMERA = true
+        private const val DEFAULT_LOCATION = false
+        private const val DEFAULT_MEDIA_PROJECTION = true
+    }
+
+    private val callbacks = mutableListOf<WeakReference<Callback>>()
+
+    var micCameraAvailable = isMicCameraEnabled()
+        private set
+    var locationAvailable = isLocationEnabled()
+        private set
+    var mediaProjectionAvailable = isMediaProjectionEnabled()
+        private set
+
+    private val devicePropertiesChangedListener =
+            DeviceConfig.OnPropertiesChangedListener { properties ->
+                if (DeviceConfig.NAMESPACE_PRIVACY == properties.namespace) {
+                    // Running on the ui executor so can iterate on callbacks
+                    if (properties.keyset.contains(MIC_CAMERA)) {
+                        micCameraAvailable = properties.getBoolean(MIC_CAMERA, DEFAULT_MIC_CAMERA)
+                        callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) }
+                    }
+
+                    if (properties.keyset.contains(LOCATION)) {
+                        locationAvailable = properties.getBoolean(LOCATION, DEFAULT_LOCATION)
+                        callbacks.forEach { it.get()?.onFlagLocationChanged(locationAvailable) }
+                    }
+
+                    if (properties.keyset.contains(MEDIA_PROJECTION)) {
+                        mediaProjectionAvailable =
+                                properties.getBoolean(MEDIA_PROJECTION, DEFAULT_MEDIA_PROJECTION)
+                        callbacks.forEach {
+                            it.get()?.onFlagMediaProjectionChanged(mediaProjectionAvailable)
+                        }
+                    }
+                }
+            }
+
+    init {
+        dumpManager.registerDumpable(TAG, this)
+        deviceConfigProxy.addOnPropertiesChangedListener(
+                DeviceConfig.NAMESPACE_PRIVACY,
+                uiExecutor,
+                devicePropertiesChangedListener)
+    }
+
+    private fun isMicCameraEnabled(): Boolean {
+        return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+                MIC_CAMERA, DEFAULT_MIC_CAMERA)
+    }
+
+    private fun isLocationEnabled(): Boolean {
+        return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+                LOCATION, DEFAULT_LOCATION)
+    }
+
+    private fun isMediaProjectionEnabled(): Boolean {
+        return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+                MEDIA_PROJECTION, DEFAULT_MEDIA_PROJECTION)
+    }
+
+    fun addCallback(callback: Callback) {
+        addCallback(WeakReference(callback))
+    }
+
+    fun removeCallback(callback: Callback) {
+        removeCallback(WeakReference(callback))
+    }
+
+    private fun addCallback(callback: WeakReference<Callback>) {
+        uiExecutor.execute {
+            callbacks.add(callback)
+        }
+    }
+
+    private fun removeCallback(callback: WeakReference<Callback>) {
+        uiExecutor.execute {
+            // Removes also if the callback is null
+            callbacks.removeIf { it.get()?.equals(callback.get()) ?: true }
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        val ipw = pw.asIndenting()
+        ipw.println("PrivacyConfig state:")
+        ipw.withIncreasedIndent {
+            ipw.println("micCameraAvailable: $micCameraAvailable")
+            ipw.println("locationAvailable: $locationAvailable")
+            ipw.println("mediaProjectionAvailable: $mediaProjectionAvailable")
+            ipw.println("Callbacks:")
+            ipw.withIncreasedIndent {
+                callbacks.forEach { callback ->
+                    callback.get()?.let { ipw.println(it) }
+                }
+            }
+        }
+        ipw.flush()
+    }
+
+    interface Callback {
+        @JvmDefault
+        fun onFlagMicCameraChanged(flag: Boolean) {}
+
+        @JvmDefault
+        fun onFlagLocationChanged(flag: Boolean) {}
+
+        @JvmDefault
+        fun onFlagMediaProjectionChanged(flag: Boolean) {}
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt
index d4e1642..03145a7 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt
@@ -165,6 +165,7 @@
             PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location
             PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera
             PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone
+            PrivacyType.TYPE_MEDIA_PROJECTION -> R.drawable.privacy_item_circle_media_projection
         }) as LayerDrawable
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
index 76199bf..8b41000 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
@@ -43,6 +43,12 @@
         com.android.internal.R.drawable.perm_group_location,
         android.Manifest.permission_group.LOCATION,
         "location"
+    ),
+    TYPE_MEDIA_PROJECTION(
+            R.string.privacy_type_media_projection,
+            R.drawable.stat_sys_cast,
+            android.Manifest.permission_group.UNDEFINED,
+            "media projection"
     );
 
     fun getName(context: Context) = context.resources.getString(nameId)
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
index cd6eb99..a676150 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
@@ -16,27 +16,18 @@
 
 package com.android.systemui.privacy
 
-import android.app.AppOpsManager
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.UserInfo
-import android.os.UserHandle
-import android.provider.DeviceConfig
 import com.android.internal.annotations.VisibleForTesting
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
 import com.android.systemui.Dumpable
-import com.android.systemui.appops.AppOpItem
 import com.android.systemui.appops.AppOpsController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.privacy.logging.PrivacyLogger
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.DeviceConfigProxy
+import com.android.systemui.util.asIndenting
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.withIncreasedIndent
 import java.io.PrintWriter
 import java.lang.ref.WeakReference
 import java.util.concurrent.Executor
@@ -44,11 +35,10 @@
 
 @SysUISingleton
 class PrivacyItemController @Inject constructor(
-    private val appOpsController: AppOpsController,
     @Main uiExecutor: DelayableExecutor,
     @Background private val bgExecutor: DelayableExecutor,
-    private val deviceConfigProxy: DeviceConfigProxy,
-    private val userTracker: UserTracker,
+    private val privacyConfig: PrivacyConfig,
+    private val privacyItemMonitors: Set<@JvmSuppressWildcards PrivacyItemMonitor>,
     private val logger: PrivacyLogger,
     private val systemClock: SystemClock,
     dumpManager: DumpManager
@@ -56,24 +46,7 @@
 
     @VisibleForTesting
     internal companion object {
-        val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA,
-                AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO,
-                AppOpsManager.OP_PHONE_CALL_MICROPHONE,
-                AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO)
-        val OPS_LOCATION = intArrayOf(
-                AppOpsManager.OP_COARSE_LOCATION,
-                AppOpsManager.OP_FINE_LOCATION)
-        val OPS = OPS_MIC_CAMERA + OPS_LOCATION
-        val intentFilter = IntentFilter().apply {
-            addAction(Intent.ACTION_USER_SWITCHED)
-            addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
-            addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
-        }
         const val TAG = "PrivacyItemController"
-        private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
-        private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
-        private const val DEFAULT_MIC_CAMERA = true
-        private const val DEFAULT_LOCATION = false
         @VisibleForTesting const val TIME_TO_HOLD_INDICATORS = 5000L
     }
 
@@ -82,23 +55,18 @@
         @Synchronized get() = field.toList() // Returns a shallow copy of the list
         @Synchronized set
 
-    private fun isMicCameraEnabled(): Boolean {
-        return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
-                MIC_CAMERA, DEFAULT_MIC_CAMERA)
-    }
-
-    private fun isLocationEnabled(): Boolean {
-        return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
-                LOCATION, DEFAULT_LOCATION)
-    }
-
-    private var currentUserIds = emptyList<Int>()
     private var listening = false
     private val callbacks = mutableListOf<WeakReference<Callback>>()
     private val internalUiExecutor = MyExecutor(uiExecutor)
-
     private var holdingRunnableCanceler: Runnable? = null
 
+    val micCameraAvailable
+        get() = privacyConfig.micCameraAvailable
+    val locationAvailable
+        get() = privacyConfig.locationAvailable
+    val allIndicatorsAvailable
+        get() = micCameraAvailable && locationAvailable && privacyConfig.mediaProjectionAvailable
+
     private val notifyChanges = Runnable {
         val list = privacyList
         callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) }
@@ -109,90 +77,33 @@
         uiExecutor.execute(notifyChanges)
     }
 
-    var micCameraAvailable = isMicCameraEnabled()
-        private set
-    var locationAvailable = isLocationEnabled()
+    private val optionsCallback = object : PrivacyConfig.Callback {
+        override fun onFlagLocationChanged(flag: Boolean) {
+            callbacks.forEach { it.get()?.onFlagLocationChanged(flag) }
+        }
 
-    var allIndicatorsAvailable = micCameraAvailable && locationAvailable
+        override fun onFlagMicCameraChanged(flag: Boolean) {
+            callbacks.forEach { it.get()?.onFlagMicCameraChanged(flag) }
+        }
 
-    private val devicePropertiesChangedListener =
-            object : DeviceConfig.OnPropertiesChangedListener {
-        override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
-            if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) &&
-                    (properties.keyset.contains(MIC_CAMERA) ||
-                            properties.keyset.contains(LOCATION))) {
-
-                // Running on the ui executor so can iterate on callbacks
-                if (properties.keyset.contains(MIC_CAMERA)) {
-                    micCameraAvailable = properties.getBoolean(MIC_CAMERA, DEFAULT_MIC_CAMERA)
-                    allIndicatorsAvailable = micCameraAvailable && locationAvailable
-                    callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) }
-                }
-
-                if (properties.keyset.contains(LOCATION)) {
-                    locationAvailable = properties.getBoolean(LOCATION, DEFAULT_LOCATION)
-                    allIndicatorsAvailable = micCameraAvailable && locationAvailable
-                    callbacks.forEach { it.get()?.onFlagLocationChanged(locationAvailable) }
-                }
-                internalUiExecutor.updateListeningState()
-            }
+        override fun onFlagMediaProjectionChanged(flag: Boolean) {
+            callbacks.forEach { it.get()?.onFlagMediaProjectionChanged(flag) }
         }
     }
 
-    private val cb = object : AppOpsController.Callback {
-        override fun onActiveStateChanged(
-            code: Int,
-            uid: Int,
-            packageName: String,
-            active: Boolean
-        ) {
-            // Check if we care about this code right now
-            if (code in OPS_LOCATION && !locationAvailable) {
-                return
-            }
-            val userId = UserHandle.getUserId(uid)
-            if (userId in currentUserIds ||
-                    code == AppOpsManager.OP_PHONE_CALL_MICROPHONE ||
-                    code == AppOpsManager.OP_PHONE_CALL_CAMERA) {
-                logger.logUpdatedItemFromAppOps(code, uid, packageName, active)
-                update(false)
-            }
-        }
-    }
-
-    @VisibleForTesting
-    internal var userTrackerCallback = object : UserTracker.Callback {
-        override fun onUserChanged(newUser: Int, userContext: Context) {
-            update(true)
-        }
-
-        override fun onProfilesChanged(profiles: List<UserInfo>) {
-            update(true)
+    private val privacyItemMonitorCallback = object : PrivacyItemMonitor.Callback {
+        override fun onPrivacyItemsChanged() {
+            update()
         }
     }
 
     init {
-        deviceConfigProxy.addOnPropertiesChangedListener(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                uiExecutor,
-                devicePropertiesChangedListener)
         dumpManager.registerDumpable(TAG, this)
+        privacyConfig.addCallback(optionsCallback)
     }
 
-    private fun unregisterListener() {
-        userTracker.removeCallback(userTrackerCallback)
-    }
-
-    private fun registerReceiver() {
-        userTracker.addCallback(userTrackerCallback, bgExecutor)
-    }
-
-    private fun update(updateUsers: Boolean) {
+    private fun update() {
         bgExecutor.execute {
-            if (updateUsers) {
-                currentUserIds = userTracker.userProfiles.map { it.id }
-                logger.logCurrentProfilesChanged(currentUserIds)
-            }
             updateListAndNotifyChanges.run()
         }
     }
@@ -207,20 +118,17 @@
      * main thread.
      */
     private fun setListeningState() {
-        val listen = !callbacks.isEmpty() and
-                (micCameraAvailable || locationAvailable)
+        val listen = callbacks.isNotEmpty()
         if (listening == listen) return
         listening = listen
         if (listening) {
-            appOpsController.addCallback(OPS, cb)
-            registerReceiver()
-            update(true)
+            privacyItemMonitors.forEach { it.startListening(privacyItemMonitorCallback) }
+            update()
         } else {
-            appOpsController.removeCallback(OPS, cb)
-            unregisterListener()
+            privacyItemMonitors.forEach { it.stopListening() }
             // Make sure that we remove all indicators and notify listeners if we are not
             // listening anymore due to indicators being disabled
-            update(false)
+            update()
         }
     }
 
@@ -259,11 +167,7 @@
             privacyList = emptyList()
             return
         }
-        val list = appOpsController.getActiveAppOps(true).filter {
-            UserHandle.getUserId(it.uid) in currentUserIds ||
-                    it.code == AppOpsManager.OP_PHONE_CALL_MICROPHONE ||
-                    it.code == AppOpsManager.OP_PHONE_CALL_CAMERA
-        }.mapNotNull { toPrivacyItem(it) }.distinct()
+        val list = privacyItemMonitors.flatMap { it.getActivePrivacyItems() }.distinct()
         privacyList = processNewList(list)
     }
 
@@ -309,35 +213,11 @@
         }
     }
 
-    private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
-        val type: PrivacyType = when (appOpItem.code) {
-            AppOpsManager.OP_PHONE_CALL_CAMERA,
-            AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
-            AppOpsManager.OP_COARSE_LOCATION,
-            AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
-            AppOpsManager.OP_PHONE_CALL_MICROPHONE,
-            AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
-            AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
-            else -> return null
-        }
-        if (type == PrivacyType.TYPE_LOCATION && !locationAvailable) {
-            return null
-        }
-        val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid)
-        return PrivacyItem(type, app, appOpItem.timeStartedElapsed, appOpItem.isDisabled)
-    }
-
-    interface Callback {
+    interface Callback : PrivacyConfig.Callback {
         fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>)
 
         @JvmDefault
         fun onFlagAllChanged(flag: Boolean) {}
-
-        @JvmDefault
-        fun onFlagMicCameraChanged(flag: Boolean) {}
-
-        @JvmDefault
-        fun onFlagLocationChanged(flag: Boolean) {}
     }
 
     private class NotifyChangesToCallback(
@@ -350,21 +230,34 @@
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.println("PrivacyItemController state:")
-        pw.println("  Listening: $listening")
-        pw.println("  Current user ids: $currentUserIds")
-        pw.println("  Privacy Items:")
-        privacyList.forEach {
-            pw.print("    ")
-            pw.println(it.toString())
-        }
-        pw.println("  Callbacks:")
-        callbacks.forEach {
-            it.get()?.let {
-                pw.print("    ")
-                pw.println(it.toString())
+        val ipw = pw.asIndenting()
+        ipw.println("PrivacyItemController state:")
+        ipw.withIncreasedIndent {
+            ipw.println("Listening: $listening")
+            ipw.println("Privacy Items:")
+            ipw.withIncreasedIndent {
+                privacyList.forEach {
+                    ipw.println(it.toString())
+                }
+            }
+
+            ipw.println("Callbacks:")
+            ipw.withIncreasedIndent {
+                callbacks.forEach {
+                    it.get()?.let {
+                        ipw.println(it.toString())
+                    }
+                }
+            }
+
+            ipw.println("PrivacyItemMonitors:")
+            ipw.withIncreasedIndent {
+                privacyItemMonitors.forEach {
+                    it.dump(ipw, args)
+                }
             }
         }
+        ipw.flush()
     }
 
     private inner class MyExecutor(
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemMonitor.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemMonitor.kt
new file mode 100644
index 0000000..5bae31e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemMonitor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 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.privacy
+
+import com.android.systemui.Dumpable
+
+interface PrivacyItemMonitor : Dumpable {
+    fun startListening(callback: Callback)
+    fun stopListening()
+    fun getActivePrivacyItems(): List<PrivacyItem>
+
+    interface Callback {
+        fun onPrivacyItemsChanged()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyModule.java b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyModule.java
new file mode 100644
index 0000000..732a310
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 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.privacy;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.multibindings.IntoSet;
+
+/** Dagger module for privacy. */
+@Module
+public interface PrivacyModule {
+    /** Binds {@link AppOpsPrivacyItemMonitor} into the set of {@link PrivacyItemMonitor}. */
+    @Binds
+    @IntoSet
+    PrivacyItemMonitor bindAppOpsPrivacyItemMonitor(
+            AppOpsPrivacyItemMonitor appOpsPrivacyItemMonitor);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index 1a268b5..1ea9347 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -44,6 +44,16 @@
         })
     }
 
+    fun logUpdatedItemFromMediaProjection(uid: Int, packageName: String, active: Boolean) {
+        log(LogLevel.INFO, {
+            int1 = uid
+            str1 = packageName
+            bool1 = active
+        }, {
+            "MediaProjection: $str1($int1), active=$bool1"
+        })
+    }
+
     fun logRetrievedPrivacyItemsList(list: List<PrivacyItem>) {
         log(LogLevel.INFO, {
             str1 = listToString(list)
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 4685c14..9a19d8d 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -39,6 +39,8 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.power.EnhancedEstimates;
 import com.android.systemui.power.dagger.PowerModule;
+import com.android.systemui.privacy.MediaProjectionPrivacyItemMonitor;
+import com.android.systemui.privacy.PrivacyItemMonitor;
 import com.android.systemui.qs.dagger.QSModule;
 import com.android.systemui.qs.tileimpl.QSFactoryImpl;
 import com.android.systemui.recents.Recents;
@@ -78,6 +80,7 @@
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.IntoSet;
 
 /**
  * A dagger module for injecting default implementations of components of System UI that may be
@@ -212,4 +215,12 @@
             NotificationListener notificationListener) {
         return new TvNotificationHandler(context, notificationListener);
     }
+
+    /**
+     * Binds {@link MediaProjectionPrivacyItemMonitor} into the set of {@link PrivacyItemMonitor}.
+     */
+    @Binds
+    @IntoSet
+    abstract PrivacyItemMonitor bindMediaProjectionPrivacyItemMonitor(
+            MediaProjectionPrivacyItemMonitor mediaProjectionPrivacyItemMonitor);
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitorTest.kt
new file mode 100644
index 0000000..db96d55
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitorTest.kt
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2022 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.privacy
+
+import android.app.AppOpsManager
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.hamcrest.Matchers.hasItem
+import org.hamcrest.Matchers.not
+import org.hamcrest.Matchers.nullValue
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThat
+import org.junit.Assert.assertTrue
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper
+class AppOpsPrivacyItemMonitorTest : SysuiTestCase() {
+
+    companion object {
+        val CURRENT_USER_ID = 1
+        val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE
+        const val TEST_PACKAGE_NAME = "test"
+
+        fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+        fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+        fun <T> any(): T = Mockito.any<T>()
+    }
+
+    @Mock
+    private lateinit var appOpsController: AppOpsController
+
+    @Mock
+    private lateinit var callback: PrivacyItemMonitor.Callback
+
+    @Mock
+    private lateinit var userTracker: UserTracker
+
+    @Mock
+    private lateinit var privacyConfig: PrivacyConfig
+
+    @Mock
+    private lateinit var logger: PrivacyLogger
+
+    @Captor
+    private lateinit var argCaptorConfigCallback: ArgumentCaptor<PrivacyConfig.Callback>
+
+    @Captor
+    private lateinit var argCaptorCallback: ArgumentCaptor<AppOpsController.Callback>
+
+    private lateinit var appOpsPrivacyItemMonitor: AppOpsPrivacyItemMonitor
+    private lateinit var executor: FakeExecutor
+
+    fun createAppOpsPrivacyItemMonitor(): AppOpsPrivacyItemMonitor {
+        return AppOpsPrivacyItemMonitor(
+                appOpsController,
+                userTracker,
+                privacyConfig,
+                executor,
+                logger)
+    }
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        executor = FakeExecutor(FakeSystemClock())
+
+        // Listen to everything by default
+        `when`(privacyConfig.micCameraAvailable).thenReturn(true)
+        `when`(privacyConfig.locationAvailable).thenReturn(true)
+        `when`(userTracker.userProfiles).thenReturn(
+                listOf(UserInfo(CURRENT_USER_ID, TEST_PACKAGE_NAME, 0)))
+
+        appOpsPrivacyItemMonitor = createAppOpsPrivacyItemMonitor()
+        verify(privacyConfig).addCallback(capture(argCaptorConfigCallback))
+    }
+
+    @Test
+    fun testStartListeningAddsAppOpsCallback() {
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+        verify(appOpsController).addCallback(eq(AppOpsPrivacyItemMonitor.OPS), any())
+    }
+
+    @Test
+    fun testStopListeningRemovesAppOpsCallback() {
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+        verify(appOpsController, never()).removeCallback(any(), any())
+
+        appOpsPrivacyItemMonitor.stopListening()
+        executor.runAllReady()
+        verify(appOpsController).removeCallback(eq(AppOpsPrivacyItemMonitor.OPS), any())
+    }
+
+    @Test
+    fun testDistinctItems() {
+        doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0)))
+                .`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        assertEquals(1, appOpsPrivacyItemMonitor.getActivePrivacyItems().size)
+    }
+
+    @Test
+    fun testSimilarItemsDifferentTimeStamp() {
+        doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 1)))
+                .`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        assertEquals(2, appOpsPrivacyItemMonitor.getActivePrivacyItems().size)
+    }
+
+    @Test
+    fun testRegisterUserTrackerCallback() {
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+        verify(userTracker, atLeastOnce()).addCallback(
+                eq(appOpsPrivacyItemMonitor.userTrackerCallback), any())
+        verify(userTracker, never()).removeCallback(
+                eq(appOpsPrivacyItemMonitor.userTrackerCallback))
+    }
+
+    @Test
+    fun testUserTrackerCallback_userChanged() {
+        appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(0, mContext)
+        executor.runAllReady()
+        verify(userTracker).userProfiles
+    }
+
+    @Test
+    fun testUserTrackerCallback_profilesChanged() {
+        appOpsPrivacyItemMonitor.userTrackerCallback.onProfilesChanged(emptyList())
+        executor.runAllReady()
+        verify(userTracker).userProfiles
+    }
+
+    @Test
+    fun testCallbackIsUpdated() {
+        doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+        reset(callback)
+
+        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
+        argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, TEST_PACKAGE_NAME, true)
+        executor.runAllReady()
+        verify(callback).onPrivacyItemsChanged()
+    }
+
+    @Test
+    fun testRemoveCallback() {
+        doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+        reset(callback)
+
+        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
+        appOpsPrivacyItemMonitor.stopListening()
+        argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, TEST_PACKAGE_NAME, true)
+        executor.runAllReady()
+        verify(callback, never()).onPrivacyItemsChanged()
+    }
+
+    @Test
+    fun testListShouldNotHaveNull() {
+        doReturn(listOf(AppOpItem(AppOpsManager.OP_ACTIVATE_VPN, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0)))
+                .`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        assertThat(appOpsPrivacyItemMonitor.getActivePrivacyItems(), not(hasItem(nullValue())))
+    }
+
+    @Test
+    fun testNotListeningWhenIndicatorsDisabled() {
+        changeMicCamera(false)
+        changeLocation(false)
+
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+        verify(appOpsController, never()).addCallback(eq(AppOpsPrivacyItemMonitor.OPS), any())
+    }
+
+    @Test
+    fun testNotSendingLocationWhenLocationDisabled() {
+        changeLocation(false)
+        executor.runAllReady()
+
+        doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0)))
+                .`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems()
+        assertEquals(1, privacyItems.size)
+        assertEquals(PrivacyType.TYPE_CAMERA, privacyItems[0].privacyType)
+    }
+
+    @Test
+    fun testNotUpdated_LocationChangeWhenLocationDisabled() {
+        doReturn(listOf(
+                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0)))
+                .`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        appOpsPrivacyItemMonitor.startListening(callback)
+        changeLocation(false)
+        executor.runAllReady()
+        reset(callback) // Clean callback
+
+        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
+        argCaptorCallback.value.onActiveStateChanged(
+                AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true)
+
+        verify(callback, never()).onPrivacyItemsChanged()
+    }
+
+    @Test
+    fun testLogActiveChanged() {
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+
+        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
+        argCaptorCallback.value.onActiveStateChanged(
+                AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true)
+
+        verify(logger).logUpdatedItemFromAppOps(
+                AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true)
+    }
+
+    @Test
+    fun testListRequestedShowPaused() {
+        appOpsPrivacyItemMonitor.getActivePrivacyItems()
+        verify(appOpsController).getActiveAppOps(true)
+    }
+
+    @Test
+    fun testListFilterCurrentUser() {
+        val otherUser = CURRENT_USER_ID + 1
+        val otherUserUid = otherUser * UserHandle.PER_USER_RANGE
+        `when`(userTracker.userProfiles)
+                .thenReturn(listOf(UserInfo(otherUser, TEST_PACKAGE_NAME, 0)))
+
+        doReturn(listOf(
+                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_CAMERA, otherUserUid, TEST_PACKAGE_NAME, 0))
+        ).`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(otherUser, mContext)
+        executor.runAllReady()
+
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+
+        val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems()
+
+        assertEquals(1, privacyItems.size)
+        assertEquals(PrivacyType.TYPE_CAMERA, privacyItems[0].privacyType)
+        assertEquals(otherUserUid, privacyItems[0].application.uid)
+    }
+
+    @Test
+    fun testAlwaysGetPhoneCameraOps() {
+        val otherUser = CURRENT_USER_ID + 1
+        `when`(userTracker.userProfiles)
+                .thenReturn(listOf(UserInfo(otherUser, TEST_PACKAGE_NAME, 0)))
+
+        doReturn(listOf(
+                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_PHONE_CALL_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0))
+        ).`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(otherUser, mContext)
+        executor.runAllReady()
+
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+
+        val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems()
+
+        assertEquals(1, privacyItems.size)
+        assertEquals(PrivacyType.TYPE_CAMERA, privacyItems[0].privacyType)
+    }
+
+    @Test
+    fun testAlwaysGetPhoneMicOps() {
+        val otherUser = CURRENT_USER_ID + 1
+        `when`(userTracker.userProfiles)
+                .thenReturn(listOf(UserInfo(otherUser, TEST_PACKAGE_NAME, 0)))
+
+        doReturn(listOf(
+                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0),
+                AppOpItem(AppOpsManager.OP_PHONE_CALL_MICROPHONE, TEST_UID, TEST_PACKAGE_NAME, 0))
+        ).`when`(appOpsController).getActiveAppOps(anyBoolean())
+
+        appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(otherUser, mContext)
+        executor.runAllReady()
+
+        appOpsPrivacyItemMonitor.startListening(callback)
+        executor.runAllReady()
+
+        val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems()
+
+        assertEquals(1, privacyItems.size)
+        assertEquals(PrivacyType.TYPE_MICROPHONE, privacyItems[0].privacyType)
+    }
+
+    @Test
+    fun testDisabledAppOpIsPaused() {
+        val item = AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0)
+        item.isDisabled = true
+        `when`(appOpsController.getActiveAppOps(anyBoolean())).thenReturn(listOf(item))
+
+        val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems()
+        assertEquals(1, privacyItems.size)
+        assertTrue(privacyItems[0].paused)
+    }
+
+    @Test
+    fun testEnabledAppOpIsNotPaused() {
+        val item = AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0)
+        `when`(appOpsController.getActiveAppOps(anyBoolean())).thenReturn(listOf(item))
+
+        val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems()
+        assertEquals(1, privacyItems.size)
+        assertFalse(privacyItems[0].paused)
+    }
+
+    private fun changeMicCamera(value: Boolean) {
+        `when`(privacyConfig.micCameraAvailable).thenReturn(value)
+        argCaptorConfigCallback.value.onFlagMicCameraChanged(value)
+    }
+
+    private fun changeLocation(value: Boolean) {
+        `when`(privacyConfig.locationAvailable).thenReturn(value)
+        argCaptorConfigCallback.value.onFlagLocationChanged(value)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt
new file mode 100644
index 0000000..272f149
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import android.provider.DeviceConfig
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.DeviceConfigProxy
+import com.android.systemui.util.DeviceConfigProxyFake
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class PrivacyConfigFlagsTest : SysuiTestCase() {
+    companion object {
+        private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
+        private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
+        private const val MEDIA_PROJECTION =
+                SystemUiDeviceConfigFlags.PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED
+    }
+
+    private lateinit var privacyConfig: PrivacyConfig
+
+    @Mock
+    private lateinit var callback: PrivacyConfig.Callback
+    @Mock
+    private lateinit var dumpManager: DumpManager
+
+    private lateinit var executor: FakeExecutor
+    private lateinit var deviceConfigProxy: DeviceConfigProxy
+
+    fun createPrivacyConfig(): PrivacyConfig {
+        return PrivacyConfig(
+                executor,
+                deviceConfigProxy,
+                dumpManager)
+    }
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        executor = FakeExecutor(FakeSystemClock())
+        deviceConfigProxy = DeviceConfigProxyFake()
+
+        privacyConfig = createPrivacyConfig()
+        privacyConfig.addCallback(callback)
+
+        executor.runAllReady()
+    }
+
+    @Test
+    fun testMicCameraListeningByDefault() {
+        assertTrue(privacyConfig.micCameraAvailable)
+    }
+
+    @Test
+    fun testMicCameraChanged() {
+        changeMicCamera(false) // default is true
+        executor.runAllReady()
+
+        verify(callback).onFlagMicCameraChanged(false)
+
+        assertFalse(privacyConfig.micCameraAvailable)
+    }
+
+    @Test
+    fun testMediaProjectionChanged() {
+        changeMediaProjection(false) // default is true
+        executor.runAllReady()
+
+        verify(callback).onFlagMediaProjectionChanged(false)
+
+        assertFalse(privacyConfig.mediaProjectionAvailable)
+    }
+
+    @Test
+    fun testLocationChanged() {
+        changeLocation(true)
+        executor.runAllReady()
+
+        verify(callback).onFlagLocationChanged(true)
+        assertTrue(privacyConfig.locationAvailable)
+    }
+
+    @Test
+    fun testMicCamAndLocationChanged() {
+        changeLocation(true)
+        changeMicCamera(false)
+        executor.runAllReady()
+
+        verify(callback, atLeastOnce()).onFlagLocationChanged(true)
+        verify(callback, atLeastOnce()).onFlagMicCameraChanged(false)
+
+        assertTrue(privacyConfig.locationAvailable)
+        assertFalse(privacyConfig.micCameraAvailable)
+    }
+
+    @Test
+    fun testMicDeleted_stillAvailable() {
+        changeMicCamera(true)
+        executor.runAllReady()
+        changeMicCamera(null)
+        executor.runAllReady()
+
+        verify(callback, never()).onFlagMicCameraChanged(false)
+        assertTrue(privacyConfig.micCameraAvailable)
+    }
+
+    private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)
+    private fun changeLocation(value: Boolean?) = changeProperty(LOCATION, value)
+    private fun changeMediaProjection(value: Boolean?) = changeProperty(MEDIA_PROJECTION, value)
+
+    private fun changeProperty(name: String, value: Boolean?) {
+        deviceConfigProxy.setProperty(
+                DeviceConfig.NAMESPACE_PRIVACY,
+                name,
+                value?.toString(),
+                false
+        )
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt
deleted file mode 100644
index 2a8611f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (C) 2020 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.privacy
-
-import android.provider.DeviceConfig
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.appops.AppOpsController
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.privacy.logging.PrivacyLogger
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.DeviceConfigProxy
-import com.android.systemui.util.DeviceConfigProxyFake
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-class PrivacyItemControllerFlagsTest : SysuiTestCase() {
-    companion object {
-        fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
-        fun <T> eq(value: T): T = Mockito.eq(value) ?: value
-        fun <T> any(): T = Mockito.any<T>()
-
-        private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
-        private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
-    }
-
-    @Mock
-    private lateinit var appOpsController: AppOpsController
-    @Mock
-    private lateinit var callback: PrivacyItemController.Callback
-    @Mock
-    private lateinit var dumpManager: DumpManager
-    @Mock
-    private lateinit var userTracker: UserTracker
-    @Mock
-    private lateinit var logger: PrivacyLogger
-
-    private lateinit var privacyItemController: PrivacyItemController
-    private lateinit var executor: FakeExecutor
-    private lateinit var deviceConfigProxy: DeviceConfigProxy
-
-    fun createPrivacyItemController(): PrivacyItemController {
-        return PrivacyItemController(
-                appOpsController,
-                executor,
-                executor,
-                deviceConfigProxy,
-                userTracker,
-                logger,
-                FakeSystemClock(),
-                dumpManager)
-    }
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        executor = FakeExecutor(FakeSystemClock())
-        deviceConfigProxy = DeviceConfigProxyFake()
-
-        privacyItemController = createPrivacyItemController()
-        privacyItemController.addCallback(callback)
-
-        executor.runAllReady()
-    }
-
-    @Test
-    fun testMicCameraListeningByDefault() {
-        assertTrue(privacyItemController.micCameraAvailable)
-    }
-
-    @Test
-    fun testMicCameraChanged() {
-        changeMicCamera(false) // default is true
-        executor.runAllReady()
-
-        verify(callback).onFlagMicCameraChanged(false)
-
-        assertFalse(privacyItemController.micCameraAvailable)
-    }
-
-    @Test
-    fun testLocationChanged() {
-        changeLocation(true)
-        executor.runAllReady()
-
-        verify(callback).onFlagLocationChanged(true)
-        assertTrue(privacyItemController.locationAvailable)
-    }
-
-    @Test
-    fun testBothChanged() {
-        changeAll(true)
-        changeMicCamera(false)
-        executor.runAllReady()
-
-        verify(callback, atLeastOnce()).onFlagLocationChanged(true)
-        verify(callback, atLeastOnce()).onFlagMicCameraChanged(false)
-
-        assertTrue(privacyItemController.locationAvailable)
-        assertFalse(privacyItemController.micCameraAvailable)
-    }
-
-    @Test
-    fun testAll_listeningToAll() {
-        changeAll(true)
-        executor.runAllReady()
-
-        verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
-    }
-
-    @Test
-    fun testMicCamera_listening() {
-        changeMicCamera(true)
-        executor.runAllReady()
-
-        verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
-    }
-
-    @Test
-    fun testLocation_listening() {
-        changeLocation(true)
-        executor.runAllReady()
-
-        verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
-    }
-
-    @Test
-    fun testAllFalse_notListening() {
-        changeAll(true)
-        executor.runAllReady()
-        changeAll(false)
-        changeMicCamera(false)
-        executor.runAllReady()
-
-        verify(appOpsController).removeCallback(any(), any())
-    }
-
-    @Test
-    fun testMicDeleted_stillListening() {
-        changeMicCamera(true)
-        executor.runAllReady()
-        changeMicCamera(null)
-        executor.runAllReady()
-
-        verify(appOpsController, never()).removeCallback(any(), any())
-    }
-
-    private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)
-    private fun changeLocation(value: Boolean?) = changeProperty(LOCATION, value)
-    private fun changeAll(value: Boolean?) {
-        changeMicCamera(value)
-        changeLocation(value)
-    }
-
-    private fun changeProperty(name: String, value: Boolean?) {
-        deviceConfigProxy.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                name,
-                value?.toString(),
-                false
-        )
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt
index e4d7b1b..d563632 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt
@@ -17,36 +17,24 @@
 package com.android.systemui.privacy
 
 import android.app.ActivityManager
-import android.app.AppOpsManager
-import android.content.pm.UserInfo
 import android.os.UserHandle
-import android.provider.DeviceConfig
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.appops.AppOpItem
-import com.android.systemui.appops.AppOpsController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.privacy.logging.PrivacyLogger
-import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.DeviceConfigProxy
 import com.android.systemui.util.DeviceConfigProxyFake
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.time.FakeSystemClock
-import org.hamcrest.Matchers.hasItem
-import org.hamcrest.Matchers.not
-import org.hamcrest.Matchers.nullValue
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertThat
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyList
 import org.mockito.Captor
 import org.mockito.Mock
@@ -71,20 +59,18 @@
         val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE
         const val TEST_PACKAGE_NAME = "test"
 
-        private const val LOCATION_INDICATOR =
-                SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
-        private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
         fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
-        fun <T> eq(value: T): T = Mockito.eq(value) ?: value
         fun <T> any(): T = Mockito.any<T>()
     }
 
     @Mock
-    private lateinit var appOpsController: AppOpsController
-    @Mock
     private lateinit var callback: PrivacyItemController.Callback
     @Mock
-    private lateinit var userTracker: UserTracker
+    private lateinit var privacyConfig: PrivacyConfig
+    @Mock
+    private lateinit var privacyItemMonitor: PrivacyItemMonitor
+    @Mock
+    private lateinit var privacyItemMonitor2: PrivacyItemMonitor
     @Mock
     private lateinit var dumpManager: DumpManager
     @Mock
@@ -92,23 +78,21 @@
     @Captor
     private lateinit var argCaptor: ArgumentCaptor<List<PrivacyItem>>
     @Captor
-    private lateinit var argCaptorCallback: ArgumentCaptor<AppOpsController.Callback>
+    private lateinit var argCaptorCallback: ArgumentCaptor<PrivacyItemMonitor.Callback>
+    @Captor
+    private lateinit var argCaptorConfigCallback: ArgumentCaptor<PrivacyConfig.Callback>
 
     private lateinit var privacyItemController: PrivacyItemController
     private lateinit var executor: FakeExecutor
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var deviceConfigProxy: DeviceConfigProxy
 
-    private val elapsedTime: Long
-        get() = fakeClock.elapsedRealtime()
-
     fun createPrivacyItemController(): PrivacyItemController {
         return PrivacyItemController(
-                appOpsController,
                 executor,
                 executor,
-                deviceConfigProxy,
-                userTracker,
+                privacyConfig,
+                setOf(privacyItemMonitor, privacyItemMonitor2),
                 logger,
                 fakeClock,
                 dumpManager)
@@ -120,43 +104,61 @@
         fakeClock = FakeSystemClock()
         executor = FakeExecutor(fakeClock)
         deviceConfigProxy = DeviceConfigProxyFake()
-
-        // Listen to everything by default
-        changeMicCamera(true)
-        changeLocation(true)
-
-        `when`(userTracker.userProfiles).thenReturn(listOf(UserInfo(CURRENT_USER_ID, "", 0)))
-
         privacyItemController = createPrivacyItemController()
     }
 
     @Test
-    fun testSetListeningTrueByAddingCallback() {
+    fun testStartListeningByAddingCallback() {
         privacyItemController.addCallback(callback)
         executor.runAllReady()
-        verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
-                any())
+        verify(privacyItemMonitor).startListening(any())
+        verify(privacyItemMonitor2).startListening(any())
         verify(callback).onPrivacyItemsChanged(anyList())
     }
 
     @Test
-    fun testSetListeningFalseByRemovingLastCallback() {
+    fun testStopListeningByRemovingLastCallback() {
         privacyItemController.addCallback(callback)
         executor.runAllReady()
-        verify(appOpsController, never()).removeCallback(any(),
-                any())
+        verify(privacyItemMonitor, never()).stopListening()
         privacyItemController.removeCallback(callback)
         executor.runAllReady()
-        verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
-                any())
+        verify(privacyItemMonitor).stopListening()
+        verify(privacyItemMonitor2).stopListening()
         verify(callback).onPrivacyItemsChanged(emptyList())
     }
 
     @Test
+    fun testPrivacyItemsAggregated() {
+        val item1 = PrivacyItem(PrivacyType.TYPE_CAMERA,
+                PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)
+        val item2 = PrivacyItem(PrivacyType.TYPE_MICROPHONE,
+                PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 1)
+        doReturn(listOf(item1))
+                .`when`(privacyItemMonitor).getActivePrivacyItems()
+        doReturn(listOf(item2))
+                .`when`(privacyItemMonitor2).getActivePrivacyItems()
+
+        privacyItemController.addCallback(callback)
+        executor.runAllReady()
+        verify(callback).onPrivacyItemsChanged(capture(argCaptor))
+        assertEquals(2, argCaptor.value.size)
+        assertTrue(argCaptor.value.contains(item1))
+        assertTrue(argCaptor.value.contains(item2))
+    }
+
+    @Test
     fun testDistinctItems() {
-        doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
-                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0)))
-                .`when`(appOpsController).getActiveAppOps(anyBoolean())
+        doReturn(listOf(
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0),
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)))
+                .`when`(privacyItemMonitor).getActivePrivacyItems()
+        doReturn(listOf(
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)))
+                .`when`(privacyItemMonitor2).getActivePrivacyItems()
 
         privacyItemController.addCallback(callback)
         executor.runAllReady()
@@ -166,9 +168,12 @@
 
     @Test
     fun testSimilarItemsDifferentTimeStamp() {
-        doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
-                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 1)))
-                .`when`(appOpsController).getActiveAppOps(anyBoolean())
+        doReturn(listOf(
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0),
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 1)))
+                .`when`(privacyItemMonitor).getActivePrivacyItems()
 
         privacyItemController.addCallback(callback)
         executor.runAllReady()
@@ -177,29 +182,6 @@
     }
 
     @Test
-    fun testRegisterCallback() {
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-        verify(userTracker, atLeastOnce()).addCallback(
-                eq(privacyItemController.userTrackerCallback), any())
-        verify(userTracker, never()).removeCallback(eq(privacyItemController.userTrackerCallback))
-    }
-
-    @Test
-    fun testCallback_userChanged() {
-        privacyItemController.userTrackerCallback.onUserChanged(0, mContext)
-        executor.runAllReady()
-        verify(userTracker).userProfiles
-    }
-
-    @Test
-    fun testReceiver_profilesChanged() {
-        privacyItemController.userTrackerCallback.onProfilesChanged(emptyList())
-        executor.runAllReady()
-        verify(userTracker).userProfiles
-    }
-
-    @Test
     fun testAddMultipleCallbacks() {
         val otherCallback = mock(PrivacyItemController.Callback::class.java)
         privacyItemController.addCallback(callback)
@@ -215,7 +197,7 @@
 
     @Test
     fun testMultipleCallbacksAreUpdated() {
-        doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
+        doReturn(emptyList<PrivacyItem>()).`when`(privacyItemMonitor).getActivePrivacyItems()
 
         val otherCallback = mock(PrivacyItemController.Callback::class.java)
         privacyItemController.addCallback(callback)
@@ -224,8 +206,8 @@
         reset(callback)
         reset(otherCallback)
 
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
+        verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
+        argCaptorCallback.value.onPrivacyItemsChanged()
         executor.runAllReady()
         verify(callback).onPrivacyItemsChanged(anyList())
         verify(otherCallback).onPrivacyItemsChanged(anyList())
@@ -233,7 +215,7 @@
 
     @Test
     fun testRemoveCallback() {
-        doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
+        doReturn(emptyList<PrivacyItem>()).`when`(privacyItemMonitor).getActivePrivacyItems()
         val otherCallback = mock(PrivacyItemController.Callback::class.java)
         privacyItemController.addCallback(callback)
         privacyItemController.addCallback(otherCallback)
@@ -242,32 +224,18 @@
         reset(callback)
         reset(otherCallback)
 
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
+        verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
         privacyItemController.removeCallback(callback)
-        argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
+        argCaptorCallback.value.onPrivacyItemsChanged()
         executor.runAllReady()
         verify(callback, never()).onPrivacyItemsChanged(anyList())
         verify(otherCallback).onPrivacyItemsChanged(anyList())
     }
 
     @Test
-    fun testListShouldNotHaveNull() {
-        doReturn(listOf(AppOpItem(AppOpsManager.OP_ACTIVATE_VPN, TEST_UID, "", 0),
-                        AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, "", 0)))
-                .`when`(appOpsController).getActiveAppOps(anyBoolean())
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-        executor.runAllReady()
-
-        verify(callback).onPrivacyItemsChanged(capture(argCaptor))
-        assertEquals(1, argCaptor.value.size)
-        assertThat(argCaptor.value, not(hasItem(nullValue())))
-    }
-
-    @Test
     fun testListShouldBeCopy() {
         val list = listOf(PrivacyItem(PrivacyType.TYPE_CAMERA,
-                PrivacyApplication("", TEST_UID), 0))
+                PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0))
         privacyItemController.privacyList = list
         val privacyList = privacyItemController.privacyList
         assertEquals(list, privacyList)
@@ -275,174 +243,35 @@
     }
 
     @Test
-    fun testNotListeningWhenIndicatorsDisabled() {
-        changeLocation(false)
-        changeMicCamera(false)
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-        verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS),
-                any())
-    }
-
-    @Test
-    fun testNotSendingLocationWhenOnlyMicCamera() {
-        changeLocation(false)
-        changeMicCamera(true)
-        executor.runAllReady()
-
-        doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
-                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, "", 0)))
-                .`when`(appOpsController).getActiveAppOps(anyBoolean())
-
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-
-        verify(callback).onPrivacyItemsChanged(capture(argCaptor))
-
-        assertEquals(1, argCaptor.value.size)
-        assertEquals(PrivacyType.TYPE_CAMERA, argCaptor.value[0].privacyType)
-    }
-
-    @Test
-    fun testNotUpdated_LocationChangeWhenOnlyMicCamera() {
-        doReturn(listOf(AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, "", 0)))
-                .`when`(appOpsController).getActiveAppOps(anyBoolean())
-
-        privacyItemController.addCallback(callback)
-        changeLocation(false)
-        changeMicCamera(true)
-        executor.runAllReady()
-        reset(callback) // Clean callback
-
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(
-                AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true)
-
-        verify(callback, never()).onPrivacyItemsChanged(any())
-    }
-
-    @Test
-    fun testLogActiveChanged() {
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(
-                AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true)
-
-        verify(logger).logUpdatedItemFromAppOps(
-                AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true)
-    }
-
-    @Test
     fun testLogListUpdated() {
-        doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0))
-        ).`when`(appOpsController).getActiveAppOps(anyBoolean())
-
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(
-                AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true)
-        executor.runAllReady()
-
-        val expected = PrivacyItem(
+        val privacyItem = PrivacyItem(
                 PrivacyType.TYPE_LOCATION,
                 PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID),
                 0
         )
 
+        doReturn(listOf(privacyItem)).`when`(privacyItemMonitor).getActivePrivacyItems()
+
+        privacyItemController.addCallback(callback)
+        executor.runAllReady()
+
+        verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
+        argCaptorCallback.value.onPrivacyItemsChanged()
+        executor.runAllReady()
+
         val captor = argumentCaptor<List<PrivacyItem>>()
         verify(logger, atLeastOnce()).logRetrievedPrivacyItemsList(capture(captor))
         // Let's look at the last log
         val values = captor.allValues
-        assertTrue(values[values.size - 1].contains(expected))
-    }
-
-    @Test
-    fun testListRequestedShowPaused() {
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-        verify(appOpsController).getActiveAppOps(true)
-    }
-
-    @Test
-    fun testListFilterCurrentUser() {
-        val otherUser = CURRENT_USER_ID + 1
-        val otherUserUid = otherUser * UserHandle.PER_USER_RANGE
-        `when`(userTracker.userProfiles).thenReturn(listOf(UserInfo(otherUser, "", 0)))
-
-        doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0),
-                AppOpItem(AppOpsManager.OP_CAMERA, otherUserUid, TEST_PACKAGE_NAME, 0))
-        ).`when`(appOpsController).getActiveAppOps(anyBoolean())
-
-        privacyItemController.userTrackerCallback.onUserChanged(otherUser, mContext)
-        executor.runAllReady()
-
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-
-        verify(callback).onPrivacyItemsChanged(capture(argCaptor))
-
-        assertEquals(1, argCaptor.value.size)
-        assertEquals(PrivacyType.TYPE_CAMERA, argCaptor.value[0].privacyType)
-        assertEquals(otherUserUid, argCaptor.value[0].application.uid)
-    }
-
-    @Test
-    fun testAlwaysGetPhoneCameraOps() {
-        val otherUser = CURRENT_USER_ID + 1
-        `when`(userTracker.userProfiles).thenReturn(listOf(UserInfo(otherUser, "", 0)))
-
-        doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0),
-                AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0),
-                AppOpItem(AppOpsManager.OP_PHONE_CALL_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0))
-        ).`when`(appOpsController).getActiveAppOps(anyBoolean())
-
-        privacyItemController.userTrackerCallback.onUserChanged(otherUser, mContext)
-        executor.runAllReady()
-
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-
-        verify(callback).onPrivacyItemsChanged(capture(argCaptor))
-
-        assertEquals(1, argCaptor.value.size)
-        assertEquals(PrivacyType.TYPE_CAMERA, argCaptor.value[0].privacyType)
-    }
-
-    @Test
-    fun testAlwaysGetPhoneMicOps() {
-        val otherUser = CURRENT_USER_ID + 1
-        `when`(userTracker.userProfiles).thenReturn(listOf(UserInfo(otherUser, "", 0)))
-
-        doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0),
-                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0),
-                AppOpItem(AppOpsManager.OP_PHONE_CALL_MICROPHONE, TEST_UID, TEST_PACKAGE_NAME, 0))
-        ).`when`(appOpsController).getActiveAppOps(anyBoolean())
-
-        privacyItemController.userTrackerCallback.onUserChanged(otherUser, mContext)
-        executor.runAllReady()
-
-        privacyItemController.addCallback(callback)
-        executor.runAllReady()
-
-        verify(callback).onPrivacyItemsChanged(capture(argCaptor))
-
-        assertEquals(1, argCaptor.value.size)
-        assertEquals(PrivacyType.TYPE_MICROPHONE, argCaptor.value[0].privacyType)
+        assertTrue(values[values.size - 1].contains(privacyItem))
     }
 
     @Test
     fun testPassageOfTimeDoesNotRemoveIndicators() {
         doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
-        )).`when`(appOpsController).getActiveAppOps(anyBoolean())
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)
+        )).`when`(privacyItemMonitor).getActivePrivacyItems()
 
         privacyItemController.addCallback(callback)
 
@@ -457,18 +286,18 @@
     fun testNotHeldAfterTimeIsOff() {
         // Start with some element at time 0
         doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
-        )).`when`(appOpsController).getActiveAppOps(anyBoolean())
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)
+        )).`when`(privacyItemMonitor).getActivePrivacyItems()
         privacyItemController.addCallback(callback)
         executor.runAllReady()
 
         // Then remove it at time HOLD + 1
-        doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
+        doReturn(emptyList<PrivacyItem>()).`when`(privacyItemMonitor).getActivePrivacyItems()
         fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS + 1)
 
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(
-                AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false)
+        verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
+        argCaptorCallback.value.onPrivacyItemsChanged()
         executor.runAllReady()
 
         // See it's not there
@@ -478,20 +307,21 @@
 
     @Test
     fun testElementNotRemovedBeforeHoldTime() {
-        // Start with some element at time 0
+        // Start with some element at current time
         doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
-        )).`when`(appOpsController).getActiveAppOps(anyBoolean())
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID),
+                        fakeClock.elapsedRealtime())
+        )).`when`(privacyItemMonitor).getActivePrivacyItems()
         privacyItemController.addCallback(callback)
         executor.runAllReady()
 
         // Then remove it at time HOLD - 1
-        doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
+        doReturn(emptyList<PrivacyItem>()).`when`(privacyItemMonitor).getActivePrivacyItems()
         fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS - 1)
 
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(
-                AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false)
+        verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
+        argCaptorCallback.value.onPrivacyItemsChanged()
         executor.runAllReady()
 
         // See it's still there
@@ -503,18 +333,18 @@
     fun testElementAutoRemovedAfterHoldTime() {
         // Start with some element at time 0
         doReturn(listOf(
-                AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
-        )).`when`(appOpsController).getActiveAppOps(anyBoolean())
+                PrivacyItem(PrivacyType.TYPE_CAMERA,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)
+        )).`when`(privacyItemMonitor).getActivePrivacyItems()
         privacyItemController.addCallback(callback)
         executor.runAllReady()
 
         // Then remove it at time HOLD - 1
-        doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
+        doReturn(emptyList<PrivacyItem>()).`when`(privacyItemMonitor).getActivePrivacyItems()
         fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS - 1)
 
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(
-                AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false)
+        verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
+        argCaptorCallback.value.onPrivacyItemsChanged()
         executor.runAllReady()
 
         fakeClock.advanceTime(2L)
@@ -526,38 +356,65 @@
     }
 
     @Test
+    fun testFlagsAll_listeningToAll() {
+        verify(privacyConfig).addCallback(capture(argCaptorConfigCallback))
+        privacyItemController.addCallback(callback)
+        `when`(privacyConfig.micCameraAvailable).thenReturn(true)
+        `when`(privacyConfig.locationAvailable).thenReturn(true)
+        `when`(privacyConfig.mediaProjectionAvailable).thenReturn(true)
+        argCaptorConfigCallback.value.onFlagMicCameraChanged(true)
+        argCaptorConfigCallback.value.onFlagLocationChanged(true)
+        argCaptorConfigCallback.value.onFlagMediaProjectionChanged(true)
+        executor.runAllReady()
+
+        assertTrue(privacyItemController.allIndicatorsAvailable)
+    }
+
+    @Test
+    fun testFlags_onFlagMicCameraChanged() {
+        verify(privacyConfig).addCallback(capture(argCaptorConfigCallback))
+        privacyItemController.addCallback(callback)
+        `when`(privacyConfig.micCameraAvailable).thenReturn(true)
+        argCaptorConfigCallback.value.onFlagMicCameraChanged(true)
+        executor.runAllReady()
+
+        assertTrue(privacyItemController.micCameraAvailable)
+        verify(callback).onFlagMicCameraChanged(true)
+    }
+
+    @Test
+    fun testFlags_onFlagLocationChanged() {
+        verify(privacyConfig).addCallback(capture(argCaptorConfigCallback))
+        privacyItemController.addCallback(callback)
+        `when`(privacyConfig.locationAvailable).thenReturn(true)
+        argCaptorConfigCallback.value.onFlagLocationChanged(true)
+        executor.runAllReady()
+
+        assertTrue(privacyItemController.locationAvailable)
+        verify(callback).onFlagLocationChanged(true)
+    }
+
+    @Test
+    fun testFlags_onFlagMediaProjectionChanged() {
+        verify(privacyConfig).addCallback(capture(argCaptorConfigCallback))
+        privacyItemController.addCallback(callback)
+        `when`(privacyConfig.mediaProjectionAvailable).thenReturn(true)
+        argCaptorConfigCallback.value.onFlagMediaProjectionChanged(true)
+        executor.runAllReady()
+
+        verify(callback).onFlagMediaProjectionChanged(true)
+    }
+
+    @Test
     fun testPausedElementsAreRemoved() {
-        val item = AppOpItem(
-                AppOpsManager.OP_RECORD_AUDIO,
-                TEST_UID,
-                TEST_PACKAGE_NAME,
-                elapsedTime
-        )
-        `when`(appOpsController.getActiveAppOps(anyBoolean())).thenReturn(listOf(item))
+        doReturn(listOf(
+                PrivacyItem(PrivacyType.TYPE_MICROPHONE,
+                        PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0, true)))
+                .`when`(privacyItemMonitor).getActivePrivacyItems()
+
         privacyItemController.addCallback(callback)
         executor.runAllReady()
 
-        item.isDisabled = true
-        fakeClock.advanceTime(1)
-        verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
-        argCaptorCallback.value.onActiveStateChanged(
-                AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false)
-
-        executor.runAllReady()
-
-        verify(callback).onPrivacyItemsChanged(emptyList())
         assertTrue(privacyItemController.privacyList.isEmpty())
     }
-
-    private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)
-    private fun changeLocation(value: Boolean?) = changeProperty(LOCATION_INDICATOR, value)
-
-    private fun changeProperty(name: String, value: Boolean?) {
-        deviceConfigProxy.setProperty(
-                DeviceConfig.NAMESPACE_PRIVACY,
-                name,
-                value?.toString(),
-                false
-        )
-    }
 }
\ No newline at end of file