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

* changes:
  Add MediaProjection privacy item type
  Extract privacy item monitoring interface, add AppOpsPrivacyItemMonitor
This commit is contained in:
Robert Horvath
2022-05-30 17:43:32 +00:00
committed by Android (Google) Code Review
18 changed files with 1364 additions and 531 deletions

View File

@@ -157,6 +157,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.
*/

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
@@ -320,6 +321,11 @@ public class FrameworkServicesModule {
return LayoutInflater.from(context);
}
@Provides
static MediaProjectionManager provideMediaProjectionManager(Context context) {
return context.getSystemService(MediaProjectionManager.class);
}
@Provides
static MediaRouter2Manager provideMediaRouter2Manager(Context context) {
return MediaRouter2Manager.getInstance(context);

View File

@@ -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,

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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) {}
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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()
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 @@ 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(

View File

@@ -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()
}
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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)
}
}

View File

@@ -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,7 +88,17 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() {
verify(callback).onFlagMicCameraChanged(false)
assertFalse(privacyItemController.micCameraAvailable)
assertFalse(privacyConfig.micCameraAvailable)
}
@Test
fun testMediaProjectionChanged() {
changeMediaProjection(false) // default is true
executor.runAllReady()
verify(callback).onFlagMediaProjectionChanged(false)
assertFalse(privacyConfig.mediaProjectionAvailable)
}
@Test
@@ -114,73 +107,36 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() {
executor.runAllReady()
verify(callback).onFlagLocationChanged(true)
assertTrue(privacyItemController.locationAvailable)
assertTrue(privacyConfig.locationAvailable)
}
@Test
fun testBothChanged() {
changeAll(true)
fun testMicCamAndLocationChanged() {
changeLocation(true)
changeMicCamera(false)
executor.runAllReady()
verify(callback, atLeastOnce()).onFlagLocationChanged(true)
verify(callback, atLeastOnce()).onFlagMicCameraChanged(false)
assertTrue(privacyItemController.locationAvailable)
assertFalse(privacyItemController.micCameraAvailable)
assertTrue(privacyConfig.locationAvailable)
assertFalse(privacyConfig.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() {
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(

View File

@@ -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,43 +104,61 @@ 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 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 @@ class PrivacyItemControllerTest : SysuiTestCase() {
@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()
@@ -176,29 +181,6 @@ class PrivacyItemControllerTest : SysuiTestCase() {
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))
}
@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)
@@ -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,207 +224,54 @@ 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)
assertTrue(list !== privacyList)
}
@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 @@ 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)
@@ -525,39 +355,66 @@ class PrivacyItemControllerTest : SysuiTestCase() {
assertTrue(privacyItemController.privacyList.isEmpty())
}
@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
)
}
}