Merge "Base "seen" logic on shade expansion and heads up" into tm-qpr-dev

This commit is contained in:
Steve Elliott
2023-02-09 17:30:59 +00:00
committed by Android (Google) Code Review
4 changed files with 183 additions and 45 deletions

View File

@@ -0,0 +1,36 @@
/*
* 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) }
}

View File

@@ -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
@@ -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
@@ -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,
@@ -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) }
@@ -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)
}
}
@@ -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) }
}

View File

@@ -0,0 +1,38 @@
/*
* 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) }
}

View File

@@ -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
@@ -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()
@@ -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)
@@ -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())
@@ -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 {
@@ -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)
@@ -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()
@@ -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()
@@ -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)
@@ -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
@@ -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)
@@ -327,6 +366,7 @@ class KeyguardCoordinatorTest : SysuiTestCase() {
val keyguardCoordinator =
KeyguardCoordinator(
testDispatcher,
headsUpManager,
keyguardNotifVisibilityProvider,
keyguardRepository,
notifPipelineFlags,
@@ -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(