Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +66 −115 Original line number Original line Diff line number Diff line Loading @@ -28,9 +28,12 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState 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.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.expansionChanges import com.android.systemui.statusbar.expansionChanges import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope Loading @@ -47,29 +50,30 @@ import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import java.io.PrintWriter import java.io.PrintWriter import javax.inject.Inject import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import kotlinx.coroutines.launch import kotlinx.coroutines.yield import kotlinx.coroutines.yield /** /** * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the * headers on the lockscreen. * lockscreen. */ */ @CoordinatorScope @CoordinatorScope class KeyguardCoordinator class KeyguardCoordinator Loading @@ -82,6 +86,7 @@ constructor( private val keyguardRepository: KeyguardRepository, private val keyguardRepository: KeyguardRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val logger: KeyguardCoordinatorLogger, private val logger: KeyguardCoordinatorLogger, private val notifPipelineFlags: NotifPipelineFlags, @Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, private val secureSettings: SecureSettings, private val secureSettings: SecureSettings, Loading @@ -90,8 +95,6 @@ constructor( ) : Coordinator, Dumpable { ) : Coordinator, Dumpable { private val unseenNotifications = mutableSetOf<NotificationEntry>() private val unseenNotifications = mutableSetOf<NotificationEntry>() private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) private var unseenFilterEnabled = false private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { override fun attach(pipeline: NotifPipeline) { Loading @@ -106,131 +109,79 @@ constructor( private fun attachUnseenFilter(pipeline: NotifPipeline) { private fun attachUnseenFilter(pipeline: NotifPipeline) { pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) pipeline.addCollectionListener(collectionListener) scope.launch { trackSeenNotifications() } scope.launch { trackUnseenNotificationsWhileUnlocked() } scope.launch { invalidateWhenUnseenSettingChanges() } scope.launch { invalidateWhenUnseenSettingChanges() } dumpManager.registerDumpable(this) dumpManager.registerDumpable(this) } } private suspend fun trackSeenNotifications() { private suspend fun trackUnseenNotificationsWhileUnlocked() { // Whether or not keyguard is visible (or occluded). // Whether or not we're actively tracking unseen notifications to mark them as seen when val isKeyguardPresent: Flow<Boolean> = // appropriate. keyguardTransitionRepository.transitions val isTrackingUnseen: Flow<Boolean> = .map { step -> step.to != KeyguardState.GONE } 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() .distinctUntilChanged() .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } // Separately track seen notifications while the device is locked, applying once the device // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is // is unlocked. // showing again val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>() var clearUnseenOnBeginTracking = false isTrackingUnseen.collectLatest { trackingUnseen -> // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes. if (!trackingUnseen) { isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean -> // Wait for the user to spend enough time on the lock screen before clearing unseen if (isKeyguardPresent) { // set when unlocked // Keyguard is not gone, notifications need to be visible for a certain threshold awaitTimeSpentNotDozing(SEEN_TIMEOUT) // before being marked as seen clearUnseenOnBeginTracking = true trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked) logger.logSeenOnLockscreen() } else { } else { // Mark all seen-while-locked notifications as seen for real. if (clearUnseenOnBeginTracking) { if (notificationsSeenWhileLocked.isNotEmpty()) { clearUnseenOnBeginTracking = false unseenNotifications.removeAll(notificationsSeenWhileLocked) logger.logAllMarkedSeenOnUnlock() logger.logAllMarkedSeenOnUnlock( unseenNotifications.clear() seenCount = notificationsSeenWhileLocked.size, remainingUnseenCount = unseenNotifications.size ) notificationsSeenWhileLocked.clear() } } unseenNotifFilter.invalidateList("keyguard no longer showing") unseenNotifFilter.invalidateList("keyguard no longer showing") // Keyguard is gone, notifications can be immediately marked as seen when they trackUnseenNotifications() // become visible. trackSeenNotificationsWhileUnlocked() } } } } } } /** private suspend fun awaitTimeSpentNotDozing(duration: Duration) { * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually keyguardRepository.isDozing * been "seen" while the device is on the keyguard. // Use transformLatest so that the timeout delay is cancelled if the device enters doze, */ // and is restarted when doze ends. private suspend fun trackSeenNotificationsWhileLocked( .transformLatest { isDozing -> notificationsSeenWhileLocked: MutableSet<NotificationEntry>, ) = coroutineScope { // Remove removed notifications from the set launch { unseenEntryRemoved.collect { entry -> if (notificationsSeenWhileLocked.remove(entry)) { logger.logRemoveSeenOnLockscreen(entry) } } } // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and // is restarted when doze ends. keyguardRepository.isDozing.collectLatest { isDozing -> if (!isDozing) { if (!isDozing) { trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked) delay(duration) // Signal timeout has completed emit(Unit) } } } } // Suspend until the first emission .first() } } /** // Track "unseen" notifications, marking them as seen when either shade is expanded or the * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration. */ private suspend fun trackSeenNotificationsWhileLockedAndNotDozing( notificationsSeenWhileLocked: MutableSet<NotificationEntry> ) = coroutineScope { // All child tracking jobs will be cancelled automatically when this is cancelled. val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>() /** * Wait for the user to spend enough time on the lock screen before removing notification * from unseen set upon unlock. */ suspend fun trackSeenDurationThreshold(entry: NotificationEntry) { if (notificationsSeenWhileLocked.remove(entry)) { logger.logResetSeenOnLockscreen(entry) } delay(SEEN_TIMEOUT) notificationsSeenWhileLocked.add(entry) trackingJobsByEntry.remove(entry) logger.logSeenOnLockscreen(entry) } /** Stop any unseen tracking when a notification is removed. */ suspend fun stopTrackingRemovedNotifs(): Nothing = unseenEntryRemoved.collect { entry -> trackingJobsByEntry.remove(entry)?.let { it.cancel() logger.logStopTrackingLockscreenSeenDuration(entry) } } /** Start tracking new notifications when they are posted. */ suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope { unseenEntryAdded.collect { entry -> logger.logTrackingLockscreenSeenDuration(entry) // If this is an update, reset the tracking. trackingJobsByEntry[entry]?.let { it.cancel() logger.logResetSeenOnLockscreen(entry) } trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } } } // Start tracking for all notifications that are currently unseen. logger.logTrackingLockscreenSeenDuration(unseenNotifications) unseenNotifications.forEach { entry -> trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } } launch { trackNewUnseenNotifs() } launch { stopTrackingRemovedNotifs() } } // Track "seen" notifications, marking them as such when either shade is expanded or the // notification becomes heads up. // notification becomes heads up. private suspend fun trackSeenNotificationsWhileUnlocked() { private suspend fun trackUnseenNotifications() { coroutineScope { coroutineScope { launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { markHeadsUpNotificationsAsSeen() } launch { markHeadsUpNotificationsAsSeen() } Loading Loading @@ -299,7 +250,6 @@ constructor( ) { ) { logger.logUnseenAdded(entry.key) logger.logUnseenAdded(entry.key) unseenNotifications.add(entry) unseenNotifications.add(entry) unseenEntryAdded.tryEmit(entry) } } } } Loading @@ -309,14 +259,12 @@ constructor( ) { ) { logger.logUnseenUpdated(entry.key) logger.logUnseenUpdated(entry.key) unseenNotifications.add(entry) unseenNotifications.add(entry) unseenEntryAdded.tryEmit(entry) } } } } override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { if (unseenNotifications.remove(entry)) { if (unseenNotifications.remove(entry)) { logger.logUnseenRemoved(entry.key) logger.logUnseenRemoved(entry.key) unseenEntryRemoved.tryEmit(entry) } } } } } } Loading Loading @@ -399,3 +347,6 @@ constructor( private val SEEN_TIMEOUT = 5.seconds private val SEEN_TIMEOUT = 5.seconds } } } } private val TransitionStep.isScreenTurningOff: Boolean get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt +4 −74 Original line number Original line Diff line number Diff line Loading @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.UnseenNotificationLog import com.android.systemui.log.dagger.UnseenNotificationLog import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject import javax.inject.Inject private const val TAG = "KeyguardCoordinator" private const val TAG = "KeyguardCoordinator" Loading @@ -29,14 +28,11 @@ class KeyguardCoordinatorLogger constructor( constructor( @UnseenNotificationLog private val buffer: LogBuffer, @UnseenNotificationLog private val buffer: LogBuffer, ) { ) { fun logSeenOnLockscreen(entry: NotificationEntry) = fun logSeenOnLockscreen() = buffer.log( buffer.log( TAG, TAG, LogLevel.DEBUG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, "Notifications on lockscreen will be marked as seen when unlocked." messagePrinter = { "Notification [$str1] on lockscreen will be marked as seen when unlocked." }, ) ) fun logTrackingUnseen(trackingUnseen: Boolean) = fun logTrackingUnseen(trackingUnseen: Boolean) = Loading @@ -47,21 +43,11 @@ constructor( messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, ) ) fun logAllMarkedSeenOnUnlock( fun logAllMarkedSeenOnUnlock() = seenCount: Int, remainingUnseenCount: Int, ) = buffer.log( buffer.log( TAG, TAG, LogLevel.DEBUG, LogLevel.DEBUG, messageInitializer = { "Notifications have been marked as seen now that device is unlocked." int1 = seenCount int2 = remainingUnseenCount }, messagePrinter = { "$int1 Notifications have been marked as seen now that device is unlocked. " + "$int2 notifications remain unseen." }, ) ) fun logShadeExpanded() = fun logShadeExpanded() = Loading Loading @@ -110,60 +96,4 @@ constructor( messageInitializer = { str1 = key }, messageInitializer = { str1 = key }, messagePrinter = { "Unseen notif has become heads up: $str1" }, messagePrinter = { "Unseen notif has become heads up: $str1" }, ) ) fun logTrackingLockscreenSeenDuration(unseenNotifications: Set<NotificationEntry>) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = unseenNotifications.joinToString { it.key } int1 = unseenNotifications.size }, messagePrinter = { "Tracking $int1 unseen notifications for lockscreen seen duration threshold: $str1" }, ) } fun logTrackingLockscreenSeenDuration(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Tracking new notification for lockscreen seen duration threshold: $str1" }, ) } fun logStopTrackingLockscreenSeenDuration(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Stop tracking removed notification for lockscreen seen duration threshold: $str1" }, ) } fun logResetSeenOnLockscreen(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Reset tracking updated notification for lockscreen seen duration threshold: $str1" }, ) } fun logRemoveSeenOnLockscreen(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Notification marked as seen on lockscreen removed: $str1" }, ) } } } packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +39 −258 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +66 −115 Original line number Original line Diff line number Diff line Loading @@ -28,9 +28,12 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState 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.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.expansionChanges import com.android.systemui.statusbar.expansionChanges import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope Loading @@ -47,29 +50,30 @@ import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import java.io.PrintWriter import java.io.PrintWriter import javax.inject.Inject import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import kotlinx.coroutines.launch import kotlinx.coroutines.yield import kotlinx.coroutines.yield /** /** * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the * headers on the lockscreen. * lockscreen. */ */ @CoordinatorScope @CoordinatorScope class KeyguardCoordinator class KeyguardCoordinator Loading @@ -82,6 +86,7 @@ constructor( private val keyguardRepository: KeyguardRepository, private val keyguardRepository: KeyguardRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val logger: KeyguardCoordinatorLogger, private val logger: KeyguardCoordinatorLogger, private val notifPipelineFlags: NotifPipelineFlags, @Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, private val secureSettings: SecureSettings, private val secureSettings: SecureSettings, Loading @@ -90,8 +95,6 @@ constructor( ) : Coordinator, Dumpable { ) : Coordinator, Dumpable { private val unseenNotifications = mutableSetOf<NotificationEntry>() private val unseenNotifications = mutableSetOf<NotificationEntry>() private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) private var unseenFilterEnabled = false private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { override fun attach(pipeline: NotifPipeline) { Loading @@ -106,131 +109,79 @@ constructor( private fun attachUnseenFilter(pipeline: NotifPipeline) { private fun attachUnseenFilter(pipeline: NotifPipeline) { pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) pipeline.addCollectionListener(collectionListener) scope.launch { trackSeenNotifications() } scope.launch { trackUnseenNotificationsWhileUnlocked() } scope.launch { invalidateWhenUnseenSettingChanges() } scope.launch { invalidateWhenUnseenSettingChanges() } dumpManager.registerDumpable(this) dumpManager.registerDumpable(this) } } private suspend fun trackSeenNotifications() { private suspend fun trackUnseenNotificationsWhileUnlocked() { // Whether or not keyguard is visible (or occluded). // Whether or not we're actively tracking unseen notifications to mark them as seen when val isKeyguardPresent: Flow<Boolean> = // appropriate. keyguardTransitionRepository.transitions val isTrackingUnseen: Flow<Boolean> = .map { step -> step.to != KeyguardState.GONE } 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() .distinctUntilChanged() .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } // Separately track seen notifications while the device is locked, applying once the device // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is // is unlocked. // showing again val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>() var clearUnseenOnBeginTracking = false isTrackingUnseen.collectLatest { trackingUnseen -> // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes. if (!trackingUnseen) { isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean -> // Wait for the user to spend enough time on the lock screen before clearing unseen if (isKeyguardPresent) { // set when unlocked // Keyguard is not gone, notifications need to be visible for a certain threshold awaitTimeSpentNotDozing(SEEN_TIMEOUT) // before being marked as seen clearUnseenOnBeginTracking = true trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked) logger.logSeenOnLockscreen() } else { } else { // Mark all seen-while-locked notifications as seen for real. if (clearUnseenOnBeginTracking) { if (notificationsSeenWhileLocked.isNotEmpty()) { clearUnseenOnBeginTracking = false unseenNotifications.removeAll(notificationsSeenWhileLocked) logger.logAllMarkedSeenOnUnlock() logger.logAllMarkedSeenOnUnlock( unseenNotifications.clear() seenCount = notificationsSeenWhileLocked.size, remainingUnseenCount = unseenNotifications.size ) notificationsSeenWhileLocked.clear() } } unseenNotifFilter.invalidateList("keyguard no longer showing") unseenNotifFilter.invalidateList("keyguard no longer showing") // Keyguard is gone, notifications can be immediately marked as seen when they trackUnseenNotifications() // become visible. trackSeenNotificationsWhileUnlocked() } } } } } } /** private suspend fun awaitTimeSpentNotDozing(duration: Duration) { * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually keyguardRepository.isDozing * been "seen" while the device is on the keyguard. // Use transformLatest so that the timeout delay is cancelled if the device enters doze, */ // and is restarted when doze ends. private suspend fun trackSeenNotificationsWhileLocked( .transformLatest { isDozing -> notificationsSeenWhileLocked: MutableSet<NotificationEntry>, ) = coroutineScope { // Remove removed notifications from the set launch { unseenEntryRemoved.collect { entry -> if (notificationsSeenWhileLocked.remove(entry)) { logger.logRemoveSeenOnLockscreen(entry) } } } // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and // is restarted when doze ends. keyguardRepository.isDozing.collectLatest { isDozing -> if (!isDozing) { if (!isDozing) { trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked) delay(duration) // Signal timeout has completed emit(Unit) } } } } // Suspend until the first emission .first() } } /** // Track "unseen" notifications, marking them as seen when either shade is expanded or the * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration. */ private suspend fun trackSeenNotificationsWhileLockedAndNotDozing( notificationsSeenWhileLocked: MutableSet<NotificationEntry> ) = coroutineScope { // All child tracking jobs will be cancelled automatically when this is cancelled. val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>() /** * Wait for the user to spend enough time on the lock screen before removing notification * from unseen set upon unlock. */ suspend fun trackSeenDurationThreshold(entry: NotificationEntry) { if (notificationsSeenWhileLocked.remove(entry)) { logger.logResetSeenOnLockscreen(entry) } delay(SEEN_TIMEOUT) notificationsSeenWhileLocked.add(entry) trackingJobsByEntry.remove(entry) logger.logSeenOnLockscreen(entry) } /** Stop any unseen tracking when a notification is removed. */ suspend fun stopTrackingRemovedNotifs(): Nothing = unseenEntryRemoved.collect { entry -> trackingJobsByEntry.remove(entry)?.let { it.cancel() logger.logStopTrackingLockscreenSeenDuration(entry) } } /** Start tracking new notifications when they are posted. */ suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope { unseenEntryAdded.collect { entry -> logger.logTrackingLockscreenSeenDuration(entry) // If this is an update, reset the tracking. trackingJobsByEntry[entry]?.let { it.cancel() logger.logResetSeenOnLockscreen(entry) } trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } } } // Start tracking for all notifications that are currently unseen. logger.logTrackingLockscreenSeenDuration(unseenNotifications) unseenNotifications.forEach { entry -> trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } } launch { trackNewUnseenNotifs() } launch { stopTrackingRemovedNotifs() } } // Track "seen" notifications, marking them as such when either shade is expanded or the // notification becomes heads up. // notification becomes heads up. private suspend fun trackSeenNotificationsWhileUnlocked() { private suspend fun trackUnseenNotifications() { coroutineScope { coroutineScope { launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { markHeadsUpNotificationsAsSeen() } launch { markHeadsUpNotificationsAsSeen() } Loading Loading @@ -299,7 +250,6 @@ constructor( ) { ) { logger.logUnseenAdded(entry.key) logger.logUnseenAdded(entry.key) unseenNotifications.add(entry) unseenNotifications.add(entry) unseenEntryAdded.tryEmit(entry) } } } } Loading @@ -309,14 +259,12 @@ constructor( ) { ) { logger.logUnseenUpdated(entry.key) logger.logUnseenUpdated(entry.key) unseenNotifications.add(entry) unseenNotifications.add(entry) unseenEntryAdded.tryEmit(entry) } } } } override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { if (unseenNotifications.remove(entry)) { if (unseenNotifications.remove(entry)) { logger.logUnseenRemoved(entry.key) logger.logUnseenRemoved(entry.key) unseenEntryRemoved.tryEmit(entry) } } } } } } Loading Loading @@ -399,3 +347,6 @@ constructor( private val SEEN_TIMEOUT = 5.seconds private val SEEN_TIMEOUT = 5.seconds } } } } private val TransitionStep.isScreenTurningOff: Boolean get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorLogger.kt +4 −74 Original line number Original line Diff line number Diff line Loading @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.UnseenNotificationLog import com.android.systemui.log.dagger.UnseenNotificationLog import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject import javax.inject.Inject private const val TAG = "KeyguardCoordinator" private const val TAG = "KeyguardCoordinator" Loading @@ -29,14 +28,11 @@ class KeyguardCoordinatorLogger constructor( constructor( @UnseenNotificationLog private val buffer: LogBuffer, @UnseenNotificationLog private val buffer: LogBuffer, ) { ) { fun logSeenOnLockscreen(entry: NotificationEntry) = fun logSeenOnLockscreen() = buffer.log( buffer.log( TAG, TAG, LogLevel.DEBUG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, "Notifications on lockscreen will be marked as seen when unlocked." messagePrinter = { "Notification [$str1] on lockscreen will be marked as seen when unlocked." }, ) ) fun logTrackingUnseen(trackingUnseen: Boolean) = fun logTrackingUnseen(trackingUnseen: Boolean) = Loading @@ -47,21 +43,11 @@ constructor( messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, ) ) fun logAllMarkedSeenOnUnlock( fun logAllMarkedSeenOnUnlock() = seenCount: Int, remainingUnseenCount: Int, ) = buffer.log( buffer.log( TAG, TAG, LogLevel.DEBUG, LogLevel.DEBUG, messageInitializer = { "Notifications have been marked as seen now that device is unlocked." int1 = seenCount int2 = remainingUnseenCount }, messagePrinter = { "$int1 Notifications have been marked as seen now that device is unlocked. " + "$int2 notifications remain unseen." }, ) ) fun logShadeExpanded() = fun logShadeExpanded() = Loading Loading @@ -110,60 +96,4 @@ constructor( messageInitializer = { str1 = key }, messageInitializer = { str1 = key }, messagePrinter = { "Unseen notif has become heads up: $str1" }, messagePrinter = { "Unseen notif has become heads up: $str1" }, ) ) fun logTrackingLockscreenSeenDuration(unseenNotifications: Set<NotificationEntry>) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = unseenNotifications.joinToString { it.key } int1 = unseenNotifications.size }, messagePrinter = { "Tracking $int1 unseen notifications for lockscreen seen duration threshold: $str1" }, ) } fun logTrackingLockscreenSeenDuration(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Tracking new notification for lockscreen seen duration threshold: $str1" }, ) } fun logStopTrackingLockscreenSeenDuration(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Stop tracking removed notification for lockscreen seen duration threshold: $str1" }, ) } fun logResetSeenOnLockscreen(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Reset tracking updated notification for lockscreen seen duration threshold: $str1" }, ) } fun logRemoveSeenOnLockscreen(entry: NotificationEntry) { buffer.log( TAG, LogLevel.DEBUG, messageInitializer = { str1 = entry.key }, messagePrinter = { "Notification marked as seen on lockscreen removed: $str1" }, ) } } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +39 −258 File changed.Preview size limit exceeded, changes collapsed. Show changes