Merge "Base "seen" logic on shade expansion and heads up" into tm-qpr-dev am: 156fdcab1b am: 39d6375ec2
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21322087 Change-Id: I435af123cc7caf664643ddb0ad8ebada7047e5ad Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user