Merge "Individually track vis duration of unseen notifs" into udc-qpr-dev am: f58e1e47df
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23904070 Change-Id: I7c3feaf7f1cf0c6d0bbca4c218d6770d8afce84e Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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