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:
committed by
Android (Google) Code Review
commit
d59d17d74a
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user