/* * 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.Utilities.SINGLE_FRAME_MS; import static com.android.launcher3.Utilities.postAsyncCallback; 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.RaceConditionTracker.ENTER; import static com.android.launcher3.util.RaceConditionTracker.EXIT; import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION; import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_FROM_APP_START_DURATION; import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.os.UserHandle; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver.OnDrawListener; import android.view.WindowManager; import android.view.animation.Interpolator; import androidx.annotation.AnyThread; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; 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.config.FeatureFlags; 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.MultiValueAlpha; import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.launcher3.util.RaceConditionTracker; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.ActivityControlHelper.ActivityInitListener; import com.android.quickstep.ActivityControlHelper.AnimationFactory; import com.android.quickstep.ActivityControlHelper.LayoutListener; import com.android.quickstep.TouchConsumer.InteractionType; import com.android.quickstep.TouchInteractionService.OverviewTouchConsumer; import com.android.quickstep.util.ClipAnimationHelper; import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.util.TransformedRect; 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.ActivityManagerWrapper; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.LatencyTrackerCompat; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; import com.android.systemui.shared.system.WindowCallbacksCompat; import java.util.StringJoiner; import java.util.function.BiFunction; @TargetApi(Build.VERSION_CODES.O) public class WindowTransformSwipeHandler { private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName(); private static final boolean DEBUG_STATES = false; // Launcher UI related states private static final int STATE_LAUNCHER_PRESENT = 1 << 0; private static final int STATE_LAUNCHER_STARTED = 1 << 1; private static final int STATE_LAUNCHER_DRAWN = 1 << 2; private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 3; // Internal initialization states private static final int STATE_APP_CONTROLLER_RECEIVED = 1 << 4; // Interaction finish states private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5; private static final int STATE_SCALED_CONTROLLER_LAST_TASK = 1 << 6; private static final int STATE_HANDLER_INVALIDATED = 1 << 7; private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 8; private static final int STATE_GESTURE_STARTED_QUICKSCRUB = 1 << 9; private static final int STATE_GESTURE_CANCELLED = 1 << 10; private static final int STATE_GESTURE_COMPLETED = 1 << 11; // States for quick switch/scrub private static final int STATE_CURRENT_TASK_FINISHED = 1 << 12; private static final int STATE_QUICK_SCRUB_START = 1 << 13; private static final int STATE_QUICK_SCRUB_END = 1 << 14; private static final int STATE_CAPTURE_SCREENSHOT = 1 << 15; private static final int STATE_SCREENSHOT_CAPTURED = 1 << 16; private static final int STATE_SCREENSHOT_VIEW_SHOWN = 1 << 17; private static final int STATE_RESUME_LAST_TASK = 1 << 18; private static final int STATE_START_NEW_TASK = 1 << 19; private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 20; private static final int LAUNCHER_UI_STATES = STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED; private static final int LONG_SWIPE_ENTER_STATE = STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED; private static final int LONG_SWIPE_START_STATE = STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED; private static final int QUICK_SCRUB_START_UI_STATE = STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START | STATE_APP_CONTROLLER_RECEIVED; // For debugging, keep in sync with above states private static final String[] STATES = new String[] { "STATE_LAUNCHER_PRESENT", "STATE_LAUNCHER_STARTED", "STATE_LAUNCHER_DRAWN", "STATE_ACTIVITY_MULTIPLIER_COMPLETE", "STATE_APP_CONTROLLER_RECEIVED", "STATE_SCALED_CONTROLLER_RECENTS", "STATE_SCALED_CONTROLLER_LAST_TASK", "STATE_HANDLER_INVALIDATED", "STATE_GESTURE_STARTED_QUICKSTEP", "STATE_GESTURE_STARTED_QUICKSCRUB", "STATE_GESTURE_CANCELLED", "STATE_GESTURE_COMPLETED", "STATE_CURRENT_TASK_FINISHED", "STATE_QUICK_SCRUB_START", "STATE_QUICK_SCRUB_END", "STATE_CAPTURE_SCREENSHOT", "STATE_SCREENSHOT_CAPTURED", "STATE_SCREENSHOT_VIEW_SHOWN", "STATE_RESUME_LAST_TASK", "STATE_START_NEW_TASK", "STATE_ASSIST_DATA_RECEIVED", }; 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"; private final ClipAnimationHelper mClipAnimationHelper; private final ClipAnimationHelper.TransformParams mTransformParams; protected Runnable mGestureEndCallback; protected boolean mIsGoingToRecents; private DeviceProfile mDp; private int mTransitionDragLength; // Shift in the range of [0, 1]. // 0 => preview snapShot is completely visible, and hotseat is completely translated down // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely // visible. private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); private boolean mDispatchedDownEvent; // To avoid UI jump when gesture is started, we offset the animation by the threshold. private float mShiftAtGestureStart = 0; private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); // An increasing identifier per single instance of OtherActivityTouchConsumer. Generally one // instance of OtherActivityTouchConsumer will only have one swipe handle, but sometimes we can // end up with multiple handlers if we get recents command in the middle of a swipe gesture. // This is used to match the corresponding activity manager callbacks in // OtherActivityTouchConsumer public final int id; private final Context mContext; private final ActivityControlHelper mActivityControlHelper; private final ActivityInitListener mActivityInitListener; private final TouchInteractionLog mTouchInteractionLog; private final int mRunningTaskId; private final RunningTaskInfo mRunningTaskInfo; private ThumbnailData mTaskSnapshot; private MultiStateCallback mStateCallback; private AnimatorPlaybackController mLauncherTransitionController; private T mActivity; private LayoutListener mLayoutListener; private RecentsView mRecentsView; private SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier; private QuickScrubController mQuickScrubController; private AnimationFactory mAnimationFactory = (t, i) -> { }; private Runnable mLauncherDrawnCallback; private boolean mWasLauncherAlreadyVisible; private boolean mPassedOverviewThreshold; private boolean mGestureStarted; private int mLogAction = Touch.SWIPE; private float mCurrentQuickScrubProgress; private boolean mQuickScrubBlocked; private @InteractionType int mInteractionType = INTERACTION_NORMAL; private final RecentsAnimationWrapper mRecentsAnimationWrapper; private final long mTouchTimeMs; private long mLauncherFrameDrawnTime; private boolean mBgLongSwipeMode = false; private boolean mUiLongSwipeMode = false; private float mLongSwipeDisplacement = 0; private LongSwipeHelper mLongSwipeController; private Bundle mAssistData; WindowTransformSwipeHandler(int id, RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs, ActivityControlHelper controller, InputConsumerController inputConsumer, TouchInteractionLog touchInteractionLog) { this.id = id; mContext = context; mRunningTaskInfo = runningTaskInfo; mRunningTaskId = runningTaskInfo.id; mTouchTimeMs = touchTimeMs; mActivityControlHelper = controller; mActivityInitListener = mActivityControlHelper .createActivityInitListener(this::onActivityInit); mTouchInteractionLog = touchInteractionLog; mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer, this::createNewTouchProxyHandler); mClipAnimationHelper = new ClipAnimationHelper(context); mTransformParams = new ClipAnimationHelper.TransformParams(); initStateCallbacks(); } private void initStateCallbacks() { mStateCallback = new MultiStateCallback() { @Override public void setState(int stateFlag) { debugNewState(stateFlag); super.setState(stateFlag); } }; // Re-setup the recents UI when gesture starts, as the state could have been changed during // that time by a previous window transition. mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_GESTURE_STARTED_QUICKSTEP, this::setupRecentsViewUi); mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSCRUB, this::initializeLauncherAnimationController); mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSTEP, this::initializeLauncherAnimationController); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, this::launcherFrameDrawn); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSTEP, this::notifyGestureStartedAsync); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSCRUB, this::notifyGestureStartedAsync); 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_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_LAST_TASK, this::resumeLastTaskForQuickstep); mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, this::resumeLastTask); mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_APP_CONTROLLER_RECEIVED, this::startNewTask); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_CAPTURE_SCREENSHOT, this::switchToScreenshot); mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED | STATE_SCALED_CONTROLLER_RECENTS, this::finishCurrentTransitionToRecents); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED | STATE_GESTURE_STARTED_QUICKSTEP, this::setupLauncherUiAfterSwipeUpAnimation); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED | STATE_GESTURE_STARTED_QUICKSTEP | STATE_ASSIST_DATA_RECEIVED, this::preloadAssistData); mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, this::invalidateHandlerWithLauncher); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED | STATE_SCALED_CONTROLLER_LAST_TASK, this::notifyTransitionCancelled); mStateCallback.addCallback(QUICK_SCRUB_START_UI_STATE, this::onQuickScrubStartUi); mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub); mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub); mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter); mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart); 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)); } } private void executeOnUiThread(Runnable action) { if (Looper.myLooper() == mMainThreadHandler.getLooper()) { action.run(); } else { postAsyncCallback(mMainThreadHandler, action); } } private void setStateOnUiThread(int stateFlag) { if (Looper.myLooper() == mMainThreadHandler.getLooper()) { mStateCallback.setState(stateFlag); } else { postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); } } private void initTransitionEndpoints(DeviceProfile dp) { mDp = dp; TransformedRect tempRect = new TransformedRect(); mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength( dp, mContext, mInteractionType, tempRect); mClipAnimationHelper.updateTargetRect(tempRect); } private long getFadeInDuration() { if (mCurrentShift.getCurrentAnimation() != null) { ObjectAnimator anim = mCurrentShift.getCurrentAnimation(); long theirDuration = anim.getDuration() - anim.getCurrentPlayTime(); // TODO: Find a better heuristic return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION)); } else { return MAX_SWIPE_DURATION; } } public void initWhenReady() { mActivityInitListener.register(); } private 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); mLayoutListener.setHandler(null); } 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(); SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, (applier) -> { mSyncTransactionApplier = applier; }); mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { if (!mBgLongSwipeMode) { updateFinalShift(); } }); mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper); mRecentsView.setClipAnimationHelper(mClipAnimationHelper); mQuickScrubController = mRecentsView.getQuickScrubController(); mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity); mStateCallback.setState(STATE_LAUNCHER_PRESENT); if (alreadyOnHome) { onLauncherStart(activity); } else { activity.setOnStartCallback(this::onLauncherStart); } return true; } private void onLauncherStart(final T activity) { if (mActivity != activity) { return; } if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { return; } mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated); AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible); if (mWasLauncherAlreadyVisible) { mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN); } else { TraceHelper.beginSection("WTS-init"); View dragLayer = activity.getDragLayer(); mActivityControlHelper.getAlphaProperty(activity).setValue(0); 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); } }); } setupRecentsViewUi(); mLayoutListener.open(); mStateCallback.setState(STATE_LAUNCHER_STARTED); } private void setupRecentsViewUi() { mRecentsView.setEnableDrawingLiveTile(false); mRecentsView.showTask(mRunningTaskId); mRecentsView.setRunningTaskHidden(true); mRecentsView.setRunningTaskIconScaledDown(true); } public void setLauncherOnDrawCallback(Runnable callback) { mLauncherDrawnCallback = callback; } private void launcherFrameDrawn() { AlphaProperty property = mActivityControlHelper.getAlphaProperty(mActivity); if (property.getValue() < 1) { if (mGestureStarted) { final MultiStateCallback callback = mStateCallback; ObjectAnimator animator = ObjectAnimator.ofFloat( property, MultiValueAlpha.VALUE, 1); animator.setDuration(getFadeInDuration()).addListener( new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); } }); animator.start(); } else { property.setValue(1); mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); } } if (mLauncherDrawnCallback != null) { mLauncherDrawnCallback.run(); } mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); } private void sendRemoteAnimationsToAnimationFactory() { mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); } private void initializeLauncherAnimationController() { mLayoutListener.setHandler(this); buildAnimationController(); if (LatencyTrackerCompat.isEnabled(mContext)) { LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); } // This method is only called when STATE_GESTURE_STARTED_QUICKSTEP/ // STATE_GESTURE_STARTED_QUICKSCRUB 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 void shiftAnimationDestinationForQuickscrub() { TransformedRect tempRect = new TransformedRect(); mActivityControlHelper .getSwipeUpDestinationAndLength(mDp, mContext, mInteractionType, tempRect); mClipAnimationHelper.updateTargetRect(tempRect); float offsetY = mActivityControlHelper.getTranslationYForQuickScrub(tempRect, mDp, mContext); float scale, offsetX; Resources res = mContext.getResources(); if (ActivityManagerWrapper.getInstance().getRecentTasks(2, UserHandle.myUserId()).size() < 2) { // There are not enough tasks, we don't need to shift offsetX = 0; scale = 1; } else { offsetX = res.getDimensionPixelSize(R.dimen.recents_page_spacing) + tempRect.rect.width(); scale = getTaskCurveScaleForOffsetX(offsetX, tempRect.rect.width()); } mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY, QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR); } 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); } @WorkerThread public void dispatchMotionEventToRecentsView(MotionEvent event) { if (mRecentsView == null) { return; } // Pass the motion events to RecentsView to allow scrolling during swipe up. if (mDispatchedDownEvent) { mRecentsView.dispatchTouchEvent(event); } else { // The first event we dispatch should be ACTION_DOWN. mDispatchedDownEvent = true; MotionEvent downEvent = MotionEvent.obtain(event); downEvent.setAction(MotionEvent.ACTION_DOWN); int flags = downEvent.getEdgeFlags(); downEvent.setEdgeFlags(flags | TouchInteractionService.EDGE_NAV_BAR); mRecentsView.dispatchTouchEvent(downEvent); downEvent.recycle(); } } @WorkerThread public void updateDisplacement(float displacement) { // We are moving in the negative x/y direction displacement = -displacement; if (displacement > mTransitionDragLength && mTransitionDragLength > 0) { mCurrentShift.updateValue(1); if (!mBgLongSwipeMode) { mBgLongSwipeMode = true; executeOnUiThread(this::onLongSwipeEnabledUi); } mLongSwipeDisplacement = displacement - mTransitionDragLength; executeOnUiThread(this::onLongSwipeDisplacementUpdated); } else { if (mBgLongSwipeMode) { mBgLongSwipeMode = false; executeOnUiThread(this::onLongSwipeDisabledUi); } float translation = Math.max(displacement, 0); float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; mCurrentShift.updateValue(shift); } } /** * Called by {@link #mLayoutListener} when launcher layout changes */ public void buildAnimationController() { initTransitionEndpoints(mActivity.getDeviceProfile()); mAnimationFactory.createActivityController(mTransitionDragLength, mInteractionType); } private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { mLauncherTransitionController = anim; mLauncherTransitionController.dispatchOnStart(); updateLauncherTransitionProgress(); } @WorkerThread private void updateFinalShift() { float shift = mCurrentShift.value; RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); if (controller != null) { float offsetX = 0; if (mRecentsView != null && mInteractionType == INTERACTION_NORMAL) { int startScroll = mRecentsView.getScrollForPage(mRecentsView.indexOfChild( mRecentsView.getRunningTaskView())); offsetX = startScroll - mRecentsView.getScrollX(); offsetX *= mRecentsView.getScaleX(); } float offsetScale = getTaskCurveScaleForOffsetX(offsetX, mClipAnimationHelper.getTargetRect().width()); SyncRtSurfaceTransactionApplierCompat syncTransactionApplier = Looper.myLooper() == mMainThreadHandler.getLooper() ? mSyncTransactionApplier : null; mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale) .setSyncTransactionApplier(syncTransactionApplier); mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, mTransformParams); boolean passedThreshold = shift > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold); if (mActivityControlHelper.shouldMinimizeSplitScreen()) { mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold); } } executeOnUiThread(this::updateFinalShiftUi); } private void updateFinalShiftUi() { if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { if (mRecentsAnimationWrapper.getController() != null && mLayoutListener != null) { mLayoutListener.open(); mLayoutListener.update(mCurrentShift.value > 1, mUiLongSwipeMode, mClipAnimationHelper.getCurrentRectWithInsets(), mClipAnimationHelper.getCurrentCornerRadius()); } } final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; if (passed != mPassedOverviewThreshold) { mPassedOverviewThreshold = passed; if (mInteractionType == INTERACTION_NORMAL && mRecentsView != null) { mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); } } // Update insets of the adjacent tasks, as we might switch to them. int runningTaskIndex = mRecentsView == null ? -1 : mRecentsView.getRunningTaskIndex(); if (mInteractionType == INTERACTION_NORMAL && runningTaskIndex >= 0) { TaskView nextTaskView = mRecentsView.getTaskViewAt(runningTaskIndex + 1); TaskView prevTaskView = mRecentsView.getTaskViewAt(runningTaskIndex - 1); if (nextTaskView != null) { nextTaskView.setFullscreenProgress(1 - mCurrentShift.value); } if (prevTaskView != null) { prevTaskView.setFullscreenProgress(1 - mCurrentShift.value); } } if (mLauncherTransitionController == null || mLauncherTransitionController .getAnimationPlayer().isStarted()) { return; } updateLauncherTransitionProgress(); } private void updateLauncherTransitionProgress() { float progress = mCurrentShift.value; mLauncherTransitionController.setPlayFraction( progress <= mShiftAtGestureStart || mShiftAtGestureStart >= 1 ? 0 : (progress - mShiftAtGestureStart) / (1 - mShiftAtGestureStart)); } public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller, RemoteAnimationTargetSet targets, Rect homeContentInsets, Rect minimizedHomeBounds) { DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext); final Rect overviewStackBounds; RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId); if (minimizedHomeBounds != null && runningTaskTarget != null) { overviewStackBounds = mActivityControlHelper .getOverviewWindowBounds(minimizedHomeBounds, runningTaskTarget); dp = dp.getMultiWindowProfile(mContext, new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height())); dp.updateInsets(homeContentInsets); } else { if (mActivity != null) { int loc[] = new int[2]; View rootView = mActivity.getRootView(); rootView.getLocationOnScreen(loc); overviewStackBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight()); } else { overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); } // If we are not in multi-window mode, home insets should be same as system insets. dp = dp.copy(mContext); dp.updateInsets(homeContentInsets); } dp.updateIsSeascape(mContext.getSystemService(WindowManager.class)); if (runningTaskTarget != null) { mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); } mClipAnimationHelper.prepareAnimation(false /* isOpening */); initTransitionEndpoints(dp); mRecentsAnimationWrapper.setController(controller, targets); mTouchInteractionLog.startRecentsAnimationCallback(targets.apps.length); setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); mPassedOverviewThreshold = false; } public void onRecentsAnimationCanceled() { mRecentsAnimationWrapper.setController(null, null); mActivityInitListener.unregister(); setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); mTouchInteractionLog.cancelRecentsAnimation(); } public void onGestureStarted() { notifyGestureStartedAsync(); mShiftAtGestureStart = mCurrentShift.value; setStateOnUiThread(mInteractionType == INTERACTION_NORMAL ? STATE_GESTURE_STARTED_QUICKSTEP : STATE_GESTURE_STARTED_QUICKSCRUB); mGestureStarted = true; mRecentsAnimationWrapper.hideCurrentInputMethod(); mRecentsAnimationWrapper.enableInputConsumer(); } /** * Notifies the launcher that the swipe gesture has started. This can be called multiple times * on both background and UI threads */ @AnyThread 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); } } @WorkerThread public void onGestureEnded(float endVelocity, float velocityX) { 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; if (mBgLongSwipeMode) { executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling, velocityX)); } else { handleNormalGestureEnd(endVelocity, isFling, velocityX); } } @UiThread private TouchConsumer createNewTouchProxyHandler() { mCurrentShift.finishAnimation(); if (mLauncherTransitionController != null) { mLauncherTransitionController.getAnimationPlayer().end(); } if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { // Hide the task view, if not already hidden setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha); } return OverviewTouchConsumer.newInstance(mActivityControlHelper, true, mTouchInteractionLog); } private void handleNormalGestureEnd(float endVelocity, boolean isFling, float velocityX) { float velocityPxPerMs = endVelocity / 1000; float velocityXPxPerMs = velocityX / 1000; long duration = MAX_SWIPE_DURATION; float currentShift = mCurrentShift.value; final boolean goingToRecents; float endShift; final float startShift; Interpolator interpolator = DEACCEL; final int nextPage = mRecentsView != null ? mRecentsView.getNextPage() : -1; final int runningTaskIndex = mRecentsView != null ? mRecentsView.getRunningTaskIndex() : -1; boolean goingToNewTask = mRecentsView != null && nextPage != runningTaskIndex; final boolean reachedOverviewThreshold = currentShift >= MIN_PROGRESS_FOR_OVERVIEW; if (!isFling) { goingToRecents = reachedOverviewThreshold && mGestureStarted; endShift = goingToRecents ? 1 : 0; long expectedDuration = Math.abs(Math.round((endShift - currentShift) * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); startShift = currentShift; interpolator = goingToRecents ? OVERSHOOT_1_2 : DEACCEL; } else { // If user scrolled to a new task, only go to recents if they already passed // the overview threshold. Otherwise, we'll snap to the new task and launch it. goingToRecents = endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold); endShift = goingToRecents ? 1 : 0; startShift = Utilities.boundToRange(currentShift - velocityPxPerMs * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1); float minFlingVelocity = mContext.getResources() .getDimension(R.dimen.quickstep_fling_min_velocity); if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { if (goingToRecents) { Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams( startShift, endShift, endShift, velocityPxPerMs, mTransitionDragLength); 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)); duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); } } } if (goingToRecents) { mRecentsAnimationWrapper.enableTouchProxy(); } else if (goingToNewTask) { // We aren't goingToRecents, and user scrolled/flung to a new task; snap to the closest // task in that direction and launch it (in startNewTask()). int taskToLaunch = runningTaskIndex + (nextPage > runningTaskIndex ? 1 : - 1); if (taskToLaunch >= mRecentsView.getTaskViewCount()) { // Scrolled to Clear all button, snap back to current task and resume it. mRecentsView.snapToPage(runningTaskIndex, Math.toIntExact(duration)); goingToNewTask = false; } else { float distance = Math.abs(mRecentsView.getScrollForPage(taskToLaunch) - mRecentsView.getScrollX()); int durationX = (int) Math.abs(distance / velocityXPxPerMs); if (durationX > MAX_SWIPE_DURATION) { durationX = Math.toIntExact(MAX_SWIPE_DURATION); } interpolator = Interpolators.scrollInterpolatorForVelocity(velocityXPxPerMs); mRecentsView.snapToPage(taskToLaunch, durationX, interpolator); duration = Math.max(duration, durationX); } } animateToProgress(startShift, endShift, duration, interpolator, goingToRecents, goingToNewTask, velocityPxPerMs); } private void doLogGesture(boolean toLauncher) { DeviceProfile dp = mDp; if (dp == null) { // We probably never received an animation controller, skip logging. return; } final int direction; if (dp.isVerticalBarLayout()) { direction = (dp.isSeascape() ^ toLauncher) ? Direction.LEFT : Direction.RIGHT; } else { direction = toLauncher ? Direction.UP : Direction.DOWN; } int dstContainerType = toLauncher ? ContainerType.TASKSWITCHER : ContainerType.APP; UserEventDispatcher.newInstance(mContext).logStateChangeAction( mLogAction, direction, ContainerType.NAVBAR, ContainerType.APP, dstContainerType, 0); } /** Animates to the given progress, where 0 is the current app and 1 is overview. */ private void animateToProgress(float start, float end, long duration, Interpolator interpolator, boolean goingToRecents, boolean goingToNewTask, float velocityPxPerMs) { mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration, interpolator, goingToRecents, goingToNewTask, velocityPxPerMs)); } private void animateToProgressInternal(float start, float end, long duration, Interpolator interpolator, boolean goingToRecents, boolean goingToNewTask, float velocityPxPerMs) { mIsGoingToRecents = goingToRecents; ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration); anim.setInterpolator(interpolator); anim.addListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { int recentsState = STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT | STATE_SCREENSHOT_VIEW_SHOWN; setStateOnUiThread(mIsGoingToRecents ? recentsState : goingToNewTask ? STATE_START_NEW_TASK : STATE_SCALED_CONTROLLER_LAST_TASK); } }); anim.start(); long startMillis = SystemClock.uptimeMillis(); executeOnUiThread(() -> { // Animate the launcher components at the same time as the window, always on UI thread. if (mLauncherTransitionController == null) { return; } if (start == end || duration <= 0) { mLauncherTransitionController.dispatchSetInterpolator(t -> end); mLauncherTransitionController.getAnimationPlayer().end(); } else { // Adjust start progress and duration in case we are on a different thread. long elapsedMillis = SystemClock.uptimeMillis() - startMillis; elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration); float elapsedProgress = (float) elapsedMillis / duration; float adjustedStart = Utilities.mapRange(elapsedProgress, start, end); long adjustedDuration = duration - elapsedMillis; // We want to use the same interpolator as the window, but need to adjust it to // interpolate over the remaining progress (end - start). mLauncherTransitionController.dispatchSetInterpolator(Interpolators.mapToProgress( interpolator, adjustedStart, end)); mLauncherTransitionController.getAnimationPlayer().setDuration(adjustedDuration); if (QUICKSTEP_SPRINGS.get()) { mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs); } else { mLauncherTransitionController.getAnimationPlayer().start(); } } }); } @UiThread private void resumeLastTaskForQuickstep() { setStateOnUiThread(STATE_RESUME_LAST_TASK); doLogGesture(false /* toLauncher */); reset(); } @UiThread private void resumeLastTask() { mRecentsAnimationWrapper.finish(false /* toRecents */, null); mTouchInteractionLog.finishRecentsAnimation(false); } @UiThread private void startNewTask() { // Launch the task user scrolled to (mRecentsView.getNextPage()). mRecentsAnimationWrapper.finish(true /* toRecents */, () -> { mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false, result -> setStateOnUiThread(STATE_HANDLER_INVALIDATED), mMainThreadHandler); }); mTouchInteractionLog.finishRecentsAnimation(false); doLogGesture(false /* toLauncher */); } public void reset() { if (mInteractionType != INTERACTION_QUICK_SCRUB) { // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be // invalidated after the quick scrub ends setStateOnUiThread(STATE_HANDLER_INVALIDATED); } } private void invalidateHandler() { mCurrentShift.finishAnimation(); if (mGestureEndCallback != null) { mGestureEndCallback.run(); } mActivityInitListener.unregister(); mTaskSnapshot = null; if (mRecentsView != null) { mRecentsView.setOnScrollChangeListener(null); } } private void invalidateHandlerWithLauncher() { mLauncherTransitionController = null; mLayoutListener.finish(); mActivityControlHelper.getAlphaProperty(mActivity).setValue(1); mRecentsView.setRunningTaskIconScaledDown(false); mQuickScrubController.cancelActiveQuickscrub(); } 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); } public void layoutListenerClosed() { mRecentsView.setRunningTaskHidden(false); if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) { mLauncherTransitionController.setPlayFraction(1); } mRecentsView.setEnableDrawingLiveTile(true); } private void switchToScreenshot() { if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); } else { boolean finishTransitionPosted = false; RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); if (controller != null) { // Update the screenshot of the task if (mTaskSnapshot == null) { mTaskSnapshot = controller.screenshotTask(mRunningTaskId); } TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); if (taskView != null) { // 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 (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 { synchronized (mRecentsAnimationWrapper) { mRecentsAnimationWrapper.finish(true /* toRecents */, () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } } mTouchInteractionLog.finishRecentsAnimation(true); } private void setupLauncherUiAfterSwipeUpAnimation() { if (mLauncherTransitionController != null) { mLauncherTransitionController.getAnimationPlayer().end(); mLauncherTransitionController = null; } mActivityControlHelper.onSwipeUpComplete(mActivity); // Animate the first icon. mRecentsView.animateUpRunningTaskIconScale(); mRecentsView.setSwipeDownShouldLaunchApp(true); RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG); doLogGesture(true /* toLauncher */); reset(); } public void onQuickScrubStart() { if (mInteractionType != INTERACTION_NORMAL) { throw new IllegalArgumentException( "Can't change interaction type from " + mInteractionType); } mInteractionType = INTERACTION_QUICK_SCRUB; mRecentsAnimationWrapper.runOnInit(this::shiftAnimationDestinationForQuickscrub); setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED); // Start the window animation without waiting for launcher. long duration = FeatureFlags.QUICK_SWITCH.get() ? QUICK_SWITCH_FROM_APP_START_DURATION : QUICK_SCRUB_FROM_APP_START_DURATION; animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToRecents */, false /* goingToNewTask */, 1f); } private void onQuickScrubStartUi() { if (!mQuickScrubController.prepareQuickScrub(TAG, FeatureFlags.QUICK_SWITCH.get())) { mQuickScrubBlocked = true; setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED); return; } if (mLauncherTransitionController != null) { mLauncherTransitionController.getAnimationPlayer().end(); mLauncherTransitionController = null; } mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false, mTouchInteractionLog); // Inform the last progress in case we skipped before. mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress); } private void onFinishedTransitionToQuickScrub() { if (mQuickScrubBlocked) { return; } mLayoutListener.finish(); mQuickScrubController.onFinishedTransitionToQuickScrub(); mRecentsView.animateUpRunningTaskIconScale(); if (mQuickScrubController.isQuickSwitch()) { // Adjust the running task so that it is centered and fills the screen. TaskView runningTask = mRecentsView.getRunningTaskView(); if (runningTask != null) { float insetHeight = mDp.heightPx - mDp.getInsets().top - mDp.getInsets().bottom; // Usually insetDiff will be 0, unless we allow apps to draw under the insets. In // that case (insetDiff != 0), we need to center in the system-specified available // height rather than launcher's inset height by adding half the insetDiff. float insetDiff = mDp.availableHeightPx - insetHeight; float topMargin = mActivity.getResources().getDimension( R.dimen.task_thumbnail_half_top_margin); runningTask.setTranslationY((insetDiff / 2 - topMargin) / mRecentsView.getScaleX()); } } RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG); } public void onQuickScrubProgress(float progress) { mCurrentQuickScrubProgress = progress; if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null || mQuickScrubBlocked || !mStateCallback.hasStates(QUICK_SCRUB_START_UI_STATE)) { return; } mQuickScrubController.onQuickScrubProgress(progress); } public void onQuickScrubEnd() { setStateOnUiThread(STATE_QUICK_SCRUB_END); } private void switchToFinalAppAfterQuickScrub() { if (mQuickScrubBlocked) { return; } mQuickScrubController.onQuickScrubEnd(); // Normally this is handled in reset(), but since we are still scrubbing after the // transition into recents, we need to defer the handler invalidation for quick scrub until // after the gesture ends setStateOnUiThread(STATE_HANDLER_INVALIDATED); } private void debugNewState(int stateFlag) { if (!DEBUG_STATES) { return; } int state = mStateCallback.getState(); StringJoiner currentStateStr = new StringJoiner(", ", "[", "]"); String stateFlagStr = "Unknown-" + stateFlag; for (int i = 0; i < STATES.length; i++) { if ((state & (i << i)) != 0) { currentStateStr.add(STATES[i]); } if (stateFlag == (1 << i)) { stateFlagStr = STATES[i] + " (" + stateFlag + ")"; } } Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + stateFlagStr + " to " + currentStateStr); } public void setGestureEndCallback(Runnable gestureEndCallback) { mGestureEndCallback = gestureEndCallback; } // Handling long swipe private void onLongSwipeEnabledUi() { mUiLongSwipeMode = true; checkLongSwipeCanEnter(); checkLongSwipeCanStart(); } private void onLongSwipeDisabledUi() { mUiLongSwipeMode = false; mStateCallback.clearState(STATE_SCREENSHOT_VIEW_SHOWN); if (mLongSwipeController != null) { mLongSwipeController.destroy(); setTargetAlphaProvider((t, a1) -> a1); // Rebuild animations buildAnimationController(); } } private void onLongSwipeDisplacementUpdated() { if (!mUiLongSwipeMode || mLongSwipeController == null) { return; } mLongSwipeController.onMove(mLongSwipeDisplacement); } private void checkLongSwipeCanEnter() { if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE) || !mActivityControlHelper.supportsLongSwipe(mActivity)) { return; } // We are entering long swipe mode, make sure the screen shot is captured. mStateCallback.setState(STATE_CAPTURE_SCREENSHOT | STATE_SCREENSHOT_VIEW_SHOWN); } private void checkLongSwipeCanStart() { if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE) || !mActivityControlHelper.supportsLongSwipe(mActivity)) { return; } RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; if (targetSet == null) { // This can happen when cancelAnimation comes on the background thread, while we are // processing the long swipe on the UI thread. return; } mLongSwipeController = mActivityControlHelper.getLongSwipeController( mActivity, mRunningTaskId); onLongSwipeDisplacementUpdated(); if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha); } } private void onLongSwipeGestureFinishUi(float velocity, boolean isFling, float velocityX) { if (!mUiLongSwipeMode || mLongSwipeController == null) { mUiLongSwipeMode = false; handleNormalGestureEnd(velocity, isFling, velocityX); return; } mUiLongSwipeMode = false; finishCurrentTransitionToRecents(); mLongSwipeController.end(velocity, isFling, () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED)); } private void setTargetAlphaProvider( BiFunction provider) { mClipAnimationHelper.setTaskAlphaCallback(provider); updateFinalShift(); } public void onAssistDataReceived(Bundle assistData) { mAssistData = assistData; setStateOnUiThread(STATE_ASSIST_DATA_RECEIVED); } private void preloadAssistData() { RecentsModel.INSTANCE.get(mContext).preloadAssistData(mRunningTaskId, mAssistData); } public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) { if (!(app.isNotInRecents || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) { return 0; } return expectedAlpha; } }