Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2f61ce52 authored by Steve Elliott's avatar Steve Elliott Committed by Automerger Merge Worker
Browse files

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

Merge "Base "seen" logic on shade expansion and heads up" into tm-qpr-dev am: 156fdcab am: 39d6375e

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21322087



Change-Id: I435af123cc7caf664643ddb0ad8ebada7047e5ad
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents 167230dc 39d6375e
Loading
Loading
Loading
Loading
+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) }
    }
+44 −29
Original line number Diff line number Diff line
@@ -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) }
}
+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) }
    }
+65 −16
Original line number Diff line number Diff line
@@ -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(