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

Commit 89ba44ff authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Chips] Keep track of each chip's current on-screens bounds.

This will let us do a nice chip-to-HUN animation where the HUN animates
starting from the chip bounds.

Bug: 393369891
Test: Verified via logging that non-chip HUNs have no bounds passed to
NSSL, and chip HUNs *do* have bounds passed to NSSL
Test: atest OngoingActivityChipsWithNotifsViewModelTest
Test: With flag disabled, verify chip-to-HUN animation still starts
under the status bar

Flag: com.android.systemui.status_bar_chip_to_hun_animation

Change-Id: Iee4f496c94c3ca4a34cad992903a1741b3833c88
parent 1837c2e9
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -534,6 +534,16 @@ flag {
  is_fixed_read_only: true
}

flag {
    name: "status_bar_chip_to_hun_animation"
    namespace: "systemui"
    description: "Implement a bespoke tap-chip-to-show-HUN animation for SB notification chips"
    bug: "393369891"
    metadata {
      purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "status_bar_window_no_custom_touch"
    namespace: "systemui"
+170 −17
Original line number Diff line number Diff line
@@ -21,7 +21,9 @@ import android.content.packageManager
import android.content.res.Configuration
import android.content.res.mainResources
import android.graphics.Bitmap
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,6 +44,7 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager
import com.android.systemui.res.R
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
import com.android.systemui.statusbar.chips.StatusBarChipToHunAnimation
import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel
import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
@@ -175,9 +178,9 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
        }

    @Test
    fun visibleChipKeys_allInactive() =
    fun visibleChipsWithBounds_allInactive() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipKeys)
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            screenRecordState.value = ScreenRecordModel.DoingNothing
            mediaProjectionState.value = MediaProjectionState.NotProjecting
@@ -261,15 +264,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
        }

    @Test
    fun visibleChipKeys_screenRecordShowAndCallShow_hasBothKeys() =
    @DisableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_screenRecordShowAndCallShow_animFlagOff_hasBothKeys() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipKeys)
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            screenRecordState.value = ScreenRecordModel.Recording
            addOngoingCallState(callNotificationKey)

            assertThat(latest)
            assertThat(latest!!.map { it.key })
                .containsExactly(
                    ScreenRecordChipViewModel.KEY,
                    "${CallChipViewModel.KEY_PREFIX}$callNotificationKey",
@@ -277,6 +281,71 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
                .inOrder()
        }

    @Test
    @EnableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_screenRecordShowAndCallShow_animFlagOn_noBoundsSet_isEmpty() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            screenRecordState.value = ScreenRecordModel.Recording
            addOngoingCallState(callNotificationKey)

            assertThat(latest).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_screenRecordShowAndCallShow_animFlagOn_boundsSetForOneChip_hasOnlyOneKey() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            val callKeyForChip = "${CallChipViewModel.KEY_PREFIX}$callNotificationKey"
            screenRecordState.value = ScreenRecordModel.Recording
            addOngoingCallState(callNotificationKey)

            underTest.onChipBoundsChanged(callKeyForChip, RectF(1f, 2f, 3f, 4f))

            assertThat(latest!![callKeyForChip]).isEqualTo(RectF(1f, 2f, 3f, 4f))
            assertThat(latest).doesNotContainKey(ScreenRecordChipViewModel.KEY)
        }

    @Test
    @EnableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_screenRecordShowAndCallShow_animFlagOn_boundsUpdated_hasUpdatedBounds() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            val callKeyForChip = "${CallChipViewModel.KEY_PREFIX}$callNotificationKey"
            addOngoingCallState(callNotificationKey)

            underTest.onChipBoundsChanged(callKeyForChip, RectF(1f, 2f, 3f, 4f))
            assertThat(latest!![callKeyForChip]).isEqualTo(RectF(1f, 2f, 3f, 4f))

            underTest.onChipBoundsChanged(callKeyForChip, RectF(10f, 20f, 30f, 40f))
            assertThat(latest!![callKeyForChip]).isEqualTo(RectF(10f, 20f, 30f, 40f))
        }

    @Test
    @EnableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_screenRecordShowAndCallShow_animFlagOn_boundsSet_hasBothKeysAndBounds() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            val callKeyForChip = "${CallChipViewModel.KEY_PREFIX}$callNotificationKey"
            screenRecordState.value = ScreenRecordModel.Recording
            addOngoingCallState(callNotificationKey)

            underTest.onChipBoundsChanged(callKeyForChip, RectF(1f, 2f, 3f, 4f))
            underTest.onChipBoundsChanged(ScreenRecordChipViewModel.KEY, RectF(5f, 6f, 7f, 8f))

            assertThat(latest!![callKeyForChip]).isEqualTo(RectF(1f, 2f, 3f, 4f))
            assertThat(latest!![ScreenRecordChipViewModel.KEY]).isEqualTo(RectF(5f, 6f, 7f, 8f))
        }

    @EnableChipsModernization
    @Test
    fun chips_screenRecordAndCallActive_inThatOrder() =
@@ -925,9 +994,40 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {

    @Test
    @DisableChipsModernization
    fun visibleChipKeys_chipsModOff_threePromotedNotifs_topTwoInList() =
    @DisableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_chipsModOff_animFlagOff_threePromotedNotifs_topTwoInList() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            setNotifs(
                listOf(
                    activeNotificationModel(
                        key = "firstNotif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                    ),
                    activeNotificationModel(
                        key = "secondNotif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                    ),
                    activeNotificationModel(
                        key = "thirdNotif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                    ),
                )
            )

            assertThat(latest!!.keys).containsExactly("firstNotif", "secondNotif")
        }

    @Test
    @EnableChipsModernization
    @DisableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_chipsModOn_animFlagOff_fourPromotedNotifs_topThreeInList() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipKeys)
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            setNotifs(
                listOf(
@@ -946,17 +1046,23 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                    ),
                    activeNotificationModel(
                        key = "fourthNotif",
                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                        promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(),
                    ),
                )
            )

            assertThat(latest).containsExactly("firstNotif", "secondNotif").inOrder()
            assertThat(latest!!.keys).containsExactly("firstNotif", "secondNotif", "thirdNotif")
        }

    @Test
    @EnableChipsModernization
    fun visibleChipKeys_chipsModOn_fourPromotedNotifs_topThreeInList() =
    @EnableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_chipsModOn_animFlagOn_fourPromotedNotifs_topThreeInListWithBounds() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipKeys)
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            setNotifs(
                listOf(
@@ -983,7 +1089,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
                )
            )

            assertThat(latest).containsExactly("firstNotif", "secondNotif", "thirdNotif").inOrder()
            underTest.onChipBoundsChanged("firstNotif", RectF(1f, 1f, 1f, 1f))
            underTest.onChipBoundsChanged("secondNotif", RectF(2f, 2f, 2f, 2f))
            underTest.onChipBoundsChanged("thirdNotif", RectF(3f, 3f, 3f, 3f))
            underTest.onChipBoundsChanged("fourthNotif", RectF(4f, 4f, 4f, 4f))

            assertThat(latest!!["firstNotif"]).isEqualTo(RectF(1f, 1f, 1f, 1f))
            assertThat(latest!!["secondNotif"]).isEqualTo(RectF(2f, 2f, 2f, 2f))
            assertThat(latest!!["thirdNotif"]).isEqualTo(RectF(3f, 3f, 3f, 3f))
            assertThat(latest).doesNotContainKey("fourthNotif")
        }

    @DisableChipsModernization
@@ -1084,9 +1198,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {

    @Test
    @DisableChipsModernization
    fun visibleChipKeys_chipsModOff_screenRecordAndCallAndPromotedNotifs_topTwoInList() =
    @DisableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_chipsModOff_animFlagOff_screenRecordAndCallAndPromotedNotifs_topTwoInList() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipKeys)
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            addOngoingCallState(callNotificationKey)
@@ -1106,7 +1221,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
                )
            )

            assertThat(latest)
            assertThat(latest!!.map { it.key })
                .containsExactly(
                    ScreenRecordChipViewModel.KEY,
                    "${CallChipViewModel.KEY_PREFIX}$callNotificationKey",
@@ -1116,9 +1231,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {

    @Test
    @EnableChipsModernization
    fun visibleChipKeys_chipsModOn_screenRecordAndCallAndPromotedNotifs_topThreeInList() =
    @DisableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_chipsModOn_animFlagOff_screenRecordAndCallAndPromotedNotifs_topThreeInList() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipKeys)
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            addOngoingCallState(callNotificationKey)
@@ -1138,7 +1254,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
                )
            )

            assertThat(latest)
            assertThat(latest!!.map { it.key })
                .containsExactly(
                    ScreenRecordChipViewModel.KEY,
                    "${CallChipViewModel.KEY_PREFIX}$callNotificationKey",
@@ -1147,6 +1263,43 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
                .inOrder()
        }

    @Test
    @EnableChipsModernization
    @EnableFlags(StatusBarChipToHunAnimation.FLAG_NAME)
    fun visibleChipsWithBounds_chipsModOn_animFlagOn_screenRecordAndCallAndPromotedNotifs_topThreeInListWithBounds() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.visibleChipsWithBounds)

            val callNotificationKey = "call"
            val callKeyForChip = "${CallChipViewModel.KEY_PREFIX}$callNotificationKey"
            addOngoingCallState(callNotificationKey)
            screenRecordState.value = ScreenRecordModel.Recording
            activeNotificationListRepository.addNotif(
                activeNotificationModel(
                    key = "notif1",
                    statusBarChipIcon = createStatusBarIconViewOrNull(),
                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                )
            )
            activeNotificationListRepository.addNotif(
                activeNotificationModel(
                    key = "notif2",
                    statusBarChipIcon = createStatusBarIconViewOrNull(),
                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                )
            )

            underTest.onChipBoundsChanged(ScreenRecordChipViewModel.KEY, RectF(1f, 1f, 1f, 1f))
            underTest.onChipBoundsChanged(callKeyForChip, RectF(2f, 2f, 2f, 2f))
            underTest.onChipBoundsChanged("notif1", RectF(3f, 3f, 3f, 3f))
            underTest.onChipBoundsChanged("notif2", RectF(4f, 4f, 4f, 4f))

            assertThat(latest!![ScreenRecordChipViewModel.KEY]).isEqualTo(RectF(1f, 1f, 1f, 1f))
            assertThat(latest!![callKeyForChip]).isEqualTo(RectF(2f, 2f, 2f, 2f))
            assertThat(latest!!["notif1"]).isEqualTo(RectF(3f, 3f, 3f, 3f))
            assertThat(latest).doesNotContainKey("notif2")
        }

    // The ranking between different chips should stay consistent between
    // OngoingActivityChipsViewModel and PromotedNotificationsInteractor.
    // Make sure to also change
+3 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel

import android.graphics.Color
import android.graphics.Rect
import android.graphics.RectF
import android.view.View
import androidx.compose.runtime.getValue
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -57,6 +58,8 @@ class FakeHomeStatusBarViewModel(
    override val ongoingActivityChips =
        ChipsVisibilityModel(MultipleOngoingActivityChipsModel(), areChipsAllowed = false)

    override fun onChipBoundsChanged(key: String, bounds: RectF) {}

    override val ongoingActivityChipsLegacy =
        MutableStateFlow(MultipleOngoingActivityChipsModelLegacy())

+67 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.chips

import com.android.systemui.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils

/** Helper for reading or using the status bar chip to hun animation flag state. */
@Suppress("NOTHING_TO_INLINE")
object StatusBarChipToHunAnimation {
    /** The aconfig flag name */
    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CHIP_TO_HUN_ANIMATION

    /** A token used for dependency declaration */
    val token: FlagToken
        get() = FlagToken(FLAG_NAME, isEnabled)

    /** Is the refactor enabled */
    @JvmStatic
    inline val isEnabled
        get() = Flags.statusBarChipToHunAnimation()

    /**
     * Called to ensure code is only run when the flag is enabled. This can be used to protect users
     * from the unintended behaviors caused by accidentally running new logic, while also crashing
     * on an eng build to ensure that the refactor author catches issues in testing.
     */
    @JvmStatic
    inline fun isUnexpectedlyInLegacyMode() =
        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)

    /**
     * Called to ensure code is only run when the flag is enabled. This will call Log.wtf if the
     * flag is not enabled to ensure that the refactor author catches issues in testing.
     *
     * NOTE: This can be useful for simple methods, but does not return the flag state, so it cannot
     * be used to implement a safe exit, and as such it does not support code stripping. If the
     * calling code will do work that is unsafe when the flag is off, it is recommended to write an
     * early return with `if (isUnexpectedlyInLegacyMode()) return`.
     */
    @JvmStatic
    inline fun expectInNewMode() {
        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
    }

    /**
     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
     * the flag is enabled to ensure that the refactor author catches issues in testing.
     */
    @JvmStatic
    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
}
+18 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.statusbar.chips.ui.compose

import android.graphics.RectF
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
@@ -25,16 +26,26 @@ import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toAndroidRectF
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder

/**
 * Composable for all ongoing activity chips shown in the status bar.
 *
 * @param onChipBoundsChanged should be invoked each time any chip has their on-screen bounds
 *   changed.
 */
@Composable
fun OngoingActivityChips(
    chips: MultipleOngoingActivityChipsModel,
    iconViewStore: NotificationIconContainerViewBinder.IconViewStore?,
    onChipBoundsChanged: (String, RectF) -> Unit,
    modifier: Modifier = Modifier,
) {
    if (StatusBarChipsReturnAnimations.isEnabled) {
@@ -63,7 +74,13 @@ fun OngoingActivityChips(
                    OngoingActivityChip(
                        model = it,
                        iconViewStore = iconViewStore,
                        modifier = Modifier.sysuiResTag(it.key),
                        modifier =
                            Modifier.sysuiResTag(it.key).onGloballyPositioned { coordinates ->
                                onChipBoundsChanged.invoke(
                                    it.key,
                                    coordinates.boundsInWindow().toAndroidRectF(),
                                )
                            },
                    )
                }
            }
Loading