Individually track vis duration of unseen notifs
Previously, if the user has been on the keyguard for 5 seconds, then we mark all unseen notifications as seen when the device is unlocked, even if some notifications were not present for the entire (or any of the) 5 second visible duration. We now track for each unseen notification that since it has been posted / the device was locked, that the user has been on the keyguard for 5 seconds. When a notification has reached this threshold, it is marked as "seen on lockscreen", and will be marked as seen for real once the device is unlocked. Note that if the notification is updated, the 5 second threshold is reset. REVERT^2: In order to reduce potential jank, we now only start tracking seen notifications when locked if the setting is enabled. Fixes: 277616032 Test: atest KeyguardCoordinatorTest Change-Id: Iec2fdc977157bda85b41c5c64bfcce6d2ec9fd7f
This commit is contained in:
@@ -28,12 +28,9 @@ import com.android.systemui.dump.DumpManager
|
||||
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
|
||||
import com.android.systemui.statusbar.notification.NotifPipelineFlags
|
||||
import com.android.systemui.statusbar.notification.collection.NotifPipeline
|
||||
import com.android.systemui.statusbar.notification.collection.NotificationEntry
|
||||
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
|
||||
@@ -50,30 +47,29 @@ import com.android.systemui.util.settings.SecureSettings
|
||||
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
|
||||
import java.io.PrintWriter
|
||||
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.Job
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
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.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
|
||||
/**
|
||||
* Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
|
||||
* headers on the lockscreen.
|
||||
* headers on the lockscreen. If enabled, it will also track and hide seen notifications on the
|
||||
* lockscreen.
|
||||
*/
|
||||
@CoordinatorScope
|
||||
class KeyguardCoordinator
|
||||
@@ -86,7 +82,6 @@ constructor(
|
||||
private val keyguardRepository: KeyguardRepository,
|
||||
private val keyguardTransitionRepository: KeyguardTransitionRepository,
|
||||
private val logger: KeyguardCoordinatorLogger,
|
||||
private val notifPipelineFlags: NotifPipelineFlags,
|
||||
@Application private val scope: CoroutineScope,
|
||||
private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
|
||||
private val secureSettings: SecureSettings,
|
||||
@@ -95,6 +90,8 @@ constructor(
|
||||
) : Coordinator, Dumpable {
|
||||
|
||||
private val unseenNotifications = mutableSetOf<NotificationEntry>()
|
||||
private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
|
||||
private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
|
||||
private var unseenFilterEnabled = false
|
||||
|
||||
override fun attach(pipeline: NotifPipeline) {
|
||||
@@ -109,79 +106,130 @@ constructor(
|
||||
private fun attachUnseenFilter(pipeline: NotifPipeline) {
|
||||
pipeline.addFinalizeFilter(unseenNotifFilter)
|
||||
pipeline.addCollectionListener(collectionListener)
|
||||
scope.launch { trackUnseenNotificationsWhileUnlocked() }
|
||||
scope.launch { invalidateWhenUnseenSettingChanges() }
|
||||
scope.launch { trackUnseenFilterSettingChanges() }
|
||||
dumpManager.registerDumpable(this)
|
||||
}
|
||||
|
||||
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
|
||||
private suspend fun trackSeenNotifications() {
|
||||
// Whether or not keyguard is visible (or occluded).
|
||||
val isKeyguardPresent: Flow<Boolean> =
|
||||
keyguardTransitionRepository.transitions
|
||||
.map { step -> step.to != KeyguardState.GONE }
|
||||
.distinctUntilChanged()
|
||||
.onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
|
||||
|
||||
// Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is
|
||||
// showing again
|
||||
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)
|
||||
clearUnseenOnBeginTracking = true
|
||||
logger.logSeenOnLockscreen()
|
||||
// Separately track seen notifications while the device is locked, applying once the device
|
||||
// is unlocked.
|
||||
val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>()
|
||||
|
||||
// Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes.
|
||||
isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean ->
|
||||
if (isKeyguardPresent) {
|
||||
// Keyguard is not gone, notifications need to be visible for a certain threshold
|
||||
// before being marked as seen
|
||||
trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked)
|
||||
} else {
|
||||
if (clearUnseenOnBeginTracking) {
|
||||
clearUnseenOnBeginTracking = false
|
||||
logger.logAllMarkedSeenOnUnlock()
|
||||
unseenNotifications.clear()
|
||||
// Mark all seen-while-locked notifications as seen for real.
|
||||
if (notificationsSeenWhileLocked.isNotEmpty()) {
|
||||
unseenNotifications.removeAll(notificationsSeenWhileLocked)
|
||||
logger.logAllMarkedSeenOnUnlock(
|
||||
seenCount = notificationsSeenWhileLocked.size,
|
||||
remainingUnseenCount = unseenNotifications.size
|
||||
)
|
||||
notificationsSeenWhileLocked.clear()
|
||||
}
|
||||
unseenNotifFilter.invalidateList("keyguard no longer showing")
|
||||
trackUnseenNotifications()
|
||||
// Keyguard is gone, notifications can be immediately marked as seen when they
|
||||
// become visible.
|
||||
trackSeenNotificationsWhileUnlocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitTimeSpentNotDozing(duration: Duration) {
|
||||
keyguardRepository.isDozing
|
||||
// Use transformLatest so that the timeout delay is cancelled if the device enters doze,
|
||||
// and is restarted when doze ends.
|
||||
.transformLatest { isDozing ->
|
||||
if (!isDozing) {
|
||||
delay(duration)
|
||||
// Signal timeout has completed
|
||||
emit(Unit)
|
||||
/**
|
||||
* Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
|
||||
* been "seen" while the device is on the keyguard.
|
||||
*/
|
||||
private suspend fun trackSeenNotificationsWhileLocked(
|
||||
notificationsSeenWhileLocked: MutableSet<NotificationEntry>,
|
||||
) = coroutineScope {
|
||||
// Remove removed notifications from the set
|
||||
launch {
|
||||
unseenEntryRemoved.collect { entry ->
|
||||
if (notificationsSeenWhileLocked.remove(entry)) {
|
||||
logger.logRemoveSeenOnLockscreen(entry)
|
||||
}
|
||||
}
|
||||
// Suspend until the first emission
|
||||
.first()
|
||||
}
|
||||
// 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) {
|
||||
trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
private suspend fun trackUnseenNotifications() {
|
||||
private suspend fun trackSeenNotificationsWhileUnlocked() {
|
||||
coroutineScope {
|
||||
launch { clearUnseenNotificationsWhenShadeIsExpanded() }
|
||||
launch { markHeadsUpNotificationsAsSeen() }
|
||||
@@ -212,7 +260,7 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun invalidateWhenUnseenSettingChanges() {
|
||||
private suspend fun trackUnseenFilterSettingChanges() {
|
||||
secureSettings
|
||||
// emit whenever the setting has changed
|
||||
.observerFlow(
|
||||
@@ -228,17 +276,23 @@ constructor(
|
||||
UserHandle.USER_CURRENT,
|
||||
) == 1
|
||||
}
|
||||
// don't emit anything if nothing has changed
|
||||
.distinctUntilChanged()
|
||||
// perform lookups on the bg thread pool
|
||||
.flowOn(bgDispatcher)
|
||||
// only track the most recent emission, if events are happening faster than they can be
|
||||
// consumed
|
||||
.conflate()
|
||||
// update local field and invalidate if necessary
|
||||
.collect { setting ->
|
||||
.collectLatest { setting ->
|
||||
// update local field and invalidate if necessary
|
||||
if (setting != unseenFilterEnabled) {
|
||||
unseenFilterEnabled = setting
|
||||
unseenNotifFilter.invalidateList("unseen setting changed")
|
||||
}
|
||||
// if the setting is enabled, then start tracking and filtering unseen notifications
|
||||
if (setting) {
|
||||
trackSeenNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +304,7 @@ constructor(
|
||||
) {
|
||||
logger.logUnseenAdded(entry.key)
|
||||
unseenNotifications.add(entry)
|
||||
unseenEntryAdded.tryEmit(entry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,12 +314,14 @@ constructor(
|
||||
) {
|
||||
logger.logUnseenUpdated(entry.key)
|
||||
unseenNotifications.add(entry)
|
||||
unseenEntryAdded.tryEmit(entry)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
|
||||
if (unseenNotifications.remove(entry)) {
|
||||
logger.logUnseenRemoved(entry.key)
|
||||
unseenEntryRemoved.tryEmit(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,6 +404,3 @@ constructor(
|
||||
private val SEEN_TIMEOUT = 5.seconds
|
||||
}
|
||||
}
|
||||
|
||||
private val TransitionStep.isScreenTurningOff: Boolean
|
||||
get() = transitionState == TransitionState.STARTED && to != KeyguardState.GONE
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator
|
||||
import com.android.systemui.log.LogBuffer
|
||||
import com.android.systemui.log.core.LogLevel
|
||||
import com.android.systemui.log.dagger.UnseenNotificationLog
|
||||
import com.android.systemui.statusbar.notification.collection.NotificationEntry
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "KeyguardCoordinator"
|
||||
@@ -28,11 +29,14 @@ class KeyguardCoordinatorLogger
|
||||
constructor(
|
||||
@UnseenNotificationLog private val buffer: LogBuffer,
|
||||
) {
|
||||
fun logSeenOnLockscreen() =
|
||||
fun logSeenOnLockscreen(entry: NotificationEntry) =
|
||||
buffer.log(
|
||||
TAG,
|
||||
LogLevel.DEBUG,
|
||||
"Notifications on lockscreen will be marked as seen when unlocked."
|
||||
messageInitializer = { str1 = entry.key },
|
||||
messagePrinter = {
|
||||
"Notification [$str1] on lockscreen will be marked as seen when unlocked."
|
||||
},
|
||||
)
|
||||
|
||||
fun logTrackingUnseen(trackingUnseen: Boolean) =
|
||||
@@ -43,11 +47,21 @@ constructor(
|
||||
messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
|
||||
)
|
||||
|
||||
fun logAllMarkedSeenOnUnlock() =
|
||||
fun logAllMarkedSeenOnUnlock(
|
||||
seenCount: Int,
|
||||
remainingUnseenCount: Int,
|
||||
) =
|
||||
buffer.log(
|
||||
TAG,
|
||||
LogLevel.DEBUG,
|
||||
"Notifications have been marked as seen now that device is unlocked."
|
||||
messageInitializer = {
|
||||
int1 = seenCount
|
||||
int2 = remainingUnseenCount
|
||||
},
|
||||
messagePrinter = {
|
||||
"$int1 Notifications have been marked as seen now that device is unlocked. " +
|
||||
"$int2 notifications remain unseen."
|
||||
},
|
||||
)
|
||||
|
||||
fun logShadeExpanded() =
|
||||
@@ -96,4 +110,60 @@ constructor(
|
||||
messageInitializer = { str1 = key },
|
||||
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" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ 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
|
||||
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
|
||||
import com.android.systemui.statusbar.notification.collection.NotifPipeline
|
||||
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
|
||||
@@ -46,11 +45,14 @@ import com.android.systemui.statusbar.notification.interruption.KeyguardNotifica
|
||||
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
|
||||
import com.android.systemui.statusbar.policy.HeadsUpManager
|
||||
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.withArgCaptor
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import java.util.function.Consumer
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineScheduler
|
||||
@@ -62,9 +64,8 @@ import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.same
|
||||
import org.mockito.Mockito.anyString
|
||||
import org.mockito.Mockito.clearInvocations
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import java.util.function.Consumer
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@SmallTest
|
||||
@@ -75,7 +76,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
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()
|
||||
private val statusBarStateController: StatusBarStateController = mock()
|
||||
@@ -136,13 +136,8 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
)
|
||||
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()
|
||||
// THEN: We are no longer listening for shade expansions
|
||||
verify(statusBarStateController, never()).addCallback(any())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +147,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
whenever(statusBarStateController.isExpanded).thenReturn(false)
|
||||
runKeyguardCoordinatorTest {
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
|
||||
)
|
||||
|
||||
// WHEN: A notification is posted
|
||||
val fakeEntry = NotificationEntryBuilder().build()
|
||||
collectionListener.onEntryAdded(fakeEntry)
|
||||
@@ -162,6 +161,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
|
||||
// WHEN: The keyguard is now showing
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// THEN: The notification is recognized as "seen" and is filtered out.
|
||||
@@ -169,6 +171,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
|
||||
// WHEN: The keyguard goes away
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// THEN: The notification is shown regardless
|
||||
@@ -182,9 +187,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
whenever(statusBarStateController.isExpanded).thenReturn(true)
|
||||
runKeyguardCoordinatorTest {
|
||||
val fakeEntry = NotificationEntryBuilder()
|
||||
val fakeEntry =
|
||||
NotificationEntryBuilder()
|
||||
.setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
|
||||
.build()
|
||||
.build()
|
||||
collectionListener.onEntryAdded(fakeEntry)
|
||||
|
||||
// WHEN: The keyguard is now showing
|
||||
@@ -202,11 +208,13 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
whenever(statusBarStateController.isExpanded).thenReturn(true)
|
||||
runKeyguardCoordinatorTest {
|
||||
val fakeEntry = NotificationEntryBuilder().build().apply {
|
||||
row = mock<ExpandableNotificationRow>().apply {
|
||||
whenever(isMediaRow).thenReturn(true)
|
||||
val fakeEntry =
|
||||
NotificationEntryBuilder().build().apply {
|
||||
row =
|
||||
mock<ExpandableNotificationRow>().apply {
|
||||
whenever(isMediaRow).thenReturn(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
collectionListener.onEntryAdded(fakeEntry)
|
||||
|
||||
// WHEN: The keyguard is now showing
|
||||
@@ -299,14 +307,12 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
runKeyguardCoordinatorTest {
|
||||
// WHEN: A new notification is posted
|
||||
val fakeSummary = NotificationEntryBuilder().build()
|
||||
val fakeChild = NotificationEntryBuilder()
|
||||
val fakeChild =
|
||||
NotificationEntryBuilder()
|
||||
.setGroup(context, "group")
|
||||
.setGroupSummary(context, false)
|
||||
.build()
|
||||
GroupEntryBuilder()
|
||||
.setSummary(fakeSummary)
|
||||
.addChild(fakeChild)
|
||||
.build()
|
||||
GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
|
||||
|
||||
collectionListener.onEntryAdded(fakeSummary)
|
||||
collectionListener.onEntryAdded(fakeChild)
|
||||
@@ -331,6 +337,10 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
runKeyguardCoordinatorTest {
|
||||
val fakeEntry = NotificationEntryBuilder().build()
|
||||
collectionListener.onEntryAdded(fakeEntry)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: five seconds have passed
|
||||
testScheduler.advanceTimeBy(5.seconds)
|
||||
@@ -338,10 +348,16 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
|
||||
// WHEN: Keyguard is no longer showing
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: Keyguard is shown again
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// THEN: The notification is now recognized as "seen" and is filtered out.
|
||||
@@ -354,11 +370,17 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
// GIVEN: Keyguard is showing, unseen notification is present
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
runKeyguardCoordinatorTest {
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
val fakeEntry = NotificationEntryBuilder().build()
|
||||
collectionListener.onEntryAdded(fakeEntry)
|
||||
|
||||
// WHEN: Keyguard is no longer showing
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
|
||||
)
|
||||
|
||||
// WHEN: Keyguard is shown again
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
@@ -369,14 +391,212 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
|
||||
// GIVEN: Keyguard is showing, not dozing, unseen notification is present
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardRepository.setIsDozing(false)
|
||||
runKeyguardCoordinatorTest {
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
val firstEntry = NotificationEntryBuilder().setId(1).build()
|
||||
collectionListener.onEntryAdded(firstEntry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: one second has passed
|
||||
testScheduler.advanceTimeBy(1.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: another unseen notification is posted
|
||||
val secondEntry = NotificationEntryBuilder().setId(2).build()
|
||||
collectionListener.onEntryAdded(secondEntry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: four more seconds have passed
|
||||
testScheduler.advanceTimeBy(4.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the keyguard is no longer showing
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: Keyguard is shown again
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// THEN: The first notification is considered seen and is filtered out.
|
||||
assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
|
||||
|
||||
// THEN: The second notification is still considered unseen and is not filtered out
|
||||
assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
|
||||
// GIVEN: Keyguard is showing, not dozing
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardRepository.setIsDozing(false)
|
||||
runKeyguardCoordinatorTest {
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: a new notification is posted
|
||||
val entry = NotificationEntryBuilder().setId(1).build()
|
||||
collectionListener.onEntryAdded(entry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: five more seconds have passed
|
||||
testScheduler.advanceTimeBy(5.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the notification is removed
|
||||
collectionListener.onEntryRemoved(entry, 0)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the notification is re-posted
|
||||
collectionListener.onEntryAdded(entry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: one more second has passed
|
||||
testScheduler.advanceTimeBy(1.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the keyguard is no longer showing
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: Keyguard is shown again
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// THEN: The notification is considered unseen and is not filtered out.
|
||||
assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
|
||||
// GIVEN: Keyguard is showing, not dozing
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardRepository.setIsDozing(false)
|
||||
runKeyguardCoordinatorTest {
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: a new notification is posted
|
||||
val entry = NotificationEntryBuilder().setId(1).build()
|
||||
collectionListener.onEntryAdded(entry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: one second has passed
|
||||
testScheduler.advanceTimeBy(1.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the notification is removed
|
||||
collectionListener.onEntryRemoved(entry, 0)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the notification is re-posted
|
||||
collectionListener.onEntryAdded(entry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: one more second has passed
|
||||
testScheduler.advanceTimeBy(1.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the keyguard is no longer showing
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: Keyguard is shown again
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// THEN: The notification is considered unseen and is not filtered out.
|
||||
assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
|
||||
// GIVEN: Keyguard is showing, not dozing
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardRepository.setIsDozing(false)
|
||||
runKeyguardCoordinatorTest {
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: a new notification is posted
|
||||
val entry = NotificationEntryBuilder().setId(1).build()
|
||||
collectionListener.onEntryAdded(entry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: one second has passed
|
||||
testScheduler.advanceTimeBy(1.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the notification is updated
|
||||
collectionListener.onEntryUpdated(entry)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: four more seconds have passed
|
||||
testScheduler.advanceTimeBy(4.seconds)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: the keyguard is no longer showing
|
||||
keyguardRepository.setKeyguardShowing(false)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// WHEN: Keyguard is shown again
|
||||
keyguardRepository.setKeyguardShowing(true)
|
||||
keyguardTransitionRepository.sendTransitionStep(
|
||||
TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
|
||||
)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// THEN: The notification is considered unseen and is not filtered out.
|
||||
assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runKeyguardCoordinatorTest(
|
||||
testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
|
||||
) {
|
||||
val testDispatcher = UnconfinedTestDispatcher()
|
||||
val testScope = TestScope(testDispatcher)
|
||||
val fakeSettings = FakeSettings().apply {
|
||||
putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
|
||||
}
|
||||
val fakeSettings =
|
||||
FakeSettings().apply {
|
||||
putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
|
||||
}
|
||||
val seenNotificationsProvider = SeenNotificationsProviderImpl()
|
||||
val keyguardCoordinator =
|
||||
KeyguardCoordinator(
|
||||
@@ -387,7 +607,6 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
keyguardRepository,
|
||||
keyguardTransitionRepository,
|
||||
mock<KeyguardCoordinatorLogger>(),
|
||||
notifPipelineFlags,
|
||||
testScope.backgroundScope,
|
||||
sectionHeaderVisibilityProvider,
|
||||
fakeSettings,
|
||||
@@ -397,11 +616,12 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
keyguardCoordinator.attach(notifPipeline)
|
||||
testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
|
||||
KeyguardCoordinatorTestScope(
|
||||
keyguardCoordinator,
|
||||
testScope,
|
||||
seenNotificationsProvider,
|
||||
fakeSettings,
|
||||
).testBlock()
|
||||
keyguardCoordinator,
|
||||
testScope,
|
||||
seenNotificationsProvider,
|
||||
fakeSettings,
|
||||
)
|
||||
.testBlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,10 +634,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
val testScheduler: TestCoroutineScheduler
|
||||
get() = scope.testScheduler
|
||||
|
||||
val onStateChangeListener: Consumer<String> =
|
||||
withArgCaptor {
|
||||
verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
|
||||
}
|
||||
val onStateChangeListener: Consumer<String> = withArgCaptor {
|
||||
verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
|
||||
}
|
||||
|
||||
val unseenFilter: NotifFilter
|
||||
get() = keyguardCoordinator.unseenNotifFilter
|
||||
@@ -426,11 +645,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
|
||||
verify(notifPipeline).addCollectionListener(capture())
|
||||
}
|
||||
|
||||
val onHeadsUpChangedListener: OnHeadsUpChangedListener get() =
|
||||
withArgCaptor { verify(headsUpManager).addListener(capture()) }
|
||||
val onHeadsUpChangedListener: OnHeadsUpChangedListener
|
||||
get() = withArgCaptor { verify(headsUpManager).addListener(capture()) }
|
||||
|
||||
val statusBarStateListener: StatusBarStateController.StateListener get() =
|
||||
withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
|
||||
val statusBarStateListener: StatusBarStateController.StateListener
|
||||
get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
|
||||
|
||||
var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
|
||||
get() =
|
||||
|
||||
Reference in New Issue
Block a user