Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +51 −11 Original line number Diff line number Diff line Loading @@ -24,6 +24,10 @@ import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.expansionChanges Loading @@ -40,22 +44,26 @@ import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import kotlinx.coroutines.yield import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds /** * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section Loading @@ -69,6 +77,7 @@ constructor( private val headsUpManager: HeadsUpManager, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, private val keyguardRepository: KeyguardRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val notifPipelineFlags: NotifPipelineFlags, @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, Loading Loading @@ -99,21 +108,46 @@ constructor( } private suspend fun trackUnseenNotificationsWhileUnlocked() { // Whether or not we're actively tracking unseen notifications to mark them as seen when // appropriate. val isTrackingUnseen: Flow<Boolean> = keyguardRepository.isKeyguardShowing // transformLatest so that we can cancel listening to keyguard transitions once // isKeyguardShowing changes (after a successful transition to the keyguard). .transformLatest { isShowing -> if (isShowing) { // If the keyguard is showing, we're not tracking unseen. emit(false) } else { // If the keyguard stops showing, then start tracking unseen notifications. emit(true) // If the screen is turning off, stop tracking, but if that transition is // cancelled, then start again. emitAll( keyguardTransitionRepository.transitions .map { step -> !step.isScreenTurningOff } ) } } // Prevent double emit of `false` caused by transition to AOD, followed by keyguard // showing .distinctUntilChanged() // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is // showing again var clearUnseenOnUnlock = false keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing -> if (isKeyguardShowing) { var clearUnseenOnBeginTracking = false isTrackingUnseen.collectLatest { trackingUnseen -> if (!trackingUnseen) { // Wait for the user to spend enough time on the lock screen before clearing unseen // set when unlocked awaitTimeSpentNotDozing(SEEN_TIMEOUT) clearUnseenOnUnlock = true clearUnseenOnBeginTracking = true } else { unseenNotifFilter.invalidateList("keyguard no longer showing") if (clearUnseenOnUnlock) { clearUnseenOnUnlock = false if (clearUnseenOnBeginTracking) { clearUnseenOnBeginTracking = false unseenNotifications.clear() } unseenNotifFilter.invalidateList("keyguard no longer showing") trackUnseenNotifications() } } Loading Loading @@ -142,7 +176,10 @@ constructor( } private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { statusBarStateController.expansionChanges.collect { isExpanded -> statusBarStateController.expansionChanges.collectLatest { isExpanded -> // Give keyguard events time to propagate, in case this expansion is part of the // keyguard transition and not the user expanding the shade yield() if (isExpanded) { unseenNotifications.clear() } Loading Loading @@ -276,3 +313,6 @@ constructor( private val SEEN_TIMEOUT = 5.seconds } } private val TransitionStep.isScreenTurningOff: Boolean get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE No newline at end of file packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +33 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.advanceTimeBy import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.NotifPipelineFlags Loading Loading @@ -69,6 +73,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { private val headsUpManager: HeadsUpManager = mock() private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() private val keyguardRepository = FakeKeyguardRepository() private val keyguardTransitionRepository = FakeKeyguardTransitionRepository() private val notifPipelineFlags: NotifPipelineFlags = mock() private val notifPipeline: NotifPipeline = mock() private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() Loading Loading @@ -117,6 +122,33 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } } @Test fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(false) runKeyguardCoordinatorTest { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: The device transitions to AOD keyguardTransitionRepository.sendTransitionStep( TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED), ) testScheduler.runCurrent() // WHEN: The shade is expanded whenever(statusBarStateController.isExpanded).thenReturn(true) statusBarStateListener.onExpandedChanged(true) testScheduler.runCurrent() // THEN: The notification is still treated as "unseen" and is not filtered out. assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() } } @Test fun unseenFilter_headsUpMarkedAsSeen() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) Loading Loading @@ -373,6 +405,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { headsUpManager, keyguardNotifVisibilityProvider, keyguardRepository, keyguardTransitionRepository, notifPipelineFlags, testScope.backgroundScope, sectionHeaderVisibilityProvider, Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +51 −11 Original line number Diff line number Diff line Loading @@ -24,6 +24,10 @@ import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.expansionChanges Loading @@ -40,22 +44,26 @@ import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import kotlinx.coroutines.yield import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds /** * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section Loading @@ -69,6 +77,7 @@ constructor( private val headsUpManager: HeadsUpManager, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, private val keyguardRepository: KeyguardRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val notifPipelineFlags: NotifPipelineFlags, @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, Loading Loading @@ -99,21 +108,46 @@ constructor( } private suspend fun trackUnseenNotificationsWhileUnlocked() { // Whether or not we're actively tracking unseen notifications to mark them as seen when // appropriate. val isTrackingUnseen: Flow<Boolean> = keyguardRepository.isKeyguardShowing // transformLatest so that we can cancel listening to keyguard transitions once // isKeyguardShowing changes (after a successful transition to the keyguard). .transformLatest { isShowing -> if (isShowing) { // If the keyguard is showing, we're not tracking unseen. emit(false) } else { // If the keyguard stops showing, then start tracking unseen notifications. emit(true) // If the screen is turning off, stop tracking, but if that transition is // cancelled, then start again. emitAll( keyguardTransitionRepository.transitions .map { step -> !step.isScreenTurningOff } ) } } // Prevent double emit of `false` caused by transition to AOD, followed by keyguard // showing .distinctUntilChanged() // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is // showing again var clearUnseenOnUnlock = false keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing -> if (isKeyguardShowing) { var clearUnseenOnBeginTracking = false isTrackingUnseen.collectLatest { trackingUnseen -> if (!trackingUnseen) { // Wait for the user to spend enough time on the lock screen before clearing unseen // set when unlocked awaitTimeSpentNotDozing(SEEN_TIMEOUT) clearUnseenOnUnlock = true clearUnseenOnBeginTracking = true } else { unseenNotifFilter.invalidateList("keyguard no longer showing") if (clearUnseenOnUnlock) { clearUnseenOnUnlock = false if (clearUnseenOnBeginTracking) { clearUnseenOnBeginTracking = false unseenNotifications.clear() } unseenNotifFilter.invalidateList("keyguard no longer showing") trackUnseenNotifications() } } Loading Loading @@ -142,7 +176,10 @@ constructor( } private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { statusBarStateController.expansionChanges.collect { isExpanded -> statusBarStateController.expansionChanges.collectLatest { isExpanded -> // Give keyguard events time to propagate, in case this expansion is part of the // keyguard transition and not the user expanding the shade yield() if (isExpanded) { unseenNotifications.clear() } Loading Loading @@ -276,3 +313,6 @@ constructor( private val SEEN_TIMEOUT = 5.seconds } } private val TransitionStep.isScreenTurningOff: Boolean get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE No newline at end of file
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +33 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.advanceTimeBy import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.NotifPipelineFlags Loading Loading @@ -69,6 +73,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { private val headsUpManager: HeadsUpManager = mock() private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() private val keyguardRepository = FakeKeyguardRepository() private val keyguardTransitionRepository = FakeKeyguardTransitionRepository() private val notifPipelineFlags: NotifPipelineFlags = mock() private val notifPipeline: NotifPipeline = mock() private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() Loading Loading @@ -117,6 +122,33 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } } @Test fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(false) runKeyguardCoordinatorTest { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: The device transitions to AOD keyguardTransitionRepository.sendTransitionStep( TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED), ) testScheduler.runCurrent() // WHEN: The shade is expanded whenever(statusBarStateController.isExpanded).thenReturn(true) statusBarStateListener.onExpandedChanged(true) testScheduler.runCurrent() // THEN: The notification is still treated as "unseen" and is not filtered out. assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() } } @Test fun unseenFilter_headsUpMarkedAsSeen() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) Loading Loading @@ -373,6 +405,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { headsUpManager, keyguardNotifVisibilityProvider, keyguardRepository, keyguardTransitionRepository, notifPipelineFlags, testScope.backgroundScope, sectionHeaderVisibilityProvider, Loading