Separate mic + camera from location

The different states are:
- No indicators enabled. Location is displayed using
LocationControllerImpl
- Only mic + camera indicators. Location is displayed using
LocationControllerImpl
- All indicators enabled. LocationControllerImpl is ignored.

Also, hardwire OP_MONITOR_HIGH_POWER_LOCATION as user sensitive.

Bug: 162552566
Test: atest com.android.systemui.privacy
Test: manual

Change-Id: I1e7f62d6fccb4a3bc4fdf2a47a8d5c8a4f55421d
This commit is contained in:
Fabian Kozynski
2020-07-31 12:50:48 -04:00
parent 07514b6192
commit 2fb81031d8
7 changed files with 375 additions and 59 deletions

View File

@@ -123,10 +123,15 @@ public final class SystemUiDeviceConfigFlags {
// Flag related to Privacy Indicators // Flag related to Privacy Indicators
/** /**
* Whether the Permissions Hub is showing. * Whether to show the complete ongoing app ops chip.
*/ */
public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_2_enabled"; public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_2_enabled";
/**
* Whether to show app ops chip for just microphone + camera.
*/
public static final String PROPERTY_MIC_CAMERA_ENABLED = "camera_mic_icons_enabled";
// Flags related to Assistant // Flags related to Assistant
/** /**

View File

@@ -281,9 +281,11 @@ public class AppOpsControllerImpl implements AppOpsController,
* @return {@code true} iff the app-op for should be shown to the user * @return {@code true} iff the app-op for should be shown to the user
*/ */
private boolean isUserVisible(int appOpCode, int uid, String packageName) { private boolean isUserVisible(int appOpCode, int uid, String packageName) {
// currently OP_SYSTEM_ALERT_WINDOW does not correspond to a platform permission // currently OP_SYSTEM_ALERT_WINDOW and OP_MONITOR_HIGH_POWER_LOCATION
// which may be user senstive, so for now always show it to the user. // does not correspond to a platform permission
if (appOpCode == AppOpsManager.OP_SYSTEM_ALERT_WINDOW) { // which may be user sensitive, so for now always show it to the user.
if (appOpCode == AppOpsManager.OP_SYSTEM_ALERT_WINDOW
|| appOpCode == AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION) {
return true; return true;
} }

View File

@@ -45,7 +45,6 @@ import javax.inject.Singleton
@Singleton @Singleton
class PrivacyItemController @Inject constructor( class PrivacyItemController @Inject constructor(
context: Context,
private val appOpsController: AppOpsController, private val appOpsController: AppOpsController,
@Main uiExecutor: DelayableExecutor, @Main uiExecutor: DelayableExecutor,
@Background private val bgExecutor: Executor, @Background private val bgExecutor: Executor,
@@ -57,16 +56,21 @@ class PrivacyItemController @Inject constructor(
@VisibleForTesting @VisibleForTesting
internal companion object { internal companion object {
val OPS = intArrayOf(AppOpsManager.OP_CAMERA, val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA,
AppOpsManager.OP_RECORD_AUDIO, AppOpsManager.OP_RECORD_AUDIO)
val OPS_LOCATION = intArrayOf(
AppOpsManager.OP_COARSE_LOCATION, AppOpsManager.OP_COARSE_LOCATION,
AppOpsManager.OP_FINE_LOCATION) AppOpsManager.OP_FINE_LOCATION)
val OPS = OPS_MIC_CAMERA + OPS_LOCATION
val intentFilter = IntentFilter().apply { val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_USER_SWITCHED) addAction(Intent.ACTION_USER_SWITCHED)
addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
} }
const val TAG = "PrivacyItemController" const val TAG = "PrivacyItemController"
private const val ALL_INDICATORS =
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED
private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
} }
@VisibleForTesting @VisibleForTesting
@@ -74,9 +78,14 @@ 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 isPermissionsHubEnabled(): Boolean { private fun isAllIndicatorsEnabled(): Boolean {
return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false) ALL_INDICATORS, false)
}
private fun isMicCameraEnabled(): Boolean {
return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
MIC_CAMERA, false)
} }
private var currentUserIds = emptyList<Int>() private var currentUserIds = emptyList<Int>()
@@ -94,23 +103,28 @@ class PrivacyItemController @Inject constructor(
uiExecutor.execute(notifyChanges) uiExecutor.execute(notifyChanges)
} }
var indicatorsAvailable = isPermissionsHubEnabled() var allIndicatorsAvailable = isAllIndicatorsEnabled()
private set private set
@VisibleForTesting var micCameraAvailable = isMicCameraEnabled()
internal val devicePropertiesChangedListener = private set
private val devicePropertiesChangedListener =
object : DeviceConfig.OnPropertiesChangedListener { object : DeviceConfig.OnPropertiesChangedListener {
override fun onPropertiesChanged(properties: DeviceConfig.Properties) { override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) && if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) &&
properties.getKeyset().contains( (properties.keyset.contains(ALL_INDICATORS) ||
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) { properties.keyset.contains(MIC_CAMERA))) {
val flag = properties.getBoolean(
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false) // Running on the ui executor so can iterate on callbacks
if (indicatorsAvailable != flag) { if (properties.keyset.contains(ALL_INDICATORS)) {
// This is happening already in the UI executor, so we can iterate in the allIndicatorsAvailable = properties.getBoolean(ALL_INDICATORS, false)
indicatorsAvailable = flag callbacks.forEach { it.get()?.onFlagAllChanged(allIndicatorsAvailable) }
callbacks.forEach { it.get()?.onFlagChanged(flag) }
} }
if (properties.keyset.contains(MIC_CAMERA)) {
micCameraAvailable = properties.getBoolean(MIC_CAMERA, false)
callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) }
}
internalUiExecutor.updateListeningState() internalUiExecutor.updateListeningState()
} }
} }
@@ -123,6 +137,10 @@ class PrivacyItemController @Inject constructor(
packageName: String, packageName: String,
active: Boolean active: Boolean
) { ) {
// Check if we care about this code right now
if (!allIndicatorsAvailable && code in OPS_LOCATION) {
return
}
val userId = UserHandle.getUserId(uid) val userId = UserHandle.getUserId(uid)
if (userId in currentUserIds) { if (userId in currentUserIds) {
update(false) update(false)
@@ -166,13 +184,16 @@ class PrivacyItemController @Inject constructor(
} }
/** /**
* Updates listening status based on whether there are callbacks and the indicators are enabled * 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 * This is only called from private (add/remove)Callback and from the config listener, all in
* main thread. * main thread.
*/ */
private fun setListeningState() { private fun setListeningState() {
val listen = !callbacks.isEmpty() and indicatorsAvailable val listen = !callbacks.isEmpty() and (allIndicatorsAvailable || micCameraAvailable)
if (listening == listen) return if (listening == listen) return
listening = listen listening = listen
if (listening) { if (listening) {
@@ -233,14 +254,19 @@ class PrivacyItemController @Inject constructor(
AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
else -> return null else -> return null
} }
if (type == PrivacyType.TYPE_LOCATION && !allIndicatorsAvailable) return null
val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid)
return PrivacyItem(type, app) return PrivacyItem(type, app)
} }
interface Callback { interface Callback {
fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>)
@JvmDefault @JvmDefault
fun onFlagChanged(flag: Boolean) {} fun onFlagAllChanged(flag: Boolean) {}
@JvmDefault
fun onFlagMicCameraChanged(flag: Boolean) {}
} }
internal inner class Receiver : BroadcastReceiver() { internal inner class Receiver : BroadcastReceiver() {

View File

@@ -151,7 +151,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
private Space mSpace; private Space mSpace;
private BatteryMeterView mBatteryRemainingIcon; private BatteryMeterView mBatteryRemainingIcon;
private RingerModeTracker mRingerModeTracker; private RingerModeTracker mRingerModeTracker;
private boolean mPermissionsHubEnabled; private boolean mAllIndicatorsEnabled;
private boolean mMicCameraIndicatorsEnabled;
private PrivacyItemController mPrivacyItemController; private PrivacyItemController mPrivacyItemController;
private final UiEventLogger mUiEventLogger; private final UiEventLogger mUiEventLogger;
@@ -178,13 +179,26 @@ public class QuickStatusBarHeader extends RelativeLayout implements
} }
@Override @Override
public void onFlagChanged(boolean flag) { public void onFlagAllChanged(boolean flag) {
if (mPermissionsHubEnabled != flag) { if (mAllIndicatorsEnabled != flag) {
mAllIndicatorsEnabled = flag;
update();
}
}
@Override
public void onFlagMicCameraChanged(boolean flag) {
if (mMicCameraIndicatorsEnabled != flag) {
mMicCameraIndicatorsEnabled = flag;
update();
}
}
private void update() {
StatusIconContainer iconContainer = requireViewById(R.id.statusIcons); StatusIconContainer iconContainer = requireViewById(R.id.statusIcons);
iconContainer.setIgnoredSlots(getIgnoredIconSlots()); iconContainer.setIgnoredSlots(getIgnoredIconSlots());
setChipVisibility(!mPrivacyChip.getPrivacyList().isEmpty()); setChipVisibility(!mPrivacyChip.getPrivacyList().isEmpty());
} }
}
}; };
@Inject @Inject
@@ -267,7 +281,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mRingerModeTextView.setSelected(true); mRingerModeTextView.setSelected(true);
mNextAlarmTextView.setSelected(true); mNextAlarmTextView.setSelected(true);
mPermissionsHubEnabled = mPrivacyItemController.getIndicatorsAvailable(); mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable();
mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable();
} }
public QuickQSPanel getHeaderQsPanel() { public QuickQSPanel getHeaderQsPanel() {
@@ -276,14 +291,16 @@ public class QuickStatusBarHeader extends RelativeLayout implements
private List<String> getIgnoredIconSlots() { private List<String> getIgnoredIconSlots() {
ArrayList<String> ignored = new ArrayList<>(); ArrayList<String> ignored = new ArrayList<>();
if (getChipEnabled()) {
ignored.add(mContext.getResources().getString( ignored.add(mContext.getResources().getString(
com.android.internal.R.string.status_bar_camera)); com.android.internal.R.string.status_bar_camera));
ignored.add(mContext.getResources().getString( ignored.add(mContext.getResources().getString(
com.android.internal.R.string.status_bar_microphone)); com.android.internal.R.string.status_bar_microphone));
if (mPermissionsHubEnabled) { if (mAllIndicatorsEnabled) {
ignored.add(mContext.getResources().getString( ignored.add(mContext.getResources().getString(
com.android.internal.R.string.status_bar_location)); com.android.internal.R.string.status_bar_location));
} }
}
return ignored; return ignored;
} }
@@ -300,7 +317,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
} }
private void setChipVisibility(boolean chipVisible) { private void setChipVisibility(boolean chipVisible) {
if (chipVisible && mPermissionsHubEnabled) { if (chipVisible && getChipEnabled()) {
mPrivacyChip.setVisibility(View.VISIBLE); mPrivacyChip.setVisibility(View.VISIBLE);
// Makes sure that the chip is logged as viewed at most once each time QS is opened // Makes sure that the chip is logged as viewed at most once each time QS is opened
// mListening makes sure that the callback didn't return after the user closed QS // mListening makes sure that the callback didn't return after the user closed QS
@@ -607,7 +624,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mAlarmController.addCallback(this); mAlarmController.addCallback(this);
mLifecycle.setCurrentState(Lifecycle.State.RESUMED); mLifecycle.setCurrentState(Lifecycle.State.RESUMED);
// Get the most up to date info // Get the most up to date info
mPermissionsHubEnabled = mPrivacyItemController.getIndicatorsAvailable(); mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable();
mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable();
mPrivacyItemController.addCallback(mPICCallback); mPrivacyItemController.addCallback(mPICCallback);
} else { } else {
mZenController.removeCallback(this); mZenController.removeCallback(this);
@@ -747,4 +765,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
updateHeaderTextContainerAlphaAnimator(); updateHeaderTextContainerAlphaAnimator();
} }
} }
private boolean getChipEnabled() {
return mMicCameraIndicatorsEnabled || mAllIndicatorsEnabled;
}
} }

View File

@@ -662,16 +662,18 @@ public class PhoneStatusBarPolicy
mIconController.setIconVisibility(mSlotCamera, showCamera); mIconController.setIconVisibility(mSlotCamera, showCamera);
mIconController.setIconVisibility(mSlotMicrophone, showMicrophone); mIconController.setIconVisibility(mSlotMicrophone, showMicrophone);
if (mPrivacyItemController.getAllIndicatorsAvailable()) {
mIconController.setIconVisibility(mSlotLocation, showLocation); mIconController.setIconVisibility(mSlotLocation, showLocation);
} }
}
@Override @Override
public void onLocationActiveChanged(boolean active) { public void onLocationActiveChanged(boolean active) {
if (!mPrivacyItemController.getIndicatorsAvailable()) updateLocation(); if (!mPrivacyItemController.getAllIndicatorsAvailable()) updateLocationFromController();
} }
// Updates the status view based on the current state of location requests. // Updates the status view based on the current state of location requests.
private void updateLocation() { private void updateLocationFromController() {
if (mLocationController.isLocationActive()) { if (mLocationController.isLocationActive()) {
mIconController.setIconVisibility(mSlotLocation, true); mIconController.setIconVisibility(mSlotLocation, true);
} else { } else {

View File

@@ -0,0 +1,219 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.privacy
import android.os.UserManager
import android.provider.DeviceConfig
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
import com.android.systemui.util.DeviceConfigProxy
import com.android.systemui.util.DeviceConfigProxyFake
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class PrivacyItemControllerFlagsTest : SysuiTestCase() {
companion object {
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
fun <T> eq(value: T): T = Mockito.eq(value) ?: value
fun <T> any(): T = Mockito.any<T>()
private const val ALL_INDICATORS =
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED
private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
}
@Mock
private lateinit var appOpsController: AppOpsController
@Mock
private lateinit var callback: PrivacyItemController.Callback
@Mock
private lateinit var userManager: UserManager
@Mock
private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock
private lateinit var dumpManager: DumpManager
private lateinit var privacyItemController: PrivacyItemController
private lateinit var executor: FakeExecutor
private lateinit var deviceConfigProxy: DeviceConfigProxy
fun PrivacyItemController(): PrivacyItemController {
return PrivacyItemController(
appOpsController,
executor,
executor,
broadcastDispatcher,
deviceConfigProxy,
userManager,
dumpManager
)
}
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
executor = FakeExecutor(FakeSystemClock())
deviceConfigProxy = DeviceConfigProxyFake()
privacyItemController = PrivacyItemController()
privacyItemController.addCallback(callback)
executor.runAllReady()
}
@Test
fun testNotListeningByDefault() {
assertFalse(privacyItemController.allIndicatorsAvailable)
assertFalse(privacyItemController.micCameraAvailable)
verify(appOpsController, never()).addCallback(any(), any())
}
@Test
fun testMicCameraChanged() {
changeMicCamera(true)
executor.runAllReady()
verify(callback).onFlagMicCameraChanged(true)
verify(callback, never()).onFlagAllChanged(anyBoolean())
assertTrue(privacyItemController.micCameraAvailable)
assertFalse(privacyItemController.allIndicatorsAvailable)
}
@Test
fun testAllChanged() {
changeAll(true)
executor.runAllReady()
verify(callback).onFlagAllChanged(true)
verify(callback, never()).onFlagMicCameraChanged(anyBoolean())
assertTrue(privacyItemController.allIndicatorsAvailable)
assertFalse(privacyItemController.micCameraAvailable)
}
@Test
fun testBothChanged() {
changeAll(true)
changeMicCamera(true)
executor.runAllReady()
verify(callback, atLeastOnce()).onFlagAllChanged(true)
verify(callback, atLeastOnce()).onFlagMicCameraChanged(true)
assertTrue(privacyItemController.allIndicatorsAvailable)
assertTrue(privacyItemController.micCameraAvailable)
}
@Test
fun testAll_listeningToAll() {
changeAll(true)
executor.runAllReady()
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
}
@Test
fun testMicCamera_listening() {
changeMicCamera(true)
executor.runAllReady()
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
}
@Test
fun testAll_listening() {
changeAll(true)
executor.runAllReady()
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any())
}
@Test
fun testAllFalse_notListening() {
changeAll(true)
executor.runAllReady()
changeAll(false)
executor.runAllReady()
verify(appOpsController).removeCallback(any(), any())
}
@Test
fun testSomeListening_stillListening() {
changeAll(true)
changeMicCamera(true)
executor.runAllReady()
changeAll(false)
executor.runAllReady()
verify(appOpsController, never()).removeCallback(any(), any())
}
@Test
fun testAllDeleted_stopListening() {
changeAll(true)
executor.runAllReady()
changeAll(null)
executor.runAllReady()
verify(appOpsController).removeCallback(any(), any())
}
@Test
fun testMicDeleted_stopListening() {
changeMicCamera(true)
executor.runAllReady()
changeMicCamera(null)
executor.runAllReady()
verify(appOpsController).removeCallback(any(), any())
}
private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)
private fun changeAll(value: Boolean?) = changeProperty(ALL_INDICATORS, value)
private fun changeProperty(name: String, value: Boolean?) {
deviceConfigProxy.setProperty(
DeviceConfig.NAMESPACE_PRIVACY,
name,
value?.toString(),
false
)
}
}

View File

@@ -18,7 +18,6 @@ package com.android.systemui.privacy
import android.app.ActivityManager import android.app.ActivityManager
import android.app.AppOpsManager import android.app.AppOpsManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.UserInfo import android.content.pm.UserInfo
import android.os.UserHandle import android.os.UserHandle
@@ -69,10 +68,11 @@ class PrivacyItemControllerTest : SysuiTestCase() {
companion object { companion object {
val CURRENT_USER_ID = ActivityManager.getCurrentUser() val CURRENT_USER_ID = ActivityManager.getCurrentUser()
val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE
const val SYSTEM_UID = 1000
const val TEST_PACKAGE_NAME = "test" const val TEST_PACKAGE_NAME = "test"
const val DEVICE_SERVICES_STRING = "Device services"
const val TAG = "PrivacyItemControllerTest" private const val ALL_INDICATORS =
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_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> eq(value: T): T = Mockito.eq(value) ?: value
fun <T> any(): T = Mockito.any<T>() fun <T> any(): T = Mockito.any<T>()
@@ -97,9 +97,8 @@ class PrivacyItemControllerTest : SysuiTestCase() {
private lateinit var executor: FakeExecutor private lateinit var executor: FakeExecutor
private lateinit var deviceConfigProxy: DeviceConfigProxy private lateinit var deviceConfigProxy: DeviceConfigProxy
fun PrivacyItemController(context: Context): PrivacyItemController { fun PrivacyItemController(): PrivacyItemController {
return PrivacyItemController( return PrivacyItemController(
context,
appOpsController, appOpsController,
executor, executor,
executor, executor,
@@ -116,11 +115,8 @@ class PrivacyItemControllerTest : SysuiTestCase() {
executor = FakeExecutor(FakeSystemClock()) executor = FakeExecutor(FakeSystemClock())
deviceConfigProxy = DeviceConfigProxyFake() deviceConfigProxy = DeviceConfigProxyFake()
appOpsController = mDependency.injectMockDependency(AppOpsController::class.java) // Listen to everything by default
changeAll(true)
deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
"true", false)
doReturn(listOf(object : UserInfo() { doReturn(listOf(object : UserInfo() {
init { init {
@@ -128,7 +124,7 @@ class PrivacyItemControllerTest : SysuiTestCase() {
} }
})).`when`(userManager).getProfiles(anyInt()) })).`when`(userManager).getProfiles(anyInt())
privacyItemController = PrivacyItemController(mContext) privacyItemController = PrivacyItemController()
} }
@Test @Test
@@ -276,15 +272,59 @@ class PrivacyItemControllerTest : SysuiTestCase() {
@Test @Test
fun testNotListeningWhenIndicatorsDisabled() { fun testNotListeningWhenIndicatorsDisabled() {
deviceConfigProxy.setProperty( changeAll(false)
DeviceConfig.NAMESPACE_PRIVACY,
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
"false",
false
)
privacyItemController.addCallback(callback) privacyItemController.addCallback(callback)
executor.runAllReady() executor.runAllReady()
verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS), verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS),
any()) any())
} }
@Test
fun testNotSendingLocationWhenOnlyMicCamera() {
changeAll(false)
changeMicCamera(true)
executor.runAllReady()
doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, "", 0)))
.`when`(appOpsController).getActiveAppOpsForUser(anyInt())
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).getActiveAppOpsForUser(anyInt())
privacyItemController.addCallback(callback)
changeAll(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())
}
private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)
private fun changeAll(value: Boolean?) = changeProperty(ALL_INDICATORS, value)
private fun changeProperty(name: String, value: Boolean?) {
deviceConfigProxy.setProperty(
DeviceConfig.NAMESPACE_PRIVACY,
name,
value?.toString(),
false
)
}
} }