diff options
5 files changed, 578 insertions, 9 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 485b9febc284..2fa9a02b9e87 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index 7e7031200988..03108deb0ecc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -17,10 +17,13 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel import android.app.PendingIntent +import android.content.ComponentName import android.content.Context import android.view.View import com.android.internal.jank.Cuj import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.ComposableControllerFactory +import com.android.systemui.animation.DelegateTransitionAnimatorController import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -48,7 +51,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn /** View model for the ongoing phone call chip shown in the status bar. */ @@ -63,14 +69,62 @@ constructor( private val activityStarter: ActivityStarter, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { + /** The transition cookie used to register and unregister launch and return animations. */ + private val cookie = + ActivityTransitionAnimator.TransitionCookie("${CallChipViewModel::class.java}") + + /** + * Used internally to determine when a launch or return animation is in progress, as these + * require special handling. + */ + private val transitionState: MutableStateFlow<TransitionState> = + MutableStateFlow(TransitionState.NoTransition) + + // Since we're combining the chip state and the transition state flows, getting the old value by + // using [pairwise()] would confuse things. This is because if the calculation is triggered by + // a change in transition state, the chip state will still show the previous and current values, + // making it difficult to figure out what actually changed. Instead we cache the old value here, + // so that at each update we can keep track of what actually changed. + private var latestState: OngoingCallModel = OngoingCallModel.NoCall + private var latestTransitionState: TransitionState = TransitionState.NoTransition + private val chipWithReturnAnimation: StateFlow<OngoingActivityChipModel> = if (StatusBarChipsReturnAnimations.isEnabled) { - interactor.ongoingCallState - .map { state -> - when (state) { - is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive() + combine(interactor.ongoingCallState, transitionState) { newState, newTransitionState -> + val oldState = latestState + latestState = newState + val oldTransitionState = latestTransitionState + latestTransitionState = newTransitionState + + logger.log( + TAG, + LogLevel.DEBUG, + {}, + { + "Call chip state updated: oldState=$oldState newState=$newState " + + "oldTransitionState=$oldTransitionState " + + "newTransitionState=$newTransitionState" + }, + ) + + when (newState) { + is OngoingCallModel.NoCall -> + OngoingActivityChipModel.Inactive( + transitionManager = getTransitionManager(newState) + ) + is OngoingCallModel.InCall -> - prepareChip(state, systemClock, isHidden = state.isAppVisible) + prepareChip( + newState, + systemClock, + isHidden = + shouldChipBeHidden( + oldState = oldState, + newState = newState, + oldTransitionState = oldTransitionState, + newTransitionState = newTransitionState, + ), + ) } } .stateIn( @@ -112,6 +166,12 @@ constructor( chipLegacy } + /** + * The controller factory that the call chip uses to register and unregister its transition + * animations. + */ + private var transitionControllerFactory: ComposableControllerFactory? = null + /** Builds an [OngoingActivityChipModel.Active] from all the relevant information. */ private fun prepareChip( state: OngoingCallModel.InCall, @@ -149,6 +209,7 @@ constructor( onClickListenerLegacy = getOnClickListener(state.intent), clickBehavior = getClickBehavior(state.intent), isHidden = isHidden, + transitionManager = getTransitionManager(state), ) } else { val startTimeInElapsedRealtime = @@ -161,6 +222,7 @@ constructor( onClickListenerLegacy = getOnClickListener(state.intent), clickBehavior = getClickBehavior(state.intent), isHidden = isHidden, + transitionManager = getTransitionManager(state), ) } } @@ -191,9 +253,21 @@ constructor( onClick = { expandable -> StatusBarChipsModernization.unsafeAssertInNewMode() val animationController = - expandable.activityTransitionController( - Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP - ) + if ( + !StatusBarChipsReturnAnimations.isEnabled || + transitionControllerFactory == null + ) { + expandable.activityTransitionController( + Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP + ) + } else { + // When return animations are enabled, we use a long-lived registration + // with controllers created on-demand by the animation library instead + // of explicitly creating one at the time of the click. By not passing + // a controller here, we let the framework do its work. Otherwise, the + // explicit controller would take precedence and override the other one. + null + } activityStarter.postStartActivityDismissingKeyguard(intent, animationController) } ) @@ -210,6 +284,120 @@ constructor( ) } + private fun getTransitionManager( + state: OngoingCallModel + ): OngoingActivityChipModel.TransitionManager? { + if (!StatusBarChipsReturnAnimations.isEnabled) return null + return if (state is OngoingCallModel.NoCall) { + OngoingActivityChipModel.TransitionManager( + unregisterTransition = { activityStarter.unregisterTransition(cookie) } + ) + } else { + val component = (state as OngoingCallModel.InCall).intent?.intent?.component + if (component != null) { + val factory = getTransitionControllerFactory(component) + OngoingActivityChipModel.TransitionManager( + factory, + registerTransition = { + activityStarter.registerTransition(cookie, factory, scope) + }, + ) + } else { + // Without a component we can't instantiate a controller factory, and without a + // factory registering an animation is impossible. In this case, the transition + // manager is empty and inert. + OngoingActivityChipModel.TransitionManager() + } + } + } + + private fun getTransitionControllerFactory( + component: ComponentName + ): ComposableControllerFactory { + var factory = transitionControllerFactory + if (factory?.component == component) return factory + + factory = + object : + ComposableControllerFactory( + cookie, + component, + launchCujType = Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, + ) { + override suspend fun createController( + forLaunch: Boolean + ): ActivityTransitionAnimator.Controller { + transitionState.value = + if (forLaunch) { + TransitionState.LaunchRequested + } else { + TransitionState.ReturnRequested + } + + val controller = + expandable + .mapNotNull { + it?.activityTransitionController( + launchCujType, + cookie, + component, + returnCujType, + isEphemeral = false, + ) + } + .first() + + return object : DelegateTransitionAnimatorController(controller) { + override val isLaunching: Boolean + get() = forLaunch + + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationStart(isExpandingFullyAbove) + transitionState.value = + if (isLaunching) { + TransitionState.Launching + } else { + TransitionState.Returning + } + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) + transitionState.value = TransitionState.NoTransition + } + + override fun onTransitionAnimationCancelled( + newKeyguardOccludedState: Boolean? + ) { + delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) + transitionState.value = TransitionState.NoTransition + } + } + } + } + + transitionControllerFactory = factory + return factory + } + + /** Define the current state of this chip's transition animation. */ + private sealed interface TransitionState { + /** Idle. */ + data object NoTransition : TransitionState + + /** Launch animation has been requested but hasn't started yet. */ + data object LaunchRequested : TransitionState + + /** Launch animation in progress. */ + data object Launching : TransitionState + + /** Return animation has been requested but hasn't started yet. */ + data object ReturnRequested : TransitionState + + /** Return animation in progress. */ + data object Returning : TransitionState + } + companion object { private val phoneIcon = Icon.Resource( @@ -217,5 +405,42 @@ constructor( ContentDescription.Resource(R.string.ongoing_call_content_description), ) private val TAG = "CallVM".pad() + + /** Determines whether or not an active call chip should be hidden. */ + private fun shouldChipBeHidden( + oldState: OngoingCallModel, + newState: OngoingCallModel.InCall, + oldTransitionState: TransitionState, + newTransitionState: TransitionState, + ): Boolean { + // The app is in the background and no transitions are ongoing (during transitions, + // [isAppVisible] must always be true). Show the chip. + if (!newState.isAppVisible) return false + + // The call has just started and is visible. Hide the chip. + if (oldState is OngoingCallModel.NoCall) return true + + // The state went from the app not being visible to visible. This happens when the chip + // is tapped and a launch animation is about to start. Keep the chip showing. + if (!(oldState as OngoingCallModel.InCall).isAppVisible) return false + + // The app was and remains visible, but the transition state has changed. A launch or + // return animation has been requested or is ongoing. Keep the chip showing. + if ( + newTransitionState is TransitionState.LaunchRequested || + newTransitionState is TransitionState.Launching || + newTransitionState is TransitionState.ReturnRequested || + newTransitionState is TransitionState.Returning + ) { + return false + } + + // The app was and remains visible, so we generally want to hide the chip. The only + // exception is if a return transition has just ended. In this case, the transition + // state changes shortly before the app visibility does. If we hide the chip between + // these two updates, this results in a flicker. We bridge the gap by keeping the chip + // showing. + return oldTransitionState != TransitionState.Returning + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index 4edb23dc9f0e..58d38903f7cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -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) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt index 407849b9fae0..700e6d93c628 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt @@ -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( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index d37a46e58882..3876d9fa77a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -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 = {}, + ) } |