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

Commit bba6e221 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[HUN] Extract HUN animation y translation values into a shared class." into main

parents b2584868 03cb55f8
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -202,6 +202,16 @@ flag {
    }
}

flag {
    name: "notifications_hun_shared_animation_values"
    namespace: "systemui"
    description: "Adds a shared class for fetching HUN animation values."
    bug: "393369891"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "notification_undo_guts_on_config_changed"
    namespace: "systemui"
+87 −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.notification.headsup

import android.platform.test.annotations.EnableFlags
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.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.Before
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
class HeadsUpAnimatorTest : SysuiTestCase() {
    @Before
    fun setUp() {
        context.getOrCreateTestableResources().apply {
            this.addOverride(R.dimen.heads_up_appear_y_above_screen, TEST_Y_ABOVE_SCREEN)
        }
    }

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

        val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true)

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

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

        val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false)

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

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

        val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true)

        assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300)

        // WHEN the resource is updated
        val newYAbove = 600
        context.getOrCreateTestableResources().apply {
            this.addOverride(R.dimen.heads_up_appear_y_above_screen, newYAbove)
        }
        underTest.updateResources(context)

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

    companion object {
        private const val TEST_Y_ABOVE_SCREEN = 50
    }
}
+25 −3
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShade
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView.FooterViewState
import com.android.systemui.statusbar.notification.headsup.AvalancheController
import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator
import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -55,7 +57,8 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
    private val avalancheController = mock<AvalancheController>()

    private val hostView = FrameLayout(context)
    private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
    private lateinit var headsUpAnimator: HeadsUpAnimator
    private lateinit var stackScrollAlgorithm: StackScrollAlgorithm
    private val notificationRow = mock<ExpandableNotificationRow>()
    private val notificationEntry = mock<NotificationEntry>()
    private val notificationEntryAdapter = mock<EntryAdapter>()
@@ -101,7 +104,10 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
        @JvmStatic
        @Parameters(name = "{0}")
        fun getParams(): List<FlagsParameterization> {
            return FlagsParameterization.allCombinationsOf().andSceneContainer()
            return FlagsParameterization.allCombinationsOf(
                    NotificationsHunSharedAnimationValues.FLAG_NAME
                )
                .andSceneContainer()
        }
    }

@@ -123,6 +129,15 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
        ambientState.isSmallScreen = true

        hostView.addView(notificationRow)

        if (NotificationsHunSharedAnimationValues.isEnabled) {
             headsUpAnimator = HeadsUpAnimator(context)
        }
        stackScrollAlgorithm = StackScrollAlgorithm(
            context,
            hostView,
            if (::headsUpAnimator.isInitialized) headsUpAnimator else null,
        )
    }

    private fun isTv(): Boolean {
@@ -403,7 +418,11 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
        ambientState.setLayoutMinHeight(2500) // Mock the height of shade
        ambientState.stackY = 2500f // Scroll over the max translation
        stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open
        if (NotificationsHunSharedAnimationValues.isEnabled) {
            headsUpAnimator.headsUpAppearHeightBottom = bottomOfScreen.toInt()
        } else {
            stackScrollAlgorithm.setHeadsUpAppearHeightBottom(bottomOfScreen.toInt())
        }
        whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
        whenever(notificationRow.isHeadsUp).thenReturn(true)
        whenever(notificationRow.isAboveShelf).thenReturn(true)
@@ -419,6 +438,9 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
        val topMargin = 100f
        ambientState.maxHeadsUpTranslation = 2000f
        ambientState.stackTopMargin = topMargin.toInt()
        if (NotificationsHunSharedAnimationValues.isEnabled) {
            headsUpAnimator.stackTopMargin = topMargin.toInt()
        }
        whenever(notificationRow.intrinsicHeight).thenReturn(100)
        whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)

+97 −5
Original line number Diff line number Diff line
@@ -16,12 +16,16 @@

package com.android.systemui.statusbar.notification.stack

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.AnimatorTestRule
import com.android.systemui.res.R
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
@@ -49,10 +53,10 @@ private const val HEADS_UP_ABOVE_SCREEN = 80
@RunWith(AndroidJUnit4::class)
@RunWithLooper
class StackStateAnimatorTest : SysuiTestCase() {

    @get:Rule val animatorTestRule = AnimatorTestRule(this)

    private lateinit var stackStateAnimator: StackStateAnimator
    private lateinit var headsUpAnimator: HeadsUpAnimator
    private val stackScroller: NotificationStackScrollLayout = mock()
    private val view: ExpandableView = mock()
    private val viewState: ExpandableViewState =
@@ -69,11 +73,20 @@ class StackStateAnimatorTest : SysuiTestCase() {

        whenever(stackScroller.context).thenReturn(context)
        whenever(view.viewState).thenReturn(viewState)
        stackStateAnimator = StackStateAnimator(mContext, stackScroller)

        if (NotificationsHunSharedAnimationValues.isEnabled) {
            headsUpAnimator = HeadsUpAnimator(context)
        }
        stackStateAnimator = StackStateAnimator(
            mContext,
            stackScroller,
            if (::headsUpAnimator.isInitialized) headsUpAnimator else null,
        )
    }

    @Test
    fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim() {
    @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
    fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim_flagOff() {
        val topMargin = 50f
        val expectedStartY = -topMargin - stackStateAnimator.mHeadsUpAppearStartAboveScreen
        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR)
@@ -94,7 +107,30 @@ class StackStateAnimatorTest : SysuiTestCase() {
    }

    @Test
    fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim() {
    @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
    fun startAnimationForEvents_headsUpFromTop_startsHeadsUpAppearAnim_flagOn() {
        val topMargin = 50f
        val expectedStartY = -topMargin - HEADS_UP_ABOVE_SCREEN
        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR)
        headsUpAnimator.stackTopMargin = topMargin.toInt()

        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
    @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
    fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOff() {
        val screenHeight = 2000f
        val expectedStartY = screenHeight + stackStateAnimator.mHeadsUpAppearStartAboveScreen
        val event =
@@ -118,7 +154,63 @@ class StackStateAnimatorTest : SysuiTestCase() {
    }

    @Test
    fun startAnimationForEvents_startsHeadsUpDisappearAnim() {
    @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
    fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOn() {
        val screenHeight = 2000f
        val expectedStartY = screenHeight + HEADS_UP_ABOVE_SCREEN
        val event =
            AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR).apply {
                headsUpFromBottom = true
            }
        headsUpAnimator.headsUpAppearHeightBottom = screenHeight.toInt()

        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)

        verify(view).setFinalActualHeight(VIEW_HEIGHT)
        verify(view, description("should animate from the bottom")).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_startsHeadsUpDisappearAnim_flagOff() {
        val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()
        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
        clearInvocations(view)
        stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0)

        verify(view)
            .performRemoveAnimation(
                /* duration= */ eq(disappearDuration),
                /* delay= */ eq(0L),
                /* translationDirection= */ eq(0f),
                /* isHeadsUpAnimation= */ eq(true),
                /* isHeadsUpCycling= */ eq(false),
                /* onStartedRunnable= */ any(),
                /* onFinishedRunnable= */ runnableCaptor.capture(),
                /* animationListener= */ any(),
                /* clipSide= */ eq(ExpandableView.ClipSide.BOTTOM),
            )

        animatorTestRule.advanceTimeBy(disappearDuration) // move to the end of SSA animations
        runnableCaptor.value.run() // execute the end runnable

        verify(view, description("should be translated to the heads up appear start"))
            .translationY = -stackStateAnimator.mHeadsUpAppearStartAboveScreen
        verify(view, description("should be called at the end of the disappear animation"))
            .removeFromTransientContainer()
    }

    @Test
    @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
    fun startAnimationForEvents_startsHeadsUpDisappearAnim_flagOn() {
        val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()
        val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
        clearInvocations(view)
+63 −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.notification.headsup

import android.content.Context
import com.android.systemui.res.R

/**
 * A class shared between [StackScrollAlgorithm] and [StackStateAnimator] to ensure all heads up
 * animations use the same animation values.
 */
class HeadsUpAnimator(context: Context) {
    init {
        NotificationsHunSharedAnimationValues.assertInNewMode()
    }

    var headsUpAppearHeightBottom: Int = 0
    var stackTopMargin: Int = 0

    private var headsUpAppearStartAboveScreen = context.fetchHeadsUpAppearStartAboveScreen()

    /**
     * Returns the Y translation for a heads-up notification animation.
     *
     * For an appear animation, the returned Y translation should be the starting value of the
     * animation. For a disappear animation, the returned Y translation should be the ending value
     * of the animation.
     */
    fun getHeadsUpYTranslation(isHeadsUpFromBottom: Boolean): Int {
        NotificationsHunSharedAnimationValues.assertInNewMode()

        if (isHeadsUpFromBottom) {
            // start from or end at the bottom of the screen
            return headsUpAppearHeightBottom + headsUpAppearStartAboveScreen
        }

        // start from or end at the top of the screen
        return -stackTopMargin - headsUpAppearStartAboveScreen
    }

    /** Should be invoked when resource values may have changed. */
    fun updateResources(context: Context) {
        headsUpAppearStartAboveScreen = context.fetchHeadsUpAppearStartAboveScreen()
    }

    private fun Context.fetchHeadsUpAppearStartAboveScreen(): Int {
        return this.resources.getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen)
    }
}
Loading