diff options
18 files changed, 1355 insertions, 522 deletions
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index d72073d1ed74..777104d89c0b 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 final class SystemUiDeviceConfigFlags { 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 000000000000..ac563dee2af5 --- /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 c1e485be0af2..1dd41a35374f 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 5b6ddd8471da..aeda20f16515 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.hardware.fingerprint.FingerprintManager; 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 @@ public class FrameworkServicesModule { } @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 535eff801878..366ef2651a92 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.lowlightclock.LowLightClockController; 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 @@ import dagger.Provides; 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 000000000000..de34cd6b23ca --- /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 000000000000..9b5a675371cc --- /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 000000000000..d652889f0082 --- /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 d4e164208167..03145a714289 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 @@ class PrivacyDialog( 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 76199bfab0e0..8b41000d2543 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 @@ enum class PrivacyType( 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 cd6eb99e259e..a676150f44a2 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 @@ import javax.inject.Inject @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 @@ class PrivacyItemController @Inject constructor( @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 @@ class PrivacyItemController @Inject constructor( @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 @@ class PrivacyItemController @Inject constructor( uiExecutor.execute(notifyChanges) } - var micCameraAvailable = isMicCameraEnabled() - private set - var locationAvailable = isLocationEnabled() - - var allIndicatorsAvailable = micCameraAvailable && locationAvailable - - 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() - } + private val optionsCallback = object : PrivacyConfig.Callback { + override fun onFlagLocationChanged(flag: Boolean) { + callbacks.forEach { it.get()?.onFlagLocationChanged(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) - } + override fun onFlagMicCameraChanged(flag: Boolean) { + callbacks.forEach { it.get()?.onFlagMicCameraChanged(flag) } } - } - @VisibleForTesting - internal var userTrackerCallback = object : UserTracker.Callback { - override fun onUserChanged(newUser: Int, userContext: Context) { - update(true) + override fun onFlagMediaProjectionChanged(flag: Boolean) { + callbacks.forEach { it.get()?.onFlagMediaProjectionChanged(flag) } } + } - 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 @@ class PrivacyItemController @Inject constructor( * 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 @@ class PrivacyItemController @Inject constructor( 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 @@ class PrivacyItemController @Inject constructor( } } - 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 @@ class PrivacyItemController @Inject constructor( } 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 000000000000..5bae31e1886e --- /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 000000000000..732a3103d4aa --- /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 1a268b5f94a9..1ea93474f954 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 @@ class PrivacyLogger @Inject constructor( }) } + 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 4685c148e7e5..9a19d8d11190 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.qs.QSFactory; 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 javax.inject.Named; 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 @@ public abstract class TvSystemUIModule { 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 000000000000..db96d5507ba1 --- /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/PrivacyItemControllerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt index 2a8611f60a9f..272f149f379b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt @@ -21,10 +21,7 @@ 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 @@ -34,9 +31,7 @@ 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 @@ -44,40 +39,28 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest -class PrivacyItemControllerFlagsTest : SysuiTestCase() { +class PrivacyConfigFlagsTest : 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 + private const val MEDIA_PROJECTION = + SystemUiDeviceConfigFlags.PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED } + private lateinit var privacyConfig: PrivacyConfig + @Mock - private lateinit var appOpsController: AppOpsController - @Mock - private lateinit var callback: PrivacyItemController.Callback + private lateinit var callback: PrivacyConfig.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, + fun createPrivacyConfig(): PrivacyConfig { + return PrivacyConfig( executor, deviceConfigProxy, - userTracker, - logger, - FakeSystemClock(), dumpManager) } @@ -87,15 +70,15 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() { executor = FakeExecutor(FakeSystemClock()) deviceConfigProxy = DeviceConfigProxyFake() - privacyItemController = createPrivacyItemController() - privacyItemController.addCallback(callback) + privacyConfig = createPrivacyConfig() + privacyConfig.addCallback(callback) executor.runAllReady() } @Test fun testMicCameraListeningByDefault() { - assertTrue(privacyItemController.micCameraAvailable) + assertTrue(privacyConfig.micCameraAvailable) } @Test @@ -105,82 +88,55 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() { 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) + assertFalse(privacyConfig.micCameraAvailable) } @Test - fun testAll_listeningToAll() { - changeAll(true) + fun testMediaProjectionChanged() { + changeMediaProjection(false) // default is true executor.runAllReady() - verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any()) - } - - @Test - fun testMicCamera_listening() { - changeMicCamera(true) - executor.runAllReady() + verify(callback).onFlagMediaProjectionChanged(false) - verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any()) + assertFalse(privacyConfig.mediaProjectionAvailable) } @Test - fun testLocation_listening() { + fun testLocationChanged() { changeLocation(true) executor.runAllReady() - verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any()) + verify(callback).onFlagLocationChanged(true) + assertTrue(privacyConfig.locationAvailable) } @Test - fun testAllFalse_notListening() { - changeAll(true) - executor.runAllReady() - changeAll(false) + fun testMicCamAndLocationChanged() { + changeLocation(true) changeMicCamera(false) executor.runAllReady() - verify(appOpsController).removeCallback(any(), any()) + verify(callback, atLeastOnce()).onFlagLocationChanged(true) + verify(callback, atLeastOnce()).onFlagMicCameraChanged(false) + + assertTrue(privacyConfig.locationAvailable) + assertFalse(privacyConfig.micCameraAvailable) } @Test - fun testMicDeleted_stillListening() { + fun testMicDeleted_stillAvailable() { changeMicCamera(true) executor.runAllReady() changeMicCamera(null) executor.runAllReady() - verify(appOpsController, never()).removeCallback(any(), any()) + 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 changeAll(value: Boolean?) { - changeMicCamera(value) - changeLocation(value) - } + private fun changeMediaProjection(value: Boolean?) = changeProperty(MEDIA_PROJECTION, value) private fun changeProperty(name: String, value: Boolean?) { deviceConfigProxy.setProperty( 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 e4d7b1b7d451..d56363231827 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 @@ class PrivacyItemControllerTest : SysuiTestCase() { 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 @@ class PrivacyItemControllerTest : SysuiTestCase() { @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,83 +104,81 @@ class PrivacyItemControllerTest : SysuiTestCase() { 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 testDistinctItems() { - doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0), - AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0))) - .`when`(appOpsController).getActiveAppOps(anyBoolean()) + 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(1, argCaptor.value.size) + assertEquals(2, argCaptor.value.size) + assertTrue(argCaptor.value.contains(item1)) + assertTrue(argCaptor.value.contains(item2)) } @Test - fun testSimilarItemsDifferentTimeStamp() { - doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0), - AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 1))) - .`when`(appOpsController).getActiveAppOps(anyBoolean()) + fun testDistinctItems() { + 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() verify(callback).onPrivacyItemsChanged(capture(argCaptor)) - assertEquals(2, argCaptor.value.size) - } - - @Test - fun testRegisterCallback() { - privacyItemController.addCallback(callback) - executor.runAllReady() - verify(userTracker, atLeastOnce()).addCallback( - eq(privacyItemController.userTrackerCallback), any()) - verify(userTracker, never()).removeCallback(eq(privacyItemController.userTrackerCallback)) + assertEquals(1, argCaptor.value.size) } @Test - fun testCallback_userChanged() { - privacyItemController.userTrackerCallback.onUserChanged(0, mContext) - executor.runAllReady() - verify(userTracker).userProfiles - } + fun testSimilarItemsDifferentTimeStamp() { + 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() - @Test - fun testReceiver_profilesChanged() { - privacyItemController.userTrackerCallback.onProfilesChanged(emptyList()) + privacyItemController.addCallback(callback) executor.runAllReady() - verify(userTracker).userProfiles + verify(callback).onPrivacyItemsChanged(capture(argCaptor)) + assertEquals(2, argCaptor.value.size) } @Test @@ -215,7 +197,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { @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 @@ class PrivacyItemControllerTest : SysuiTestCase() { 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 @@ class PrivacyItemControllerTest : SysuiTestCase() { @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 @@ class PrivacyItemControllerTest : SysuiTestCase() { 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 @@ class PrivacyItemControllerTest : SysuiTestCase() { } @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 ) - 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() + doReturn(listOf(privacyItem)).`when`(privacyItemMonitor).getActivePrivacyItems() 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) + verify(privacyItemMonitor).startListening(capture(argCaptorCallback)) + argCaptorCallback.value.onPrivacyItemsChanged() 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) + 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(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 @@ class PrivacyItemControllerTest : SysuiTestCase() { 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 @@ class PrivacyItemControllerTest : SysuiTestCase() { @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 @@ class PrivacyItemControllerTest : SysuiTestCase() { 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 @@ class PrivacyItemControllerTest : SysuiTestCase() { } @Test - fun testPausedElementsAreRemoved() { - val item = AppOpItem( - AppOpsManager.OP_RECORD_AUDIO, - TEST_UID, - TEST_PACKAGE_NAME, - elapsedTime - ) - `when`(appOpsController.getActiveAppOps(anyBoolean())).thenReturn(listOf(item)) + 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() - item.isDisabled = true - fakeClock.advanceTime(1) - verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) - argCaptorCallback.value.onActiveStateChanged( - AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) + 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() - verify(callback).onPrivacyItemsChanged(emptyList()) - assertTrue(privacyItemController.privacyList.isEmpty()) + 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) } - private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value) - private fun changeLocation(value: Boolean?) = changeProperty(LOCATION_INDICATOR, value) + @Test + fun testFlags_onFlagMediaProjectionChanged() { + verify(privacyConfig).addCallback(capture(argCaptorConfigCallback)) + privacyItemController.addCallback(callback) + `when`(privacyConfig.mediaProjectionAvailable).thenReturn(true) + argCaptorConfigCallback.value.onFlagMediaProjectionChanged(true) + executor.runAllReady() - private fun changeProperty(name: String, value: Boolean?) { - deviceConfigProxy.setProperty( - DeviceConfig.NAMESPACE_PRIVACY, - name, - value?.toString(), - false - ) + verify(callback).onFlagMediaProjectionChanged(true) + } + + @Test + fun testPausedElementsAreRemoved() { + doReturn(listOf( + PrivacyItem(PrivacyType.TYPE_MICROPHONE, + PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0, true))) + .`when`(privacyItemMonitor).getActivePrivacyItems() + + privacyItemController.addCallback(callback) + executor.runAllReady() + + assertTrue(privacyItemController.privacyList.isEmpty()) } }
\ No newline at end of file |