diff options
3 files changed, 291 insertions, 26 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt new file mode 100644 index 000000000000..d268e358690f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +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 javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Filter out notifications on the lockscreen if the lockscreen hosted dream is active. If the user + * stops dreaming, pulls the shade down or unlocks the device, then the notifications are unhidden. + */ +@CoordinatorScope +class DreamCoordinator +@Inject +constructor( + private val statusBarStateController: SysuiStatusBarStateController, + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, +) : Coordinator { + private var isOnKeyguard = false + private var isLockscreenHostedDream = false + + override fun attach(pipeline: NotifPipeline) { + pipeline.addPreGroupFilter(filter) + statusBarStateController.addCallback(statusBarStateListener) + scope.launch { attachFilterOnDreamingStateChange() } + recordStatusBarState(statusBarStateController.state) + } + + private val filter = + object : NotifFilter("LockscreenHostedDreamFilter") { + var isFiltering = false + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean { + return isFiltering + } + inline fun update(msg: () -> String) { + val wasFiltering = isFiltering + isFiltering = isLockscreenHostedDream && isOnKeyguard + if (wasFiltering != isFiltering) { + invalidateList(msg()) + } + } + } + + private val statusBarStateListener = + object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + recordStatusBarState(newState) + } + } + + private suspend fun attachFilterOnDreamingStateChange() { + keyguardRepository.isActiveDreamLockscreenHosted.collect { isDreaming -> + recordDreamingState(isDreaming) + } + } + + private fun recordStatusBarState(newState: Int) { + isOnKeyguard = newState == StatusBarState.KEYGUARD + filter.update { "recordStatusBarState: " + StatusBarState.toString(newState) } + } + + private fun recordDreamingState(isDreaming: Boolean) { + isLockscreenHostedDream = isDreaming + filter.update { "recordLockscreenHostedDreamState: $isDreaming" } + } +} 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 e5953cfc07cd..0ccab9e46b72 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 @@ -15,6 +15,8 @@ */ 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.NotifPipeline import com.android.systemui.statusbar.notification.collection.PipelineDumpable import com.android.systemui.statusbar.notification.collection.PipelineDumper @@ -31,32 +33,34 @@ interface NotifCoordinators : Coordinator, PipelineDumpable @CoordinatorScope class NotifCoordinatorsImpl @Inject constructor( - sectionStyleProvider: SectionStyleProvider, - dataStoreCoordinator: DataStoreCoordinator, - hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, - hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, - keyguardCoordinator: KeyguardCoordinator, - rankingCoordinator: RankingCoordinator, - appOpsCoordinator: AppOpsCoordinator, - deviceProvisionedCoordinator: DeviceProvisionedCoordinator, - bubbleCoordinator: BubbleCoordinator, - headsUpCoordinator: HeadsUpCoordinator, - gutsCoordinator: GutsCoordinator, - conversationCoordinator: ConversationCoordinator, - debugModeCoordinator: DebugModeCoordinator, - groupCountCoordinator: GroupCountCoordinator, - groupWhenCoordinator: GroupWhenCoordinator, - mediaCoordinator: MediaCoordinator, - preparationCoordinator: PreparationCoordinator, - remoteInputCoordinator: RemoteInputCoordinator, - rowAppearanceCoordinator: RowAppearanceCoordinator, - stackCoordinator: StackCoordinator, - shadeEventCoordinator: ShadeEventCoordinator, - smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, - viewConfigCoordinator: ViewConfigCoordinator, - visualStabilityCoordinator: VisualStabilityCoordinator, - sensitiveContentCoordinator: SensitiveContentCoordinator, - dismissibilityCoordinator: DismissibilityCoordinator, + sectionStyleProvider: SectionStyleProvider, + featureFlags: FeatureFlags, + dataStoreCoordinator: DataStoreCoordinator, + hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, + hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, + keyguardCoordinator: KeyguardCoordinator, + rankingCoordinator: RankingCoordinator, + appOpsCoordinator: AppOpsCoordinator, + deviceProvisionedCoordinator: DeviceProvisionedCoordinator, + bubbleCoordinator: BubbleCoordinator, + headsUpCoordinator: HeadsUpCoordinator, + gutsCoordinator: GutsCoordinator, + conversationCoordinator: ConversationCoordinator, + debugModeCoordinator: DebugModeCoordinator, + groupCountCoordinator: GroupCountCoordinator, + groupWhenCoordinator: GroupWhenCoordinator, + mediaCoordinator: MediaCoordinator, + preparationCoordinator: PreparationCoordinator, + remoteInputCoordinator: RemoteInputCoordinator, + rowAppearanceCoordinator: RowAppearanceCoordinator, + stackCoordinator: StackCoordinator, + shadeEventCoordinator: ShadeEventCoordinator, + smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, + viewConfigCoordinator: ViewConfigCoordinator, + visualStabilityCoordinator: VisualStabilityCoordinator, + sensitiveContentCoordinator: SensitiveContentCoordinator, + dismissibilityCoordinator: DismissibilityCoordinator, + dreamCoordinator: DreamCoordinator, ) : NotifCoordinators { private val mCoreCoordinators: MutableList<CoreCoordinator> = ArrayList() @@ -96,6 +100,10 @@ class NotifCoordinatorsImpl @Inject constructor( mCoordinators.add(remoteInputCoordinator) mCoordinators.add(dismissibilityCoordinator) + if (featureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) { + mCoordinators.add(dreamCoordinator) + } + // Manually add Ordered Sections mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp mOrderedSections.add(appOpsCoordinator.sectioner) // ForegroundService diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt new file mode 100644 index 000000000000..a544cad03b77 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 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.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +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.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DreamCoordinatorTest : SysuiTestCase() { + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController + @Mock private lateinit var notifPipeline: NotifPipeline + @Mock private lateinit var filterListener: Pluggable.PluggableListener<NotifFilter> + + private val keyguardRepository = FakeKeyguardRepository() + private var fakeEntry: NotificationEntry = NotificationEntryBuilder().build() + val testDispatcher = UnconfinedTestDispatcher() + val testScope = TestScope(testDispatcher) + + private lateinit var filter: NotifFilter + private lateinit var statusBarListener: StatusBarStateController.StateListener + private lateinit var dreamCoordinator: DreamCoordinator + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + + // Build the coordinator + dreamCoordinator = + DreamCoordinator( + statusBarStateController, + testScope.backgroundScope, + keyguardRepository + ) + + // Attach the pipeline and capture the listeners/filters that it registers + dreamCoordinator.attach(notifPipeline) + + filter = withArgCaptor { verify(notifPipeline).addPreGroupFilter(capture()) } + filter.setInvalidationListener(filterListener) + + statusBarListener = withArgCaptor { + verify(statusBarStateController).addCallback(capture()) + } + } + + @Test + fun hideNotifications_whenDreamingAndOnKeyguard() = + testScope.runTest { + // GIVEN we are on keyguard and not dreaming + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(false) + runCurrent() + + // THEN notifications are not filtered out + verifyPipelinesNotInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse() + + // WHEN dreaming starts and the active dream is hosted in lockscreen + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + runCurrent() + + // THEN pipeline is notified and notifications should all be filtered out + verifyPipelinesInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + + @Test + fun showNotifications_whenDreamingAndNotOnKeyguard() = + testScope.runTest { + // GIVEN we are on the keyguard and active dream is hosted in lockscreen + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + runCurrent() + + // THEN pipeline is notified and notifications are all filtered out + verifyPipelinesInvalidated() + clearPipelineInvocations() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN we are no longer on the keyguard + statusBarListener.onStateChanged(StatusBarState.SHADE) + + // THEN pipeline is notified and notifications are not filtered out + verifyPipelinesInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + + @Test + fun showNotifications_whenOnKeyguardAndNotDreaming() = + testScope.runTest { + // GIVEN we are on the keyguard and active dream is hosted in lockscreen + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + runCurrent() + + // THEN pipeline is notified and notifications are all filtered out + verifyPipelinesInvalidated() + clearPipelineInvocations() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN the lockscreen hosted dream stops + keyguardRepository.setIsActiveDreamLockscreenHosted(false) + runCurrent() + + // THEN pipeline is notified and notifications are not filtered out + verifyPipelinesInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + + private fun verifyPipelinesInvalidated() { + verify(filterListener).onPluggableInvalidated(eq(filter), any()) + } + + private fun verifyPipelinesNotInvalidated() { + verify(filterListener, never()).onPluggableInvalidated(eq(filter), any()) + } + + private fun clearPipelineInvocations() { + clearInvocations(filterListener) + } +} |