blob: 33c7c4d718342a04e42e9c46ca1fd958b69ac906 [file] [log] [blame]
/*
* 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<T extends BaseDraggingActivity> {
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<T> 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<T> 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<RemoteAnimationTargetCompat, Float, Float> 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;
}
}