diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java index cd3d6a84a352d..d7e0efc973cba 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java @@ -24,15 +24,15 @@ public class AppOpItem { private int mCode; private int mUid; private String mPackageName; - private long mTimeStarted; + private long mTimeStartedElapsed; private StringBuilder mState; private boolean mIsDisabled; - public AppOpItem(int code, int uid, String packageName, long timeStarted) { + public AppOpItem(int code, int uid, String packageName, long timeStartedElapsed) { this.mCode = code; this.mUid = uid; this.mPackageName = packageName; - this.mTimeStarted = timeStarted; + this.mTimeStartedElapsed = timeStartedElapsed; mState = new StringBuilder() .append("AppOpItem(") .append("Op code=").append(code).append(", ") @@ -53,8 +53,8 @@ public class AppOpItem { return mPackageName; } - public long getTimeStarted() { - return mTimeStarted; + public long getTimeStartedElapsed() { + return mTimeStartedElapsed; } public void setDisabled(boolean misDisabled) { diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index e53f5c97bb5c3..74f855b02a43e 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -50,6 +50,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; import com.android.systemui.util.Assert; +import com.android.systemui.util.time.SystemClock; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -83,8 +84,8 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon private final AppOpsManager mAppOps; private final AudioManager mAudioManager; private final LocationManager mLocationManager; - private final PackageManager mPackageManager; private final IndividualSensorPrivacyController mSensorPrivacyController; + private final SystemClock mClock; // mLocationProviderPackages are cached and updated only occasionally private static final long LOCATION_PROVIDER_UPDATE_FREQUENCY_MS = 30000; @@ -126,7 +127,8 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon PermissionFlagsCache cache, AudioManager audioManager, IndividualSensorPrivacyController sensorPrivacyController, - BroadcastDispatcher dispatcher + BroadcastDispatcher dispatcher, + SystemClock clock ) { mDispatcher = dispatcher; mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); @@ -142,8 +144,8 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon || mSensorPrivacyController.isSensorBlocked(MICROPHONE); mCameraDisabled = mSensorPrivacyController.isSensorBlocked(CAMERA); mLocationManager = context.getSystemService(LocationManager.class); - mPackageManager = context.getPackageManager(); mContext = context; + mClock = clock; dumpManager.registerDumpable(TAG, this); } @@ -252,7 +254,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon synchronized (mActiveItems) { AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName); if (item == null && active) { - item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); + item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime()); if (code == AppOpsManager.OP_RECORD_AUDIO) { item.setDisabled(isAnyRecordingPausedLocked(uid)); } else if (code == AppOpsManager.OP_CAMERA) { @@ -294,7 +296,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon synchronized (mNotedItems) { item = getAppOpItemLocked(mNotedItems, code, uid, packageName); if (item == null) { - item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); + item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime()); mNotedItems.add(item); if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); createdNew = true; diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt index 8a429e2e424d4..4c617edd8542c 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt @@ -50,8 +50,14 @@ enum class PrivacyType( fun getIcon(context: Context) = context.resources.getDrawable(iconId, context.theme) } -data class PrivacyItem(val privacyType: PrivacyType, val application: PrivacyApplication) { - fun toLog(): String = "(${privacyType.logName}, ${application.packageName}(${application.uid}))" +private const val UNKNOWN_TIMESTAMP = -1L +data class PrivacyItem( + val privacyType: PrivacyType, + val application: PrivacyApplication, + val timeStampElapsed: Long = UNKNOWN_TIMESTAMP +) { + val log = "(${privacyType.logName}, ${application.packageName}(${application.uid}), " + + "$timeStampElapsed)" } data class PrivacyApplication(val packageName: String, val uid: Int) diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt index 03d6154706ebe..f7e2a31ce727a 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -36,6 +36,7 @@ import com.android.systemui.privacy.logging.PrivacyLogger import com.android.systemui.settings.UserTracker import com.android.systemui.util.DeviceConfigProxy import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import java.io.FileDescriptor import java.io.PrintWriter import java.lang.ref.WeakReference @@ -50,6 +51,7 @@ class PrivacyItemController @Inject constructor( private val deviceConfigProxy: DeviceConfigProxy, private val userTracker: UserTracker, private val logger: PrivacyLogger, + private val systemClock: SystemClock, dumpManager: DumpManager ) : Dumpable { @@ -70,10 +72,9 @@ class PrivacyItemController @Inject constructor( 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_ALL_INDICATORS = false private const val DEFAULT_MIC_CAMERA = true private const val DEFAULT_LOCATION = false - const val TIME_TO_HOLD_INDICATORS = 5000L + @VisibleForTesting const val TIME_TO_HOLD_INDICATORS = 5000L } @VisibleForTesting @@ -95,8 +96,8 @@ class PrivacyItemController @Inject constructor( private var listening = false private val callbacks = mutableListOf>() private val internalUiExecutor = MyExecutor(uiExecutor) - private var holdingIndicators = false - private var holdIndicatorsCancelled: Runnable? = null + + private var holdingRunnableCanceler: Runnable? = null private val notifyChanges = Runnable { val list = privacyList @@ -108,11 +109,6 @@ class PrivacyItemController @Inject constructor( uiExecutor.execute(notifyChanges) } - private val stopHoldingAndNotifyChanges = Runnable { - updatePrivacyList(true) - uiExecutor.execute(notifyChanges) - } - var micCameraAvailable = isMicCameraEnabled() private set var locationAvailable = isLocationEnabled() @@ -189,14 +185,6 @@ 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) { @@ -261,14 +249,12 @@ class PrivacyItemController @Inject constructor( removeCallback(WeakReference(callback)) } - private fun updatePrivacyList(stopHolding: Boolean = false) { + private fun updatePrivacyList() { + holdingRunnableCanceler?.run()?.also { + holdingRunnableCanceler = null + } if (!listening) { privacyList = emptyList() - if (holdingIndicators) { - holdIndicatorsCancelled?.run() - logger.cancelIndicatorsHold() - holdingIndicators = false - } return } val list = appOpsController.getActiveAppOpsForUser(UserHandle.USER_ALL).filter { @@ -276,43 +262,36 @@ 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) + privacyList = processNewList(list) } /** - * 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]. + * Figure out which items have not been around for long enough and put them back in the list. * - * 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. + * Also schedule when we should check again to remove expired items. Because we always retrieve + * the current list, we have the latest info. + * + * @param list map of list retrieved from [AppOpsController]. + * @return a list that may have added items that should be kept for some time. */ - private fun processNewList(list: List, 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() + private fun processNewList(list: List): List { + logger.logRetrievedPrivacyItemsList(list) + + // Anything earlier than this timestamp can be removed + val removeBeforeTime = systemClock.elapsedRealtime() - TIME_TO_HOLD_INDICATORS + val mustKeep = privacyList.filter { it.timeStampElapsed > removeBeforeTime && it !in list } + + // There are items we must keep because they haven't been around for enough time. + if (mustKeep.isNotEmpty()) { + logger.logPrivacyItemsToHold(mustKeep) + val earliestTime = mustKeep.minByOrNull { it.timeStampElapsed }!!.timeStampElapsed + + // Update the list again when the earliest item should be removed. + val delay = earliestTime - removeBeforeTime + logger.logPrivacyItemsUpdateScheduled(delay) + holdingRunnableCanceler = bgExecutor.executeDelayed(updateListAndNotifyChanges, delay) } - // Else. We are not holding, we were not showing anything and the new list is empty. Ignore. + return list + mustKeep } private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? { @@ -329,7 +308,7 @@ class PrivacyItemController @Inject constructor( return null } val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) - return PrivacyItem(type, app) + return PrivacyItem(type, app, appOpItem.timeStartedElapsed) } interface Callback { 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 ebf6ae9be22f0..acce945736f03 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -22,10 +22,13 @@ import com.android.systemui.log.LogLevel import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.PrivacyLog import com.android.systemui.privacy.PrivacyDialog +import com.android.systemui.privacy.PrivacyItem +import java.text.SimpleDateFormat +import java.util.Locale import javax.inject.Inject private const val TAG = "PrivacyLog" - +private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) class PrivacyLogger @Inject constructor( @PrivacyLog private val buffer: LogBuffer ) { @@ -41,31 +44,29 @@ class PrivacyLogger @Inject constructor( }) } - fun logUpdatedPrivacyItemsList(listAsString: String) { + fun logRetrievedPrivacyItemsList(list: List) { log(LogLevel.INFO, { - str1 = listAsString + str1 = listToString(list) }, { - "Updated list: $str1" + "Retrieved list to process: $str1" }) } - fun startIndicatorsHold(time: Long) { + fun logPrivacyItemsToHold(list: List) { log(LogLevel.DEBUG, { - int1 = time.toInt() / 1000 + str1 = listToString(list) }, { - "Starting privacy indicators hold for $int1 seconds" + "Holding items: $str1" }) } - fun cancelIndicatorsHold() { - log(LogLevel.VERBOSE, {}, { - "Cancel privacy indicators hold" - }) - } - - fun finishIndicatorsHold() { - log(LogLevel.DEBUG, {}, { - "Finish privacy indicators hold" + fun logPrivacyItemsUpdateScheduled(delay: Long) { + log(LogLevel.INFO, { + val scheduledFor = System.currentTimeMillis() + delay + val formattedTimestamp = DATE_FORMAT.format(scheduledFor) + str1 = formattedTimestamp + }, { + "Updating items scheduled for $str1" }) } @@ -130,6 +131,10 @@ class PrivacyLogger @Inject constructor( }) } + private fun listToString(list: List): String { + return list.joinToString(separator = ", ", transform = PrivacyItem::log) + } + private inline fun log( logLevel: LogLevel, initializer: LogMessage.() -> Unit, diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java index 2526990dfd405..8252ff48bd816 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java @@ -53,6 +53,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -137,7 +138,8 @@ public class AppOpsControllerTest extends SysuiTestCase { mFlagsCache, mAudioManager, mSensorPrivacyController, - mDispatcher + mDispatcher, + new FakeSystemClock() ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt index f991e718122ea..2a8611f60a9f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerFlagsTest.kt @@ -69,7 +69,7 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() { private lateinit var executor: FakeExecutor private lateinit var deviceConfigProxy: DeviceConfigProxy - fun PrivacyItemController(): PrivacyItemController { + fun createPrivacyItemController(): PrivacyItemController { return PrivacyItemController( appOpsController, executor, @@ -77,6 +77,7 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() { deviceConfigProxy, userTracker, logger, + FakeSystemClock(), dumpManager) } @@ -86,7 +87,7 @@ class PrivacyItemControllerFlagsTest : SysuiTestCase() { executor = FakeExecutor(FakeSystemClock()) deviceConfigProxy = DeviceConfigProxyFake() - privacyItemController = PrivacyItemController() + privacyItemController = createPrivacyItemController() privacyItemController.addCallback(callback) executor.runAllReady() 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 b87c7a6ad2d95..bba1c6a00675c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt @@ -99,7 +99,10 @@ class PrivacyItemControllerTest : SysuiTestCase() { private lateinit var fakeClock: FakeSystemClock private lateinit var deviceConfigProxy: DeviceConfigProxy - fun PrivacyItemController(): PrivacyItemController { + private val elapsedTime: Long + get() = fakeClock.elapsedRealtime() + + fun createPrivacyItemController(): PrivacyItemController { return PrivacyItemController( appOpsController, executor, @@ -107,6 +110,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { deviceConfigProxy, userTracker, logger, + fakeClock, dumpManager) } @@ -123,7 +127,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { `when`(userTracker.userProfiles).thenReturn(listOf(UserInfo(CURRENT_USER_ID, "", 0))) - privacyItemController = PrivacyItemController() + privacyItemController = createPrivacyItemController() } @Test @@ -151,7 +155,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { @Test fun testDistinctItems() { doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0), - AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 1))) + AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0))) .`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) @@ -160,6 +164,18 @@ class PrivacyItemControllerTest : SysuiTestCase() { assertEquals(1, argCaptor.value.size) } + @Test + fun testSimilarItemsDifferentTimeStamp() { + doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0), + AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 1))) + .`when`(appOpsController).getActiveAppOpsForUser(anyInt()) + + privacyItemController.addCallback(callback) + executor.runAllReady() + verify(callback).onPrivacyItemsChanged(capture(argCaptor)) + assertEquals(2, argCaptor.value.size) + } + @Test fun testRegisterCallback() { privacyItemController.addCallback(callback) @@ -251,7 +267,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { @Test fun testListShouldBeCopy() { val list = listOf(PrivacyItem(PrivacyType.TYPE_CAMERA, - PrivacyApplication("", TEST_UID))) + PrivacyApplication("", TEST_UID), 0)) privacyItemController.privacyList = list val privacyList = privacyItemController.privacyList assertEquals(list, privacyList) @@ -334,14 +350,15 @@ class PrivacyItemControllerTest : SysuiTestCase() { val expected = PrivacyItem( PrivacyType.TYPE_LOCATION, - PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID) + PrivacyApplication(TEST_PACKAGE_NAME, TEST_UID), + 0 ) - val captor = argumentCaptor() - verify(logger, atLeastOnce()).logUpdatedPrivacyItemsList(capture(captor)) + val captor = argumentCaptor>() + verify(logger, atLeastOnce()).logRetrievedPrivacyItemsList(capture(captor)) // Let's look at the last log val values = captor.allValues - assertTrue(values[values.size - 1].contains(expected.toLog())) + assertTrue(values[values.size - 1].contains(expected)) } @Test @@ -424,7 +441,7 @@ class PrivacyItemControllerTest : SysuiTestCase() { @Test fun testPassageOfTimeDoesNotRemoveIndicators() { doReturn(listOf( - AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) + AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) privacyItemController.addCallback(callback) @@ -437,86 +454,75 @@ class PrivacyItemControllerTest : SysuiTestCase() { } @Test - fun testHoldingAfterEmptyBeforeTimeExpires() { + fun testNotHeldAfterTimeIsOff() { + // Start with some element at time 0 doReturn(listOf( - AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) + AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) - privacyItemController.addCallback(callback) executor.runAllReady() - verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) + // Then remove it at time HOLD + 1 + doReturn(emptyList()).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) + fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS + 1) - `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) + verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) 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() - + // See it's not there verify(callback).onPrivacyItemsChanged(emptyList()) assertTrue(privacyItemController.privacyList.isEmpty()) } @Test - fun testHoldingStopsIfNewIndicatorsAppear() { + fun testElementNotRemovedBeforeHoldTime() { + // Start with some element at time 0 doReturn(listOf( - AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0) + AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime) )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) - privacyItemController.addCallback(callback) executor.runAllReady() - verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) + // Then remove it at time HOLD - 1 + doReturn(emptyList()).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) + fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS - 1) - `when`(appOpsController.getActiveAppOpsForUser(anyInt())).thenReturn(emptyList()) + verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) 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() - + // See it's still there verify(callback, never()).onPrivacyItemsChanged(emptyList()) - verify(callback, atLeastOnce()).onPrivacyItemsChanged(capture(argCaptor)) + assertTrue(privacyItemController.privacyList.isNotEmpty()) + } - val lastList = argCaptor.allValues.last() - assertEquals(1, lastList.size) - assertEquals(PrivacyType.TYPE_MICROPHONE, lastList.single().privacyType) + @Test + fun testElementAutoRemovedAfterHoldTime() { + // Start with some element at time 0 + doReturn(listOf( + AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, elapsedTime) + )).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) + privacyItemController.addCallback(callback) + executor.runAllReady() + + // Then remove it at time HOLD - 1 + doReturn(emptyList()).`when`(appOpsController).getActiveAppOpsForUser(anyInt()) + fakeClock.advanceTime(PrivacyItemController.TIME_TO_HOLD_INDICATORS - 1) + + verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) + argCaptorCallback.value.onActiveStateChanged( + AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, false) + executor.runAllReady() + + fakeClock.advanceTime(2L) + executor.runAllReady() + + // See it was auto-removed + verify(callback).onPrivacyItemsChanged(emptyList()) + assertTrue(privacyItemController.privacyList.isEmpty()) } private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)