Loading packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt 0 → 100644 +36 −0 Original line number Diff line number Diff line /* * 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 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.plugins.statusbar.StatusBarStateController import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow /** Returns a [Flow] that emits whenever [StatusBarStateController.isExpanded] changes value. */ val StatusBarStateController.expansionChanges: Flow<Boolean> get() = conflatedCallbackFlow { val listener = object : StatusBarStateController.StateListener { override fun onExpandedChanged(isExpanded: Boolean) { trySend(isExpanded) } } trySend(isExpanded) addCallback(listener) awaitClose { removeCallback(listener) } } packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +44 −29 Original line number Diff line number Diff line Loading @@ -16,16 +16,15 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.database.ContentObserver import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background 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.expansionChanges import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry Loading @@ -35,15 +34,14 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxy import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOn Loading @@ -60,6 +58,7 @@ class KeyguardCoordinator @Inject constructor( @Background private val bgDispatcher: CoroutineDispatcher, private val headsUpManager: HeadsUpManager, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, private val keyguardRepository: KeyguardRepository, private val notifPipelineFlags: NotifPipelineFlags, Loading Loading @@ -87,28 +86,53 @@ constructor( private fun attachUnseenFilter(pipeline: NotifPipeline) { pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) scope.launch { clearUnseenWhenKeyguardIsDismissed() } scope.launch { trackUnseenNotificationsWhileUnlocked() } scope.launch { invalidateWhenUnseenSettingChanges() } } private suspend fun clearUnseenWhenKeyguardIsDismissed() { // Use collectLatest so that the suspending block is cancelled if isKeyguardShowing changes // during the timeout period private suspend fun trackUnseenNotificationsWhileUnlocked() { // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is // showing again keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing -> if (!isKeyguardShowing) { unseenNotifFilter.invalidateList("keyguard no longer showing") delay(SEEN_TIMEOUT) trackUnseenNotifications() } } } private suspend fun trackUnseenNotifications() { coroutineScope { launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { markHeadsUpNotificationsAsSeen() } } } private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { statusBarStateController.expansionChanges.collect { isExpanded -> if (isExpanded) { unseenNotifications.clear() } } } private suspend fun markHeadsUpNotificationsAsSeen() { headsUpManager.allEntries .filter { it.isRowPinned } .forEach { unseenNotifications.remove(it) } headsUpManager.headsUpEvents.collect { (entry, isHun) -> if (isHun) { unseenNotifications.remove(entry) } } } private suspend fun invalidateWhenUnseenSettingChanges() { secureSettings // emit whenever the setting has changed .settingChangesForUser( Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, .observerFlow( UserHandle.USER_ALL, Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, ) // perform a query immediately .onStart { emit(Unit) } Loading Loading @@ -136,13 +160,17 @@ constructor( private val collectionListener = object : NotifCollectionListener { override fun onEntryAdded(entry: NotificationEntry) { if (keyguardRepository.isKeyguardShowing()) { if ( keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded ) { unseenNotifications.add(entry) } } override fun onEntryUpdated(entry: NotificationEntry) { if (keyguardRepository.isKeyguardShowing()) { if ( keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded ) { unseenNotifications.add(entry) } } Loading Loading @@ -212,18 +240,5 @@ constructor( companion object { private const val TAG = "KeyguardCoordinator" private val SEEN_TIMEOUT = 5.seconds } } private fun SettingsProxy.settingChangesForUser(name: String, userHandle: Int): Flow<Unit> = conflatedCallbackFlow { val observer = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { trySend(Unit) } } registerContentObserverForUser(name, observer, userHandle) awaitClose { unregisterContentObserver(observer) } } packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerExt.kt 0 → 100644 +38 −0 Original line number Diff line number Diff line /* * 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.policy import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.statusbar.notification.collection.NotificationEntry import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow /** * Returns a [Flow] that emits events whenever a [NotificationEntry] enters or exists the "heads up" * state. */ val HeadsUpManager.headsUpEvents: Flow<Pair<NotificationEntry, Boolean>> get() = conflatedCallbackFlow { val listener = object : OnHeadsUpChangedListener { override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { trySend(entry to isHeadsUp) } } addListener(listener) awaitClose { removeListener(listener) } } packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +65 −16 Original line number Diff line number Diff line Loading @@ -38,6 +38,8 @@ import com.android.systemui.statusbar.notification.collection.provider.SeenNotif import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl 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.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor Loading @@ -63,6 +65,7 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) class KeyguardCoordinatorTest : SysuiTestCase() { private val headsUpManager: HeadsUpManager = mock() private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() private val keyguardRepository = FakeKeyguardRepository() private val notifPipelineFlags: NotifPipelineFlags = mock() Loading Loading @@ -90,8 +93,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a notification is present // 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) Loading @@ -112,12 +116,45 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } } @Test fun unseenFilter_headsUpMarkedAsSeen() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, shade is not expanded keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(false) runKeyguardCoordinatorTest { // 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) 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 unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and an ongoing notification is present // 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).setOngoing(true).build()) Loading @@ -137,8 +174,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a media notification is present // 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 { Loading @@ -160,8 +198,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterUpdatesSeenProviderWhenSuppressing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a notification is present // 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) Loading @@ -185,8 +224,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterInvalidatesWhenSettingChanges() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing // 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() Loading Loading @@ -237,8 +277,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterSeenGroupSummaryWithUnseenChild() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a notification is present // 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() Loading Loading @@ -276,11 +317,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: Keyguard is no longer showing for 5 seconds // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) testScheduler.runCurrent() testScheduler.advanceTimeBy(5.seconds.inWholeMilliseconds) testScheduler.runCurrent() // When: Shade is expanded statusBarStateListener.onExpandedChanged(true) // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) Loading @@ -292,7 +333,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } @Test fun unseenNotificationIsNotMarkedAsSeenIfTimeThresholdNotMet() { fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is showing, unseen notification is present Loading @@ -301,10 +342,8 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: Keyguard is no longer showing for <5 seconds // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) testScheduler.runCurrent() testScheduler.advanceTimeBy(1.seconds.inWholeMilliseconds) // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) Loading @@ -327,6 +366,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val keyguardCoordinator = KeyguardCoordinator( testDispatcher, headsUpManager, keyguardNotifVisibilityProvider, keyguardRepository, notifPipelineFlags, Loading Loading @@ -364,12 +404,21 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val unseenFilter: NotifFilter get() = keyguardCoordinator.unseenNotifFilter // TODO(254647461): Remove lazy once Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and // removed // TODO(254647461): Remove lazy from these properties once // Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and removed val collectionListener: NotifCollectionListener by lazy { withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) } } val onHeadsUpChangedListener: OnHeadsUpChangedListener by lazy { withArgCaptor { verify(headsUpManager).addListener(capture()) } } val statusBarStateListener: StatusBarStateController.StateListener by lazy { withArgCaptor { verify(statusBarStateController).addCallback(capture()) } } var showOnlyUnseenNotifsOnKeyguardSetting: Boolean get() = fakeSettings.getIntForUser( Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt 0 → 100644 +36 −0 Original line number Diff line number Diff line /* * 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 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.plugins.statusbar.StatusBarStateController import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow /** Returns a [Flow] that emits whenever [StatusBarStateController.isExpanded] changes value. */ val StatusBarStateController.expansionChanges: Flow<Boolean> get() = conflatedCallbackFlow { val listener = object : StatusBarStateController.StateListener { override fun onExpandedChanged(isExpanded: Boolean) { trySend(isExpanded) } } trySend(isExpanded) addCallback(listener) awaitClose { removeCallback(listener) } }
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +44 −29 Original line number Diff line number Diff line Loading @@ -16,16 +16,15 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.database.ContentObserver import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background 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.expansionChanges import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry Loading @@ -35,15 +34,14 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxy import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOn Loading @@ -60,6 +58,7 @@ class KeyguardCoordinator @Inject constructor( @Background private val bgDispatcher: CoroutineDispatcher, private val headsUpManager: HeadsUpManager, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, private val keyguardRepository: KeyguardRepository, private val notifPipelineFlags: NotifPipelineFlags, Loading Loading @@ -87,28 +86,53 @@ constructor( private fun attachUnseenFilter(pipeline: NotifPipeline) { pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) scope.launch { clearUnseenWhenKeyguardIsDismissed() } scope.launch { trackUnseenNotificationsWhileUnlocked() } scope.launch { invalidateWhenUnseenSettingChanges() } } private suspend fun clearUnseenWhenKeyguardIsDismissed() { // Use collectLatest so that the suspending block is cancelled if isKeyguardShowing changes // during the timeout period private suspend fun trackUnseenNotificationsWhileUnlocked() { // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is // showing again keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing -> if (!isKeyguardShowing) { unseenNotifFilter.invalidateList("keyguard no longer showing") delay(SEEN_TIMEOUT) trackUnseenNotifications() } } } private suspend fun trackUnseenNotifications() { coroutineScope { launch { clearUnseenNotificationsWhenShadeIsExpanded() } launch { markHeadsUpNotificationsAsSeen() } } } private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { statusBarStateController.expansionChanges.collect { isExpanded -> if (isExpanded) { unseenNotifications.clear() } } } private suspend fun markHeadsUpNotificationsAsSeen() { headsUpManager.allEntries .filter { it.isRowPinned } .forEach { unseenNotifications.remove(it) } headsUpManager.headsUpEvents.collect { (entry, isHun) -> if (isHun) { unseenNotifications.remove(entry) } } } private suspend fun invalidateWhenUnseenSettingChanges() { secureSettings // emit whenever the setting has changed .settingChangesForUser( Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, .observerFlow( UserHandle.USER_ALL, Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, ) // perform a query immediately .onStart { emit(Unit) } Loading Loading @@ -136,13 +160,17 @@ constructor( private val collectionListener = object : NotifCollectionListener { override fun onEntryAdded(entry: NotificationEntry) { if (keyguardRepository.isKeyguardShowing()) { if ( keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded ) { unseenNotifications.add(entry) } } override fun onEntryUpdated(entry: NotificationEntry) { if (keyguardRepository.isKeyguardShowing()) { if ( keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded ) { unseenNotifications.add(entry) } } Loading Loading @@ -212,18 +240,5 @@ constructor( companion object { private const val TAG = "KeyguardCoordinator" private val SEEN_TIMEOUT = 5.seconds } } private fun SettingsProxy.settingChangesForUser(name: String, userHandle: Int): Flow<Unit> = conflatedCallbackFlow { val observer = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { trySend(Unit) } } registerContentObserverForUser(name, observer, userHandle) awaitClose { unregisterContentObserver(observer) } }
packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerExt.kt 0 → 100644 +38 −0 Original line number Diff line number Diff line /* * 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.policy import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.statusbar.notification.collection.NotificationEntry import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow /** * Returns a [Flow] that emits events whenever a [NotificationEntry] enters or exists the "heads up" * state. */ val HeadsUpManager.headsUpEvents: Flow<Pair<NotificationEntry, Boolean>> get() = conflatedCallbackFlow { val listener = object : OnHeadsUpChangedListener { override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { trySend(entry to isHeadsUp) } } addListener(listener) awaitClose { removeListener(listener) } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +65 −16 Original line number Diff line number Diff line Loading @@ -38,6 +38,8 @@ import com.android.systemui.statusbar.notification.collection.provider.SeenNotif import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl 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.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor Loading @@ -63,6 +65,7 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) class KeyguardCoordinatorTest : SysuiTestCase() { private val headsUpManager: HeadsUpManager = mock() private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() private val keyguardRepository = FakeKeyguardRepository() private val notifPipelineFlags: NotifPipelineFlags = mock() Loading Loading @@ -90,8 +93,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a notification is present // 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) Loading @@ -112,12 +116,45 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } } @Test fun unseenFilter_headsUpMarkedAsSeen() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, shade is not expanded keyguardRepository.setKeyguardShowing(false) whenever(statusBarStateController.isExpanded).thenReturn(false) runKeyguardCoordinatorTest { // 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) 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 unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and an ongoing notification is present // 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).setOngoing(true).build()) Loading @@ -137,8 +174,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a media notification is present // 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 { Loading @@ -160,8 +198,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterUpdatesSeenProviderWhenSuppressing() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a notification is present // 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) Loading @@ -185,8 +224,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterInvalidatesWhenSettingChanges() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing // 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() Loading Loading @@ -237,8 +277,9 @@ class KeyguardCoordinatorTest : SysuiTestCase() { fun unseenFilterSeenGroupSummaryWithUnseenChild() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is not showing, and a notification is present // 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() Loading Loading @@ -276,11 +317,11 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: Keyguard is no longer showing for 5 seconds // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) testScheduler.runCurrent() testScheduler.advanceTimeBy(5.seconds.inWholeMilliseconds) testScheduler.runCurrent() // When: Shade is expanded statusBarStateListener.onExpandedChanged(true) // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) Loading @@ -292,7 +333,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } @Test fun unseenNotificationIsNotMarkedAsSeenIfTimeThresholdNotMet() { fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() { whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) // GIVEN: Keyguard is showing, unseen notification is present Loading @@ -301,10 +342,8 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val fakeEntry = NotificationEntryBuilder().build() collectionListener.onEntryAdded(fakeEntry) // WHEN: Keyguard is no longer showing for <5 seconds // WHEN: Keyguard is no longer showing keyguardRepository.setKeyguardShowing(false) testScheduler.runCurrent() testScheduler.advanceTimeBy(1.seconds.inWholeMilliseconds) // WHEN: Keyguard is shown again keyguardRepository.setKeyguardShowing(true) Loading @@ -327,6 +366,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val keyguardCoordinator = KeyguardCoordinator( testDispatcher, headsUpManager, keyguardNotifVisibilityProvider, keyguardRepository, notifPipelineFlags, Loading Loading @@ -364,12 +404,21 @@ class KeyguardCoordinatorTest : SysuiTestCase() { val unseenFilter: NotifFilter get() = keyguardCoordinator.unseenNotifFilter // TODO(254647461): Remove lazy once Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and // removed // TODO(254647461): Remove lazy from these properties once // Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and removed val collectionListener: NotifCollectionListener by lazy { withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) } } val onHeadsUpChangedListener: OnHeadsUpChangedListener by lazy { withArgCaptor { verify(headsUpManager).addListener(capture()) } } val statusBarStateListener: StatusBarStateController.StateListener by lazy { withArgCaptor { verify(statusBarStateController).addCallback(capture()) } } var showOnlyUnseenNotifsOnKeyguardSetting: Boolean get() = fakeSettings.getIntForUser( Loading