/* * Copyright (C) 2018 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.quickstep; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs; import static com.android.launcher3.util.RaceConditionTracker.ENTER; import static com.android.launcher3.util.RaceConditionTracker.EXIT; import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG; import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME; import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK; import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.NEW_TASK; import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.RECENTS; import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE; import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.PointF; import android.graphics.RectF; import android.os.Build; import android.os.SystemClock; import android.view.View; import android.view.View.OnApplyWindowInsetsListener; import android.view.ViewTreeObserver.OnDrawListener; import android.view.WindowInsets; import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.logging.UserEventDispatcher; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.launcher3.util.RaceConditionTracker; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.ActivityControlHelper.AnimationFactory; import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory; import com.android.quickstep.SysUINavigationMode.Mode; import com.android.quickstep.inputconsumers.InputConsumer; import com.android.quickstep.inputconsumers.OverviewInputConsumer; import com.android.quickstep.util.ClipAnimationHelper.TargetAlphaProvider; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.ShelfPeekAnim; import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState; import com.android.quickstep.util.SwipeAnimationTargetSet; import com.android.quickstep.views.LiveTileOverlay; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.LatencyTrackerCompat; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.WindowCallbacksCompat; @TargetApi(Build.VERSION_CODES.O) public class WindowTransformSwipeHandler extends BaseSwipeUpHandler implements OnApplyWindowInsetsListener { private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName(); private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null; private static int getFlagForIndex(int index, String name) { if (DEBUG_STATES) { STATE_NAMES[index] = name; } return 1 << index; } // Launcher UI related states private static final int STATE_LAUNCHER_PRESENT = getFlagForIndex(0, "STATE_LAUNCHER_PRESENT"); private static final int STATE_LAUNCHER_STARTED = getFlagForIndex(1, "STATE_LAUNCHER_STARTED"); private static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN"); // Internal initialization states private static final int STATE_APP_CONTROLLER_RECEIVED = getFlagForIndex(3, "STATE_APP_CONTROLLER_RECEIVED"); // Interaction finish states private static final int STATE_SCALED_CONTROLLER_HOME = getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME"); private static final int STATE_SCALED_CONTROLLER_RECENTS = getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS"); private static final int STATE_HANDLER_INVALIDATED = getFlagForIndex(6, "STATE_HANDLER_INVALIDATED"); private static final int STATE_GESTURE_STARTED = getFlagForIndex(7, "STATE_GESTURE_STARTED"); private static final int STATE_GESTURE_CANCELLED = getFlagForIndex(8, "STATE_GESTURE_CANCELLED"); private static final int STATE_GESTURE_COMPLETED = getFlagForIndex(9, "STATE_GESTURE_COMPLETED"); private static final int STATE_CAPTURE_SCREENSHOT = getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT"); private static final int STATE_SCREENSHOT_CAPTURED = getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED"); private static final int STATE_SCREENSHOT_VIEW_SHOWN = getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN"); private static final int STATE_RESUME_LAST_TASK = getFlagForIndex(13, "STATE_RESUME_LAST_TASK"); private static final int STATE_START_NEW_TASK = getFlagForIndex(14, "STATE_START_NEW_TASK"); private static final int STATE_CURRENT_TASK_FINISHED = getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED"); private static final int LAUNCHER_UI_STATES = STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED; public enum GestureEndTarget { HOME(1, STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT, true, false, ContainerType.WORKSPACE, false), RECENTS(1, STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT | STATE_SCREENSHOT_VIEW_SHOWN, true, false, ContainerType.TASKSWITCHER, true), NEW_TASK(0, STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT, false, true, ContainerType.APP, true), LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP, false); GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued, int containerType, boolean recentsAttachedToAppWindow) { this.endShift = endShift; this.endState = endState; this.isLauncher = isLauncher; this.canBeContinued = canBeContinued; this.containerType = containerType; this.recentsAttachedToAppWindow = recentsAttachedToAppWindow; } /** 0 is app, 1 is overview */ public final float endShift; /** The state to apply when we reach this final target */ public final int endState; /** Whether the target is in the launcher activity */ public final boolean isLauncher; /** Whether the user can start a new gesture while this one is finishing */ public final boolean canBeContinued; /** Used to log where the user ended up after the gesture ends */ public final int containerType; /** Whether RecentsView should be attached to the window as we animate to this target */ public final boolean recentsAttachedToAppWindow; } public static final long MAX_SWIPE_DURATION = 350; public static final long MIN_SWIPE_DURATION = 80; public static final long MIN_OVERSHOOT_DURATION = 120; public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f; private static final float SWIPE_DURATION_MULTIPLIER = Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured"; public static final long RECENTS_ATTACH_DURATION = 300; /** * Used as the page index for logging when we return to the last task at the end of the gesture. */ private static final int LOG_NO_OP_PAGE_INDEX = -1; private GestureEndTarget mGestureEndTarget; // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise private RunningWindowAnim mRunningWindowAnim; private boolean mIsShelfPeeking; private boolean mContinuingLastGesture; private ThumbnailData mTaskSnapshot; // Used to control launcher components throughout the swipe gesture. private AnimatorPlaybackController mLauncherTransitionController; private boolean mHasLauncherTransitionControllerStarted; private AnimationFactory mAnimationFactory = (t) -> { }; private LiveTileOverlay mLiveTileOverlay = new LiveTileOverlay(); private boolean mLiveTileOverlayAttached = false; private boolean mWasLauncherAlreadyVisible; private boolean mPassedOverviewThreshold; private boolean mGestureStarted; private int mLogAction = Touch.SWIPE; private int mLogDirection = Direction.UP; private PointF mDownPos; private boolean mIsLikelyToStartNewTask; private final long mTouchTimeMs; private long mLauncherFrameDrawnTime; public WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs, OverviewComponentObserver overviewComponentObserver, boolean continuingLastGesture, InputConsumerController inputConsumer, RecentsModel recentsModel) { super(context, overviewComponentObserver, recentsModel, inputConsumer, runningTaskInfo.id); mTouchTimeMs = touchTimeMs; mContinuingLastGesture = continuingLastGesture; initStateCallbacks(); } private void initStateCallbacks() { mStateCallback = new MultiStateCallback(STATE_NAMES); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED, this::onLauncherPresentAndGestureStarted); mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED, this::initializeLauncherAnimationController); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, this::launcherFrameDrawn); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED | STATE_GESTURE_CANCELLED, this::resetStateForAnimationCancel); mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED, this::sendRemoteAnimationsToAnimationFactory); mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, this::resumeLastTask); mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED, this::startNewTask); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT, this::switchToScreenshot); mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED | STATE_SCALED_CONTROLLER_RECENTS, this::finishCurrentTransitionToRecents); mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED | STATE_SCALED_CONTROLLER_HOME, this::finishCurrentTransitionToHome); mStateCallback.addCallback(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED, this::reset); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED | STATE_GESTURE_STARTED, this::setupLauncherUiAfterSwipeUpToRecentsAnimation); mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, this::invalidateHandlerWithLauncher); mStateCallback.addCallback(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK, this::notifyTransitionCancelled); mStateCallback.addCallback(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED, mRecentsAnimationWrapper::enableInputConsumer); if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT, (b) -> mRecentsView.setRunningTaskHidden(!b)); } } @Override protected boolean onActivityInit(final T activity, Boolean alreadyOnHome) { if (mActivity == activity) { return true; } if (mActivity != null) { // The launcher may have been recreated as a result of device rotation. int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES; initStateCallbacks(); mStateCallback.setState(oldState); } mWasLauncherAlreadyVisible = alreadyOnHome; mActivity = activity; // Override the visibility of the activity until the gesture actually starts and we swipe // up, or until we transition home and the home animation is composed if (alreadyOnHome) { mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); } else { mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); } mRecentsView = activity.getOverviewPanel(); linkRecentsViewScroll(); addLiveTileOverlay(); mStateCallback.setState(STATE_LAUNCHER_PRESENT); if (alreadyOnHome) { onLauncherStart(activity); } else { activity.setOnStartCallback(this::onLauncherStart); } setupRecentsViewUi(); return true; } @Override protected boolean moveWindowWithRecentsScroll() { return mGestureEndTarget != HOME; } private void onLauncherStart(final T activity) { if (mActivity != activity) { return; } if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { return; } // If we've already ended the gesture and are going home, don't prepare recents UI, // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL. if (mGestureEndTarget != HOME) { Runnable initAnimFactory = () -> { mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated); maybeUpdateRecentsAttachedState(false /* animate */); }; if (mWasLauncherAlreadyVisible) { // Launcher is visible, but might be about to stop. Thus, if we prepare recents // now, it might get overridden by moveToRestState() in onStop(). To avoid this, // wait until the next gesture (and possibly launcher) starts. mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory); } else { initAnimFactory.run(); } } AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible, AbstractFloatingView.TYPE_LISTENER); if (mWasLauncherAlreadyVisible) { mStateCallback.setState(STATE_LAUNCHER_DRAWN); } else { TraceHelper.beginSection("WTS-init"); View dragLayer = activity.getDragLayer(); dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { @Override public void onDraw() { TraceHelper.endSection("WTS-init", "Launcher frame is drawn"); dragLayer.post(() -> dragLayer.getViewTreeObserver().removeOnDrawListener(this)); if (activity != mActivity) { return; } mStateCallback.setState(STATE_LAUNCHER_DRAWN); } }); } activity.getRootView().setOnApplyWindowInsetsListener(this); mStateCallback.setState(STATE_LAUNCHER_STARTED); } private void onLauncherPresentAndGestureStarted() { // Re-setup the recents UI when gesture starts, as the state could have been changed during // that time by a previous window transition. setupRecentsViewUi(); notifyGestureStartedAsync(); } private void setupRecentsViewUi() { if (mContinuingLastGesture) { updateSysUiFlags(mCurrentShift.value); return; } mRecentsView.onGestureAnimationStart(mRunningTaskId); } private void launcherFrameDrawn() { mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); } private void sendRemoteAnimationsToAnimationFactory() { mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); } private void initializeLauncherAnimationController() { buildAnimationController(); if (LatencyTrackerCompat.isEnabled(mContext)) { LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); } // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the // high-res thumbnail loader here once we are sure that we will end up in an overview state RecentsModel.INSTANCE.get(mContext).getThumbnailCache() .getHighResLoadingState().setVisible(true); } private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) { float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing); float interpolation = Math.min(1, offsetX / distanceToReachEdge); return TaskView.getCurveScaleForInterpolation(interpolation); } @Override public void onMotionPauseChanged(boolean isPaused) { setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION); } public void maybeUpdateRecentsAttachedState() { maybeUpdateRecentsAttachedState(true /* animate */); } /** * Determines whether to show or hide RecentsView. The window is always * synchronized with its corresponding TaskView in RecentsView, so if * RecentsView is shown, it will appear to be attached to the window. * * Note this method has no effect unless the navigation mode is NO_BUTTON. */ private void maybeUpdateRecentsAttachedState(boolean animate) { if (mMode != Mode.NO_BUTTON || mRecentsView == null) { return; } RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationWrapper.targetSet == null ? null : mRecentsAnimationWrapper.targetSet.findTask(mRunningTaskId); final boolean recentsAttachedToAppWindow; int runningTaskIndex = mRecentsView.getRunningTaskIndex(); if (mGestureEndTarget != null) { recentsAttachedToAppWindow = mGestureEndTarget.recentsAttachedToAppWindow; } else if (mContinuingLastGesture && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) { recentsAttachedToAppWindow = true; animate = false; } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) { // The window is going away so make sure recents is always visible in this case. recentsAttachedToAppWindow = true; animate = false; } else { recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask; if (animate) { // Only animate if an adjacent task view is visible on screen. TaskView adjacentTask1 = mRecentsView.getNextTaskView(); TaskView adjacentTask2 = mRecentsView.getPreviousTaskView(); float prevTranslationX = mRecentsView.getTranslationX(); mRecentsView.setTranslationX(0); animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT)) || (adjacentTask2 != null && adjacentTask2.getGlobalVisibleRect(TEMP_RECT)); mRecentsView.setTranslationX(prevTranslationX); } } mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate); } @Override public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) { mIsLikelyToStartNewTask = isLikelyToStartNewTask; maybeUpdateRecentsAttachedState(); } } @UiThread public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) { mAnimationFactory.setShelfState(shelfState, interpolator, duration); boolean wasShelfPeeking = mIsShelfPeeking; mIsShelfPeeking = shelfState == PEEK; if (mIsShelfPeeking != wasShelfPeeking) { maybeUpdateRecentsAttachedState(); } if (shelfState.shouldPreformHaptic) { performHapticFeedback(); } } private void buildAnimationController() { if (mGestureEndTarget == HOME || mHasLauncherTransitionControllerStarted) { // We don't want a new mLauncherTransitionController if mGestureEndTarget == HOME (it // has its own animation) or if we're already animating the current controller. return; } initTransitionEndpoints(mActivity.getDeviceProfile()); mAnimationFactory.createActivityController(mTransitionDragLength); } @Override public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { WindowInsets result = view.onApplyWindowInsets(windowInsets); buildAnimationController(); return result; } private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { mLauncherTransitionController = anim; mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor); mAnimationFactory.adjustActivityControllerInterpolators(); mLauncherTransitionController.dispatchOnStart(); updateLauncherTransitionProgress(); } @Override public Intent getLaunchIntent() { return mOverviewComponentObserver.getOverviewIntent(); } @Override public void updateFinalShift() { SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController(); if (controller != null) { applyTransformUnchecked(); updateSysUiFlags(mCurrentShift.value); } if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { if (mRecentsAnimationWrapper.getController() != null) { mLiveTileOverlay.update(mClipAnimationHelper.getCurrentRectWithInsets(), mClipAnimationHelper.getCurrentCornerRadius()); } } final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; if (passed != mPassedOverviewThreshold) { mPassedOverviewThreshold = passed; if (mMode != Mode.NO_BUTTON) { performHapticFeedback(); } } if (mLauncherTransitionController == null || mLauncherTransitionController .getAnimationPlayer().isStarted()) { return; } updateLauncherTransitionProgress(); } private void updateLauncherTransitionProgress() { if (mGestureEndTarget == HOME) { return; } // Normalize the progress to 0 to 1, as the animation controller will clamp it to that // anyway. The controller mimics the drag length factor by applying it to its interpolators. float progress = mCurrentShift.value / mDragLengthFactor; mLauncherTransitionController.setPlayFraction(progress); } /** * @param windowProgress 0 == app, 1 == overview */ private void updateSysUiFlags(float windowProgress) { if (mRecentsView != null) { TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen(); int centermostTaskFlags = centermostTask == null ? 0 : centermostTask.getThumbnail().getSysUiStatusNavFlags(); boolean useHomeScreenFlags = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; // We will handle the sysui flags based on the centermost task view. mRecentsAnimationWrapper.setWindowThresholdCrossed(centermostTaskFlags != 0 || useHomeScreenFlags); int sysuiFlags = useHomeScreenFlags ? 0 : centermostTaskFlags; mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags); } } @Override public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) { super.onRecentsAnimationStart(targetSet); TOUCH_INTERACTION_LOG.addLog("startRecentsAnimationCallback", targetSet.apps.length); setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); mPassedOverviewThreshold = false; } @Override public void onRecentsAnimationCanceled() { mRecentsAnimationWrapper.setController(null); mActivityInitListener.unregister(); setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); TOUCH_INTERACTION_LOG.addLog("cancelRecentsAnimation"); } @Override public void onGestureStarted() { notifyGestureStartedAsync(); setStateOnUiThread(STATE_GESTURE_STARTED); mGestureStarted = true; } /** * Notifies the launcher that the swipe gesture has started. This can be called multiple times. */ @UiThread private void notifyGestureStartedAsync() { final T curActivity = mActivity; if (curActivity != null) { // Once the gesture starts, we can no longer transition home through the button, so // reset the force override of the activity visibility mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); } } /** * Called as a result on ACTION_CANCEL to return the UI to the start state. */ @Override public void onGestureCancelled() { updateDisplacement(0); setStateOnUiThread(STATE_GESTURE_COMPLETED); mLogAction = Touch.SWIPE_NOOP; handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */); } /** * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen. * @param velocity The x and y components of the velocity when the gesture ends. * @param downPos The x and y value of where the gesture started. */ @Override public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) { float flingThreshold = mContext.getResources() .getDimension(R.dimen.quickstep_fling_threshold_velocity); boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; setStateOnUiThread(STATE_GESTURE_COMPLETED); mLogAction = isFling ? Touch.FLING : Touch.SWIPE; boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x); if (isVelocityVertical) { mLogDirection = velocity.y < 0 ? Direction.UP : Direction.DOWN; } else { mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT; } mDownPos = downPos; handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */); } @Override protected InputConsumer createNewInputProxyHandler() { endRunningWindowAnim(mGestureEndTarget == HOME /* cancel */); endLauncherTransitionController(); if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { // Hide the task view, if not already hidden setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha); } BaseDraggingActivity activity = mActivityControlHelper.getCreatedActivity(); return activity == null ? InputConsumer.NO_OP : new OverviewInputConsumer(activity, null, true); } private void endRunningWindowAnim(boolean cancel) { if (mRunningWindowAnim != null) { if (cancel) { mRunningWindowAnim.cancel(); } else { mRunningWindowAnim.end(); } } } private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling, boolean isCancel) { final GestureEndTarget endTarget; final boolean goingToNewTask; if (mRecentsView != null) { if (!mRecentsAnimationWrapper.hasTargets()) { // If there are no running tasks, then we can assume that this is a continuation of // the last gesture, but after the recents animation has finished goingToNewTask = true; } else { final int runningTaskIndex = mRecentsView.getRunningTaskIndex(); final int taskToLaunch = mRecentsView.getNextPage(); goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex; } } else { goingToNewTask = false; } final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; if (!isFling) { if (isCancel) { endTarget = LAST_TASK; } else if (mMode == Mode.NO_BUTTON) { if (mIsShelfPeeking) { endTarget = RECENTS; } else if (goingToNewTask) { endTarget = NEW_TASK; } else { endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME; } } else { endTarget = reachedOverviewThreshold && mGestureStarted ? RECENTS : goingToNewTask ? NEW_TASK : LAST_TASK; } } else { // If swiping at a diagonal, base end target on the faster velocity. boolean isSwipeUp = endVelocity < 0; boolean willGoToNewTaskOnSwipeUp = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity); if (mMode == Mode.NO_BUTTON && isSwipeUp && !willGoToNewTaskOnSwipeUp) { endTarget = HOME; } else if (mMode == Mode.NO_BUTTON && isSwipeUp && !mIsShelfPeeking) { // If swiping at a diagonal, base end target on the faster velocity. endTarget = NEW_TASK; } else if (isSwipeUp) { endTarget = !reachedOverviewThreshold && willGoToNewTaskOnSwipeUp ? NEW_TASK : RECENTS; } else { endTarget = goingToNewTask ? NEW_TASK : LAST_TASK; } } int stateFlags = OverviewInteractionState.INSTANCE.get(mActivity).getSystemUiStateFlags(); if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0 && (endTarget == RECENTS || endTarget == LAST_TASK)) { return LAST_TASK; } return endTarget; } @UiThread private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity, boolean isCancel) { PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000); long duration = MAX_SWIPE_DURATION; float currentShift = mCurrentShift.value; final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity, isFling, isCancel); float endShift = endTarget.endShift; final float startShift; Interpolator interpolator = DEACCEL; if (!isFling) { long expectedDuration = Math.abs(Math.round((endShift - currentShift) * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); startShift = currentShift; interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL; } else { startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor); float minFlingVelocity = mContext.getResources() .getDimension(R.dimen.quickstep_fling_min_velocity); if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { if (endTarget == RECENTS && mMode != Mode.NO_BUTTON) { Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams( startShift, endShift, endShift, endVelocity / 1000, mTransitionDragLength, mContext); endShift = overshoot.end; interpolator = overshoot.interpolator; duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION, MAX_SWIPE_DURATION); } else { float distanceToTravel = (endShift - currentShift) * mTransitionDragLength; // we want the page's snap velocity to approximately match the velocity at // which the user flings, so we scale the duration by a value near to the // derivative of the scroll interpolator at zero, ie. 2. long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y)); duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); if (endTarget == RECENTS) { interpolator = OVERSHOOT_1_2; } } } } if (endTarget.isLauncher) { mRecentsAnimationWrapper.enableInputProxy(); } if (endTarget == HOME) { setShelfState(ShelfAnimState.CANCEL, LINEAR, 0); duration = Math.max(MIN_OVERSHOOT_DURATION, duration); } else if (endTarget == RECENTS) { mLiveTileOverlay.startIconAnimation(); if (mRecentsView != null) { int nearestPage = mRecentsView.getPageNearestToCenterOfScreen(); if (mRecentsView.getNextPage() != nearestPage) { // We shouldn't really scroll to the next page when swiping up to recents. // Only allow settling on the next page if it's nearest to the center. mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration)); } if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) { mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION); } duration = Math.max(duration, mRecentsView.getScroller().getDuration()); } if (mMode == Mode.NO_BUTTON) { setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration); } } else if (endTarget == NEW_TASK || endTarget == LAST_TASK) { // Let RecentsView handle the scrolling to the task, which we launch in startNewTask() // or resumeLastTask(). if (mRecentsView != null) { duration = Math.max(duration, mRecentsView.getScroller().getDuration()); } } animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs); } private void doLogGesture(GestureEndTarget endTarget) { DeviceProfile dp = mDp; if (dp == null || mDownPos == null) { // We probably never received an animation controller, skip logging. return; } int pageIndex = endTarget == LAST_TASK ? LOG_NO_OP_PAGE_INDEX : mRecentsView.getNextPage(); UserEventDispatcher.newInstance(mContext).logStateChangeAction( mLogAction, mLogDirection, (int) mDownPos.x, (int) mDownPos.y, ContainerType.NAVBAR, ContainerType.APP, endTarget.containerType, pageIndex); } /** Animates to the given progress, where 0 is the current app and 1 is overview. */ @UiThread private void animateToProgress(float start, float end, long duration, Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration, interpolator, target, velocityPxPerMs)); } @UiThread private void animateToProgressInternal(float start, float end, long duration, Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { mGestureEndTarget = target; maybeUpdateRecentsAttachedState(); if (mGestureEndTarget == HOME) { HomeAnimationFactory homeAnimFactory; if (mActivity != null) { homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity); } else { homeAnimFactory = new HomeAnimationFactory() { @NonNull @Override public RectF getWindowTargetRect() { RectF fallbackTarget = new RectF(mClipAnimationHelper.getTargetRect()); Utilities.scaleRectFAboutCenter(fallbackTarget, 0.25f); return fallbackTarget; } @NonNull @Override public AnimatorPlaybackController createActivityAnimationToHome() { return AnimatorPlaybackController.wrap(new AnimatorSet(), duration); } }; mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, isPresent -> mRecentsView.startHome()); } RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory); windowAnim.addAnimatorListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { setStateOnUiThread(target.endState); } }); windowAnim.start(velocityPxPerMs); homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y); mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim); mLauncherTransitionController = null; } else { ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end); windowAnim.setDuration(duration).setInterpolator(interpolator); windowAnim.addUpdateListener(valueAnimator -> { if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) { // Views typically don't compute scroll when invisible as an optimization, // but in our case we need to since the window offset depends on the scroll. mRecentsView.computeScroll(); } }); windowAnim.addListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { if (target == NEW_TASK && mRecentsView != null && mRecentsView.getNextPage() == mRecentsView.getRunningTaskIndex()) { // We are about to launch the current running task, so use LAST_TASK state // instead of NEW_TASK. This could happen, for example, if our scroll is // aborted after we determined the target to be NEW_TASK. setStateOnUiThread(LAST_TASK.endState); } else { setStateOnUiThread(target.endState); } } }); windowAnim.start(); mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim); } // Always play the entire launcher animation when going home, since it is separate from // the animation that has been controlled thus far. if (mGestureEndTarget == HOME) { start = 0; } // We want to use the same interpolator as the window, but need to adjust it to // interpolate over the remaining progress (end - start). TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress( interpolator, start, end); if (mLauncherTransitionController == null) { return; } if (start == end || duration <= 0) { mLauncherTransitionController.dispatchSetInterpolator(t -> end); } else { mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator); mAnimationFactory.adjustActivityControllerInterpolators(); } mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration)); if (QUICKSTEP_SPRINGS.get()) { mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y); } mLauncherTransitionController.getAnimationPlayer().start(); mHasLauncherTransitionControllerStarted = true; } /** * Creates an animation that transforms the current app window into the home app. * @param startProgress The progress of {@link #mCurrentShift} to start the window from. * @param homeAnimationFactory The home animation factory. */ @Override protected RectFSpringAnim createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory) { RectFSpringAnim anim = super.createWindowAnimationToHome(startProgress, homeAnimationFactory); anim.addOnUpdateListener((r, p) -> updateSysUiFlags(Math.max(p, mCurrentShift.value))); anim.addAnimatorListener(new AnimationSuccessListener() { @Override public void onAnimationStart(Animator animation) { if (mActivity != null) { removeLiveTileOverlay(); } } @Override public void onAnimationSuccess(Animator animator) { if (mRecentsView != null) { mRecentsView.post(mRecentsView::resetTaskVisuals); } // Make sure recents is in its final state maybeUpdateRecentsAttachedState(false); mActivityControlHelper.onSwipeUpToHomeComplete(mActivity); } }); return anim; } @Override public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) { if (mGestureEndTarget != null) { sharedState.canGestureBeContinued = mGestureEndTarget.canBeContinued; sharedState.goingToLauncher = mGestureEndTarget.isLauncher; } if (sharedState.canGestureBeContinued) { cancelCurrentAnimation(sharedState); } else { reset(); } } @UiThread private void resumeLastTask() { mRecentsAnimationWrapper.finish(false /* toRecents */, null); TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", false); doLogGesture(LAST_TASK); reset(); } @UiThread private void startNewTask() { startNewTask(STATE_HANDLER_INVALIDATED, success -> { if (!success) { // We couldn't launch the task, so take user to overview so they can // decide what to do instead of staying in this broken state. endLauncherTransitionController(); updateSysUiFlags(1 /* windowProgress == overview */); } doLogGesture(NEW_TASK); }); } private void reset() { setStateOnUiThread(STATE_HANDLER_INVALIDATED); } /** * Cancels any running animation so that the active target can be overriden by a new swipe * handle (in case of quick switch). */ private void cancelCurrentAnimation(SwipeSharedState sharedState) { mCanceled = true; mCurrentShift.cancelAnimation(); if (mLauncherTransitionController != null && mLauncherTransitionController .getAnimationPlayer().isStarted()) { mLauncherTransitionController.getAnimationPlayer().cancel(); } if (mFinishingRecentsAnimationForNewTaskId != -1) { // If we are canceling mid-starting a new task, switch to the screenshot since the // recents animation has finished switchToScreenshot(); TaskView newRunningTaskView = mRecentsView.getTaskView( mFinishingRecentsAnimationForNewTaskId); int newRunningTaskId = newRunningTaskView != null ? newRunningTaskView.getTask().key.id : -1; mRecentsView.setCurrentTask(newRunningTaskId); sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId); } } private void invalidateHandler() { endRunningWindowAnim(false /* cancel */); if (mGestureEndCallback != null) { mGestureEndCallback.run(); } mActivityInitListener.unregister(); mTaskSnapshot = null; } private void invalidateHandlerWithLauncher() { endLauncherTransitionController(); mRecentsView.onGestureAnimationEnd(); mActivity.getRootView().setOnApplyWindowInsetsListener(null); removeLiveTileOverlay(); } private void endLauncherTransitionController() { setShelfState(ShelfAnimState.CANCEL, LINEAR, 0); if (mLauncherTransitionController != null) { mLauncherTransitionController.getAnimationPlayer().end(); mLauncherTransitionController = null; } } private void notifyTransitionCancelled() { mAnimationFactory.onTransitionCancelled(); } private void resetStateForAnimationCancel() { boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted; mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible); // Leave the pending invisible flag, as it may be used by wallpaper open animation. mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); } private void switchToScreenshot() { if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); } else if (!mRecentsAnimationWrapper.hasTargets()) { // If there are no targets, then we don't need to capture anything setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); } else { boolean finishTransitionPosted = false; SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController(); if (controller != null) { // Update the screenshot of the task if (mTaskSnapshot == null) { mTaskSnapshot = controller.screenshotTask(mRunningTaskId); } final TaskView taskView; if (mGestureEndTarget == HOME) { // Capture the screenshot before finishing the transition to home to ensure it's // taken in the correct orientation, but no need to update the thumbnail. taskView = null; } else { taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); } if (taskView != null && !mCanceled) { // Defer finishing the animation until the next launcher frame with the // new thumbnail finishTransitionPosted = new WindowCallbacksCompat(taskView) { // The number of frames to defer until we actually finish the animation private int mDeferFrameCount = 2; @Override public void onPostDraw(Canvas canvas) { // If we were cancelled after this was attached, do not update // the state. if (mCanceled) { detach(); return; } if (mDeferFrameCount > 0) { mDeferFrameCount--; // Workaround, detach and reattach to invalidate the root node for // another draw detach(); attach(); taskView.invalidate(); return; } setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); detach(); } }.attach(); } } if (!finishTransitionPosted) { // If we haven't posted a draw callback, set the state immediately. RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, ENTER); setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, EXIT); } } } private void finishCurrentTransitionToRecents() { if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); } else if (!mRecentsAnimationWrapper.hasTargets()) { // If there are no targets, then there is nothing to finish setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); } else { synchronized (mRecentsAnimationWrapper) { mRecentsAnimationWrapper.finish(true /* toRecents */, () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } } TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true); } private void finishCurrentTransitionToHome() { synchronized (mRecentsAnimationWrapper) { mRecentsAnimationWrapper.finish(true /* toRecents */, () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED), true /* sendUserLeaveHint */); } TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true); doLogGesture(HOME); } private void setupLauncherUiAfterSwipeUpToRecentsAnimation() { endLauncherTransitionController(); mActivityControlHelper.onSwipeUpToRecentsComplete(mActivity); mRecentsAnimationWrapper.setDeferCancelUntilNextTransition(true /* defer */, true /* screenshot */); mRecentsView.onSwipeUpAnimationSuccess(); RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG); doLogGesture(RECENTS); reset(); } private void setTargetAlphaProvider(TargetAlphaProvider provider) { mClipAnimationHelper.setTaskAlphaCallback(provider); updateFinalShift(); } private synchronized void addLiveTileOverlay() { if (!mLiveTileOverlayAttached) { mActivity.getRootView().getOverlay().add(mLiveTileOverlay); mRecentsView.setLiveTileOverlay(mLiveTileOverlay); mLiveTileOverlayAttached = true; } } private synchronized void removeLiveTileOverlay() { if (mLiveTileOverlayAttached) { mActivity.getRootView().getOverlay().remove(mLiveTileOverlay); mRecentsView.setLiveTileOverlay(null); mLiveTileOverlayAttached = false; } } public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, float expectedAlpha) { if (!isNotInRecents(app)) { return 0; } return expectedAlpha; } private static boolean isNotInRecents(RemoteAnimationTargetCompat app) { return app.isNotInRecents || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; } }