blob: 410daa3cdbc5cf82119e386af4c0c7cdce047322 [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.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION;
import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
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.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.UserHandle;
import android.support.annotation.AnyThread;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnDrawListener;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
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.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.SyncRtSurfaceTransactionApplier;
import com.android.systemui.shared.system.WindowCallbacksCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
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_APP = 1 << 6;
private static final int STATE_HANDLER_INVALIDATED = 1 << 7;
private static final int STATE_GESTURE_STARTED = 1 << 8;
private static final int STATE_GESTURE_CANCELLED = 1 << 9;
private static final int STATE_GESTURE_COMPLETED = 1 << 10;
// States for quick switch/scrub
private static final int STATE_CURRENT_TASK_FINISHED = 1 << 11;
private static final int STATE_QUICK_SCRUB_START = 1 << 12;
private static final int STATE_QUICK_SCRUB_END = 1 << 13;
private static final int STATE_CAPTURE_SCREENSHOT = 1 << 14;
private static final int STATE_SCREENSHOT_CAPTURED = 1 << 15;
private static final int STATE_RESUME_LAST_TASK = 1 << 16;
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;
// 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_APP",
"STATE_HANDLER_INVALIDATED",
"STATE_GESTURE_STARTED",
"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_RESUME_LAST_TASK",
};
public static final long MAX_SWIPE_DURATION = 350;
public static final long MIN_SWIPE_DURATION = 80;
private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
private static final float SWIPE_DURATION_MULTIPLIER =
Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
private final ClipAnimationHelper mClipAnimationHelper = new ClipAnimationHelper();
protected Runnable mGestureEndCallback;
protected boolean mIsGoingToHome;
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 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 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 SyncRtSurfaceTransactionApplier mSyncTransactionApplier;
private QuickScrubController mQuickScrubController;
private AnimationFactory mAnimationFactory = (t, i) -> { };
private Runnable mLauncherDrawnCallback;
private boolean mWasLauncherAlreadyVisible;
private boolean mGestureStarted;
private int mLogAction = Touch.SWIPE;
private float mCurrentQuickScrubProgress;
private boolean mQuickScrubBlocked;
private @InteractionType int mInteractionType = INTERACTION_NORMAL;
private InputConsumerController mInputConsumer =
InputConsumerController.getRecentsAnimationInputConsumer();
private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper();
private final long mTouchTimeMs;
private long mLauncherFrameDrawnTime;
private boolean mBgLongSwipeMode = false;
private boolean mUiLongSwipeMode = false;
private float mLongSwipeDisplacement = 0;
private LongSwipeHelper mLongSwipeController;
WindowTransformSwipeHandler(int id, RunningTaskInfo runningTaskInfo, Context context,
long touchTimeMs, ActivityControlHelper<T> controller) {
this.id = id;
mContext = context;
mRunningTaskInfo = runningTaskInfo;
mRunningTaskId = runningTaskInfo.id;
mTouchTimeMs = touchTimeMs;
mActivityControlHelper = controller;
mActivityInitListener = mActivityControlHelper
.createActivityInitListener(this::onActivityInit);
initStateCallbacks();
// Register the input consumer on the UI thread, to ensure that it runs after any pending
// unregister calls
executeOnUiThread(mInputConsumer::registerInputConsumer);
}
private void initStateCallbacks() {
mStateCallback = new MultiStateCallback() {
@Override
public void setState(int stateFlag) {
debugNewState(stateFlag);
super.setState(stateFlag);
}
};
mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
this::initializeLauncherAnimationController);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
this::launcherFrameDrawn);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
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_APP,
this::resumeLastTaskForQuickstep);
mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
this::resumeLastTask);
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::finishCurrentTransitionToHome);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
| STATE_ACTIVITY_MULTIPLIER_COMPLETE
| STATE_SCALED_CONTROLLER_RECENTS
| STATE_CURRENT_TASK_FINISHED
| STATE_GESTURE_COMPLETED,
this::setupLauncherUiAfterSwipeUpAnimation);
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_APP,
this::notifyTransitionCancelled);
mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START
| STATE_APP_CONTROLLER_RECEIVED, this::onQuickScrubStart);
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);
}
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();
mSyncTransactionApplier = new SyncRtSurfaceTransactionApplier(mRecentsView);
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, 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);
}
});
}
mRecentsView.showTask(mRunningTaskId);
mRecentsView.setRunningTaskHidden(true);
mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */);
mLayoutListener.open();
mStateCallback.setState(STATE_LAUNCHER_STARTED);
}
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));
}
}
public void updateInteractionType(@InteractionType int interactionType) {
if (mInteractionType != INTERACTION_NORMAL) {
throw new IllegalArgumentException(
"Can't change interaction type from " + mInteractionType);
}
if (interactionType != INTERACTION_QUICK_SCRUB) {
throw new IllegalArgumentException(
"Can't change interaction type to " + interactionType);
}
mInteractionType = interactionType;
mRecentsAnimationWrapper.runOnInit(this::shiftAnimationDestinationForQuickscrub);
setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED);
// Start the window animation without waiting for launcher.
animateToProgress(mCurrentShift.value, 1f, QUICK_SCRUB_FROM_APP_START_DURATION, LINEAR);
}
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();
float distanceToReachEdge = mDp.widthPx / 2 + tempRect.rect.width() / 2 +
res.getDimensionPixelSize(R.dimen.recents_page_spacing);
float interpolation = Math.min(1, offsetX / distanceToReachEdge);
scale = TaskView.getCurveScaleForInterpolation(interpolation);
}
mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY,
QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR);
}
@WorkerThread
public void updateDisplacement(float displacement) {
// We are moving in the negative x/y direction
displacement = -displacement;
if (displacement > mTransitionDragLength) {
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();
mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
}
@WorkerThread
private void updateFinalShift() {
float shift = mCurrentShift.value;
RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController();
if (controller != null) {
mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, shift,
Looper.myLooper() == mMainThreadHandler.getLooper()
? mSyncTransactionApplier
: null);
// TODO: This logic is spartanic!
boolean passedThreshold = shift > 0.12f;
mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold);
if (mActivityControlHelper.shouldMinimizeSplitScreen()) {
mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold);
}
}
executeOnUiThread(this::updateFinalShiftUi);
}
private void updateFinalShiftUi() {
if (mLauncherTransitionController == null || mLauncherTransitionController
.getAnimationPlayer().isStarted()) {
return;
}
mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
}
public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller,
RemoteAnimationTargetSet targets, Rect homeContentInsets, Rect minimizedHomeBounds) {
LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
InvariantDeviceProfile idp = appState == null ?
new InvariantDeviceProfile(mContext) : appState.getInvariantDeviceProfile();
DeviceProfile dp = idp.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.
Rect insets = new Rect();
WindowManagerWrapper.getInstance().getStableInsets(insets);
dp = dp.copy(mContext);
dp.updateInsets(insets);
}
dp.updateIsSeascape(mContext.getSystemService(WindowManager.class));
if (runningTaskTarget != null) {
mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
}
mClipAnimationHelper.prepareAnimation(false /* isOpening */);
initTransitionEndpoints(dp);
mRecentsAnimationWrapper.setController(controller, targets);
setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
}
public void onRecentsAnimationCanceled() {
mRecentsAnimationWrapper.setController(null, null);
mActivityInitListener.unregister();
setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
}
public void onGestureStarted() {
notifyGestureStartedAsync();
setStateOnUiThread(STATE_GESTURE_STARTED);
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 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));
} else {
handleNormalGestureEnd(endVelocity, isFling);
}
}
private void handleNormalGestureEnd(float endVelocity, boolean isFling) {
float velocityPxPerMs = endVelocity / 1000;
long duration = MAX_SWIPE_DURATION;
final float endShift;
final float startShift;
final Interpolator interpolator;
if (!isFling) {
endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted ? 1 : 0;
long expectedDuration = Math.abs(Math.round((endShift - mCurrentShift.value)
* MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
startShift = mCurrentShift.value;
interpolator = DEACCEL;
} else {
endShift = endVelocity < 0 ? 1 : 0;
interpolator = endVelocity < 0
? Interpolators.overshootInterpolatorForVelocity(velocityPxPerMs, 2f)
: DEACCEL;
float minFlingVelocity = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_min_velocity);
if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
float distanceToTravel = (endShift - mCurrentShift.value) * 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);
}
startShift = Utilities.boundToRange(mCurrentShift.value - velocityPxPerMs
* SINGLE_FRAME_MS / (mTransitionDragLength), 0, 1);
}
animateToProgress(startShift, endShift, duration, interpolator);
}
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, dp).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) {
mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
interpolator));
}
private void animateToProgressInternal(float start, float end, long duration,
Interpolator interpolator) {
mIsGoingToHome = Float.compare(end, 1) == 0;
ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration);
anim.setInterpolator(interpolator);
anim.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
setStateOnUiThread(mIsGoingToHome
? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT)
: STATE_SCALED_CONTROLLER_APP);
}
});
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 && !mWasLauncherAlreadyVisible
&& start != end && duration > 0) {
// 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);
mLauncherTransitionController.getAnimationPlayer().start();
}
});
}
@UiThread
private void resumeLastTaskForQuickstep() {
setStateOnUiThread(STATE_RESUME_LAST_TASK);
doLogGesture(false /* toLauncher */);
reset();
}
@UiThread
private void resumeLastTask() {
mRecentsAnimationWrapper.finish(false /* toHome */, null);
}
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();
mInputConsumer.unregisterInputConsumer();
mTaskSnapshot = null;
}
private void invalidateHandlerWithLauncher() {
mLauncherTransitionController = null;
mLayoutListener.finish();
mActivityControlHelper.getAlphaProperty(mActivity).setValue(1);
mRecentsView.setRunningTaskHidden(false);
mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, false /* animate */);
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() {
if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) {
mLauncherTransitionController.setPlayFraction(1);
}
}
private void switchToScreenshot() {
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);
mRecentsView.setRunningTaskHidden(false);
if (taskView != null) {
// Defer finishing the animation until the next launcher frame with the
// new thumbnail
finishTransitionPosted = new WindowCallbacksCompat(taskView) {
@Override
public void onPostDraw(Canvas canvas) {
setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
detach();
}
}.attach();
}
}
if (!finishTransitionPosted) {
// If we haven't posted a draw callback, set the state immediately.
setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
}
}
private void finishCurrentTransitionToHome() {
synchronized (mRecentsAnimationWrapper) {
mRecentsAnimationWrapper.finish(true /* toHome */,
() -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
}
}
private void setupLauncherUiAfterSwipeUpAnimation() {
if (mLauncherTransitionController != null) {
mLauncherTransitionController.getAnimationPlayer().end();
mLauncherTransitionController = null;
}
mActivityControlHelper.onSwipeUpComplete(mActivity);
// Animate the first icon.
mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */);
mRecentsView.setSwipeDownShouldLaunchApp(true);
RecentsModel.getInstance(mContext).onOverviewShown(false, TAG);
doLogGesture(true /* toLauncher */);
reset();
}
private void onQuickScrubStart() {
if (!mQuickScrubController.prepareQuickScrub(TAG)) {
mQuickScrubBlocked = true;
setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED);
return;
}
if (mLauncherTransitionController != null) {
mLauncherTransitionController.getAnimationPlayer().end();
mLauncherTransitionController = null;
}
mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false);
// Inform the last progress in case we skipped before.
mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress);
}
private void onFinishedTransitionToQuickScrub() {
if (mQuickScrubBlocked) {
return;
}
mQuickScrubController.onFinishedTransitionToQuickScrub();
}
public void onQuickScrubProgress(float progress) {
mCurrentQuickScrubProgress = progress;
if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null
|| mQuickScrubBlocked) {
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;
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);
}
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, mRecentsAnimationWrapper.targetSet);
onLongSwipeDisplacementUpdated();
setTargetAlphaProvider(mLongSwipeController::getTargetAlpha);
}
private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) {
if (!mUiLongSwipeMode || mLongSwipeController == null) {
mUiLongSwipeMode = false;
handleNormalGestureEnd(velocity, isFling);
return;
}
mUiLongSwipeMode = false;
finishCurrentTransitionToHome();
mLongSwipeController.end(velocity, isFling,
() -> setStateOnUiThread(STATE_HANDLER_INVALIDATED));
}
private void setTargetAlphaProvider(
BiFunction<RemoteAnimationTargetCompat, Float, Float> provider) {
mClipAnimationHelper.setTaskAlphaCallback(provider);
updateFinalShift();
}
}