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

Commit c25bc7a0 authored by Luca Zuccarini's avatar Luca Zuccarini Committed by Android (Google) Code Review
Browse files

Merge "Introduce TransitionManager and functioning return animations to call chips." into main

parents ab631004 63bada69
Loading
Loading
Loading
Loading
+296 −0
Original line number Diff line number Diff line
@@ -17,12 +17,17 @@
package com.android.systemui.statusbar.chips.call.ui.viewmodel

import android.app.PendingIntent
import android.content.ComponentName
import android.content.Intent
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.data.repository.activityManagerRepository
import com.android.systemui.activity.data.repository.fake
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -51,6 +56,7 @@ import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -481,6 +487,294 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
            verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(pendingIntent, null)
        }

    @Test
    @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
    @EnableChipsModernization
    fun chipWithReturnAnimation_updatesCorrectly_withStateAndTransitionState() =
        kosmos.runTest {
            val pendingIntent = mock<PendingIntent>()
            val intent = mock<Intent>()
            whenever(pendingIntent.intent).thenReturn(intent)
            val component = mock<ComponentName>()
            whenever(intent.component).thenReturn(component)

            val expandable = mock<Expandable>()
            val activityController = mock<ActivityTransitionAnimator.Controller>()
            whenever(
                    expandable.activityTransitionController(
                        anyOrNull(),
                        anyOrNull(),
                        any(),
                        anyOrNull(),
                        any(),
                    )
                )
                .thenReturn(activityController)

            val latest by collectLastValue(underTest.chip)

            // Start off with no call.
            removeOngoingCallState(key = NOTIFICATION_KEY)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
            assertThat(latest!!.transitionManager!!.controllerFactory).isNull()

            // Call starts [NoCall -> InCall(isAppVisible=true), NoTransition].
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 345,
                contentIntent = pendingIntent,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
            val factory = latest!!.transitionManager!!.controllerFactory
            assertThat(factory!!.component).isEqualTo(component)

            // Request a return transition [InCall(isAppVisible=true), NoTransition ->
            // ReturnRequested].
            factory.onCompose(expandable)
            var controller = factory.createController(forLaunch = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // Start the return transition [InCall(isAppVisible=true), ReturnRequested ->
            // Returning].
            controller.onTransitionAnimationStart(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // End the return transition [InCall(isAppVisible=true), Returning -> NoTransition].
            controller.onTransitionAnimationEnd(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // Settle the return transition [InCall(isAppVisible=true) ->
            // InCall(isAppVisible=false), NoTransition].
            kosmos.activityManagerRepository.fake.setIsAppVisible(NOTIFICATION_UID, false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // Trigger a launch transition [InCall(isAppVisible=false) -> InCall(isAppVisible=true),
            // NoTransition].
            kosmos.activityManagerRepository.fake.setIsAppVisible(NOTIFICATION_UID, true)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // Request the return transition [InCall(isAppVisible=true), NoTransition ->
            // LaunchRequested].
            controller = factory.createController(forLaunch = true)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // Start the return transition [InCall(isAppVisible=true), LaunchRequested ->
            // Launching].
            controller.onTransitionAnimationStart(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // End the return transition [InCall(isAppVisible=true), Launching -> NoTransition].
            controller.onTransitionAnimationStart(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
            assertThat(latest!!.transitionManager!!.controllerFactory).isEqualTo(factory)

            // End the call with the app visible [InCall(isAppVisible=true) -> NoCall,
            // NoTransition].
            removeOngoingCallState(key = NOTIFICATION_KEY)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
            assertThat(latest!!.transitionManager!!.controllerFactory).isNull()

            // End the call with the app hidden [InCall(isAppVisible=false) -> NoCall,
            // NoTransition].
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 345,
                contentIntent = pendingIntent,
                isAppVisible = false,
            )
            removeOngoingCallState(key = NOTIFICATION_KEY)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
            assertThat(latest!!.transitionManager!!.controllerFactory).isNull()
        }

    @Test
    @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
    fun chipLegacy_hasNoTransitionAnimationInformation() =
        kosmos.runTest {
            val latest by collectLastValue(underTest.chip)

            // NoCall
            removeOngoingCallState(key = NOTIFICATION_KEY)
            assertThat(latest!!.transitionManager).isNull()

            // InCall with visible app
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 345,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            assertThat(latest!!.transitionManager).isNull()

            // InCall with hidden app
            kosmos.activityManagerRepository.fake.setIsAppVisible(NOTIFICATION_UID, false)
            assertThat(latest!!.transitionManager).isNull()
        }

    @Test
    @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
    @EnableChipsModernization
    fun chipWithReturnAnimation_chipDataChangesMidTransition() =
        kosmos.runTest {
            val pendingIntent = mock<PendingIntent>()
            val intent = mock<Intent>()
            whenever(pendingIntent.intent).thenReturn(intent)
            val component = mock<ComponentName>()
            whenever(intent.component).thenReturn(component)

            val expandable = mock<Expandable>()
            val activityController = mock<ActivityTransitionAnimator.Controller>()
            whenever(
                    expandable.activityTransitionController(
                        anyOrNull(),
                        anyOrNull(),
                        any(),
                        anyOrNull(),
                        any(),
                    )
                )
                .thenReturn(activityController)

            val latest by collectLastValue(underTest.chip)

            // Start with the app visible and trigger a return animation.
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 345,
                contentIntent = pendingIntent,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            var factory = latest!!.transitionManager!!.controllerFactory!!
            factory.onCompose(expandable)
            var controller = factory.createController(forLaunch = false)
            controller.onTransitionAnimationStart(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()

            // The chip changes state.
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 0,
                contentIntent = pendingIntent,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()

            // Reset the state and trigger a launch animation.
            controller.onTransitionAnimationEnd(isExpandingFullyAbove = false)
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 345,
                contentIntent = pendingIntent,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            factory = latest!!.transitionManager!!.controllerFactory!!
            factory.onCompose(expandable)
            controller = factory.createController(forLaunch = true)
            controller.onTransitionAnimationStart(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()

            // The chip changes state.
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = -2,
                contentIntent = pendingIntent,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
        }

    @Test
    @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
    @EnableChipsModernization
    fun chipWithReturnAnimation_chipDisappearsMidTransition() =
        kosmos.runTest {
            val pendingIntent = mock<PendingIntent>()
            val intent = mock<Intent>()
            whenever(pendingIntent.intent).thenReturn(intent)
            val component = mock<ComponentName>()
            whenever(intent.component).thenReturn(component)

            val expandable = mock<Expandable>()
            val activityController = mock<ActivityTransitionAnimator.Controller>()
            whenever(
                    expandable.activityTransitionController(
                        anyOrNull(),
                        anyOrNull(),
                        any(),
                        anyOrNull(),
                        any(),
                    )
                )
                .thenReturn(activityController)

            val latest by collectLastValue(underTest.chip)

            // Start with the app visible and trigger a return animation.
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 345,
                contentIntent = pendingIntent,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            var factory = latest!!.transitionManager!!.controllerFactory!!
            factory.onCompose(expandable)
            var controller = factory.createController(forLaunch = false)
            controller.onTransitionAnimationStart(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()

            // The chip disappears.
            removeOngoingCallState(key = NOTIFICATION_KEY)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)

            // Reset the state and trigger a launch animation.
            controller.onTransitionAnimationEnd(isExpandingFullyAbove = false)
            addOngoingCallState(
                key = NOTIFICATION_KEY,
                startTimeMs = 345,
                contentIntent = pendingIntent,
                uid = NOTIFICATION_UID,
                isAppVisible = true,
            )
            factory = latest!!.transitionManager!!.controllerFactory!!
            factory.onCompose(expandable)
            controller = factory.createController(forLaunch = true)
            controller.onTransitionAnimationStart(isExpandingFullyAbove = false)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
            assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()

            // The chip disappears.
            removeOngoingCallState(key = NOTIFICATION_KEY)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
        }

    companion object {
        fun createStatusBarIconViewOrNull(): StatusBarIconView? =
            if (StatusBarConnectedDisplays.isEnabled) {
@@ -500,6 +794,8 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
                }
                .build()

        private const val NOTIFICATION_KEY = "testKey"
        private const val NOTIFICATION_UID = 12345
        private const val PROMOTED_BACKGROUND_COLOR = 65
        private const val PROMOTED_PRIMARY_TEXT_COLOR = 98

+233 −8

File changed.

Preview size limit exceeded, changes collapsed.

+3 −0
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.load
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
@@ -90,6 +91,8 @@ fun OngoingActivityChip(
            },
        borderStroke = borderStroke,
        onClick = onClick,
        useModifierBasedImplementation = StatusBarChipsReturnAnimations.isEnabled,
        transitionControllerFactory = model.transitionManager?.controllerFactory,
    ) {
        ChipBody(model, iconViewStore, isClickable = onClick != null)
    }
+14 −0
Original line number Diff line number Diff line
@@ -21,12 +21,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.res.R
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

@@ -36,6 +38,18 @@ fun OngoingActivityChips(
    iconViewStore: NotificationIconContainerViewBinder.IconViewStore?,
    modifier: Modifier = Modifier,
) {
    if (StatusBarChipsReturnAnimations.isEnabled) {
        SideEffect {
            // Active chips must always be capable of animating to/from activities, even when they
            // are hidden. Therefore we always register their transitions.
            for (chip in chips.active) chip.transitionManager?.registerTransition?.invoke()
            // Inactive chips and chips in the overflow are never shown, so they must not have any
            // registered transition.
            for (chip in chips.overflow) chip.transitionManager?.unregisterTransition?.invoke()
            for (chip in chips.inactive) chip.transitionManager?.unregisterTransition?.invoke()
        }
    }

    val shownChips = chips.active.filter { !it.isHidden }
    if (shownChips.isNotEmpty()) {
        Row(
+32 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.CurrentTimeMillisLong
import android.annotation.ElapsedRealtimeLong
import android.os.SystemClock
import android.view.View
import com.android.systemui.animation.ComposableControllerFactory
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -33,6 +34,9 @@ sealed class OngoingActivityChipModel {
    /** Condensed name representing the model, used for logs. */
    abstract val logName: String

    /** Object used to manage the behavior of this chip during activity launch and returns. */
    abstract val transitionManager: TransitionManager?

    /**
     * This chip shouldn't be shown.
     *
@@ -40,7 +44,10 @@ sealed class OngoingActivityChipModel {
     *   animated, and false if that transition should *not* be animated (i.e. the chip view should
     *   immediately disappear).
     */
    data class Inactive(val shouldAnimate: Boolean = true) : OngoingActivityChipModel() {
    data class Inactive(
        val shouldAnimate: Boolean = true,
        override val transitionManager: TransitionManager? = null,
    ) : OngoingActivityChipModel() {
        override val logName = "Inactive(anim=$shouldAnimate)"
    }

@@ -61,6 +68,7 @@ sealed class OngoingActivityChipModel {
        open val onClickListenerLegacy: View.OnClickListener?,
        /** Data class that determines how clicks on the chip should be handled. */
        open val clickBehavior: ClickBehavior,
        override val transitionManager: TransitionManager?,
        /**
         * Whether this chip should be hidden. This can be the case depending on system states (like
         * which apps are in the foreground and whether there is an ongoing transition.
@@ -77,6 +85,7 @@ sealed class OngoingActivityChipModel {
            override val colors: ColorsModel,
            override val onClickListenerLegacy: View.OnClickListener?,
            override val clickBehavior: ClickBehavior,
            override val transitionManager: TransitionManager? = null,
            override val isHidden: Boolean = false,
            override val shouldAnimate: Boolean = true,
        ) :
@@ -86,6 +95,7 @@ sealed class OngoingActivityChipModel {
                colors,
                onClickListenerLegacy,
                clickBehavior,
                transitionManager,
                isHidden,
                shouldAnimate,
            ) {
@@ -122,6 +132,7 @@ sealed class OngoingActivityChipModel {
            val isEventInFuture: Boolean = false,
            override val onClickListenerLegacy: View.OnClickListener?,
            override val clickBehavior: ClickBehavior,
            override val transitionManager: TransitionManager? = null,
            override val isHidden: Boolean = false,
            override val shouldAnimate: Boolean = true,
        ) :
@@ -131,6 +142,7 @@ sealed class OngoingActivityChipModel {
                colors,
                onClickListenerLegacy,
                clickBehavior,
                transitionManager,
                isHidden,
                shouldAnimate,
            ) {
@@ -157,6 +169,7 @@ sealed class OngoingActivityChipModel {
            @CurrentTimeMillisLong val time: Long,
            override val onClickListenerLegacy: View.OnClickListener?,
            override val clickBehavior: ClickBehavior,
            override val transitionManager: TransitionManager? = null,
            override val isHidden: Boolean = false,
            override val shouldAnimate: Boolean = true,
        ) :
@@ -166,6 +179,7 @@ sealed class OngoingActivityChipModel {
                colors,
                onClickListenerLegacy,
                clickBehavior,
                transitionManager,
                isHidden,
                shouldAnimate,
            ) {
@@ -185,6 +199,7 @@ sealed class OngoingActivityChipModel {
            override val colors: ColorsModel,
            /** The number of seconds until an event is started. */
            val secondsUntilStarted: Long,
            override val transitionManager: TransitionManager? = null,
            override val isHidden: Boolean = false,
            override val shouldAnimate: Boolean = true,
        ) :
@@ -194,6 +209,7 @@ sealed class OngoingActivityChipModel {
                colors,
                onClickListenerLegacy = null,
                clickBehavior = ClickBehavior.None,
                transitionManager,
                isHidden,
                shouldAnimate,
            ) {
@@ -209,6 +225,7 @@ sealed class OngoingActivityChipModel {
            val text: String,
            override val onClickListenerLegacy: View.OnClickListener? = null,
            override val clickBehavior: ClickBehavior,
            override val transitionManager: TransitionManager? = null,
            override val isHidden: Boolean = false,
            override val shouldAnimate: Boolean = true,
        ) :
@@ -218,6 +235,7 @@ sealed class OngoingActivityChipModel {
                colors,
                onClickListenerLegacy,
                clickBehavior,
                transitionManager,
                isHidden,
                shouldAnimate,
            ) {
@@ -271,4 +289,17 @@ sealed class OngoingActivityChipModel {
        /** Clicking the chip will show the heads up notification associated with the chip. */
        data class ShowHeadsUpNotification(val onClick: () -> Unit) : ClickBehavior
    }

    /** Defines the behavior of the chip with respect to activity launch and return transitions. */
    data class TransitionManager(
        /** The factory used to create the controllers that animate the chip. */
        val controllerFactory: ComposableControllerFactory? = null,
        /**
         * Used to create a registration for this chip using [controllerFactory]. Must be
         * idempotent.
         */
        val registerTransition: () -> Unit = {},
        /** Used to remove the existing registration for this chip, if any. */
        val unregisterTransition: () -> Unit = {},
    )
}