Loading packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +60 −5 Original line number Diff line number Diff line Loading @@ -46,7 +46,7 @@ import javax.inject.Inject class PrivacyItemController @Inject constructor( private val appOpsController: AppOpsController, @Main uiExecutor: DelayableExecutor, @Background private val bgExecutor: Executor, @Background private val bgExecutor: DelayableExecutor, private val deviceConfigProxy: DeviceConfigProxy, private val userTracker: UserTracker, private val logger: PrivacyLogger, Loading Loading @@ -75,6 +75,7 @@ class PrivacyItemController @Inject constructor( private const val DEFAULT_ALL_INDICATORS = false private const val DEFAULT_MIC_CAMERA = true private const val DEFAULT_LOCATION = false const val TIME_TO_HOLD_INDICATORS = 5000L } @VisibleForTesting Loading @@ -101,6 +102,8 @@ class PrivacyItemController @Inject constructor( private var listening = false private val callbacks = mutableListOf<WeakReference<Callback>>() private val internalUiExecutor = MyExecutor(uiExecutor) private var holdingIndicators = false private var holdIndicatorsCancelled: Runnable? = null private val notifyChanges = Runnable { val list = privacyList Loading @@ -112,6 +115,11 @@ class PrivacyItemController @Inject constructor( uiExecutor.execute(notifyChanges) } private val stopHoldingAndNotifyChanges = Runnable { updatePrivacyList(true) uiExecutor.execute(notifyChanges) } var allIndicatorsAvailable = isAllIndicatorsEnabled() private set var micCameraAvailable = isMicCameraEnabled() Loading Loading @@ -193,6 +201,14 @@ class PrivacyItemController @Inject constructor( userTracker.addCallback(userTrackerCallback, bgExecutor) } private fun setHoldTimer() { holdIndicatorsCancelled?.run() holdingIndicators = true holdIndicatorsCancelled = bgExecutor.executeDelayed({ stopHoldingAndNotifyChanges.run() }, TIME_TO_HOLD_INDICATORS) } private fun update(updateUsers: Boolean) { bgExecutor.execute { if (updateUsers) { Loading Loading @@ -257,9 +273,14 @@ class PrivacyItemController @Inject constructor( removeCallback(WeakReference(callback)) } private fun updatePrivacyList() { private fun updatePrivacyList(stopHolding: Boolean = false) { if (!listening) { privacyList = emptyList() if (holdingIndicators) { holdIndicatorsCancelled?.run() logger.cancelIndicatorsHold() holdingIndicators = false } return } val list = appOpsController.getActiveAppOpsForUser(UserHandle.USER_ALL).filter { Loading @@ -267,9 +288,43 @@ class PrivacyItemController @Inject constructor( it.code == AppOpsManager.OP_PHONE_CALL_MICROPHONE || it.code == AppOpsManager.OP_PHONE_CALL_CAMERA }.mapNotNull { toPrivacyItem(it) }.distinct() processNewList(list, stopHolding) } /** * The controller will only go from indicators to no indicators (and notify its listeners), if * [TIME_TO_HOLD_INDICATORS] has passed since it received an empty list from [AppOpsController]. * * If holding the last list (in the [TIME_TO_HOLD_INDICATORS] period) and a new non-empty list * is retrieved from [AppOpsController], it will stop holding and notify about the new list. */ private fun processNewList(list: List<PrivacyItem>, stopHolding: Boolean) { if (list.isNotEmpty()) { // The new elements is not empty, so regardless of whether we are holding or not, we // clear the holding flag and cancel the delayed runnable. if (holdingIndicators) { holdIndicatorsCancelled?.run() logger.cancelIndicatorsHold() holdingIndicators = false } logger.logUpdatedPrivacyItemsList( list.joinToString(separator = ", ", transform = PrivacyItem::toLog)) privacyList = list } else if (holdingIndicators && stopHolding) { // We are holding indicators, received an empty list and were told to stop holding. logger.finishIndicatorsHold() logger.logUpdatedPrivacyItemsList("") holdingIndicators = false privacyList = list } else if (holdingIndicators && !stopHolding) { // Empty list while we are holding. Ignore } else if (!holdingIndicators && privacyList.isNotEmpty()) { // We are not holding, we were showing some indicators but now we should show nothing. // Start holding. logger.startIndicatorsHold(TIME_TO_HOLD_INDICATORS) setHoldTimer() } // Else. We are not holding, we were not showing anything and the new list is empty. Ignore. } private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? { Loading packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +20 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,26 @@ class PrivacyLogger @Inject constructor( }) } fun startIndicatorsHold(time: Long) { log(LogLevel.DEBUG, { int1 = time.toInt() / 1000 }, { "Starting privacy indicators hold for $int1 seconds" }) } fun cancelIndicatorsHold() { log(LogLevel.VERBOSE, {}, { "Cancel privacy indicators hold" }) } fun finishIndicatorsHold() { log(LogLevel.DEBUG, {}, { "Finish privacy indicators hold" }) } fun logCurrentProfilesChanged(profiles: List<Int>) { log(LogLevel.INFO, { str1 = profiles.toString() Loading packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt +101 −1 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { private lateinit var privacyItemController: PrivacyItemController private lateinit var executor: FakeExecutor private lateinit var fakeClock: FakeSystemClock private lateinit var deviceConfigProxy: DeviceConfigProxy fun PrivacyItemController(): PrivacyItemController { Loading @@ -113,7 +114,8 @@ class PrivacyItemControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(FakeSystemClock()) fakeClock = FakeSystemClock() executor = FakeExecutor(fakeClock) deviceConfigProxy = DeviceConfigProxyFake() // Listen to everything by default Loading Loading @@ -420,6 +422,104 @@ class PrivacyItemControllerTest : SysuiTestCase() { assertEquals(PrivacyType.TYPE_MICROPHONE, argCaptor.value[0].privacyType) } @Test fun testPassageOfTimeDoesNotRemoveIndicators() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS * 10) executor.runAllReady() verify(callback, never()).onPrivacyItemsChanged(emptyList()) assertTrue(privacyItemController.privacyList.isNotEmpty()) } @Test fun testHoldingAfterEmptyBeforeTimeExpires() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) executor.runAllReady() verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) executor.runAllReady() fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS / 5) executor.runAllReady() verify(callback, never()).onPrivacyItemsChanged(emptyList()) assertTrue(privacyItemController.privacyList.isNotEmpty()) } @Test fun testAfterHoldingIndicatorsAreEmpty() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) executor.runAllReady() verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) executor.runAllReady() executor.advanceClockToLast() executor.runAllReady() verify(callback).onPrivacyItemsChanged(emptyList()) assertTrue(privacyItemController.privacyList.isEmpty()) } @Test fun testHoldingStopsIfNewIndicatorsAppear() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) executor.runAllReady() verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) executor.runAllReady() fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS / 2) executor.runAllReady() doReturn(listOf( AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true) executor.runAllReady() executor.advanceClockToLast() executor.runAllReady() verify(callback, never()).onPrivacyItemsChanged(emptyList()) verify(callback, atLeastOnce()).onPrivacyItemsChanged(capture(argCaptor)) val lastList = argCaptor.allValues.last() assertEquals(1, lastList.size) assertEquals(PrivacyType.TYPE_MICROPHONE, lastList.single().privacyType) } private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value) private fun changeAll(value: Boolean?) = changeProperty(ALL_INDICATORS, value) Loading Loading
packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +60 −5 Original line number Diff line number Diff line Loading @@ -46,7 +46,7 @@ import javax.inject.Inject class PrivacyItemController @Inject constructor( private val appOpsController: AppOpsController, @Main uiExecutor: DelayableExecutor, @Background private val bgExecutor: Executor, @Background private val bgExecutor: DelayableExecutor, private val deviceConfigProxy: DeviceConfigProxy, private val userTracker: UserTracker, private val logger: PrivacyLogger, Loading Loading @@ -75,6 +75,7 @@ class PrivacyItemController @Inject constructor( private const val DEFAULT_ALL_INDICATORS = false private const val DEFAULT_MIC_CAMERA = true private const val DEFAULT_LOCATION = false const val TIME_TO_HOLD_INDICATORS = 5000L } @VisibleForTesting Loading @@ -101,6 +102,8 @@ class PrivacyItemController @Inject constructor( private var listening = false private val callbacks = mutableListOf<WeakReference<Callback>>() private val internalUiExecutor = MyExecutor(uiExecutor) private var holdingIndicators = false private var holdIndicatorsCancelled: Runnable? = null private val notifyChanges = Runnable { val list = privacyList Loading @@ -112,6 +115,11 @@ class PrivacyItemController @Inject constructor( uiExecutor.execute(notifyChanges) } private val stopHoldingAndNotifyChanges = Runnable { updatePrivacyList(true) uiExecutor.execute(notifyChanges) } var allIndicatorsAvailable = isAllIndicatorsEnabled() private set var micCameraAvailable = isMicCameraEnabled() Loading Loading @@ -193,6 +201,14 @@ class PrivacyItemController @Inject constructor( userTracker.addCallback(userTrackerCallback, bgExecutor) } private fun setHoldTimer() { holdIndicatorsCancelled?.run() holdingIndicators = true holdIndicatorsCancelled = bgExecutor.executeDelayed({ stopHoldingAndNotifyChanges.run() }, TIME_TO_HOLD_INDICATORS) } private fun update(updateUsers: Boolean) { bgExecutor.execute { if (updateUsers) { Loading Loading @@ -257,9 +273,14 @@ class PrivacyItemController @Inject constructor( removeCallback(WeakReference(callback)) } private fun updatePrivacyList() { private fun updatePrivacyList(stopHolding: Boolean = false) { if (!listening) { privacyList = emptyList() if (holdingIndicators) { holdIndicatorsCancelled?.run() logger.cancelIndicatorsHold() holdingIndicators = false } return } val list = appOpsController.getActiveAppOpsForUser(UserHandle.USER_ALL).filter { Loading @@ -267,9 +288,43 @@ class PrivacyItemController @Inject constructor( it.code == AppOpsManager.OP_PHONE_CALL_MICROPHONE || it.code == AppOpsManager.OP_PHONE_CALL_CAMERA }.mapNotNull { toPrivacyItem(it) }.distinct() processNewList(list, stopHolding) } /** * The controller will only go from indicators to no indicators (and notify its listeners), if * [TIME_TO_HOLD_INDICATORS] has passed since it received an empty list from [AppOpsController]. * * If holding the last list (in the [TIME_TO_HOLD_INDICATORS] period) and a new non-empty list * is retrieved from [AppOpsController], it will stop holding and notify about the new list. */ private fun processNewList(list: List<PrivacyItem>, stopHolding: Boolean) { if (list.isNotEmpty()) { // The new elements is not empty, so regardless of whether we are holding or not, we // clear the holding flag and cancel the delayed runnable. if (holdingIndicators) { holdIndicatorsCancelled?.run() logger.cancelIndicatorsHold() holdingIndicators = false } logger.logUpdatedPrivacyItemsList( list.joinToString(separator = ", ", transform = PrivacyItem::toLog)) privacyList = list } else if (holdingIndicators && stopHolding) { // We are holding indicators, received an empty list and were told to stop holding. logger.finishIndicatorsHold() logger.logUpdatedPrivacyItemsList("") holdingIndicators = false privacyList = list } else if (holdingIndicators && !stopHolding) { // Empty list while we are holding. Ignore } else if (!holdingIndicators && privacyList.isNotEmpty()) { // We are not holding, we were showing some indicators but now we should show nothing. // Start holding. logger.startIndicatorsHold(TIME_TO_HOLD_INDICATORS) setHoldTimer() } // Else. We are not holding, we were not showing anything and the new list is empty. Ignore. } private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? { Loading
packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +20 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,26 @@ class PrivacyLogger @Inject constructor( }) } fun startIndicatorsHold(time: Long) { log(LogLevel.DEBUG, { int1 = time.toInt() / 1000 }, { "Starting privacy indicators hold for $int1 seconds" }) } fun cancelIndicatorsHold() { log(LogLevel.VERBOSE, {}, { "Cancel privacy indicators hold" }) } fun finishIndicatorsHold() { log(LogLevel.DEBUG, {}, { "Finish privacy indicators hold" }) } fun logCurrentProfilesChanged(profiles: List<Int>) { log(LogLevel.INFO, { str1 = profiles.toString() Loading
packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt +101 −1 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { private lateinit var privacyItemController: PrivacyItemController private lateinit var executor: FakeExecutor private lateinit var fakeClock: FakeSystemClock private lateinit var deviceConfigProxy: DeviceConfigProxy fun PrivacyItemController(): PrivacyItemController { Loading @@ -113,7 +114,8 @@ class PrivacyItemControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(FakeSystemClock()) fakeClock = FakeSystemClock() executor = FakeExecutor(fakeClock) deviceConfigProxy = DeviceConfigProxyFake() // Listen to everything by default Loading Loading @@ -420,6 +422,104 @@ class PrivacyItemControllerTest : SysuiTestCase() { assertEquals(PrivacyType.TYPE_MICROPHONE, argCaptor.value[0].privacyType) } @Test fun testPassageOfTimeDoesNotRemoveIndicators() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS * 10) executor.runAllReady() verify(callback, never()).onPrivacyItemsChanged(emptyList()) assertTrue(privacyItemController.privacyList.isNotEmpty()) } @Test fun testHoldingAfterEmptyBeforeTimeExpires() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) executor.runAllReady() verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) executor.runAllReady() fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS / 5) executor.runAllReady() verify(callback, never()).onPrivacyItemsChanged(emptyList()) assertTrue(privacyItemController.privacyList.isNotEmpty()) } @Test fun testAfterHoldingIndicatorsAreEmpty() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) executor.runAllReady() verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) executor.runAllReady() executor.advanceClockToLast() executor.runAllReady() verify(callback).onPrivacyItemsChanged(emptyList()) assertTrue(privacyItemController.privacyList.isEmpty()) } @Test fun testHoldingStopsIfNewIndicatorsAppear() { doReturn(listOf( AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) executor.runAllReady() verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) executor.runAllReady() fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS / 2) executor.runAllReady() doReturn(listOf( AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true) executor.runAllReady() executor.advanceClockToLast() executor.runAllReady() verify(callback, never()).onPrivacyItemsChanged(emptyList()) verify(callback, atLeastOnce()).onPrivacyItemsChanged(capture(argCaptor)) val lastList = argCaptor.allValues.last() assertEquals(1, lastList.size) assertEquals(PrivacyType.TYPE_MICROPHONE, lastList.single().privacyType) } private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value) private fun changeAll(value: Boolean?) = changeProperty(ALL_INDICATORS, value) Loading