Loading packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt +113 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.core.Logger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -136,6 +137,118 @@ class ActivityManagerRepositoryTest : SysuiTestCase() { assertThat(latest).isFalse() } @Test fun createAppVisibilityFlow_fetchesInitialValue_trueWithLastVisibleTime() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND) fakeSystemClock.setCurrentTimeMillis(5000) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) assertThat(latest!!.isAppCurrentlyVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(5000) } @Test fun createAppVisibilityFlow_fetchesInitialValue_falseWithoutLastVisibleTime() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) fakeSystemClock.setCurrentTimeMillis(5000) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) assertThat(latest!!.isAppCurrentlyVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() } @Test fun createAppVisibilityFlow_getsImportanceUpdates_updatesLastVisibleTimeOnlyWhenVisible() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) fakeSystemClock.setCurrentTimeMillis(5000) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) assertThat(latest!!.isAppCurrentlyVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) val listener = listenerCaptor.firstValue // WHEN the app becomes visible fakeSystemClock.setCurrentTimeMillis(7000) listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) // THEN the status and lastAppVisibleTime are updated assertThat(latest!!.isAppCurrentlyVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) // WHEN the app is no longer visible listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING) // THEN the lastAppVisibleTime is preserved assertThat(latest!!.isAppCurrentlyVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) // WHEN the app is visible again fakeSystemClock.setCurrentTimeMillis(9000) listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) // THEN the lastAppVisibleTime is updated assertThat(latest!!.isAppCurrentlyVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) } @Test fun createAppVisibilityFlow_ignoresUpdatesForOtherUids() = kosmos.runTest { val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) val listener = listenerCaptor.firstValue listener.onUidImportance(THIS_UID, IMPORTANCE_GONE) assertThat(latest!!.isAppCurrentlyVisible).isFalse() // WHEN another UID becomes foreground listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND) // THEN this UID still stays not visible assertThat(latest!!.isAppCurrentlyVisible).isFalse() } @Test fun createAppVisibilityFlow_securityExceptionOnUidRegistration_ok() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) whenever(activityManager.addOnUidImportanceListener(any(), any())) .thenThrow(SecurityException()) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) // Verify no crash, and we get a value emitted assertThat(latest!!.isAppCurrentlyVisible).isFalse() } /** Regression test for b/216248574. */ @Test fun createAppVisibilityFlow_getUidImportanceThrowsException_ok() = kosmos.runTest { whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException()) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) // Verify no crash, and we get a value emitted assertThat(latest!!.isAppCurrentlyVisible).isFalse() } companion object { private const val THIS_UID = 558 private const val LOG_TAG = "LogTag" Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +50 −17 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -183,7 +184,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = null, promotedContent = PROMOTED_CONTENT, ), 32L, creationTime = 32L, ) val latest by collectLastValue(underTest.notificationChip) Loading Loading @@ -246,7 +247,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ) val underTest = factory.create(startingNotif, 123L) val underTest = factory.create(startingNotif, creationTime = 123L) val latest by collectLastValue(underTest.notificationChip) assertThat(latest).isNotNull() Loading Loading @@ -306,9 +307,10 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { } @Test fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() = fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrueWithTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = true fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( Loading @@ -325,12 +327,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) } @Test fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() = fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalseWithNoTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( Loading @@ -347,11 +351,15 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() } @Test fun notificationChip_updatesWhenAppIsVisible() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( activeNotificationModel( Loading @@ -365,32 +373,39 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) activityManagerRepository.fake.setIsAppVisible(UID, false) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() activityManagerRepository.fake.setIsAppVisible(UID, true) fakeSystemClock.setCurrentTimeMillis(11000) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) activityManagerRepository.fake.setIsAppVisible(UID, false) fakeSystemClock.setCurrentTimeMillis(13000) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) fakeSystemClock.setCurrentTimeMillis(15000) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(15000) } // Note: This test is theoretically impossible because the notification key should contain the // UID, so if the UID changes then the key would also change and a new interactor would be // created. But, test it just in case. @Test fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() = fun notificationChip_updatedUid_newUidIsIgnoredButOtherDataNotIgnored() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false val hiddenUid = 100 val shownUid = 101 val originalUid = 100 val newUid = 101 val underTest = factory.create( activeNotificationModel( key = "notif", uid = hiddenUid, uid = originalUid, statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ), Loading @@ -402,16 +417,34 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { // WHEN the notif gets a new UID that starts as visible activityManagerRepository.fake.startingIsAppVisibleValue = true val newPromotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.shortCriticalText = "Arrived" } val newPromotedContent = newPromotedContentBuilder.build() underTest.setNotification( activeNotificationModel( key = "notif", uid = shownUid, uid = newUid, statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, promotedContent = newPromotedContent, ) ) // THEN we re-fetch the app visibility state with the new UID // THEN we do update other fields like promoted content assertThat(latest!!.promotedContent).isEqualTo(newPromotedContent) // THEN we don't fetch the app visibility state for the new UID assertThat(latest!!.isAppVisible).isFalse() // AND don't listen to updates for the new UID activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = false) activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = true) assertThat(latest!!.isAppVisible).isFalse() // AND we still use updates from the old UID // TODO(b/364653005): This particular behavior isn't great, can we do better? activityManagerRepository.fake.setIsAppVisible(originalUid, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() } Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +241 −19 File changed.Preview size limit exceeded, changes collapsed. Show changes packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt 0 → 100644 +29 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.activity.data.model /** Describes an app's previous and current visibility to the user. */ data class AppVisibilityModel( /** True if the app is currently visible to the user and false otherwise. */ val isAppCurrentlyVisible: Boolean = false, /** * The last time this app became visible to the user, in * [com.android.systemui.util.time.SystemClock.currentTimeMillis] units. Null if the app hasn't * become visible since the flow started collection. */ val lastAppVisibleTime: Long? = null, ) packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt +46 −0 Original line number Diff line number Diff line Loading @@ -18,9 +18,11 @@ package com.android.systemui.activity.data.repository import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.core.Logger import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext Loading @@ -29,9 +31,23 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan /** Repository for interfacing with [ActivityManager]. */ interface ActivityManagerRepository { /** * Given a UID, creates a flow that emits details about when the process with the given UID was * and is visible to the user. * * @param identifyingLogTag a tag identifying who created this flow, used for logging. */ fun createAppVisibilityFlow( creationUid: Int, logger: Logger, identifyingLogTag: String, ): Flow<AppVisibilityModel> /** * Given a UID, creates a flow that emits true when the process with the given UID is visible to * the user and false otherwise. Loading @@ -50,8 +66,38 @@ class ActivityManagerRepositoryImpl @Inject constructor( @Background private val backgroundContext: CoroutineContext, private val systemClock: SystemClock, private val activityManager: ActivityManager, ) : ActivityManagerRepository { override fun createAppVisibilityFlow( creationUid: Int, logger: Logger, identifyingLogTag: String, ): Flow<AppVisibilityModel> { return createIsAppVisibleFlow(creationUid, logger, identifyingLogTag) .distinctUntilChanged() .scan(initial = AppVisibilityModel()) { oldState: AppVisibilityModel, newIsVisible: Boolean -> if (newIsVisible) { val lastAppVisibleTime = systemClock.currentTimeMillis() logger.d({ "$str1: Setting lastAppVisibleTime=$long1" }) { str1 = identifyingLogTag long1 = lastAppVisibleTime } AppVisibilityModel( isAppCurrentlyVisible = true, lastAppVisibleTime = lastAppVisibleTime, ) } else { // Reset the current status while maintaining the lastAppVisibleTime oldState.copy(isAppCurrentlyVisible = false) } } .distinctUntilChanged() } override fun createIsAppVisibleFlow( creationUid: Int, logger: Logger, Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt +113 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.core.Logger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -136,6 +137,118 @@ class ActivityManagerRepositoryTest : SysuiTestCase() { assertThat(latest).isFalse() } @Test fun createAppVisibilityFlow_fetchesInitialValue_trueWithLastVisibleTime() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND) fakeSystemClock.setCurrentTimeMillis(5000) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) assertThat(latest!!.isAppCurrentlyVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(5000) } @Test fun createAppVisibilityFlow_fetchesInitialValue_falseWithoutLastVisibleTime() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) fakeSystemClock.setCurrentTimeMillis(5000) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) assertThat(latest!!.isAppCurrentlyVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() } @Test fun createAppVisibilityFlow_getsImportanceUpdates_updatesLastVisibleTimeOnlyWhenVisible() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) fakeSystemClock.setCurrentTimeMillis(5000) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) assertThat(latest!!.isAppCurrentlyVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) val listener = listenerCaptor.firstValue // WHEN the app becomes visible fakeSystemClock.setCurrentTimeMillis(7000) listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) // THEN the status and lastAppVisibleTime are updated assertThat(latest!!.isAppCurrentlyVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) // WHEN the app is no longer visible listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING) // THEN the lastAppVisibleTime is preserved assertThat(latest!!.isAppCurrentlyVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) // WHEN the app is visible again fakeSystemClock.setCurrentTimeMillis(9000) listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) // THEN the lastAppVisibleTime is updated assertThat(latest!!.isAppCurrentlyVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) } @Test fun createAppVisibilityFlow_ignoresUpdatesForOtherUids() = kosmos.runTest { val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) val listener = listenerCaptor.firstValue listener.onUidImportance(THIS_UID, IMPORTANCE_GONE) assertThat(latest!!.isAppCurrentlyVisible).isFalse() // WHEN another UID becomes foreground listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND) // THEN this UID still stays not visible assertThat(latest!!.isAppCurrentlyVisible).isFalse() } @Test fun createAppVisibilityFlow_securityExceptionOnUidRegistration_ok() = kosmos.runTest { whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) whenever(activityManager.addOnUidImportanceListener(any(), any())) .thenThrow(SecurityException()) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) // Verify no crash, and we get a value emitted assertThat(latest!!.isAppCurrentlyVisible).isFalse() } /** Regression test for b/216248574. */ @Test fun createAppVisibilityFlow_getUidImportanceThrowsException_ok() = kosmos.runTest { whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException()) val latest by collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) // Verify no crash, and we get a value emitted assertThat(latest!!.isAppCurrentlyVisible).isFalse() } companion object { private const val THIS_UID = 558 private const val LOG_TAG = "LogTag" Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +50 −17 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -183,7 +184,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = null, promotedContent = PROMOTED_CONTENT, ), 32L, creationTime = 32L, ) val latest by collectLastValue(underTest.notificationChip) Loading Loading @@ -246,7 +247,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ) val underTest = factory.create(startingNotif, 123L) val underTest = factory.create(startingNotif, creationTime = 123L) val latest by collectLastValue(underTest.notificationChip) assertThat(latest).isNotNull() Loading Loading @@ -306,9 +307,10 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { } @Test fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() = fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrueWithTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = true fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( Loading @@ -325,12 +327,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) } @Test fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() = fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalseWithNoTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( Loading @@ -347,11 +351,15 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() } @Test fun notificationChip_updatesWhenAppIsVisible() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( activeNotificationModel( Loading @@ -365,32 +373,39 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) activityManagerRepository.fake.setIsAppVisible(UID, false) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isNull() activityManagerRepository.fake.setIsAppVisible(UID, true) fakeSystemClock.setCurrentTimeMillis(11000) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) activityManagerRepository.fake.setIsAppVisible(UID, false) fakeSystemClock.setCurrentTimeMillis(13000) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) fakeSystemClock.setCurrentTimeMillis(15000) activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() assertThat(latest!!.lastAppVisibleTime).isEqualTo(15000) } // Note: This test is theoretically impossible because the notification key should contain the // UID, so if the UID changes then the key would also change and a new interactor would be // created. But, test it just in case. @Test fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() = fun notificationChip_updatedUid_newUidIsIgnoredButOtherDataNotIgnored() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false val hiddenUid = 100 val shownUid = 101 val originalUid = 100 val newUid = 101 val underTest = factory.create( activeNotificationModel( key = "notif", uid = hiddenUid, uid = originalUid, statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ), Loading @@ -402,16 +417,34 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { // WHEN the notif gets a new UID that starts as visible activityManagerRepository.fake.startingIsAppVisibleValue = true val newPromotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.shortCriticalText = "Arrived" } val newPromotedContent = newPromotedContentBuilder.build() underTest.setNotification( activeNotificationModel( key = "notif", uid = shownUid, uid = newUid, statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, promotedContent = newPromotedContent, ) ) // THEN we re-fetch the app visibility state with the new UID // THEN we do update other fields like promoted content assertThat(latest!!.promotedContent).isEqualTo(newPromotedContent) // THEN we don't fetch the app visibility state for the new UID assertThat(latest!!.isAppVisible).isFalse() // AND don't listen to updates for the new UID activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = false) activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = true) assertThat(latest!!.isAppVisible).isFalse() // AND we still use updates from the old UID // TODO(b/364653005): This particular behavior isn't great, can we do better? activityManagerRepository.fake.setIsAppVisible(originalUid, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +241 −19 File changed.Preview size limit exceeded, changes collapsed. Show changes
packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt 0 → 100644 +29 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.activity.data.model /** Describes an app's previous and current visibility to the user. */ data class AppVisibilityModel( /** True if the app is currently visible to the user and false otherwise. */ val isAppCurrentlyVisible: Boolean = false, /** * The last time this app became visible to the user, in * [com.android.systemui.util.time.SystemClock.currentTimeMillis] units. Null if the app hasn't * become visible since the flow started collection. */ val lastAppVisibleTime: Long? = null, )
packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt +46 −0 Original line number Diff line number Diff line Loading @@ -18,9 +18,11 @@ package com.android.systemui.activity.data.repository import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.core.Logger import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext Loading @@ -29,9 +31,23 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan /** Repository for interfacing with [ActivityManager]. */ interface ActivityManagerRepository { /** * Given a UID, creates a flow that emits details about when the process with the given UID was * and is visible to the user. * * @param identifyingLogTag a tag identifying who created this flow, used for logging. */ fun createAppVisibilityFlow( creationUid: Int, logger: Logger, identifyingLogTag: String, ): Flow<AppVisibilityModel> /** * Given a UID, creates a flow that emits true when the process with the given UID is visible to * the user and false otherwise. Loading @@ -50,8 +66,38 @@ class ActivityManagerRepositoryImpl @Inject constructor( @Background private val backgroundContext: CoroutineContext, private val systemClock: SystemClock, private val activityManager: ActivityManager, ) : ActivityManagerRepository { override fun createAppVisibilityFlow( creationUid: Int, logger: Logger, identifyingLogTag: String, ): Flow<AppVisibilityModel> { return createIsAppVisibleFlow(creationUid, logger, identifyingLogTag) .distinctUntilChanged() .scan(initial = AppVisibilityModel()) { oldState: AppVisibilityModel, newIsVisible: Boolean -> if (newIsVisible) { val lastAppVisibleTime = systemClock.currentTimeMillis() logger.d({ "$str1: Setting lastAppVisibleTime=$long1" }) { str1 = identifyingLogTag long1 = lastAppVisibleTime } AppVisibilityModel( isAppCurrentlyVisible = true, lastAppVisibleTime = lastAppVisibleTime, ) } else { // Reset the current status while maintaining the lastAppVisibleTime oldState.copy(isAppCurrentlyVisible = false) } } .distinctUntilChanged() } override fun createIsAppVisibleFlow( creationUid: Int, logger: Logger, Loading