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

Commit 4fba341d authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Notif] Stop HUN animation at status bar if HUN has notif chip.

If a notification has a status bar chip, we don't want the HUN animation
to cover that chip. This is especially obvious if you tap the status bar
chip to show the HUN, but in general we don't want the HUN to obscure
the chip.

This CL does this by having NotificationStackScrollLayoutController
maintain a list of currently visible chips.
 - For the appear animation, the new HeadsUpAnimationEvent class stores
   whether there's a status bar chip and StackStateAnimator uses it.
 - For the disappear animation, we set the status bar chip status on the
   ExpandableNotificationRow and StackScrollAlgorithm uses it.

This also should be pretty extensible if we later want the HUN appear
animation to come from the chip or the HUN disappear animation to return
back into the chip.

Fixes: 393369891
Bug: 364653005
Flag: com.android.systemui.status_bar_notification_chips

Test: Send promoted notification, tap status bar chip -> verify HUN
animation starts and stops at the status bar height for both folded &
unfolded
Test: Trigger normal HUN -> verify HUN animation goes on top of status
bar like usual
Test: Tap status bar chip then open that notification's app -> verify
HUN disappear animation goes on top of status bar like usual
Test: Trigger a promoted notification HUN while that app is visible,
then close app before HUN animates away -> verify HUN disappear
animation stops at status bar

Test: atest HeadsUpAnimatorTest StackStateAnimatorTest
StackScrollAlgorithmTest

Change-Id: Ib170238f2e2084120777ae2885e64d8e7b2eb68b
parent bdb2c65f
Loading
Loading
Loading
Loading
+79 −0
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip
import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
@@ -171,6 +172,18 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
            assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
        }

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

            screenRecordState.value = ScreenRecordModel.DoingNothing
            mediaProjectionState.value = MediaProjectionState.NotProjecting
            setNotifs(emptyList())

            assertThat(latest).isEmpty()
        }

    @Test
    fun primaryChip_screenRecordShow_restHidden_screenRecordShown() =
        kosmos.runTest {
@@ -245,6 +258,20 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
            assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel())
        }

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

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

            assertThat(latest)
                .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey)
                .inOrder()
        }

    @EnableChipsModernization
    @Test
    fun chips_screenRecordAndCallActive_inThatOrder() =
@@ -864,6 +891,37 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
            assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
        }

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

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

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

    @DisableChipsModernization
    @Test
    fun chipsLegacy_callAndPromotedNotifs_primaryIsCallSecondaryIsNotif() =
@@ -957,6 +1015,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
            assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel())
        }

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

            val callNotificationKey = "call"
            addOngoingCallState(callNotificationKey)
            screenRecordState.value = ScreenRecordModel.Recording
            activeNotificationListRepository.addNotif(
                activeNotificationModel(
                    key = "notif",
                    statusBarChipIcon = createStatusBarIconViewOrNull(),
                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
                )
            )

            assertThat(latest)
                .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey)
                .inOrder()
        }

    @EnableChipsModernization
    @Test
    fun chips_screenRecordAndCallAndPromotedNotif_notifInOverflow() =
+48 −9
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.res.R
import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.Before
@@ -30,6 +32,8 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
class HeadsUpAnimatorTest : SysuiTestCase() {
    private val kosmos = testKosmos()

    @Before
    fun setUp() {
        context.getOrCreateTestableResources().apply {
@@ -38,34 +42,64 @@ class HeadsUpAnimatorTest : SysuiTestCase() {
    }

    @Test
    fun getHeadsUpYTranslation_fromBottomTrue_usesBottomAndYAbove() {
        val underTest = HeadsUpAnimator(context)
    fun getHeadsUpYTranslation_fromBottomTrue_hasStatusBarChipFalse_usesBottomAndYAbove() {
        val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy)
        underTest.stackTopMargin = 30
        underTest.headsUpAppearHeightBottom = 300

        val yTranslation =
            underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = false)

        assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300)
    }

    @Test
    fun getHeadsUpYTranslation_fromBottomTrue_hasStatusBarChipTrue_usesBottomAndYAbove() {
        val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy)
        underTest.stackTopMargin = 30
        underTest.headsUpAppearHeightBottom = 300

        val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true)
        val yTranslation =
            underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = true)

        // fromBottom takes priority
        assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300)
    }

    @Test
    fun getHeadsUpYTranslation_fromBottomFalse_usesTopMarginAndYAbove() {
        val underTest = HeadsUpAnimator(context)
    fun getHeadsUpYTranslation_fromBottomFalse_hasStatusBarChipFalse_usesTopMarginAndYAbove() {
        val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy)
        underTest.stackTopMargin = 30
        underTest.headsUpAppearHeightBottom = 300

        val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false)
        val yTranslation =
            underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false, hasStatusBarChip = false)

        assertThat(yTranslation).isEqualTo(-30 - TEST_Y_ABOVE_SCREEN)
    }

    @Test
    fun getHeadsUpYTranslation_fromBottomFalse_hasStatusBarChipTrue_usesTopMarginAndStatusBarHeight() {
        val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy)
        underTest.stackTopMargin = 30
        underTest.headsUpAppearHeightBottom = 300
        kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = 75
        underTest.updateResources(context)

        val yTranslation =
            underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false, hasStatusBarChip = true)

        assertThat(yTranslation).isEqualTo(75 - 30)
    }

    @Test
    fun getHeadsUpYTranslation_resourcesUpdated() {
        val underTest = HeadsUpAnimator(context)
        val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy)
        underTest.stackTopMargin = 30
        underTest.headsUpAppearHeightBottom = 300

        val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true)
        val yTranslation =
            underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = false)

        assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300)

@@ -77,7 +111,12 @@ class HeadsUpAnimatorTest : SysuiTestCase() {
        underTest.updateResources(context)

        // THEN HeadsUpAnimator knows about it
        assertThat(underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true))
        assertThat(
                underTest.getHeadsUpYTranslation(
                    isHeadsUpFromBottom = true,
                    hasStatusBarChip = false,
                )
            )
            .isEqualTo(newYAbove + 300)
    }

+53 −6
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ package com.android.systemui.statusbar.notification.stack

import android.annotation.DimenRes
import android.content.pm.PackageManager
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
@@ -19,6 +20,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.RoundableState
import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -32,6 +34,8 @@ import com.android.systemui.statusbar.notification.headsup.NotificationsHunShare
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy
import com.android.systemui.testKosmos
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
import org.junit.Assume
@@ -53,6 +57,8 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {

    @JvmField @Rule var expect: Expect = Expect.create()

    private val kosmos = testKosmos()

    private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>()
    private val avalancheController = mock<AvalancheController>()

@@ -131,9 +137,10 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
        hostView.addView(notificationRow)

        if (NotificationsHunSharedAnimationValues.isEnabled) {
             headsUpAnimator = HeadsUpAnimator(context)
            headsUpAnimator = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy)
        }
        stackScrollAlgorithm = StackScrollAlgorithm(
        stackScrollAlgorithm =
            StackScrollAlgorithm(
                context,
                hostView,
                if (::headsUpAnimator.isInitialized) headsUpAnimator else null,
@@ -449,6 +456,46 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
        )
    }

    @Test
    @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
    fun resetViewStates_hunAnimatingAway_noStatusBarChip_hunTranslatedToTopOfScreen() {
        val topMargin = 100f
        ambientState.maxHeadsUpTranslation = 2000f
        ambientState.stackTopMargin = topMargin.toInt()
        headsUpAnimator?.stackTopMargin = topMargin.toInt()
        whenever(notificationRow.intrinsicHeight).thenReturn(100)

        val statusBarHeight = 432
        kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight
        headsUpAnimator!!.updateResources(context)

        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
        whenever(notificationRow.hasStatusBarChipDuringHeadsUpAnimation()).thenReturn(false)

        resetViewStates_hunYTranslationIs(
            expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen
        )
    }

    @Test
    @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
    fun resetViewStates_hunAnimatingAway_withStatusBarChip_hunTranslatedToBottomOfStatusBar() {
        val topMargin = 100f
        ambientState.maxHeadsUpTranslation = 2000f
        ambientState.stackTopMargin = topMargin.toInt()
        headsUpAnimator?.stackTopMargin = topMargin.toInt()
        whenever(notificationRow.intrinsicHeight).thenReturn(100)

        val statusBarHeight = 432
        kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight
        headsUpAnimator!!.updateResources(context)

        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
        whenever(notificationRow.hasStatusBarChipDuringHeadsUpAnimation()).thenReturn(true)

        resetViewStates_hunYTranslationIs(expected = statusBarHeight - topMargin)
    }

    @Test
    fun resetViewStates_hunAnimatingAway_bottomNotClipped() {
        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
+68 −7
Original line number Diff line number Diff line
@@ -26,12 +26,15 @@ import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.AnimatorTestRule
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator
import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent
import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR
import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR
import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -46,7 +49,6 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.description
import org.mockito.Mockito.eq
import org.mockito.Mockito.verify
import org.mockito.kotlin.doNothing

private const val VIEW_HEIGHT = 100
private const val FULL_SHADE_APPEAR_TRANSLATION = 300
@@ -60,6 +62,8 @@ class StackStateAnimatorTest : SysuiTestCase() {
    @get:Rule val setFlagsRule = SetFlagsRule()
    @get:Rule val animatorTestRule = AnimatorTestRule(this)

    private val kosmos = testKosmos()

    private lateinit var stackStateAnimator: StackStateAnimator
    private lateinit var headsUpAnimator: HeadsUpAnimator
    private val stackScroller: NotificationStackScrollLayout = mock()
@@ -80,9 +84,10 @@ class StackStateAnimatorTest : SysuiTestCase() {
        whenever(view.viewState).thenReturn(viewState)

        if (NotificationsHunSharedAnimationValues.isEnabled) {
            headsUpAnimator = HeadsUpAnimator(context)
            headsUpAnimator = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy)
        }
        stackStateAnimator = StackStateAnimator(
        stackStateAnimator =
            StackStateAnimator(
                mContext,
                stackScroller,
                if (::headsUpAnimator.isInitialized) headsUpAnimator else null,
@@ -133,6 +138,62 @@ class StackStateAnimatorTest : SysuiTestCase() {
            )
    }

    @Test
    @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
    fun startAnimationForEvents_headsUpFromTop_andHasStatusBarChipFalse() {
        val statusBarHeight = 156
        val topMargin = 50f
        val expectedStartY = -topMargin - HEADS_UP_ABOVE_SCREEN

        headsUpAnimator.stackTopMargin = topMargin.toInt()
        kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight
        headsUpAnimator.updateResources(context)

        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR)
        event.headsUpHasStatusBarChip = false

        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)

        verify(view).setFinalActualHeight(VIEW_HEIGHT)
        verify(view, description("should animate from the top")).translationY = expectedStartY
        verify(view)
            .performAddAnimation(
                /* delay= */ 0L,
                /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(),
                /* isHeadsUpAppear= */ true,
                /* isHeadsUpCycling= */ false,
                /* onEndRunnable= */ null,
            )
    }

    @Test
    @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
    fun startAnimationForEvents_headsUpFromTop_andHasStatusBarChipTrue() {
        val statusBarHeight = 156
        val topMargin = 50f
        val expectedStartY = statusBarHeight - topMargin

        headsUpAnimator!!.stackTopMargin = topMargin.toInt()
        kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight
        headsUpAnimator!!.updateResources(context)

        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR)
        event.headsUpHasStatusBarChip = true

        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)

        verify(view).setFinalActualHeight(VIEW_HEIGHT)
        verify(view, description("should animate below status bar")).translationY = expectedStartY
        verify(view)
            .performAddAnimation(
                /* delay= */ 0L,
                /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(),
                /* isHeadsUpAppear= */ true,
                /* isHeadsUpCycling= */ false,
                /* onEndRunnable= */ null,
            )
    }

    @Test
    @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
    fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOff() {
+20 −0
Original line number Diff line number Diff line
@@ -350,6 +350,26 @@ constructor(
                .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModelLegacy())
        }

    private val activeChips =
        if (StatusBarChipsModernization.isEnabled) {
            chips.map { it.active }
        } else {
            chipsLegacy.map {
                val list = mutableListOf<OngoingActivityChipModel.Active>()
                if (it.primary is OngoingActivityChipModel.Active) {
                    list.add(it.primary)
                }
                if (it.secondary is OngoingActivityChipModel.Active) {
                    list.add(it.secondary)
                }
                list
            }
        }

    /** A flow modeling just the keys for the currently visible chips. */
    val visibleChipKeys: Flow<List<String>> =
        activeChips.map { chips -> chips.filter { !it.isHidden }.map { it.key } }

    /**
     * Sort the given chip [bundle] in order of priority, and divide the chips between active,
     * overflow, and inactive (see [MultipleOngoingActivityChipsModel] for a description of each).
Loading