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

Commit f5212b6f authored by Caitlin Shkuratov's avatar Caitlin Shkuratov Committed by Android (Google) Code Review
Browse files

Merge "[SB][Notif] Don't re-compose chips if it's un-necessary." into main

parents 2647e101 c05a93cb
Loading
Loading
Loading
Loading
+91 −0
Original line number Original line Diff line number Diff line
@@ -288,6 +288,97 @@ class NotifChipsViewModelTest : SysuiTestCase() {
            assertIsNotifKey(latest!![1], secondKey)
            assertIsNotifKey(latest!![1], secondKey)
        }
        }


    @Test
    fun chips_notifTimeAndSystemTimeBothUpdated_modelNotRecreated() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.chips)

            val currentTime = 3.minutes.inWholeMilliseconds
            fakeSystemClock.setCurrentTimeMillis(currentTime)

            val oldPromotedContentBuilder =
                PromotedNotificationContentBuilder("notif").applyToShared {
                    this.time = When.Time(currentTime)
                }
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = oldPromotedContentBuilder.build(),
                    )
                )
            )

            assertThat(latest).hasSize(1)
            assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            val oldModel = latest!![0]

            // WHEN the system time advances and the promoted content updates to that new time also
            val newTime = currentTime + 2.minutes.inWholeMilliseconds
            fakeSystemClock.setCurrentTimeMillis(newTime)
            val newPromotedContentBuilder =
                PromotedNotificationContentBuilder("notif").applyToShared {
                    this.time = When.Time(newTime)
                }
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = newPromotedContentBuilder.build(),
                    )
                )
            )

            // THEN we don't re-create the model because we still won't show the time
            assertThat(latest).hasSize(1)
            assertThat(latest!![0]).isSameInstanceAs(oldModel)
        }

    @Test
    fun chips_irrelevantPromotedContentUpdated_modelNotRecreated() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.chips)

            val oldPromotedContentBuilder =
                PromotedNotificationContentBuilder("notif").applyToShared {
                    this.subText = "Old subtext"
                }
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = oldPromotedContentBuilder.build(),
                    )
                )
            )

            assertThat(latest).hasSize(1)
            assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            val oldModel = latest!![0]

            // WHEN promoted content updates with an irrelevant field
            val newPromotedContentBuilder =
                PromotedNotificationContentBuilder("notif").applyToShared {
                    this.subText = "New subtext"
                }
            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "notif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = newPromotedContentBuilder.build(),
                    )
                )
            )

            // THEN we don't re-create the model
            assertThat(latest).hasSize(1)
            assertThat(latest!![0]).isSameInstanceAs(oldModel)
        }

    @Test
    @Test
    fun chips_appStartsAsVisible_isHiddenTrue() =
    fun chips_appStartsAsVisible_isHiddenTrue() =
        kosmos.runTest {
        kosmos.runTest {
+109 −57
Original line number Original line Diff line number Diff line
@@ -18,12 +18,14 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel


import android.content.Context
import android.content.Context
import android.view.View
import android.view.View
import com.android.internal.logging.InstanceId
import com.android.systemui.Flags
import com.android.systemui.Flags
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
@@ -41,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch


/** A view model for status bar chips for promoted ongoing notifications. */
/** A view model for status bar chips for promoted ongoing notifications. */
@@ -54,13 +57,62 @@ constructor(
    headsUpNotificationInteractor: HeadsUpNotificationInteractor,
    headsUpNotificationInteractor: HeadsUpNotificationInteractor,
    private val systemClock: SystemClock,
    private val systemClock: SystemClock,
) {
) {

    /**
     * A flow that prunes the incoming [NotificationChipModel] instances to just the information
     * each status bar chip needs.
     */
    private val notificationChipsWithPrunedContent: Flow<List<PrunedNotificationChipModel>> =
        notifChipsInteractor.allNotificationChips
            .map { chips -> chips.map { it.toPrunedModel() } }
            .distinctUntilChanged()

    private fun NotificationChipModel.toPrunedModel(): PrunedNotificationChipModel {
        // Chips are never shown when locked, so it's safe to use the version with sensitive content
        val content = promotedContent.privateVersion

        val time =
            when (val rawTime = content.time) {
                null -> null
                is PromotedNotificationContentModel.When.Time -> {
                    if (
                        rawTime.currentTimeMillis >=
                            systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
                    ) {
                        rawTime
                    } else {
                        // Don't show a `when` time that's close to now or in the past because it's
                        // likely that the app didn't intentionally set the `when` time to be shown
                        // in the status bar chip.
                        // TODO(b/393369213): If a notification sets a `when` time in the future and
                        // then that time comes and goes, the chip *will* start showing times in the
                        // past. Not going to fix this right now because the Compose implementation
                        // automatically handles this for us and we're hoping to launch the
                        // notification chips at the same time as the Compose chips.
                        null
                    }
                }
                is PromotedNotificationContentModel.When.Chronometer -> rawTime
            }
        return PrunedNotificationChipModel(
            key = key,
            appName = appName,
            statusBarChipIconView = statusBarChipIconView,
            text = content.shortCriticalText,
            time = time,
            wasPromotedAutomatically = content.wasPromotedAutomatically,
            isAppVisible = isAppVisible,
            instanceId = instanceId,
        )
    }

    /**
    /**
     * A flow modeling the current notification chips. Emits an empty list if there are no
     * A flow modeling the current notification chips. Emits an empty list if there are no
     * notifications that are eligible to show a status bar chip.
     * notifications that are eligible to show a status bar chip.
     */
     */
    val chips: Flow<List<OngoingActivityChipModel.Active>> =
    val chips: Flow<List<OngoingActivityChipModel.Active>> =
        combine(
        combine(
                notifChipsInteractor.allNotificationChips,
                notificationChipsWithPrunedContent,
                headsUpNotificationInteractor.statusBarHeadsUpState,
                headsUpNotificationInteractor.statusBarHeadsUpState,
            ) { notifications, headsUpState ->
            ) { notifications, headsUpState ->
                notifications.map { it.toActivityChipModel(headsUpState) }
                notifications.map { it.toActivityChipModel(headsUpState) }
@@ -68,12 +120,11 @@ constructor(
            .distinctUntilChanged()
            .distinctUntilChanged()


    /** Converts the notification to the [OngoingActivityChipModel] object. */
    /** Converts the notification to the [OngoingActivityChipModel] object. */
    private fun NotificationChipModel.toActivityChipModel(
    private fun PrunedNotificationChipModel.toActivityChipModel(
        headsUpState: TopPinnedState
        headsUpState: TopPinnedState
    ): OngoingActivityChipModel.Active {
    ): OngoingActivityChipModel.Active {
        PromotedNotificationUi.unsafeAssertInNewMode()
        PromotedNotificationUi.unsafeAssertInNewMode()
        // Chips are never shown when locked, so it's safe to use the version with sensitive content

        val chipContent = promotedContent.privateVersion
        val contentDescription = getContentDescription(this.appName)
        val contentDescription = getContentDescription(this.appName)
        val icon =
        val icon =
            if (this.statusBarChipIconView != null) {
            if (this.statusBarChipIconView != null) {
@@ -131,12 +182,12 @@ constructor(
            )
            )
        }
        }


        if (chipContent.shortCriticalText != null) {
        if (text != null) {
            return OngoingActivityChipModel.Active.Text(
            return OngoingActivityChipModel.Active.Text(
                key = this.key,
                key = this.key,
                icon = icon,
                icon = icon,
                colors = colors,
                colors = colors,
                text = chipContent.shortCriticalText,
                text = text,
                onClickListenerLegacy = onClickListenerLegacy,
                onClickListenerLegacy = onClickListenerLegacy,
                clickBehavior = clickBehavior,
                clickBehavior = clickBehavior,
                isHidden = isHidden,
                isHidden = isHidden,
@@ -144,11 +195,11 @@ constructor(
            )
            )
        }
        }


        if (Flags.promoteNotificationsAutomatically() && chipContent.wasPromotedAutomatically) {
        if (Flags.promoteNotificationsAutomatically() && wasPromotedAutomatically) {
            // When we're promoting notifications automatically, the `when` time set on the
            // When we're promoting notifications automatically, the `when` time set on the
            // notification will likely just be set to the current time, which would cause the chip
            // notification will likely just be set to the current time, which would cause the chip
            // to always show "now". We don't want early testers to get that experience since it's
            // to always show "now". We don't want early testers to get that experience since it's
            // not what will happen at launch, so just don't show any time.onometerstate
            // not what will happen at launch, so just don't show any time.
            return OngoingActivityChipModel.Active.IconOnly(
            return OngoingActivityChipModel.Active.IconOnly(
                key = this.key,
                key = this.key,
                icon = icon,
                icon = icon,
@@ -160,8 +211,9 @@ constructor(
            )
            )
        }
        }


        if (chipContent.time == null) {
        return when (time) {
            return OngoingActivityChipModel.Active.IconOnly(
            null -> {
                OngoingActivityChipModel.Active.IconOnly(
                    key = this.key,
                    key = this.key,
                    icon = icon,
                    icon = icon,
                    colors = colors,
                    colors = colors,
@@ -171,50 +223,25 @@ constructor(
                    instanceId = instanceId,
                    instanceId = instanceId,
                )
                )
            }
            }

        when (chipContent.time) {
            is PromotedNotificationContentModel.When.Time -> {
            is PromotedNotificationContentModel.When.Time -> {
                return if (
                    chipContent.time.currentTimeMillis >=
                        systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
                ) {
                OngoingActivityChipModel.Active.ShortTimeDelta(
                OngoingActivityChipModel.Active.ShortTimeDelta(
                    key = this.key,
                    key = this.key,
                    icon = icon,
                    icon = icon,
                    colors = colors,
                    colors = colors,
                        time = chipContent.time.currentTimeMillis,
                    time = time.currentTimeMillis,
                    onClickListenerLegacy = onClickListenerLegacy,
                    onClickListenerLegacy = onClickListenerLegacy,
                    clickBehavior = clickBehavior,
                    clickBehavior = clickBehavior,
                    isHidden = isHidden,
                    isHidden = isHidden,
                    instanceId = instanceId,
                    instanceId = instanceId,
                )
                )
                } else {
                    // Don't show a `when` time that's close to now or in the past because it's
                    // likely that the app didn't intentionally set the `when` time to be shown in
                    // the status bar chip.
                    // TODO(b/393369213): If a notification sets a `when` time in the future and
                    // then that time comes and goes, the chip *will* start showing times in the
                    // past. Not going to fix this right now because the Compose implementation
                    // automatically handles this for us and we're hoping to launch the notification
                    // chips at the same time as the Compose chips.
                    return OngoingActivityChipModel.Active.IconOnly(
                        key = this.key,
                        icon = icon,
                        colors = colors,
                        onClickListenerLegacy = onClickListenerLegacy,
                        clickBehavior = clickBehavior,
                        isHidden = isHidden,
                        instanceId = instanceId,
                    )
                }
            }
            }
            is PromotedNotificationContentModel.When.Chronometer -> {
            is PromotedNotificationContentModel.When.Chronometer -> {
                return OngoingActivityChipModel.Active.Timer(
                OngoingActivityChipModel.Active.Timer(
                    key = this.key,
                    key = this.key,
                    icon = icon,
                    icon = icon,
                    colors = colors,
                    colors = colors,
                    startTimeMs = chipContent.time.elapsedRealtimeMillis,
                    startTimeMs = time.elapsedRealtimeMillis,
                    isEventInFuture = chipContent.time.isCountDown,
                    isEventInFuture = time.isCountDown,
                    onClickListenerLegacy = onClickListenerLegacy,
                    onClickListenerLegacy = onClickListenerLegacy,
                    clickBehavior = clickBehavior,
                    clickBehavior = clickBehavior,
                    isHidden = isHidden,
                    isHidden = isHidden,
@@ -236,6 +263,31 @@ constructor(
        )
        )
    }
    }


    /**
     * Model that prunes data from [NotificationChipModel] to just the information the status bar
     * chip needs.
     *
     * Used so that we don't re-create the chip [OngoingActivityChipModel] classes with new click
     * listeners unless absolutely necessary, which helps the chips re-compose less frequently. See
     * b/393456147.
     */
    private data class PrunedNotificationChipModel(
        val key: String,
        val appName: String,
        val statusBarChipIconView: StatusBarIconView?,
        /**
         * The text to show in the chip, or null if text shouldn't be shown. Text takes precedence
         * over [time].
         */
        val text: String?,
        /** The time to show in the chip, or null if the time shouldn't be shown. */
        val time: PromotedNotificationContentModel.When?,
        /** See [PromotedNotificationContentModel.wasPromotedAutomatically]. */
        val wasPromotedAutomatically: Boolean,
        val isAppVisible: Boolean,
        val instanceId: InstanceId?,
    )

    companion object {
    companion object {
        /**
        /**
         * Notifications must have a `when` time of at least 1 minute in the future in order for the
         * Notifications must have a `when` time of at least 1 minute in the future in order for the