Extract privacy item monitoring interface, add AppOpsPrivacyItemMonitor am: 3a89e62694
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18375273 Change-Id: I3b6d3a611a3688bd2a304e397afb10f6a9ea3399 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -46,6 +46,7 @@ import com.android.systemui.lowlightclock.LowLightClockController;
|
|||||||
import com.android.systemui.model.SysUiState;
|
import com.android.systemui.model.SysUiState;
|
||||||
import com.android.systemui.navigationbar.NavigationBarComponent;
|
import com.android.systemui.navigationbar.NavigationBarComponent;
|
||||||
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
|
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
|
||||||
|
import com.android.systemui.privacy.PrivacyModule;
|
||||||
import com.android.systemui.recents.Recents;
|
import com.android.systemui.recents.Recents;
|
||||||
import com.android.systemui.screenshot.dagger.ScreenshotModule;
|
import com.android.systemui.screenshot.dagger.ScreenshotModule;
|
||||||
import com.android.systemui.settings.dagger.SettingsModule;
|
import com.android.systemui.settings.dagger.SettingsModule;
|
||||||
@@ -122,6 +123,7 @@ import dagger.Provides;
|
|||||||
LogModule.class,
|
LogModule.class,
|
||||||
PeopleHubModule.class,
|
PeopleHubModule.class,
|
||||||
PluginModule.class,
|
PluginModule.class,
|
||||||
|
PrivacyModule.class,
|
||||||
QsFrameTranslateModule.class,
|
QsFrameTranslateModule.class,
|
||||||
ScreenshotModule.class,
|
ScreenshotModule.class,
|
||||||
SensorModule.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,135 @@
|
|||||||
|
/*
|
||||||
|
* 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 DEFAULT_MIC_CAMERA = true
|
||||||
|
private const val DEFAULT_LOCATION = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callbacks = mutableListOf<WeakReference<Callback>>()
|
||||||
|
|
||||||
|
var micCameraAvailable = isMicCameraEnabled()
|
||||||
|
private set
|
||||||
|
var locationAvailable = isLocationEnabled()
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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("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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,27 +16,18 @@
|
|||||||
|
|
||||||
package com.android.systemui.privacy
|
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.annotations.VisibleForTesting
|
||||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
|
|
||||||
import com.android.systemui.Dumpable
|
import com.android.systemui.Dumpable
|
||||||
import com.android.systemui.appops.AppOpItem
|
|
||||||
import com.android.systemui.appops.AppOpsController
|
import com.android.systemui.appops.AppOpsController
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
import com.android.systemui.dagger.qualifiers.Background
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
import com.android.systemui.dagger.qualifiers.Main
|
import com.android.systemui.dagger.qualifiers.Main
|
||||||
import com.android.systemui.dump.DumpManager
|
import com.android.systemui.dump.DumpManager
|
||||||
import com.android.systemui.privacy.logging.PrivacyLogger
|
import com.android.systemui.privacy.logging.PrivacyLogger
|
||||||
import com.android.systemui.settings.UserTracker
|
import com.android.systemui.util.asIndenting
|
||||||
import com.android.systemui.util.DeviceConfigProxy
|
|
||||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||||
import com.android.systemui.util.time.SystemClock
|
import com.android.systemui.util.time.SystemClock
|
||||||
|
import com.android.systemui.util.withIncreasedIndent
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
@@ -44,11 +35,10 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
class PrivacyItemController @Inject constructor(
|
class PrivacyItemController @Inject constructor(
|
||||||
private val appOpsController: AppOpsController,
|
|
||||||
@Main uiExecutor: DelayableExecutor,
|
@Main uiExecutor: DelayableExecutor,
|
||||||
@Background private val bgExecutor: DelayableExecutor,
|
@Background private val bgExecutor: DelayableExecutor,
|
||||||
private val deviceConfigProxy: DeviceConfigProxy,
|
private val privacyConfig: PrivacyConfig,
|
||||||
private val userTracker: UserTracker,
|
private val privacyItemMonitors: Set<@JvmSuppressWildcards PrivacyItemMonitor>,
|
||||||
private val logger: PrivacyLogger,
|
private val logger: PrivacyLogger,
|
||||||
private val systemClock: SystemClock,
|
private val systemClock: SystemClock,
|
||||||
dumpManager: DumpManager
|
dumpManager: DumpManager
|
||||||
@@ -56,24 +46,7 @@ class PrivacyItemController @Inject constructor(
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal companion object {
|
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"
|
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
|
@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 get() = field.toList() // Returns a shallow copy of the list
|
||||||
@Synchronized set
|
@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 var listening = false
|
||||||
private val callbacks = mutableListOf<WeakReference<Callback>>()
|
private val callbacks = mutableListOf<WeakReference<Callback>>()
|
||||||
private val internalUiExecutor = MyExecutor(uiExecutor)
|
private val internalUiExecutor = MyExecutor(uiExecutor)
|
||||||
|
|
||||||
private var holdingRunnableCanceler: Runnable? = null
|
private var holdingRunnableCanceler: Runnable? = null
|
||||||
|
|
||||||
|
val micCameraAvailable
|
||||||
|
get() = privacyConfig.micCameraAvailable
|
||||||
|
val locationAvailable
|
||||||
|
get() = privacyConfig.locationAvailable
|
||||||
|
val allIndicatorsAvailable
|
||||||
|
get() = micCameraAvailable && locationAvailable
|
||||||
|
|
||||||
private val notifyChanges = Runnable {
|
private val notifyChanges = Runnable {
|
||||||
val list = privacyList
|
val list = privacyList
|
||||||
callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) }
|
callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) }
|
||||||
@@ -109,90 +77,29 @@ class PrivacyItemController @Inject constructor(
|
|||||||
uiExecutor.execute(notifyChanges)
|
uiExecutor.execute(notifyChanges)
|
||||||
}
|
}
|
||||||
|
|
||||||
var micCameraAvailable = isMicCameraEnabled()
|
private val optionsCallback = object : PrivacyConfig.Callback {
|
||||||
private set
|
override fun onFlagLocationChanged(flag: Boolean) {
|
||||||
var locationAvailable = isLocationEnabled()
|
callbacks.forEach { it.get()?.onFlagLocationChanged(flag) }
|
||||||
|
|
||||||
var allIndicatorsAvailable = micCameraAvailable && locationAvailable
|
|
||||||
|
|
||||||
private val devicePropertiesChangedListener =
|
|
||||||
object : DeviceConfig.OnPropertiesChangedListener {
|
|
||||||
override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
|
|
||||||
if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) &&
|
|
||||||
(properties.keyset.contains(MIC_CAMERA) ||
|
|
||||||
properties.keyset.contains(LOCATION))) {
|
|
||||||
|
|
||||||
// Running on the ui executor so can iterate on callbacks
|
|
||||||
if (properties.keyset.contains(MIC_CAMERA)) {
|
|
||||||
micCameraAvailable = properties.getBoolean(MIC_CAMERA, DEFAULT_MIC_CAMERA)
|
|
||||||
allIndicatorsAvailable = micCameraAvailable && locationAvailable
|
|
||||||
callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (properties.keyset.contains(LOCATION)) {
|
override fun onFlagMicCameraChanged(flag: Boolean) {
|
||||||
locationAvailable = properties.getBoolean(LOCATION, DEFAULT_LOCATION)
|
callbacks.forEach { it.get()?.onFlagMicCameraChanged(flag) }
|
||||||
allIndicatorsAvailable = micCameraAvailable && locationAvailable
|
|
||||||
callbacks.forEach { it.get()?.onFlagLocationChanged(locationAvailable) }
|
|
||||||
}
|
|
||||||
internalUiExecutor.updateListeningState()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cb = object : AppOpsController.Callback {
|
private val privacyItemMonitorCallback = object : PrivacyItemMonitor.Callback {
|
||||||
override fun onActiveStateChanged(
|
override fun onPrivacyItemsChanged() {
|
||||||
code: Int,
|
update()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
deviceConfigProxy.addOnPropertiesChangedListener(
|
|
||||||
DeviceConfig.NAMESPACE_PRIVACY,
|
|
||||||
uiExecutor,
|
|
||||||
devicePropertiesChangedListener)
|
|
||||||
dumpManager.registerDumpable(TAG, this)
|
dumpManager.registerDumpable(TAG, this)
|
||||||
|
privacyConfig.addCallback(optionsCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unregisterListener() {
|
private fun update() {
|
||||||
userTracker.removeCallback(userTrackerCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun registerReceiver() {
|
|
||||||
userTracker.addCallback(userTrackerCallback, bgExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun update(updateUsers: Boolean) {
|
|
||||||
bgExecutor.execute {
|
bgExecutor.execute {
|
||||||
if (updateUsers) {
|
|
||||||
currentUserIds = userTracker.userProfiles.map { it.id }
|
|
||||||
logger.logCurrentProfilesChanged(currentUserIds)
|
|
||||||
}
|
|
||||||
updateListAndNotifyChanges.run()
|
updateListAndNotifyChanges.run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,20 +114,17 @@ class PrivacyItemController @Inject constructor(
|
|||||||
* main thread.
|
* main thread.
|
||||||
*/
|
*/
|
||||||
private fun setListeningState() {
|
private fun setListeningState() {
|
||||||
val listen = !callbacks.isEmpty() and
|
val listen = callbacks.isNotEmpty()
|
||||||
(micCameraAvailable || locationAvailable)
|
|
||||||
if (listening == listen) return
|
if (listening == listen) return
|
||||||
listening = listen
|
listening = listen
|
||||||
if (listening) {
|
if (listening) {
|
||||||
appOpsController.addCallback(OPS, cb)
|
privacyItemMonitors.forEach { it.startListening(privacyItemMonitorCallback) }
|
||||||
registerReceiver()
|
update()
|
||||||
update(true)
|
|
||||||
} else {
|
} else {
|
||||||
appOpsController.removeCallback(OPS, cb)
|
privacyItemMonitors.forEach { it.stopListening() }
|
||||||
unregisterListener()
|
|
||||||
// Make sure that we remove all indicators and notify listeners if we are not
|
// Make sure that we remove all indicators and notify listeners if we are not
|
||||||
// listening anymore due to indicators being disabled
|
// listening anymore due to indicators being disabled
|
||||||
update(false)
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,11 +163,7 @@ class PrivacyItemController @Inject constructor(
|
|||||||
privacyList = emptyList()
|
privacyList = emptyList()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val list = appOpsController.getActiveAppOps(true).filter {
|
val list = privacyItemMonitors.flatMap { it.getActivePrivacyItems() }.distinct()
|
||||||
UserHandle.getUserId(it.uid) in currentUserIds ||
|
|
||||||
it.code == AppOpsManager.OP_PHONE_CALL_MICROPHONE ||
|
|
||||||
it.code == AppOpsManager.OP_PHONE_CALL_CAMERA
|
|
||||||
}.mapNotNull { toPrivacyItem(it) }.distinct()
|
|
||||||
privacyList = processNewList(list)
|
privacyList = processNewList(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,35 +209,11 @@ class PrivacyItemController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
|
interface Callback : PrivacyConfig.Callback {
|
||||||
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 {
|
|
||||||
fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>)
|
fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>)
|
||||||
|
|
||||||
@JvmDefault
|
@JvmDefault
|
||||||
fun onFlagAllChanged(flag: Boolean) {}
|
fun onFlagAllChanged(flag: Boolean) {}
|
||||||
|
|
||||||
@JvmDefault
|
|
||||||
fun onFlagMicCameraChanged(flag: Boolean) {}
|
|
||||||
|
|
||||||
@JvmDefault
|
|
||||||
fun onFlagLocationChanged(flag: Boolean) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NotifyChangesToCallback(
|
private class NotifyChangesToCallback(
|
||||||
@@ -350,23 +226,36 @@ class PrivacyItemController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun dump(pw: PrintWriter, args: Array<out String>) {
|
override fun dump(pw: PrintWriter, args: Array<out String>) {
|
||||||
pw.println("PrivacyItemController state:")
|
val ipw = pw.asIndenting()
|
||||||
pw.println(" Listening: $listening")
|
ipw.println("PrivacyItemController state:")
|
||||||
pw.println(" Current user ids: $currentUserIds")
|
ipw.withIncreasedIndent {
|
||||||
pw.println(" Privacy Items:")
|
ipw.println("Listening: $listening")
|
||||||
|
ipw.println("Privacy Items:")
|
||||||
|
ipw.withIncreasedIndent {
|
||||||
privacyList.forEach {
|
privacyList.forEach {
|
||||||
pw.print(" ")
|
ipw.println(it.toString())
|
||||||
pw.println(it.toString())
|
|
||||||
}
|
}
|
||||||
pw.println(" Callbacks:")
|
}
|
||||||
|
|
||||||
|
ipw.println("Callbacks:")
|
||||||
|
ipw.withIncreasedIndent {
|
||||||
callbacks.forEach {
|
callbacks.forEach {
|
||||||
it.get()?.let {
|
it.get()?.let {
|
||||||
pw.print(" ")
|
ipw.println(it.toString())
|
||||||
pw.println(it.toString())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipw.println("PrivacyItemMonitors:")
|
||||||
|
ipw.withIncreasedIndent {
|
||||||
|
privacyItemMonitors.forEach {
|
||||||
|
it.dump(ipw, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipw.flush()
|
||||||
|
}
|
||||||
|
|
||||||
private inner class MyExecutor(
|
private inner class MyExecutor(
|
||||||
private val delegate: DelayableExecutor
|
private val delegate: DelayableExecutor
|
||||||
) : Executor {
|
) : Executor {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 androidx.test.filters.SmallTest
|
||||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
|
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
|
||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.android.systemui.appops.AppOpsController
|
|
||||||
import com.android.systemui.dump.DumpManager
|
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.DeviceConfigProxy
|
||||||
import com.android.systemui.util.DeviceConfigProxyFake
|
import com.android.systemui.util.DeviceConfigProxyFake
|
||||||
import com.android.systemui.util.concurrency.FakeExecutor
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
@@ -34,9 +31,7 @@ import org.junit.Assert.assertTrue
|
|||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.ArgumentCaptor
|
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito
|
|
||||||
import org.mockito.Mockito.atLeastOnce
|
import org.mockito.Mockito.atLeastOnce
|
||||||
import org.mockito.Mockito.never
|
import org.mockito.Mockito.never
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
@@ -44,40 +39,26 @@ import org.mockito.MockitoAnnotations
|
|||||||
|
|
||||||
@RunWith(AndroidTestingRunner::class)
|
@RunWith(AndroidTestingRunner::class)
|
||||||
@SmallTest
|
@SmallTest
|
||||||
class PrivacyItemControllerFlagsTest : SysuiTestCase() {
|
class PrivacyConfigFlagsTest : SysuiTestCase() {
|
||||||
companion object {
|
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 MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
|
||||||
private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
|
private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lateinit var privacyConfig: PrivacyConfig
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var appOpsController: AppOpsController
|
private lateinit var callback: PrivacyConfig.Callback
|
||||||
@Mock
|
|
||||||
private lateinit var callback: PrivacyItemController.Callback
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var dumpManager: DumpManager
|
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 executor: FakeExecutor
|
||||||
private lateinit var deviceConfigProxy: DeviceConfigProxy
|
private lateinit var deviceConfigProxy: DeviceConfigProxy
|
||||||
|
|
||||||
fun createPrivacyItemController(): PrivacyItemController {
|
fun createPrivacyConfig(): PrivacyConfig {
|
||||||
return PrivacyItemController(
|
return PrivacyConfig(
|
||||||
appOpsController,
|
|
||||||
executor,
|
|
||||||
executor,
|
executor,
|
||||||
deviceConfigProxy,
|
deviceConfigProxy,
|
||||||
userTracker,
|
|
||||||
logger,
|
|
||||||
FakeSystemClock(),
|
|
||||||
dumpManager)
|
dumpManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +68,15 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() {
|
|||||||
executor = FakeExecutor(FakeSystemClock())
|
executor = FakeExecutor(FakeSystemClock())
|
||||||
deviceConfigProxy = DeviceConfigProxyFake()
|
deviceConfigProxy = DeviceConfigProxyFake()
|
||||||
|
|
||||||
privacyItemController = createPrivacyItemController()
|
privacyConfig = createPrivacyConfig()
|
||||||
privacyItemController.addCallback(callback)
|
privacyConfig.addCallback(callback)
|
||||||
|
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMicCameraListeningByDefault() {
|
fun testMicCameraListeningByDefault() {
|
||||||
assertTrue(privacyItemController.micCameraAvailable)
|
assertTrue(privacyConfig.micCameraAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -105,7 +86,7 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() {
|
|||||||
|
|
||||||
verify(callback).onFlagMicCameraChanged(false)
|
verify(callback).onFlagMicCameraChanged(false)
|
||||||
|
|
||||||
assertFalse(privacyItemController.micCameraAvailable)
|
assertFalse(privacyConfig.micCameraAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -114,7 +95,7 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() {
|
|||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
|
|
||||||
verify(callback).onFlagLocationChanged(true)
|
verify(callback).onFlagLocationChanged(true)
|
||||||
assertTrue(privacyItemController.locationAvailable)
|
assertTrue(privacyConfig.locationAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -126,53 +107,19 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() {
|
|||||||
verify(callback, atLeastOnce()).onFlagLocationChanged(true)
|
verify(callback, atLeastOnce()).onFlagLocationChanged(true)
|
||||||
verify(callback, atLeastOnce()).onFlagMicCameraChanged(false)
|
verify(callback, atLeastOnce()).onFlagMicCameraChanged(false)
|
||||||
|
|
||||||
assertTrue(privacyItemController.locationAvailable)
|
assertTrue(privacyConfig.locationAvailable)
|
||||||
assertFalse(privacyItemController.micCameraAvailable)
|
assertFalse(privacyConfig.micCameraAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testAll_listeningToAll() {
|
fun testMicDeleted_stillAvailable() {
|
||||||
changeAll(true)
|
|
||||||
executor.runAllReady()
|
|
||||||
|
|
||||||
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMicCamera_listening() {
|
|
||||||
changeMicCamera(true)
|
|
||||||
executor.runAllReady()
|
|
||||||
|
|
||||||
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testLocation_listening() {
|
|
||||||
changeLocation(true)
|
|
||||||
executor.runAllReady()
|
|
||||||
|
|
||||||
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testAllFalse_notListening() {
|
|
||||||
changeAll(true)
|
|
||||||
executor.runAllReady()
|
|
||||||
changeAll(false)
|
|
||||||
changeMicCamera(false)
|
|
||||||
executor.runAllReady()
|
|
||||||
|
|
||||||
verify(appOpsController).removeCallback(any(), any())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testMicDeleted_stillListening() {
|
|
||||||
changeMicCamera(true)
|
changeMicCamera(true)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
changeMicCamera(null)
|
changeMicCamera(null)
|
||||||
executor.runAllReady()
|
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 changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)
|
||||||
@@ -17,36 +17,24 @@
|
|||||||
package com.android.systemui.privacy
|
package com.android.systemui.privacy
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.app.AppOpsManager
|
|
||||||
import android.content.pm.UserInfo
|
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.provider.DeviceConfig
|
|
||||||
import android.testing.AndroidTestingRunner
|
import android.testing.AndroidTestingRunner
|
||||||
import android.testing.TestableLooper.RunWithLooper
|
import android.testing.TestableLooper.RunWithLooper
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
|
|
||||||
import com.android.systemui.SysuiTestCase
|
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.dump.DumpManager
|
||||||
import com.android.systemui.privacy.logging.PrivacyLogger
|
import com.android.systemui.privacy.logging.PrivacyLogger
|
||||||
import com.android.systemui.settings.UserTracker
|
|
||||||
import com.android.systemui.util.DeviceConfigProxy
|
import com.android.systemui.util.DeviceConfigProxy
|
||||||
import com.android.systemui.util.DeviceConfigProxyFake
|
import com.android.systemui.util.DeviceConfigProxyFake
|
||||||
import com.android.systemui.util.concurrency.FakeExecutor
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
import com.android.systemui.util.mockito.argumentCaptor
|
import com.android.systemui.util.mockito.argumentCaptor
|
||||||
import com.android.systemui.util.time.FakeSystemClock
|
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.assertEquals
|
||||||
import org.junit.Assert.assertThat
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.ArgumentCaptor
|
import org.mockito.ArgumentCaptor
|
||||||
import org.mockito.ArgumentMatchers.anyBoolean
|
|
||||||
import org.mockito.ArgumentMatchers.anyList
|
import org.mockito.ArgumentMatchers.anyList
|
||||||
import org.mockito.Captor
|
import org.mockito.Captor
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
@@ -71,20 +59,18 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE
|
val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE
|
||||||
const val TEST_PACKAGE_NAME = "test"
|
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> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
|
||||||
fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
|
||||||
fun <T> any(): T = Mockito.any<T>()
|
fun <T> any(): T = Mockito.any<T>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mock
|
|
||||||
private lateinit var appOpsController: AppOpsController
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var callback: PrivacyItemController.Callback
|
private lateinit var callback: PrivacyItemController.Callback
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var userTracker: UserTracker
|
private lateinit var privacyConfig: PrivacyConfig
|
||||||
|
@Mock
|
||||||
|
private lateinit var privacyItemMonitor: PrivacyItemMonitor
|
||||||
|
@Mock
|
||||||
|
private lateinit var privacyItemMonitor2: PrivacyItemMonitor
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var dumpManager: DumpManager
|
private lateinit var dumpManager: DumpManager
|
||||||
@Mock
|
@Mock
|
||||||
@@ -92,23 +78,21 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
@Captor
|
@Captor
|
||||||
private lateinit var argCaptor: ArgumentCaptor<List<PrivacyItem>>
|
private lateinit var argCaptor: ArgumentCaptor<List<PrivacyItem>>
|
||||||
@Captor
|
@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 privacyItemController: PrivacyItemController
|
||||||
private lateinit var executor: FakeExecutor
|
private lateinit var executor: FakeExecutor
|
||||||
private lateinit var fakeClock: FakeSystemClock
|
private lateinit var fakeClock: FakeSystemClock
|
||||||
private lateinit var deviceConfigProxy: DeviceConfigProxy
|
private lateinit var deviceConfigProxy: DeviceConfigProxy
|
||||||
|
|
||||||
private val elapsedTime: Long
|
|
||||||
get() = fakeClock.elapsedRealtime()
|
|
||||||
|
|
||||||
fun createPrivacyItemController(): PrivacyItemController {
|
fun createPrivacyItemController(): PrivacyItemController {
|
||||||
return PrivacyItemController(
|
return PrivacyItemController(
|
||||||
appOpsController,
|
|
||||||
executor,
|
executor,
|
||||||
executor,
|
executor,
|
||||||
deviceConfigProxy,
|
privacyConfig,
|
||||||
userTracker,
|
setOf(privacyItemMonitor, privacyItemMonitor2),
|
||||||
logger,
|
logger,
|
||||||
fakeClock,
|
fakeClock,
|
||||||
dumpManager)
|
dumpManager)
|
||||||
@@ -120,43 +104,61 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
fakeClock = FakeSystemClock()
|
fakeClock = FakeSystemClock()
|
||||||
executor = FakeExecutor(fakeClock)
|
executor = FakeExecutor(fakeClock)
|
||||||
deviceConfigProxy = DeviceConfigProxyFake()
|
deviceConfigProxy = DeviceConfigProxyFake()
|
||||||
|
|
||||||
// Listen to everything by default
|
|
||||||
changeMicCamera(true)
|
|
||||||
changeLocation(true)
|
|
||||||
|
|
||||||
`when`(userTracker.userProfiles).thenReturn(listOf(UserInfo(CURRENT_USER_ID, "", 0)))
|
|
||||||
|
|
||||||
privacyItemController = createPrivacyItemController()
|
privacyItemController = createPrivacyItemController()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSetListeningTrueByAddingCallback() {
|
fun testStartListeningByAddingCallback() {
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
|
verify(privacyItemMonitor).startListening(any())
|
||||||
any())
|
verify(privacyItemMonitor2).startListening(any())
|
||||||
verify(callback).onPrivacyItemsChanged(anyList())
|
verify(callback).onPrivacyItemsChanged(anyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSetListeningFalseByRemovingLastCallback() {
|
fun testStopListeningByRemovingLastCallback() {
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
verify(appOpsController, never()).removeCallback(any(),
|
verify(privacyItemMonitor, never()).stopListening()
|
||||||
any())
|
|
||||||
privacyItemController.removeCallback(callback)
|
privacyItemController.removeCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
|
verify(privacyItemMonitor).stopListening()
|
||||||
any())
|
verify(privacyItemMonitor2).stopListening()
|
||||||
verify(callback).onPrivacyItemsChanged(emptyList())
|
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
|
@Test
|
||||||
fun testDistinctItems() {
|
fun testDistinctItems() {
|
||||||
doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
|
doReturn(listOf(
|
||||||
AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0)))
|
PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
.`when`(appOpsController).getActiveAppOps(anyBoolean())
|
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)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
@@ -166,9 +168,12 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSimilarItemsDifferentTimeStamp() {
|
fun testSimilarItemsDifferentTimeStamp() {
|
||||||
doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
|
doReturn(listOf(
|
||||||
AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 1)))
|
PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
.`when`(appOpsController).getActiveAppOps(anyBoolean())
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0),
|
||||||
|
PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 1)))
|
||||||
|
.`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
|
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
@@ -176,29 +181,6 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
assertEquals(2, argCaptor.value.size)
|
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
|
@Test
|
||||||
fun testAddMultipleCallbacks() {
|
fun testAddMultipleCallbacks() {
|
||||||
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
||||||
@@ -215,7 +197,7 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMultipleCallbacksAreUpdated() {
|
fun testMultipleCallbacksAreUpdated() {
|
||||||
doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
|
doReturn(emptyList<PrivacyItem>()).`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
|
|
||||||
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
@@ -224,8 +206,8 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
reset(callback)
|
reset(callback)
|
||||||
reset(otherCallback)
|
reset(otherCallback)
|
||||||
|
|
||||||
verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
|
verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
|
||||||
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
|
argCaptorCallback.value.onPrivacyItemsChanged()
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
verify(callback).onPrivacyItemsChanged(anyList())
|
verify(callback).onPrivacyItemsChanged(anyList())
|
||||||
verify(otherCallback).onPrivacyItemsChanged(anyList())
|
verify(otherCallback).onPrivacyItemsChanged(anyList())
|
||||||
@@ -233,7 +215,7 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRemoveCallback() {
|
fun testRemoveCallback() {
|
||||||
doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOps(anyBoolean())
|
doReturn(emptyList<PrivacyItem>()).`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
privacyItemController.addCallback(otherCallback)
|
privacyItemController.addCallback(otherCallback)
|
||||||
@@ -242,207 +224,54 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
reset(callback)
|
reset(callback)
|
||||||
reset(otherCallback)
|
reset(otherCallback)
|
||||||
|
|
||||||
verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
|
verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
|
||||||
privacyItemController.removeCallback(callback)
|
privacyItemController.removeCallback(callback)
|
||||||
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
|
argCaptorCallback.value.onPrivacyItemsChanged()
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
verify(callback, never()).onPrivacyItemsChanged(anyList())
|
verify(callback, never()).onPrivacyItemsChanged(anyList())
|
||||||
verify(otherCallback).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
|
@Test
|
||||||
fun testListShouldBeCopy() {
|
fun testListShouldBeCopy() {
|
||||||
val list = listOf(PrivacyItem(PrivacyType.TYPE_CAMERA,
|
val list = listOf(PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
PrivacyApplication("", TEST_UID), 0))
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0))
|
||||||
privacyItemController.privacyList = list
|
privacyItemController.privacyList = list
|
||||||
val privacyList = privacyItemController.privacyList
|
val privacyList = privacyItemController.privacyList
|
||||||
assertEquals(list, privacyList)
|
assertEquals(list, privacyList)
|
||||||
assertTrue(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
|
@Test
|
||||||
fun testLogListUpdated() {
|
fun testLogListUpdated() {
|
||||||
doReturn(listOf(
|
val privacyItem = PrivacyItem(
|
||||||
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(
|
|
||||||
PrivacyType.TYPE_LOCATION,
|
PrivacyType.TYPE_LOCATION,
|
||||||
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID),
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID),
|
||||||
0
|
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>>()
|
val captor = argumentCaptor<List<PrivacyItem>>()
|
||||||
verify(logger, atLeastOnce()).logRetrievedPrivacyItemsList(capture(captor))
|
verify(logger, atLeastOnce()).logRetrievedPrivacyItemsList(capture(captor))
|
||||||
// Let's look at the last log
|
// Let's look at the last log
|
||||||
val values = captor.allValues
|
val values = captor.allValues
|
||||||
assertTrue(values[values.size - 1].contains(expected))
|
assertTrue(values[values.size - 1].contains(privacyItem))
|
||||||
}
|
|
||||||
|
|
||||||
@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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testPassageOfTimeDoesNotRemoveIndicators() {
|
fun testPassageOfTimeDoesNotRemoveIndicators() {
|
||||||
doReturn(listOf(
|
doReturn(listOf(
|
||||||
AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
|
PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
)).`when`(appOpsController).getActiveAppOps(anyBoolean())
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)
|
||||||
|
)).`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
|
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
|
|
||||||
@@ -457,18 +286,18 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
fun testNotHeldAfterTimeIsOff() {
|
fun testNotHeldAfterTimeIsOff() {
|
||||||
// Start with some element at time 0
|
// Start with some element at time 0
|
||||||
doReturn(listOf(
|
doReturn(listOf(
|
||||||
AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
|
PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
)).`when`(appOpsController).getActiveAppOps(anyBoolean())
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)
|
||||||
|
)).`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
|
|
||||||
// Then remove it at time HOLD + 1
|
// 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)
|
fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS + 1)
|
||||||
|
|
||||||
verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
|
verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
|
||||||
argCaptorCallback.value.onActiveStateChanged(
|
argCaptorCallback.value.onPrivacyItemsChanged()
|
||||||
AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false)
|
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
|
|
||||||
// See it's not there
|
// See it's not there
|
||||||
@@ -478,20 +307,21 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testElementNotRemovedBeforeHoldTime() {
|
fun testElementNotRemovedBeforeHoldTime() {
|
||||||
// Start with some element at time 0
|
// Start with some element at current time
|
||||||
doReturn(listOf(
|
doReturn(listOf(
|
||||||
AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
|
PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
)).`when`(appOpsController).getActiveAppOps(anyBoolean())
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID),
|
||||||
|
fakeClock.elapsedRealtime())
|
||||||
|
)).`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
|
|
||||||
// Then remove it at time HOLD - 1
|
// 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)
|
fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS - 1)
|
||||||
|
|
||||||
verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
|
verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
|
||||||
argCaptorCallback.value.onActiveStateChanged(
|
argCaptorCallback.value.onPrivacyItemsChanged()
|
||||||
AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false)
|
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
|
|
||||||
// See it's still there
|
// See it's still there
|
||||||
@@ -503,18 +333,18 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
fun testElementAutoRemovedAfterHoldTime() {
|
fun testElementAutoRemovedAfterHoldTime() {
|
||||||
// Start with some element at time 0
|
// Start with some element at time 0
|
||||||
doReturn(listOf(
|
doReturn(listOf(
|
||||||
AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime)
|
PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||||
)).`when`(appOpsController).getActiveAppOps(anyBoolean())
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0)
|
||||||
|
)).`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
|
|
||||||
// Then remove it at time HOLD - 1
|
// 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)
|
fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS - 1)
|
||||||
|
|
||||||
verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
|
verify(privacyItemMonitor).startListening(capture(argCaptorCallback))
|
||||||
argCaptorCallback.value.onActiveStateChanged(
|
argCaptorCallback.value.onPrivacyItemsChanged()
|
||||||
AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false)
|
|
||||||
executor.runAllReady()
|
executor.runAllReady()
|
||||||
|
|
||||||
fakeClock.advanceTime(2L)
|
fakeClock.advanceTime(2L)
|
||||||
@@ -525,39 +355,53 @@ class PrivacyItemControllerTest : SysuiTestCase() {
|
|||||||
assertTrue(privacyItemController.privacyList.isEmpty())
|
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)
|
||||||
|
argCaptorConfigCallback.value.onFlagMicCameraChanged(true)
|
||||||
|
argCaptorConfigCallback.value.onFlagLocationChanged(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
|
@Test
|
||||||
fun testPausedElementsAreRemoved() {
|
fun testPausedElementsAreRemoved() {
|
||||||
val item = AppOpItem(
|
doReturn(listOf(
|
||||||
AppOpsManager.OP_RECORD_AUDIO,
|
PrivacyItem(PrivacyType.TYPE_MICROPHONE,
|
||||||
TEST_UID,
|
PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), 0, true)))
|
||||||
TEST_PACKAGE_NAME,
|
.`when`(privacyItemMonitor).getActivePrivacyItems()
|
||||||
elapsedTime
|
|
||||||
)
|
|
||||||
`when`(appOpsController.getActiveAppOps(anyBoolean())).thenReturn(listOf(item))
|
|
||||||
privacyItemController.addCallback(callback)
|
privacyItemController.addCallback(callback)
|
||||||
executor.runAllReady()
|
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())
|
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