diff options
| author | 2024-07-17 13:41:22 +0000 | |
|---|---|---|
| committer | 2024-07-17 13:41:22 +0000 | |
| commit | cea78fc7d5923fe86bf1dfcd1f46019f0f987025 (patch) | |
| tree | 678718f7f61c7bd5e58cfb52f4c8cea9122d037b | |
| parent | 6f1cdd9266b545325d1f46c510d44b5ee0c1213d (diff) | |
| parent | 88132e811f346d3716899ef21e4005191175dab0 (diff) | |
Merge "Split the unseen logic out of the KeyguardCoordinator" into main
5 files changed, 1213 insertions, 1119 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt new file mode 100644 index 000000000000..6ddc07432a16 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt @@ -0,0 +1,683 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.app.Notification +import android.os.UserHandle +import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.andSceneContainer +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.data.repository.Idle +import com.android.systemui.scene.data.repository.setTransition +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +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.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.same +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class OriginalUnseenKeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() { + + private val kosmos = Kosmos() + + private val headsUpManager: HeadsUpManager = mock() + private val keyguardRepository = FakeKeyguardRepository() + private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val notifPipeline: NotifPipeline = mock() + private val statusBarStateController: StatusBarStateController = mock() + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Test + fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() { + // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN: The keyguard goes away + keyguardRepository.setKeyguardShowing(false) + testScheduler.runCurrent() + + // THEN: The notification is shown regardless + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() { + // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(false) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The device transitions to AOD + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + this.testScheduler, + ) + testScheduler.runCurrent() + + // THEN: We are no longer listening for shade expansions + verify(statusBarStateController, never()).addCallback(any()) + } + } + + @Test + fun unseenFilter_headsUpMarkedAsSeen() { + // GIVEN: Keyguard is not showing, shade is not expanded + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(false) + runKeyguardCoordinatorTest { + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + ) + + // WHEN: A notification is posted + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: That notification is heads up + onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true) + testScheduler.runCurrent() + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) + ) + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN: The keyguard goes away + keyguardRepository.setKeyguardShowing(false) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE) + ) + + // THEN: The notification is shown regardless + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() { + // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = + NotificationEntryBuilder() + .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build()) + .build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "ongoing" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() { + // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = + NotificationEntryBuilder().build().apply { + row = + mock<ExpandableNotificationRow>().apply { + whenever(isMediaRow).thenReturn(true) + } + } + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "media" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterUpdatesSeenProviderWhenSuppressing() { + // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN: The filter is cleaned up + unseenFilter.onCleanup() + + // THEN: The SeenNotificationProvider has been updated to reflect the suppression + assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue() + } + } + + @Test + fun unseenFilterInvalidatesWhenSettingChanges() { + // GIVEN: Keyguard is not showing, and shade is expanded + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + // GIVEN: A notification is present + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // GIVEN: The setting for filtering unseen notifications is disabled + showOnlyUnseenNotifsOnKeyguardSetting = false + + // GIVEN: The pipeline has registered the unseen filter for invalidation + val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock() + unseenFilter.setInvalidationListener(invalidationListener) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is not filtered out + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + + // WHEN: The secure setting is changed + showOnlyUnseenNotifsOnKeyguardSetting = true + + // THEN: The pipeline is invalidated + verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), any()) + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + } + + @Test + fun unseenFilterAllowsNewNotif() { + // GIVEN: Keyguard is showing, no notifications present + keyguardRepository.setKeyguardShowing(true) + runKeyguardCoordinatorTest { + // WHEN: A new notification is posted + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // THEN: The notification is recognized as "unseen" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterSeenGroupSummaryWithUnseenChild() { + // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + // WHEN: A new notification is posted + val fakeSummary = NotificationEntryBuilder().build() + val fakeChild = + NotificationEntryBuilder() + .setGroup(context, "group") + .setGroupSummary(context, false) + .build() + GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build() + + collectionListener.onEntryAdded(fakeSummary) + collectionListener.onEntryAdded(fakeChild) + + // WHEN: Keyguard is now showing, both notifications are marked as seen + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // WHEN: The child notification is now unseen + collectionListener.onEntryUpdated(fakeChild) + + // THEN: The summary is not filtered out, because the child is unseen + assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() { + // GIVEN: Keyguard is showing, not dozing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: five seconds have passed + testScheduler.advanceTimeBy(5.seconds) + testScheduler.runCurrent() + + // WHEN: Keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + ) + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) + ) + + // THEN: The notification is now recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + } + + @Test + fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() { + // GIVEN: Keyguard is showing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: Keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is not recognized as "seen" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() { + // GIVEN: Keyguard is showing, not dozing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, 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) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + ) + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN) + ) + + // 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.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + 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.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + 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.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + 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.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + 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.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + 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.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + 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 seenNotificationsInteractor = + SeenNotificationsInteractor(ActiveNotificationListRepository()) + val keyguardCoordinator = + OriginalUnseenKeyguardCoordinator( + testDispatcher, + mock<DumpManager>(), + headsUpManager, + keyguardRepository, + kosmos.keyguardTransitionInteractor, + KeyguardCoordinatorLogger(logcatLogBuffer()), + testScope.backgroundScope, + fakeSettings, + seenNotificationsInteractor, + statusBarStateController, + ) + keyguardCoordinator.attach(notifPipeline) + testScope.runTest { + KeyguardCoordinatorTestScope( + keyguardCoordinator, + testScope, + seenNotificationsInteractor, + fakeSettings, + ) + .testBlock() + } + } + + private inner class KeyguardCoordinatorTestScope( + private val keyguardCoordinator: OriginalUnseenKeyguardCoordinator, + private val scope: TestScope, + val seenNotificationsInteractor: SeenNotificationsInteractor, + private val fakeSettings: FakeSettings, + ) : CoroutineScope by scope { + val testScheduler: TestCoroutineScheduler + get() = scope.testScheduler + + val unseenFilter: NotifFilter + get() = keyguardCoordinator.unseenNotifFilter + + val collectionListener: NotifCollectionListener = + argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue + + val onHeadsUpChangedListener: OnHeadsUpChangedListener + get() = argumentCaptor { verify(headsUpManager).addListener(capture()) }.lastValue + + var showOnlyUnseenNotifsOnKeyguardSetting: Boolean + get() = + fakeSettings.getIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + UserHandle.USER_CURRENT, + ) == 1 + set(value) { + fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + if (value) 1 else 2, + UserHandle.USER_CURRENT, + ) + } + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt index 55c6790d4fb1..b1b2a653fde2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt @@ -16,62 +16,15 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import android.app.NotificationManager -import android.os.UserHandle -import android.provider.Settings -import androidx.annotation.VisibleForTesting -import com.android.systemui.Dumpable -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.expansionChanges -import com.android.systemui.statusbar.notification.collection.GroupEntry -import com.android.systemui.statusbar.notification.collection.ListEntry 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 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype -import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING -import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN -import com.android.systemui.statusbar.policy.HeadsUpManager -import com.android.systemui.statusbar.policy.headsUpEvents -import com.android.systemui.util.asIndenting -import com.android.systemui.util.indentIfPossible -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.Companion.seconds -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -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.flowOf -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield /** * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section @@ -82,24 +35,10 @@ import kotlinx.coroutines.yield class KeyguardCoordinator @Inject constructor( - @Background private val bgDispatcher: CoroutineDispatcher, - private val dumpManager: DumpManager, - private val headsUpManager: HeadsUpManager, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, - private val keyguardRepository: KeyguardRepository, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor, - private val logger: KeyguardCoordinatorLogger, - @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, - private val secureSettings: SecureSettings, - private val seenNotificationsInteractor: SeenNotificationsInteractor, private val statusBarStateController: StatusBarStateController, -) : 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 +) : Coordinator { override fun attach(pipeline: NotifPipeline) { setupInvalidateNotifListCallbacks() @@ -107,385 +46,14 @@ constructor( pipeline.addFinalizeFilter(notifFilter) keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter) updateSectionHeadersVisibility() - attachUnseenFilter(pipeline) - } - - private fun attachUnseenFilter(pipeline: NotifPipeline) { - if (NotificationMinimalismPrototype.V2.isEnabled) { - pipeline.addPromoter(unseenNotifPromoter) - pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs) - } - pipeline.addFinalizeFilter(unseenNotifFilter) - pipeline.addCollectionListener(collectionListener) - scope.launch { trackUnseenFilterSettingChanges() } - dumpManager.registerDumpable(this) - } - - private suspend fun trackSeenNotifications() { - // Whether or not keyguard is visible (or occluded). - val isKeyguardPresent: Flow<Boolean> = - keyguardTransitionInteractor - .transitionValue( - scene = Scenes.Gone, - stateWithoutSceneContainer = KeyguardState.GONE, - ) - .map { it == 0f } - .distinctUntilChanged() - .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } - - // 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 { - // 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") - // Keyguard is gone, notifications can be immediately marked as seen when they - // become visible. - trackSeenNotificationsWhileUnlocked() - } - } - } - - /** - * 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) - } - } - } - // 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) - } - } - } - - /** - * 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 trackSeenNotificationsWhileUnlocked() { - coroutineScope { - launch { clearUnseenNotificationsWhenShadeIsExpanded() } - launch { markHeadsUpNotificationsAsSeen() } - } - } - - private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { - statusBarStateController.expansionChanges.collectLatest { isExpanded -> - // Give keyguard events time to propagate, in case this expansion is part of the - // keyguard transition and not the user expanding the shade - yield() - if (isExpanded) { - logger.logShadeExpanded() - unseenNotifications.clear() - } - } - } - - private suspend fun markHeadsUpNotificationsAsSeen() { - headsUpManager.allEntries - .filter { it.isRowPinned } - .forEach { unseenNotifications.remove(it) } - headsUpManager.headsUpEvents.collect { (entry, isHun) -> - if (isHun) { - logger.logUnseenHun(entry.key) - unseenNotifications.remove(entry) - } - } } - private fun unseenFeatureEnabled(): Flow<Boolean> { - if ( - NotificationMinimalismPrototype.V1.isEnabled || - NotificationMinimalismPrototype.V2.isEnabled - ) { - return flowOf(true) - } - return secureSettings - // emit whenever the setting has changed - .observerFlow( - UserHandle.USER_ALL, - Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - ) - // perform a query immediately - .onStart { emit(Unit) } - // for each change, lookup the new value - .map { - secureSettings.getIntForUser( - name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - def = 0, - userHandle = 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() - } - - private suspend fun trackUnseenFilterSettingChanges() { - unseenFeatureEnabled().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() - } - } - } - - private val collectionListener = - object : NotifCollectionListener { - override fun onEntryAdded(entry: NotificationEntry) { - if ( - keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded - ) { - logger.logUnseenAdded(entry.key) - unseenNotifications.add(entry) - unseenEntryAdded.tryEmit(entry) - } - } - - override fun onEntryUpdated(entry: NotificationEntry) { - if ( - keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded - ) { - 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) - } - } - } - - private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return - // Only ever elevate a top unseen notification on keyguard, not even locked shade - if (statusBarStateController.state != StatusBarState.KEYGUARD) { - seenNotificationsInteractor.setTopOngoingNotification(null) - seenNotificationsInteractor.setTopUnseenNotification(null) - return - } - // On keyguard pick the top-ranked unseen or ongoing notification to elevate - val nonSummaryEntries: Sequence<NotificationEntry> = - list - .asSequence() - .flatMap { - when (it) { - is NotificationEntry -> listOfNotNull(it) - is GroupEntry -> it.children - else -> error("unhandled type of $it") - } - } - .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT } - seenNotificationsInteractor.setTopOngoingNotification( - nonSummaryEntries - .filter { ColorizedFgsCoordinator.isRichOngoing(it) } - .minByOrNull { it.ranking.rank } - ) - seenNotificationsInteractor.setTopUnseenNotification( - nonSummaryEntries - .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications } - .minByOrNull { it.ranking.rank } - ) - } - - @VisibleForTesting - internal val unseenNotifPromoter = - object : NotifPromoter("$TAG-unseen") { - override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false - else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false - else - seenNotificationsInteractor.isTopOngoingNotification(child) || - seenNotificationsInteractor.isTopUnseenNotification(child) - } - - val topOngoingSectioner = - object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { - override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false - return entry.anyEntry { notificationEntry -> - seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) - } - } - } - - val topUnseenSectioner = - object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { - override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false - return entry.anyEntry { notificationEntry -> - seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) - } - } - } - - private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) = - when { - predicate(representativeEntry) -> true - this !is GroupEntry -> false - else -> children.any(predicate) - } - - @VisibleForTesting - internal val unseenNotifFilter = - object : NotifFilter("$TAG-unseen") { - - var hasFilteredAnyNotifs = false - - /** - * Encapsulates a definition of "being on the keyguard". Note that these two definitions - * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does - * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing] - * is any state where the keyguard has not been dismissed, including locked shade and - * occluded lock screen. - * - * Returning false for locked shade and occluded states means that this filter will - * allow seen notifications to appear in the locked shade. - */ - private fun isOnKeyguard(): Boolean = - if (NotificationMinimalismPrototype.V2.isEnabled) { - false // disable this feature under this prototype - } else if ( - NotificationMinimalismPrototype.V1.isEnabled && - NotificationMinimalismPrototype.V1.showOnLockedShade - ) { - statusBarStateController.state == StatusBarState.KEYGUARD - } else { - keyguardRepository.isKeyguardShowing() - } - - override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = - when { - // Don't apply filter if the setting is disabled - !unseenFilterEnabled -> false - // Don't apply filter if the keyguard isn't currently showing - !isOnKeyguard() -> false - // Don't apply the filter if the notification is unseen - unseenNotifications.contains(entry) -> false - // Don't apply the filter to (non-promoted) group summaries - // - summary will be pruned if necessary, depending on if children are filtered - entry.parent?.summary == entry -> false - // Check that the entry satisfies certain characteristics that would bypass the - // filter - shouldIgnoreUnseenCheck(entry) -> false - else -> true - }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered } - - override fun onCleanup() { - logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs) - seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs) - hasFilteredAnyNotifs = false - } - } - private val notifFilter: NotifFilter = object : NotifFilter(TAG) { override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = keyguardNotificationVisibilityProvider.shouldHideNotification(entry) } - private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean = - when { - entry.isMediaNotification -> true - entry.sbn.isOngoing -> true - else -> false - } - // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on // these same updates private fun setupInvalidateNotifListCallbacks() {} @@ -502,22 +70,7 @@ constructor( sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections } - override fun dump(pw: PrintWriter, args: Array<out String>) = - with(pw.asIndenting()) { - println( - "notificationListInteractor.hasFilteredOutSeenNotifications.value=" + - seenNotificationsInteractor.hasFilteredOutSeenNotifications.value - ) - println("unseen notifications:") - indentIfPossible { - for (notification in unseenNotifications) { - println(notification.key) - } - } - } - companion object { private const val TAG = "KeyguardCoordinator" - private val SEEN_TIMEOUT = 5.seconds } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index e0389820aedf..99327d1fe116 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -17,7 +17,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED -import com.android.systemui.statusbar.notification.collection.* +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag +import com.android.systemui.statusbar.notification.collection.PipelineDumpable +import com.android.systemui.statusbar.notification.collection.PipelineDumper +import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider @@ -42,6 +46,7 @@ constructor( hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, keyguardCoordinator: KeyguardCoordinator, + unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator, rankingCoordinator: RankingCoordinator, colorizedFgsCoordinator: ColorizedFgsCoordinator, deviceProvisionedCoordinator: DeviceProvisionedCoordinator, @@ -82,6 +87,7 @@ constructor( mCoordinators.add(hideLocallyDismissedNotifsCoordinator) mCoordinators.add(hideNotifsForOtherUsersCoordinator) mCoordinators.add(keyguardCoordinator) + mCoordinators.add(unseenKeyguardCoordinator) mCoordinators.add(rankingCoordinator) mCoordinators.add(colorizedFgsCoordinator) mCoordinators.add(deviceProvisionedCoordinator) @@ -115,11 +121,11 @@ constructor( // Manually add Ordered Sections if (NotificationMinimalismPrototype.V2.isEnabled) { - mOrderedSections.add(keyguardCoordinator.topOngoingSectioner) // Top Ongoing + mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing } mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp if (NotificationMinimalismPrototype.V2.isEnabled) { - mOrderedSections.add(keyguardCoordinator.topUnseenSectioner) // Top Unseen + mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen } mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService if (PriorityPeopleSection.isEnabled) { @@ -131,10 +137,10 @@ constructor( } mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting if (NotificationClassificationFlag.isEnabled) { - mOrderedSections.add(bundleCoordinator.newsSectioner); - mOrderedSections.add(bundleCoordinator.socialSectioner); - mOrderedSections.add(bundleCoordinator.recsSectioner); - mOrderedSections.add(bundleCoordinator.promoSectioner); + mOrderedSections.add(bundleCoordinator.newsSectioner) + mOrderedSections.add(bundleCoordinator.socialSectioner) + mOrderedSections.add(bundleCoordinator.recsSectioner) + mOrderedSections.add(bundleCoordinator.promoSectioner) } mOrderedSections.add(rankingCoordinator.silentSectioner) // Silent mOrderedSections.add(rankingCoordinator.minimizedSectioner) // Minimized diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt new file mode 100644 index 000000000000..5dd1663f712f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.os.UserHandle +import android.provider.Settings +import androidx.annotation.VisibleForTesting +import com.android.systemui.Dumpable +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.expansionChanges +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.ListEntry +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 +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING +import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN +import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.statusbar.policy.headsUpEvents +import com.android.systemui.util.asIndenting +import com.android.systemui.util.indentIfPossible +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.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +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.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +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. If enabled, it will also track and hide seen notifications on the + * lockscreen. + */ +@CoordinatorScope +@SuppressLint("SharedFlowCreation") +class OriginalUnseenKeyguardCoordinator +@Inject +constructor( + @Background private val bgDispatcher: CoroutineDispatcher, + private val dumpManager: DumpManager, + private val headsUpManager: HeadsUpManager, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val logger: KeyguardCoordinatorLogger, + @Application private val scope: CoroutineScope, + private val secureSettings: SecureSettings, + private val seenNotificationsInteractor: SeenNotificationsInteractor, + private val statusBarStateController: StatusBarStateController, +) : 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) { + if (NotificationMinimalismPrototype.V2.isEnabled) { + pipeline.addPromoter(unseenNotifPromoter) + pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs) + } + pipeline.addFinalizeFilter(unseenNotifFilter) + pipeline.addCollectionListener(collectionListener) + scope.launch { trackUnseenFilterSettingChanges() } + dumpManager.registerDumpable(this) + } + + private suspend fun trackSeenNotifications() { + // Whether or not keyguard is visible (or occluded). + val isKeyguardPresentFlow: Flow<Boolean> = + keyguardTransitionInteractor + .transitionValue( + scene = Scenes.Gone, + stateWithoutSceneContainer = KeyguardState.GONE, + ) + .map { it == 0f } + .distinctUntilChanged() + .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } + + // 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. + isKeyguardPresentFlow.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 { + // 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") + // Keyguard is gone, notifications can be immediately marked as seen when they + // become visible. + trackSeenNotificationsWhileUnlocked() + } + } + } + + /** + * 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) + } + } + } + // 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) + } + } + } + + /** + * 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 trackSeenNotificationsWhileUnlocked() { + coroutineScope { + launch { clearUnseenNotificationsWhenShadeIsExpanded() } + launch { markHeadsUpNotificationsAsSeen() } + } + } + + private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { + statusBarStateController.expansionChanges.collectLatest { isExpanded -> + // Give keyguard events time to propagate, in case this expansion is part of the + // keyguard transition and not the user expanding the shade + yield() + if (isExpanded) { + logger.logShadeExpanded() + unseenNotifications.clear() + } + } + } + + private suspend fun markHeadsUpNotificationsAsSeen() { + headsUpManager.allEntries + .filter { it.isRowPinned } + .forEach { unseenNotifications.remove(it) } + headsUpManager.headsUpEvents.collect { (entry, isHun) -> + if (isHun) { + logger.logUnseenHun(entry.key) + unseenNotifications.remove(entry) + } + } + } + + private fun unseenFeatureEnabled(): Flow<Boolean> { + if ( + NotificationMinimalismPrototype.V1.isEnabled || + NotificationMinimalismPrototype.V2.isEnabled + ) { + return flowOf(true) + } + return secureSettings + // emit whenever the setting has changed + .observerFlow( + UserHandle.USER_ALL, + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + ) + // perform a query immediately + .onStart { emit(Unit) } + // for each change, lookup the new value + .map { + secureSettings.getIntForUser( + name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + def = 0, + userHandle = 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() + } + + private suspend fun trackUnseenFilterSettingChanges() { + unseenFeatureEnabled().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() + } + } + } + + private val collectionListener = + object : NotifCollectionListener { + override fun onEntryAdded(entry: NotificationEntry) { + if ( + keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded + ) { + logger.logUnseenAdded(entry.key) + unseenNotifications.add(entry) + unseenEntryAdded.tryEmit(entry) + } + } + + override fun onEntryUpdated(entry: NotificationEntry) { + if ( + keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded + ) { + 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) + } + } + } + + private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { + if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return + // Only ever elevate a top unseen notification on keyguard, not even locked shade + if (statusBarStateController.state != StatusBarState.KEYGUARD) { + seenNotificationsInteractor.setTopOngoingNotification(null) + seenNotificationsInteractor.setTopUnseenNotification(null) + return + } + // On keyguard pick the top-ranked unseen or ongoing notification to elevate + val nonSummaryEntries: Sequence<NotificationEntry> = + list + .asSequence() + .flatMap { + when (it) { + is NotificationEntry -> listOfNotNull(it) + is GroupEntry -> it.children + else -> error("unhandled type of $it") + } + } + .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT } + seenNotificationsInteractor.setTopOngoingNotification( + nonSummaryEntries + .filter { ColorizedFgsCoordinator.isRichOngoing(it) } + .minByOrNull { it.ranking.rank } + ) + seenNotificationsInteractor.setTopUnseenNotification( + nonSummaryEntries + .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications } + .minByOrNull { it.ranking.rank } + ) + } + + @VisibleForTesting + val unseenNotifPromoter = + object : NotifPromoter("$TAG-unseen") { + override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = + if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false + else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false + else + seenNotificationsInteractor.isTopOngoingNotification(child) || + seenNotificationsInteractor.isTopUnseenNotification(child) + } + + val topOngoingSectioner = + object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { + override fun isInSection(entry: ListEntry): Boolean { + if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false + return entry.anyEntry { notificationEntry -> + seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) + } + } + } + + val topUnseenSectioner = + object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { + override fun isInSection(entry: ListEntry): Boolean { + if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false + return entry.anyEntry { notificationEntry -> + seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) + } + } + } + + private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) = + when { + predicate(representativeEntry) -> true + this !is GroupEntry -> false + else -> children.any(predicate) + } + + @VisibleForTesting + val unseenNotifFilter = + object : NotifFilter("$TAG-unseen") { + + var hasFilteredAnyNotifs = false + + /** + * Encapsulates a definition of "being on the keyguard". Note that these two definitions + * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does + * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing] + * is any state where the keyguard has not been dismissed, including locked shade and + * occluded lock screen. + * + * Returning false for locked shade and occluded states means that this filter will + * allow seen notifications to appear in the locked shade. + */ + private fun isOnKeyguard(): Boolean = + if (NotificationMinimalismPrototype.V2.isEnabled) { + false // disable this feature under this prototype + } else if ( + NotificationMinimalismPrototype.V1.isEnabled && + NotificationMinimalismPrototype.V1.showOnLockedShade + ) { + statusBarStateController.state == StatusBarState.KEYGUARD + } else { + keyguardRepository.isKeyguardShowing() + } + + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = + when { + // Don't apply filter if the setting is disabled + !unseenFilterEnabled -> false + // Don't apply filter if the keyguard isn't currently showing + !isOnKeyguard() -> false + // Don't apply the filter if the notification is unseen + unseenNotifications.contains(entry) -> false + // Don't apply the filter to (non-promoted) group summaries + // - summary will be pruned if necessary, depending on if children are filtered + entry.parent?.summary == entry -> false + // Check that the entry satisfies certain characteristics that would bypass the + // filter + shouldIgnoreUnseenCheck(entry) -> false + else -> true + }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered } + + override fun onCleanup() { + logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs) + seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs) + hasFilteredAnyNotifs = false + } + } + + private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean = + when { + entry.isMediaNotification -> true + entry.sbn.isOngoing -> true + else -> false + } + + override fun dump(pw: PrintWriter, args: Array<out String>) = + with(pw.asIndenting()) { + println( + "notificationListInteractor.hasFilteredOutSeenNotifications.value=" + + seenNotificationsInteractor.hasFilteredOutSeenNotifications.value + ) + println("unseen notifications:") + indentIfPossible { + for (notification in unseenNotifications) { + println(notification.key) + } + } + } + + companion object { + private const val TAG = "OriginalUnseenKeyguardCoordinator" + private val SEEN_TIMEOUT = 5.seconds + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt index d87b3e23b471..4218be26c58e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt @@ -13,88 +13,57 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.statusbar.notification.collection.coordinator -import android.app.Notification -import android.os.UserHandle -import android.platform.test.flag.junit.FlagsParameterization -import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.andSceneContainer -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.scene.data.repository.Idle -import com.android.systemui.scene.data.repository.setTransition -import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider -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 -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test 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 org.mockito.Mockito.`when` as whenever -import platform.test.runner.parameterized.ParameterizedAndroidJunit4 -import platform.test.runner.parameterized.Parameters +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest -@RunWith(ParameterizedAndroidJunit4::class) -class KeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() { +@RunWith(AndroidJUnit4::class) +class KeyguardCoordinatorTest : SysuiTestCase() { - private val kosmos = Kosmos() - - private val headsUpManager: HeadsUpManager = mock() private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() - private val keyguardRepository = FakeKeyguardRepository() - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val notifPipeline: NotifPipeline = mock() private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() private val statusBarStateController: StatusBarStateController = mock() - init { - mSetFlagsRule.setFlagsParameterization(flags) + private lateinit var onStateChangeListener: Consumer<String> + + @Before + fun setup() { + val keyguardCoordinator = + KeyguardCoordinator( + keyguardNotifVisibilityProvider, + sectionHeaderVisibilityProvider, + statusBarStateController, + ) + keyguardCoordinator.attach(notifPipeline) + onStateChangeListener = + argumentCaptor { + verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) + } + .lastValue } @Test - fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest { + fun testSetSectionHeadersVisibleInShade() { clearInvocations(sectionHeaderVisibilityProvider) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) onStateChangeListener.accept("state change") @@ -102,617 +71,10 @@ class KeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest { + fun testSetSectionHeadersNotVisibleOnKeyguard() { clearInvocations(sectionHeaderVisibilityProvider) whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) onStateChangeListener.accept("state change") verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false) } - - @Test - fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() { - // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - - // WHEN: The keyguard goes away - keyguardRepository.setKeyguardShowing(false) - testScheduler.runCurrent() - - // THEN: The notification is shown regardless - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() { - // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(false) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The device transitions to AOD - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - this.testScheduler, - ) - testScheduler.runCurrent() - - // THEN: We are no longer listening for shade expansions - verify(statusBarStateController, never()).addCallback(any()) - } - } - - @Test - fun unseenFilter_headsUpMarkedAsSeen() { - // GIVEN: Keyguard is not showing, shade is not expanded - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(false) - runKeyguardCoordinatorTest { - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) - ) - - // WHEN: A notification is posted - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: That notification is heads up - onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true) - testScheduler.runCurrent() - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) - ) - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - - // WHEN: The keyguard goes away - keyguardRepository.setKeyguardShowing(false) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE) - ) - - // THEN: The notification is shown regardless - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() { - // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = - NotificationEntryBuilder() - .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build()) - .build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "ongoing" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() { - // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = - NotificationEntryBuilder().build().apply { - row = - mock<ExpandableNotificationRow>().apply { - whenever(isMediaRow).thenReturn(true) - } - } - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "media" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterUpdatesSeenProviderWhenSuppressing() { - // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - - // WHEN: The filter is cleaned up - unseenFilter.onCleanup() - - // THEN: The SeenNotificationProvider has been updated to reflect the suppression - assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue() - } - } - - @Test - fun unseenFilterInvalidatesWhenSettingChanges() { - // GIVEN: Keyguard is not showing, and shade is expanded - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - // GIVEN: A notification is present - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // GIVEN: The setting for filtering unseen notifications is disabled - showOnlyUnseenNotifsOnKeyguardSetting = false - - // GIVEN: The pipeline has registered the unseen filter for invalidation - val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock() - unseenFilter.setInvalidationListener(invalidationListener) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is not filtered out - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - - // WHEN: The secure setting is changed - showOnlyUnseenNotifsOnKeyguardSetting = true - - // THEN: The pipeline is invalidated - verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), anyString()) - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - } - } - - @Test - fun unseenFilterAllowsNewNotif() { - // GIVEN: Keyguard is showing, no notifications present - keyguardRepository.setKeyguardShowing(true) - runKeyguardCoordinatorTest { - // WHEN: A new notification is posted - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // THEN: The notification is recognized as "unseen" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterSeenGroupSummaryWithUnseenChild() { - // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - // WHEN: A new notification is posted - val fakeSummary = NotificationEntryBuilder().build() - val fakeChild = - NotificationEntryBuilder() - .setGroup(context, "group") - .setGroupSummary(context, false) - .build() - GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build() - - collectionListener.onEntryAdded(fakeSummary) - collectionListener.onEntryAdded(fakeChild) - - // WHEN: Keyguard is now showing, both notifications are marked as seen - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // WHEN: The child notification is now unseen - collectionListener.onEntryUpdated(fakeChild) - - // THEN: The summary is not filtered out, because the child is unseen - assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse() - } - } - - @Test - fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() { - // GIVEN: Keyguard is showing, not dozing, unseen notification is present - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: five seconds have passed - testScheduler.advanceTimeBy(5.seconds) - testScheduler.runCurrent() - - // WHEN: Keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) - ) - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) - ) - - // THEN: The notification is now recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - } - } - - @Test - fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() { - // GIVEN: Keyguard is showing, unseen notification is present - keyguardRepository.setKeyguardShowing(true) - runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: Keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is not recognized as "seen" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() { - // GIVEN: Keyguard is showing, not dozing, unseen notification is present - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, 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) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) - ) - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN) - ) - - // 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.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - 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.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - 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.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - 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.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - 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.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - 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.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - 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 seenNotificationsInteractor = - SeenNotificationsInteractor(ActiveNotificationListRepository()) - val keyguardCoordinator = - KeyguardCoordinator( - testDispatcher, - mock<DumpManager>(), - headsUpManager, - keyguardNotifVisibilityProvider, - keyguardRepository, - kosmos.keyguardTransitionInteractor, - KeyguardCoordinatorLogger(logcatLogBuffer()), - testScope.backgroundScope, - sectionHeaderVisibilityProvider, - fakeSettings, - seenNotificationsInteractor, - statusBarStateController, - ) - keyguardCoordinator.attach(notifPipeline) - testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { - KeyguardCoordinatorTestScope( - keyguardCoordinator, - testScope, - seenNotificationsInteractor, - fakeSettings, - ) - .testBlock() - } - } - - private inner class KeyguardCoordinatorTestScope( - private val keyguardCoordinator: KeyguardCoordinator, - private val scope: TestScope, - val seenNotificationsInteractor: SeenNotificationsInteractor, - private val fakeSettings: FakeSettings, - ) : CoroutineScope by scope { - val testScheduler: TestCoroutineScheduler - get() = scope.testScheduler - - val onStateChangeListener: Consumer<String> = withArgCaptor { - verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) - } - - val unseenFilter: NotifFilter - get() = keyguardCoordinator.unseenNotifFilter - - val collectionListener: NotifCollectionListener = withArgCaptor { - verify(notifPipeline).addCollectionListener(capture()) - } - - val onHeadsUpChangedListener: OnHeadsUpChangedListener - get() = withArgCaptor { verify(headsUpManager).addListener(capture()) } - - val statusBarStateListener: StatusBarStateController.StateListener - get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) } - - var showOnlyUnseenNotifsOnKeyguardSetting: Boolean - get() = - fakeSettings.getIntForUser( - Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - UserHandle.USER_CURRENT, - ) == 1 - set(value) { - fakeSettings.putIntForUser( - Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - if (value) 1 else 2, - UserHandle.USER_CURRENT, - ) - } - } - - companion object { - @JvmStatic - @Parameters(name = "{0}") - fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf().andSceneContainer() - } - } } |