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

Commit c71e4c0e authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Notif] Stable-sort notif chips by first appearance time.

Bug: 364653005
Bug: 385724468
Flag: com.android.systemui.status_bar_notification_chips

Test: Trigger 2 notif chips, then tap each one in turn -> verify chip
order never changes
Test: Trigger 2 notif chips, and send updates for the one that was
originally posted earlier -> verify earlier-posted notif chip always
stays second
Test: atest StatusBarNotificationChipsInteractorTest

Change-Id: I45a7244531904a9e042f50205666169f0c8262e2
parent f5d90f4d
Loading
Loading
Loading
Loading
+23 −13
Original line number Diff line number Diff line
@@ -52,7 +52,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                    promotedContent = PROMOTED_CONTENT,
                )

            val underTest = factory.create(startingNotif)
            val underTest = factory.create(startingNotif, creationTime = 1)

            val latest by collectLastValue(underTest.notificationChip)

@@ -71,7 +71,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        key = "notif1",
                        statusBarChipIcon = originalIconView,
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -99,7 +100,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        key = "notif1",
                        statusBarChipIcon = originalIconView,
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -127,7 +129,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        key = "notif1",
                        statusBarChipIcon = originalIconView,
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -153,7 +156,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        key = "notif1",
                        statusBarChipIcon = null,
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -171,7 +175,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        key = "notif1",
                        statusBarChipIcon = null,
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -189,7 +194,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                    statusBarChipIcon = mock(),
                    promotedContent = PROMOTED_CONTENT,
                )
            val underTest = factory.create(startingNotif)
            val underTest = factory.create(startingNotif, creationTime = 1)
            val latest by collectLastValue(underTest.notificationChip)
            assertThat(latest).isNotNull()

@@ -214,7 +219,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                    statusBarChipIcon = mock(),
                    promotedContent = PROMOTED_CONTENT,
                )
            val underTest = factory.create(startingNotif)
            val underTest = factory.create(startingNotif, creationTime = 1)
            val latest by collectLastValue(underTest.notificationChip)
            assertThat(latest).isNotNull()

@@ -239,7 +244,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        key = "notif1",
                        statusBarChipIcon = mock(),
                        promotedContent = null,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -259,7 +265,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        uid = UID,
                        statusBarChipIcon = mock(),
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -279,7 +286,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        uid = UID,
                        statusBarChipIcon = mock(),
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -297,7 +305,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        uid = UID,
                        statusBarChipIcon = mock(),
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -330,7 +339,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        uid = hiddenUid,
                        statusBarChipIcon = mock(),
                        promotedContent = PROMOTED_CONTENT,
                    )
                    ),
                    creationTime = 1,
                )
            val latest by collectLastValue(underTest.notificationChip)
            assertThat(latest).isNotNull()
+88 −0
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
@@ -248,6 +249,93 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
            assertThat(latest!![0].key).isEqualTo("notif")
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_sortedBasedOnFirstAppearanceTime() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.notificationChips)

            val firstIcon = mock<StatusBarIconView>()
            val secondIcon = mock<StatusBarIconView>()

            // First, add notif1 at t=1000
            fakeSystemClock.setCurrentTimeMillis(1000)
            val notif1 =
                activeNotificationModel(
                    key = "notif1",
                    statusBarChipIcon = firstIcon,
                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
                )
            setNotifs(listOf(notif1))
            assertThat(latest).hasSize(1)
            assertThat(latest!![0].key).isEqualTo("notif1")

            // WHEN we add notif2 at t=2000
            fakeSystemClock.advanceTime(1000)
            val notif2 =
                activeNotificationModel(
                    key = "notif2",
                    statusBarChipIcon = secondIcon,
                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
                )
            setNotifs(listOf(notif1, notif2))

            // THEN notif2 is ranked above notif1 because it appeared later
            assertThat(latest).hasSize(2)
            assertThat(latest!![0].key).isEqualTo("notif2")
            assertThat(latest!![1].key).isEqualTo("notif1")

            // WHEN notif1 and notif2 swap places
            setNotifs(listOf(notif2, notif1))

            // THEN notif2 is still ranked above notif1 to preserve chip ordering
            assertThat(latest).hasSize(2)
            assertThat(latest!![0].key).isEqualTo("notif2")
            assertThat(latest!![1].key).isEqualTo("notif1")

            // WHEN notif1 and notif2 swap places again
            setNotifs(listOf(notif1, notif2))

            // THEN notif2 is still ranked above notif1 to preserve chip ordering
            assertThat(latest).hasSize(2)
            assertThat(latest!![0].key).isEqualTo("notif2")
            assertThat(latest!![1].key).isEqualTo("notif1")

            // WHEN notif1 gets an update
            val notif1NewPromotedContent =
                PromotedNotificationContentModel.Builder("notif1").apply {
                    this.shortCriticalText = "Arrived"
                }
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif1",
                        statusBarChipIcon = firstIcon,
                        promotedContent = notif1NewPromotedContent.build(),
                    ),
                    notif2,
                )
            )

            // THEN notif2 is still ranked above notif1 to preserve chip ordering
            assertThat(latest).hasSize(2)
            assertThat(latest!![0].key).isEqualTo("notif2")
            assertThat(latest!![1].key).isEqualTo("notif1")

            // WHEN notif1 disappears and then reappears
            fakeSystemClock.advanceTime(1000)
            setNotifs(listOf(notif2))
            assertThat(latest).hasSize(1)

            fakeSystemClock.advanceTime(1000)
            setNotifs(listOf(notif2, notif1))

            // THEN notif1 is now ranked first
            assertThat(latest).hasSize(2)
            assertThat(latest!![0].key).isEqualTo("notif1")
            assertThat(latest!![1].key).isEqualTo("notif2")
        }

    @Test
    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
    fun notificationChips_notifChangesKey() =
+7 −1
Original line number Diff line number Diff line
@@ -42,12 +42,15 @@ import kotlinx.coroutines.flow.map
 *
 * [StatusBarNotificationChipsInteractor] will collect all the individual instances of this
 * interactor and send all the necessary information to the UI layer.
 *
 * @property creationTime the time when the notification first appeared as promoted.
 */
@OptIn(ExperimentalCoroutinesApi::class)
class SingleNotificationChipInteractor
@AssistedInject
constructor(
    @Assisted startingModel: ActiveNotificationModel,
    @Assisted val creationTime: Long,
    private val activityManagerRepository: ActivityManagerRepository,
    @StatusBarChipsLog private val logBuffer: LogBuffer,
) {
@@ -142,6 +145,9 @@ constructor(

    @AssistedFactory
    fun interface Factory {
        fun create(startingModel: ActiveNotificationModel): SingleNotificationChipInteractor
        fun create(
            startingModel: ActiveNotificationModel,
            creationTime: Long,
        ): SingleNotificationChipInteractor
    }
}
+21 −6
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import com.android.systemui.statusbar.chips.notification.domain.model.Notificati
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -48,6 +49,7 @@ class StatusBarNotificationChipsInteractor
@Inject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    private val systemClock: SystemClock,
    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
    private val singleNotificationChipInteractorFactory: SingleNotificationChipInteractor.Factory,
    @StatusBarChipsLog private val logBuffer: LogBuffer,
@@ -95,21 +97,26 @@ constructor(
            activeNotificationsInteractor.promotedOngoingNotifications
                .pairwise(initialValue = emptyList())
                .collect { (oldNotifs, currentNotifs) ->
                    val removedNotifs = oldNotifs.minus(currentNotifs.toSet())
                    removedNotifs.forEach { removedNotif ->
                        val wasRemoved = promotedNotificationInteractorMap.remove(removedNotif.key)
                    val removedNotifKeys =
                        oldNotifs.map { it.key }.minus(currentNotifs.map { it.key }.toSet())
                    removedNotifKeys.forEach { removedNotifKey ->
                        val wasRemoved = promotedNotificationInteractorMap.remove(removedNotifKey)
                        if (wasRemoved == null) {
                            logger.w({
                                "Attempted to remove $str1 from interactor map but it wasn't present"
                            }) {
                                str1 = removedNotif.key
                                str1 = removedNotifKey
                            }
                        }
                    }

                    currentNotifs.forEach { notif ->
                        val interactor =
                            promotedNotificationInteractorMap.computeIfAbsent(notif.key) {
                                singleNotificationChipInteractorFactory.create(notif)
                                singleNotificationChipInteractorFactory.create(
                                    notif,
                                    creationTime = systemClock.currentTimeMillis(),
                                )
                            }
                        interactor.setNotification(notif)
                    }
@@ -130,7 +137,15 @@ constructor(
    val notificationChips: Flow<List<NotificationChipModel>> =
        if (StatusBarNotifChips.isEnabled) {
            // For all our current interactors...
            promotedNotificationInteractors.flatMapLatest { interactors ->
            promotedNotificationInteractors.flatMapLatest { intrs ->
                // Stable-sort the promoted notifications by when they first appeared so that:
                // 1) The chips don't switch places if the older chip gets a notification update.
                // 2) The chips don't switch places when the second chip is tapped. (Whichever
                // notification is showing heads-up is considered to be the top notification, which
                // means tapping the second chip would move it to be the first chip if we didn't
                // sort by appearance time here.)
                // 3) Older chips get hidden if there's not enough room for all chips.
                val interactors = intrs.sortedByDescending { it.creationTime }
                if (interactors.isNotEmpty()) {
                    // Combine each interactor's [notificationChip] flow...
                    val allNotificationChips: List<Flow<NotificationChipModel?>> =
+0 −3
Original line number Diff line number Diff line
@@ -141,9 +141,6 @@ constructor(
                wasAdded = false,
                wasUpdated = false,
                // Force-set this notification to show heads-up.
                // TODO(b/364653005): This means that if you tap on the second notification chip,
                // then it moves to become the first chip because whatever notification is showing
                // heads-up is considered to be the top notification.
                shouldHeadsUpEver = true,
                shouldHeadsUpAgain = true,
                isPinnedByUser = true,
Loading