diff options
| author | 2021-01-14 14:08:24 -0500 | |
|---|---|---|
| committer | 2021-01-14 14:09:56 -0500 | |
| commit | d78ae0f93c2771e996119c28890298c64a3132ad (patch) | |
| tree | 9e6458712537f8f53bd7a7ebdd0362488c07d6b7 | |
| parent | 23c36e41d14c476cef430c90413b4d9a723eb914 (diff) | |
Hold privacy indicators for 5 sec
If privacy indicators would go from showing to not showing (no active
uses), they will be held for 5 extra seconds before disappearing.
This timeout is reset if new indicators become active.
Test: atest SystemUITests
Fixes: 177449285
Change-Id: Icda7d395ed01a28a482911662624a871ac7cd757
3 files changed, 181 insertions, 6 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt index b2c2aa309811..f72900b7f6bf 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -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, @@ -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 @@ -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 @@ -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() @@ -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) { @@ -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 { @@ -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() - logger.logUpdatedPrivacyItemsList( - list.joinToString(separator = ", ", transform = PrivacyItem::toLog)) - privacyList = list + 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? { diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt index c88676e713b3..f3b8d2e5fbc0 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -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() 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 a8b305614a4a..7ca468edfd9c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt @@ -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 { @@ -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 @@ -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) |