diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 95e7737906..21c4d8cffb 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -56,9 +56,11 @@ import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELE import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED; import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; +import static com.android.quickstep.TaskViewUtils.extractTargetsAndStates; import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -77,6 +79,7 @@ import android.graphics.RectF; import android.os.IBinder; import android.os.SystemClock; import android.util.Log; +import android.util.Pair; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; @@ -90,6 +93,7 @@ import android.view.animation.Interpolator; import android.widget.Toast; import android.window.DesktopModeFlags; import android.window.PictureInPictureSurfaceTransaction; +import android.window.WindowAnimationState; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -143,6 +147,7 @@ import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; import com.android.quickstep.views.TaskContainer; import com.android.quickstep.views.TaskView; +import com.android.systemui.animation.TransitionAnimator; import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -156,6 +161,8 @@ import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils; +import kotlin.Unit; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -165,8 +172,6 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.function.Consumer; -import kotlin.Unit; - /** * Handles the navigation gestures when Launcher is the default home activity. */ @@ -347,6 +352,9 @@ public abstract class AbsSwipeUpHandler< // Indicates whether the divider is shown, only used when split screen is activated. private boolean mIsDividerShown = true; private boolean mStartMovingTasks; + // Whether the animation to home should be handed off to another handler once the gesture is + // committed. + protected boolean mHandOffAnimationToHome = false; @Nullable private RemoteAnimationTargets.ReleaseCheck mSwipePipToHomeReleaseCheck = null; @@ -945,6 +953,10 @@ public abstract class AbsSwipeUpHandler< mSwipePipToHomeReleaseCheck = new RemoteAnimationTargets.ReleaseCheck(); mSwipePipToHomeReleaseCheck.setCanRelease(true); mRecentsAnimationTargets.addReleaseCheck(mSwipePipToHomeReleaseCheck); + if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) { + mHandOffAnimationToHome = + targets.extras.getBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, false); + } // Only initialize the device profile, if it has not been initialized before, as in some // configurations targets.homeContentInsets may not be correct. @@ -1629,6 +1641,10 @@ public abstract class AbsSwipeUpHandler< } windowAnim = createWindowAnimationToHome(start, homeAnimFactory); + if (mHandOffAnimationToHome) { + handOffAnimation(velocityPxPerMs); + } + windowAnim[0].addAnimatorListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { @@ -1711,6 +1727,19 @@ public abstract class AbsSwipeUpHandler< } } + private void handOffAnimation(PointF velocityPxPerMs) { + if (!TransitionAnimator.Companion.longLivedReturnAnimationsEnabled() + || mRecentsAnimationController == null) { + return; + } + + Pair targetsAndStates = + extractTargetsAndStates(mRemoteTargetHandles, velocityPxPerMs); + mRecentsAnimationController.handOffAnimation( + targetsAndStates.first, targetsAndStates.second); + ActiveGestureProtoLogProxy.logHandOffAnimation(); + } + private int calculateWindowRotation(RemoteAnimationTarget runningTaskTarget, RecentsOrientedState orientationState) { if (runningTaskTarget.rotationChange != 0) { diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java index dacafd47ac..6087dc214e 100644 --- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java +++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java @@ -46,7 +46,6 @@ import com.android.launcher3.views.ClipIconView; import com.android.launcher3.views.FloatingIconView; import com.android.launcher3.views.FloatingView; import com.android.launcher3.widget.LauncherAppWidgetHostView; -import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.ScalingWorkspaceRevealAnim; import com.android.quickstep.util.StaggeredWorkspaceAnim; @@ -54,6 +53,7 @@ import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.views.FloatingWidgetView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; +import com.android.systemui.animation.TransitionAnimator; import com.android.systemui.shared.system.InputConsumerController; import java.util.Collections; @@ -108,7 +108,9 @@ public class LauncherSwipeHandlerV2 extends AbsSwipeUpHandler< mContainer.getRootView().setForceHideBackArrow(true); - if (!canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) { + boolean handOffAnimation = TransitionAnimator.Companion.longLivedReturnAnimationsEnabled() + && mHandOffAnimationToHome; + if (handOffAnimation || !canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) { return new LauncherHomeAnimationFactory() { @Nullable diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java index 60fcff86f3..145773d0c2 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java @@ -21,9 +21,11 @@ import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.WindowManagerGlobal; import android.window.PictureInPictureSurfaceTransaction; +import android.window.WindowAnimationState; import androidx.annotation.UiThread; @@ -32,6 +34,7 @@ import com.android.internal.os.IResultReceiver; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.RunnableList; import com.android.quickstep.util.ActiveGestureProtoLogProxy; +import com.android.systemui.animation.TransitionAnimator; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; @@ -89,6 +92,16 @@ public class RecentsAnimationController { } } + @UiThread + public void handOffAnimation(RemoteAnimationTarget[] targets, WindowAnimationState[] states) { + if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) { + UI_HELPER_EXECUTOR.execute(() -> mController.handOffAnimation(targets, states)); + } else { + Log.e(TAG, "Tried to hand off the animation, but the feature is disabled", + new Exception()); + } + } + @UiThread public void finishAnimationToHome() { finishController(true /* toRecents */, null, false /* sendUserLeaveHint */); diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java index 07ee4793de..783c87c8af 100644 --- a/quickstep/src/com/android/quickstep/TaskViewUtils.java +++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java @@ -47,12 +47,15 @@ import android.content.ComponentName; import android.content.Context; import android.graphics.Matrix; import android.graphics.Matrix.ScaleToFit; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; +import android.util.Pair; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.View; import android.window.TransitionInfo; +import android.window.WindowAnimationState; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -784,4 +787,43 @@ public final class TaskViewUtils { animatorHandler.accept(dockFadeAnimator); return dockFadeAnimator; } + + /** + * Creates an array of {@link RemoteAnimationTarget}s and a matching array of + * {@link WindowAnimationState}s from the provided handles. + * Important: the ordering of the two arrays is the same, so the state at each index of the + * second applies to the target in the same index of the first. + * + * @param handles The handles wrapping each target. + * @param velocityPxPerMs The current velocity of the target animations. + */ + @NonNull + public static Pair extractTargetsAndStates( + @NonNull RemoteTargetHandle[] handles, @NonNull PointF velocityPxPerMs) { + RemoteAnimationTarget[] targets = new RemoteAnimationTarget[handles.length]; + WindowAnimationState[] animationStates = new WindowAnimationState[handles.length]; + long timestamp = System.currentTimeMillis(); + + for (int i = 0; i < handles.length; i++) { + targets[i] = handles[i].getTransformParams().getTargetSet().apps[i]; + + TaskViewSimulator taskViewSimulator = handles[i].getTaskViewSimulator(); + RectF startRect = taskViewSimulator.getCurrentRect(); + float cornerRadius = taskViewSimulator.getCurrentCornerRadius(); + + WindowAnimationState state = new WindowAnimationState(); + state.timestamp = timestamp; + state.bounds = new RectF( + startRect.left, startRect.top, startRect.right, startRect.bottom); + state.topLeftRadius = cornerRadius; + state.topRightRadius = cornerRadius; + state.bottomRightRadius = cornerRadius; + state.bottomLeftRadius = cornerRadius; + state.velocityPxPerMs = velocityPxPerMs; + + animationStates[i] = state; + } + + return new Pair<>(targets, animationStates); + } } diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java index f43a125aa4..00910365ff 100644 --- a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java +++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java @@ -96,6 +96,12 @@ public class ActiveGestureProtoLogProxy { + "force finish recents animation complete; clearing state callback."); } + public static void logHandOffAnimation() { + ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.handOffAnimation"); + if (!enableActiveGestureProtoLog()) return; + ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.handOffAnimation"); + } + public static void logFinishRecentsAnimationOnTasksAppeared() { ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared"); if (!enableActiveGestureProtoLog()) return; diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java index 6b95f8d3dd..970bdecc80 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java @@ -17,6 +17,7 @@ package com.android.quickstep; import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; @@ -28,6 +29,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -40,6 +42,9 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.ViewTreeObserver; @@ -58,6 +63,7 @@ import com.android.quickstep.fallback.window.RecentsWindowManager; import com.android.quickstep.util.ContextInitListener; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.Flags; import com.android.systemui.shared.system.InputConsumerController; import org.junit.Before; @@ -103,14 +109,8 @@ public abstract class AbsSwipeUpHandlerTestCase< /* startBounds= */ null, /* taskInfo= */ mRunningTaskInfo, /* allowEnterPip= */ false); - protected final RecentsAnimationTargets mRecentsAnimationTargets = new RecentsAnimationTargets( - new RemoteAnimationTarget[] {mRemoteAnimationTarget}, - new RemoteAnimationTarget[] {mRemoteAnimationTarget}, - new RemoteAnimationTarget[] {mRemoteAnimationTarget}, - /* homeContentInsets= */ new Rect(), - /* minimizedHomeBounds= */ null, - new Bundle()); + protected RecentsAnimationTargets mRecentsAnimationTargets; protected TaskAnimationManager mTaskAnimationManager; protected RecentsAnimationDeviceState mRecentsAnimationDeviceState; @@ -127,6 +127,22 @@ public abstract class AbsSwipeUpHandlerTestCase< @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUpAnimationTargets() { + Bundle extras = new Bundle(); + extras.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, true); + mRecentsAnimationTargets = new RecentsAnimationTargets( + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + new RemoteAnimationTarget[] {mRemoteAnimationTarget}, + /* homeContentInsets= */ new Rect(), + /* minimizedHomeBounds= */ null, + extras); + } + @Before public void setUpRunningTaskInfo() { mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN) @@ -237,6 +253,30 @@ public abstract class AbsSwipeUpHandlerTestCase< }); } + @EnableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED}) + @Test + public void testHomeGesture_handsOffAnimation() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController).handOffAnimation(any(), any()); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + + @DisableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED}) + @Test + public void testHomeGesture_doesNotHandOffAnimation_withFlagsDisabled() { + createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME); + + runOnMainSync(() -> { + verify(mRecentsAnimationController, never()).handOffAnimation(any(), any()); + verifyRecentsAnimationFinishedAndCallCallback(); + }); + } + @Test public void testHomeGesture_invalidatesHandlerAfterParallelAnim() { ValueAnimator parallelAnim = new ValueAnimator();