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