From e534fb7782e5ffdb7e99969e12266d691609506a Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Wed, 5 Aug 2020 12:53:14 -0400 Subject: [PATCH] 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. Fixes: 162552566 Test: atest com.android.systemui.privacy Test: manual Change-Id: I1e7f62d6fccb4a3bc4fdf2a47a8d5c8a4f55421d Merged-In: I1e7f62d6fccb4a3bc4fdf2a47a8d5c8a4f55421d --- .../sysui/SystemUiDeviceConfigFlags.java | 7 +- .../systemui/appops/AppOpsControllerImpl.java | 8 +- .../systemui/privacy/PrivacyItemController.kt | 77 ++++-- .../systemui/qs/QuickStatusBarHeader.java | 74 +++--- .../statusbar/phone/PhoneStatusBarPolicy.java | 10 +- .../privacy/PrivacyItemControllerFlagsTest.kt | 219 ++++++++++++++++++ .../privacy/PrivacyItemControllerTest.kt | 97 +++++--- 7 files changed, 403 insertions(+), 89 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index ea3d2de13ce6d..eb59f0f59be10 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -123,10 +123,15 @@ public final class SystemUiDeviceConfigFlags { // 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"; + /** + * 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 /** diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index ce8e285ff52f5..779613011daea 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -279,9 +279,11 @@ public class AppOpsControllerImpl implements AppOpsController, * @return {@code true} iff the app-op for should be shown to the user */ private boolean isUserVisible(int appOpCode, int uid, String packageName) { - // currently OP_SYSTEM_ALERT_WINDOW does not correspond to a platform permission - // which may be user senstive, so for now always show it to the user. - if (appOpCode == AppOpsManager.OP_SYSTEM_ALERT_WINDOW) { + // currently OP_SYSTEM_ALERT_WINDOW and OP_MONITOR_HIGH_POWER_LOCATION + // does not correspond to a platform permission + // 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; } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt index 8001ecc9697d9..2be69c10a77b0 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -45,27 +45,32 @@ import javax.inject.Singleton @Singleton class PrivacyItemController @Inject constructor( - context: Context, private val appOpsController: AppOpsController, @Main uiExecutor: DelayableExecutor, @Background private val bgExecutor: Executor, private val broadcastDispatcher: BroadcastDispatcher, private val deviceConfigProxy: DeviceConfigProxy, + private val userManager: UserManager, dumpManager: DumpManager ) : Dumpable { @VisibleForTesting internal companion object { - val OPS = intArrayOf(AppOpsManager.OP_CAMERA, - AppOpsManager.OP_RECORD_AUDIO, + val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA, + AppOpsManager.OP_RECORD_AUDIO) + val OPS_LOCATION = intArrayOf( AppOpsManager.OP_COARSE_LOCATION, AppOpsManager.OP_FINE_LOCATION) + val OPS = OPS_MIC_CAMERA + OPS_LOCATION val intentFilter = IntentFilter().apply { addAction(Intent.ACTION_USER_SWITCHED) addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) } const val TAG = "PrivacyItemController" + private const val ALL_INDICATORS = + SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED + private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED } @VisibleForTesting @@ -73,12 +78,16 @@ class PrivacyItemController @Inject constructor( @Synchronized get() = field.toList() // Returns a shallow copy of the list @Synchronized set - fun isPermissionsHubEnabled(): Boolean { + fun isAllIndicatorsEnabled(): Boolean { 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 val userManager = context.getSystemService(UserManager::class.java) private var currentUserIds = emptyList() private var listening = false private val callbacks = mutableListOf>() @@ -86,7 +95,7 @@ class PrivacyItemController @Inject constructor( private val notifyChanges = Runnable { val list = privacyList - callbacks.forEach { it.get()?.privacyChanged(list) } + callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) } } private val updateListAndNotifyChanges = Runnable { @@ -94,20 +103,32 @@ class PrivacyItemController @Inject constructor( uiExecutor.execute(notifyChanges) } - private var indicatorsAvailable = isPermissionsHubEnabled() - @VisibleForTesting - internal val devicePropertiesChangedListener = + var allIndicatorsAvailable = isAllIndicatorsEnabled() + private set + var micCameraAvailable = isMicCameraEnabled() + private set + + private val devicePropertiesChangedListener = object : DeviceConfig.OnPropertiesChangedListener { override fun onPropertiesChanged(properties: DeviceConfig.Properties) { - if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) && - properties.getKeyset().contains( - SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) { - indicatorsAvailable = properties.getBoolean( - SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false) - internalUiExecutor.updateListeningState() + if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) && + (properties.keyset.contains(ALL_INDICATORS) || + properties.keyset.contains(MIC_CAMERA))) { + + // Running on the ui executor so can iterate on callbacks + if (properties.keyset.contains(ALL_INDICATORS)) { + allIndicatorsAvailable = properties.getBoolean(ALL_INDICATORS, false) + callbacks.forEach { it.get()?.onFlagAllChanged(allIndicatorsAvailable) } + } + + if (properties.keyset.contains(MIC_CAMERA)) { + micCameraAvailable = properties.getBoolean(MIC_CAMERA, false) + callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) } + } + internalUiExecutor.updateListeningState() + } } } - } private val cb = object : AppOpsController.Callback { override fun onActiveStateChanged( @@ -116,6 +137,10 @@ class PrivacyItemController @Inject constructor( packageName: String, active: Boolean ) { + // Check if we care about this code right now + if (!allIndicatorsAvailable && code in OPS_LOCATION) { + return + } val userId = UserHandle.getUserId(uid) if (userId in currentUserIds) { update(false) @@ -159,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 * main thread. */ private fun setListeningState() { - val listen = !callbacks.isEmpty() and indicatorsAvailable + val listen = !callbacks.isEmpty() and (allIndicatorsAvailable || micCameraAvailable) if (listening == listen) return listening = listen if (listening) { @@ -226,13 +254,20 @@ class PrivacyItemController @Inject constructor( AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE else -> return null } + if (type == PrivacyType.TYPE_LOCATION && !allIndicatorsAvailable) return null val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) return PrivacyItem(type, app) } // Used by containing class to get notified of changes interface Callback { - fun privacyChanged(privacyItems: List) + fun onPrivacyItemsChanged(privacyItems: List) + + @JvmDefault + fun onFlagAllChanged(flag: Boolean) {} + + @JvmDefault + fun onFlagMicCameraChanged(flag: Boolean) {} } internal inner class Receiver : BroadcastReceiver() { @@ -248,7 +283,7 @@ class PrivacyItemController @Inject constructor( private val list: List ) : Runnable { override fun run() { - callback?.privacyChanged(list) + callback?.onPrivacyItemsChanged(list) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index a559a5467663d..58c723c58daad 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -33,7 +33,6 @@ import android.media.AudioManager; import android.os.Handler; import android.os.Looper; import android.provider.AlarmClock; -import android.provider.DeviceConfig; import android.provider.Settings; import android.service.notification.ZenModeConfig; import android.text.format.DateUtils; @@ -59,7 +58,6 @@ import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.UiEventLogger; import com.android.settingslib.Utils; import com.android.systemui.BatteryMeterView; @@ -154,7 +152,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements private Space mSpace; private BatteryMeterView mBatteryRemainingIcon; private RingerModeTracker mRingerModeTracker; - private boolean mPermissionsHubEnabled; + private boolean mAllIndicatorsEnabled; + private boolean mMicCameraIndicatorsEnabled; private PrivacyItemController mPrivacyItemController; private final UiEventLogger mUiEventLogger; @@ -173,27 +172,34 @@ public class QuickStatusBarHeader extends RelativeLayout implements private float mKeyguardExpansionFraction; private boolean mPrivacyChipLogged = false; - private final DeviceConfig.OnPropertiesChangedListener mPropertiesListener = - new DeviceConfig.OnPropertiesChangedListener() { - @Override - public void onPropertiesChanged(DeviceConfig.Properties properties) { - if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) - && properties.getKeyset() - .contains(SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) { - mPermissionsHubEnabled = properties.getBoolean( - SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false); - StatusIconContainer iconContainer = findViewById(R.id.statusIcons); - iconContainer.setIgnoredSlots(getIgnoredIconSlots()); - } - } - }; - private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() { @Override - public void privacyChanged(List privacyItems) { + public void onPrivacyItemsChanged(List privacyItems) { mPrivacyChip.setPrivacyList(privacyItems); setChipVisibility(!privacyItems.isEmpty()); } + + @Override + public void onFlagAllChanged(boolean 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); + iconContainer.setIgnoredSlots(getIgnoredIconSlots()); + setChipVisibility(!mPrivacyChip.getPrivacyList().isEmpty()); + } }; @Inject @@ -276,7 +282,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements mRingerModeTextView.setSelected(true); mNextAlarmTextView.setSelected(true); - mPermissionsHubEnabled = mPrivacyItemController.isPermissionsHubEnabled(); + mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); + mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); } public QuickQSPanel getHeaderQsPanel() { @@ -285,13 +292,15 @@ public class QuickStatusBarHeader extends RelativeLayout implements private List getIgnoredIconSlots() { ArrayList ignored = new ArrayList<>(); - ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_camera)); - ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_microphone)); - if (mPermissionsHubEnabled) { + if (getChipEnabled()) { ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_location)); + com.android.internal.R.string.status_bar_camera)); + ignored.add(mContext.getResources().getString( + com.android.internal.R.string.status_bar_microphone)); + if (mAllIndicatorsEnabled) { + ignored.add(mContext.getResources().getString( + com.android.internal.R.string.status_bar_location)); + } } return ignored; @@ -309,7 +318,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements } private void setChipVisibility(boolean chipVisible) { - if (chipVisible && mPermissionsHubEnabled) { + if (chipVisible && getChipEnabled()) { mPrivacyChip.setVisibility(View.VISIBLE); // 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 @@ -519,9 +528,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements }); mStatusBarIconController.addIconGroup(mIconManager); requestApplyInsets(); - // Change the ignored slots when DeviceConfig flag changes - DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY, - mContext.getMainExecutor(), mPropertiesListener); } @Override @@ -601,7 +607,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements setListening(false); mRingerModeTracker.getRingerModeInternal().removeObservers(this); mStatusBarIconController.removeIconGroup(mIconManager); - DeviceConfig.removeOnPropertiesChangedListener(mPropertiesListener); super.onDetachedFromWindow(); } @@ -619,6 +624,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements mZenController.addCallback(this); mAlarmController.addCallback(this); mLifecycle.setCurrentState(Lifecycle.State.RESUMED); + // Get the most up to date info + mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); + mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); mPrivacyItemController.addCallback(mPICCallback); } else { mZenController.removeCallback(this); @@ -760,4 +768,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements updateHeaderTextContainerAlphaAnimator(); } } + + private boolean getChipEnabled() { + return mMicCameraIndicatorsEnabled || mAllIndicatorsEnabled; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 01b6fbf1f3616..1ae5747118660 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -631,7 +631,7 @@ public class PhoneStatusBarPolicy } @Override // PrivacyItemController.Callback - public void privacyChanged(List privacyItems) { + public void onPrivacyItemsChanged(List privacyItems) { updatePrivacyItems(privacyItems); } @@ -664,16 +664,18 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotCamera, showCamera); mIconController.setIconVisibility(mSlotMicrophone, showMicrophone); - mIconController.setIconVisibility(mSlotLocation, showLocation); + if (mPrivacyItemController.getAllIndicatorsAvailable()) { + mIconController.setIconVisibility(mSlotLocation, showLocation); + } } @Override public void onLocationActiveChanged(boolean active) { - if (!mPrivacyItemController.isPermissionsHubEnabled()) updateLocation(); + if (!mPrivacyItemController.getAllIndicatorsAvailable()) updateLocationFromController(); } // Updates the status view based on the current state of location requests. - private void updateLocation() { + private void updateLocationFromController() { if (mLocationController.isLocationActive()) { mIconController.setIconVisibility(mSlotLocation, true); } else { diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt new file mode 100644 index 0000000000000..4ba29e6e02a61 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt @@ -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 capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() + fun eq(value: T): T = Mockito.eq(value) ?: value + fun any(): T = Mockito.any() + + 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 + ) + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt index 1f7baa9c4fcd3..5c5df2639cdd9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.privacy import android.app.ActivityManager import android.app.AppOpsManager -import android.content.Context import android.content.Intent import android.content.pm.UserInfo import android.os.UserHandle @@ -69,10 +68,11 @@ class PrivacyItemControllerTest : SysuiTestCase() { companion object { val CURRENT_USER_ID = ActivityManager.getCurrentUser() val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE - const val SYSTEM_UID = 1000 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 capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() fun eq(value: T): T = Mockito.eq(value) ?: value fun any(): T = Mockito.any() @@ -97,14 +97,14 @@ class PrivacyItemControllerTest : SysuiTestCase() { private lateinit var executor: FakeExecutor private lateinit var deviceConfigProxy: DeviceConfigProxy - fun PrivacyItemController(context: Context): PrivacyItemController { + fun PrivacyItemController(): PrivacyItemController { return PrivacyItemController( - context, appOpsController, executor, executor, broadcastDispatcher, deviceConfigProxy, + userManager, dumpManager ) } @@ -115,12 +115,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { executor = FakeExecutor(FakeSystemClock()) deviceConfigProxy = DeviceConfigProxyFake() - appOpsController = mDependency.injectMockDependency(AppOpsController::class.java) - mContext.addMockSystemService(UserManager::class.java, userManager) - - deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_PRIVACY, - SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, - "true", false) + changeAll(true) doReturn(listOf(object : UserInfo() { init { @@ -128,7 +123,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { } })).`when`(userManager).getProfiles(anyInt()) - privacyItemController = PrivacyItemController(mContext) + privacyItemController = PrivacyItemController() } @Test @@ -137,7 +132,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { executor.runAllReady() verify(appOpsController).addCallback(eq(PrivacyItemController.OPS), any()) - verify(callback).privacyChanged(anyList()) + verify(callback).onPrivacyItemsChanged(anyList()) } @Test @@ -150,7 +145,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { executor.runAllReady() verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS), any()) - verify(callback).privacyChanged(emptyList()) + verify(callback).onPrivacyItemsChanged(emptyList()) } @Test @@ -161,7 +156,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { privacyItemController.addCallback(callback) executor.runAllReady() - verify(callback).privacyChanged(capture(argCaptor)) + verify(callback).onPrivacyItemsChanged(capture(argCaptor)) assertEquals(1, argCaptor.value.size) } @@ -204,11 +199,11 @@ class PrivacyItemControllerTest : SysuiTestCase() { val otherCallback = mock(PrivacyItemController.Callback::class.java) privacyItemController.addCallback(callback) executor.runAllReady() - verify(callback).privacyChanged(anyList()) + verify(callback).onPrivacyItemsChanged(anyList()) privacyItemController.addCallback(otherCallback) executor.runAllReady() - verify(otherCallback).privacyChanged(anyList()) + verify(otherCallback).onPrivacyItemsChanged(anyList()) // Adding a callback should not unnecessarily call previous ones verifyNoMoreInteractions(callback) } @@ -227,8 +222,8 @@ class PrivacyItemControllerTest : SysuiTestCase() { verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true) executor.runAllReady() - verify(callback).privacyChanged(anyList()) - verify(otherCallback).privacyChanged(anyList()) + verify(callback).onPrivacyItemsChanged(anyList()) + verify(otherCallback).onPrivacyItemsChanged(anyList()) } @Test @@ -246,8 +241,8 @@ class PrivacyItemControllerTest : SysuiTestCase() { privacyItemController.removeCallback(callback) argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true) executor.runAllReady() - verify(callback, never()).privacyChanged(anyList()) - verify(otherCallback).privacyChanged(anyList()) + verify(callback, never()).onPrivacyItemsChanged(anyList()) + verify(otherCallback).onPrivacyItemsChanged(anyList()) } @Test @@ -259,7 +254,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { executor.runAllReady() executor.runAllReady() - verify(callback).privacyChanged(capture(argCaptor)) + verify(callback).onPrivacyItemsChanged(capture(argCaptor)) assertEquals(1, argCaptor.value.size) assertThat(argCaptor.value, not(hasItem(nullValue()))) } @@ -276,15 +271,59 @@ class PrivacyItemControllerTest : SysuiTestCase() { @Test fun testNotListeningWhenIndicatorsDisabled() { - deviceConfigProxy.setProperty( - DeviceConfig.NAMESPACE_PRIVACY, - SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, - "false", - false - ) + changeAll(false) privacyItemController.addCallback(callback) executor.runAllReady() verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS), 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 + ) + } } \ No newline at end of file