3f7df53dda
Bug: 74794600 Test: manual test Change-Id: Id34a70abfa5de9dd741848efa3b82fae608c0df0
893 lines
38 KiB
Java
893 lines
38 KiB
Java
/*
|
|
* 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.LauncherState.FAST_OVERVIEW;
|
|
import static com.android.launcher3.LauncherState.OVERVIEW;
|
|
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
|
|
import static com.android.launcher3.anim.Interpolators.LINEAR;
|
|
import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
|
|
import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
|
|
import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_START_DURATION;
|
|
import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
|
|
import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
|
|
import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SWITCH;
|
|
import static com.android.quickstep.TouchConsumer.isInteractionQuick;
|
|
import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
|
|
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorSet;
|
|
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.Matrix;
|
|
import android.graphics.Matrix.ScaleToFit;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.metrics.LogMaker;
|
|
import android.os.Build;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.os.SystemClock;
|
|
import android.support.annotation.UiThread;
|
|
import android.support.annotation.WorkerThread;
|
|
import android.util.Log;
|
|
import android.view.View;
|
|
import android.view.ViewTreeObserver.OnDrawListener;
|
|
|
|
import com.android.launcher3.AbstractFloatingView;
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.Launcher;
|
|
import com.android.launcher3.LauncherAppState;
|
|
import com.android.launcher3.LauncherState;
|
|
import com.android.launcher3.MainThreadExecutor;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.allapps.AllAppsTransitionController;
|
|
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.TraceHelper;
|
|
import com.android.launcher3.util.ViewOnDrawExecutor;
|
|
import com.android.quickstep.TouchConsumer.InteractionType;
|
|
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.recents.utilities.RectFEvaluator;
|
|
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.TransactionCompat;
|
|
import com.android.systemui.shared.system.WindowManagerWrapper;
|
|
|
|
import java.util.StringJoiner;
|
|
|
|
class EventLogTags {
|
|
private EventLogTags() {
|
|
} // don't instantiate
|
|
|
|
/** 524292 sysui_multi_action (content|4) */
|
|
public static final int SYSUI_MULTI_ACTION = 524292;
|
|
|
|
public static void writeSysuiMultiAction(Object[] content) {
|
|
android.util.EventLog.writeEvent(SYSUI_MULTI_ACTION, content);
|
|
}
|
|
}
|
|
|
|
class MetricsLogger {
|
|
private static MetricsLogger sMetricsLogger;
|
|
|
|
private static MetricsLogger getLogger() {
|
|
if (sMetricsLogger == null) {
|
|
sMetricsLogger = new MetricsLogger();
|
|
}
|
|
return sMetricsLogger;
|
|
}
|
|
|
|
protected void saveLog(Object[] rep) {
|
|
EventLogTags.writeSysuiMultiAction(rep);
|
|
}
|
|
|
|
public void write(LogMaker content) {
|
|
if (content.getType() == 0/*MetricsEvent.TYPE_UNKNOWN*/) {
|
|
content.setType(4/*MetricsEvent.TYPE_ACTION*/);
|
|
}
|
|
saveLog(content.serialize());
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.O)
|
|
public class WindowTransformSwipeHandler extends BaseSwipeInteractionHandler {
|
|
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;
|
|
|
|
// States for quick switch/scrub
|
|
private static final int STATE_SWITCH_TO_SCREENSHOT_COMPLETE = 1 << 10;
|
|
private static final int STATE_QUICK_SWITCH = 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 LAUNCHER_UI_STATES =
|
|
STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE
|
|
| STATE_LAUNCHER_STARTED;
|
|
|
|
// 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_SWITCH_TO_SCREENSHOT_COMPLETE",
|
|
"STATE_QUICK_SWITCH",
|
|
"STATE_QUICK_SCRUB_START",
|
|
"STATE_QUICK_SCRUB_END"
|
|
};
|
|
|
|
private static final long MAX_SWIPE_DURATION = 200;
|
|
private static final long MIN_SWIPE_DURATION = 80;
|
|
|
|
private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
|
|
|
|
// The bounds of the source app in device coordinates
|
|
private final Rect mSourceStackBounds = new Rect();
|
|
// The insets of the source app
|
|
private final Rect mSourceInsets = new Rect();
|
|
// The source app bounds with the source insets applied, in the source app window coordinates
|
|
private final RectF mSourceRect = new RectF();
|
|
// The bounds of the task view in launcher window coordinates
|
|
private final RectF mTargetRect = new RectF();
|
|
// Doesn't change after initialized, used as an anchor when changing mTargetRect
|
|
private final RectF mInitialTargetRect = new RectF();
|
|
// The insets to be used for clipping the app window, which can be larger than mSourceInsets
|
|
// if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
|
|
// app window coordinates.
|
|
private final RectF mSourceWindowClipInsets = new RectF();
|
|
|
|
// The bounds of launcher (not including insets) in device coordinates
|
|
private final Rect mHomeStackBounds = new Rect();
|
|
// The clip rect in source app window coordinates
|
|
private final Rect mClipRect = new Rect();
|
|
private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
|
|
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 MainThreadExecutor mMainExecutor = new MainThreadExecutor();
|
|
|
|
private final Context mContext;
|
|
private final int mRunningTaskId;
|
|
|
|
private MultiStateCallback mStateCallback;
|
|
private AnimatorPlaybackController mLauncherTransitionController;
|
|
|
|
private Launcher mLauncher;
|
|
private LauncherLayoutListener mLauncherLayoutListener;
|
|
private RecentsView mRecentsView;
|
|
private QuickScrubController mQuickScrubController;
|
|
|
|
private Runnable mLauncherDrawnCallback;
|
|
|
|
private boolean mWasLauncherAlreadyVisible;
|
|
|
|
private float mCurrentDisplacement;
|
|
private boolean mGestureStarted;
|
|
private int mLogAction = Touch.SWIPE;
|
|
|
|
private @InteractionType int mInteractionType = INTERACTION_NORMAL;
|
|
|
|
private InputConsumerController mInputConsumer =
|
|
InputConsumerController.getRecentsAnimationInputConsumer();
|
|
|
|
private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper();
|
|
private Matrix mTmpMatrix = new Matrix();
|
|
private final long mTouchTimeMs;
|
|
private long mLauncherFrameDrawnTime;
|
|
private final MetricsLogger mMetricsLogger = new MetricsLogger();
|
|
|
|
WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs) {
|
|
mContext = context;
|
|
mRunningTaskId = runningTaskInfo.id;
|
|
mTouchTimeMs = touchTimeMs;
|
|
// Register the input consumer on the UI thread, to ensure that it runs after any pending
|
|
// unregister calls
|
|
mMainExecutor.execute(mInputConsumer::registerInputConsumer);
|
|
initStateCallbacks();
|
|
}
|
|
|
|
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::notifyGestureStarted);
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
|
|
| STATE_GESTURE_CANCELLED,
|
|
this::resetStateForAnimationCancel);
|
|
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
|
|
| STATE_SCALED_CONTROLLER_APP,
|
|
this::resumeLastTask);
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
|
|
| STATE_ACTIVITY_MULTIPLIER_COMPLETE
|
|
| STATE_SCALED_CONTROLLER_RECENTS,
|
|
this::switchToScreenshot);
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
|
|
| STATE_ACTIVITY_MULTIPLIER_COMPLETE
|
|
| STATE_SCALED_CONTROLLER_RECENTS
|
|
| STATE_SWITCH_TO_SCREENSHOT_COMPLETE,
|
|
this::setupLauncherUiAfterSwipeUpAnimation);
|
|
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP,
|
|
this::reset);
|
|
|
|
mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
|
|
this::invalidateHandlerWithLauncher);
|
|
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SWITCH,
|
|
this::onQuickInteractionStart);
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SCRUB_START,
|
|
this::onQuickInteractionStart);
|
|
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
|
|
| STATE_QUICK_SWITCH, this::switchToFinalAppAfterQuickSwitch);
|
|
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
|
|
| STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub);
|
|
}
|
|
|
|
private void setStateOnUiThread(int stateFlag) {
|
|
Handler handler = mMainExecutor.getHandler();
|
|
if (Looper.myLooper() == handler.getLooper()) {
|
|
mStateCallback.setState(stateFlag);
|
|
} else {
|
|
postAtFrontOfQueueAsynchronously(handler, () -> mStateCallback.setState(stateFlag));
|
|
}
|
|
}
|
|
|
|
private void initTransitionEndpoints(DeviceProfile dp) {
|
|
mDp = dp;
|
|
mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
|
|
mSourceStackBounds.width() - mSourceInsets.right,
|
|
mSourceStackBounds.height() - mSourceInsets.bottom);
|
|
|
|
Rect tempRect = new Rect();
|
|
RecentsView.getPageRect(dp, mContext, tempRect);
|
|
|
|
mTargetRect.set(tempRect);
|
|
mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
|
|
mHomeStackBounds.top - mSourceStackBounds.top);
|
|
mInitialTargetRect.set(mTargetRect);
|
|
|
|
// Calculate the clip based on the target rect (since the content insets and the
|
|
// launcher insets may differ, so the aspect ratio of the target rect can differ
|
|
// from the source rect. The difference between the target rect (scaled to the
|
|
// source rect) is the amount to clip on each edge.
|
|
RectF scaledTargetRect = new RectF(mTargetRect);
|
|
Utilities.scaleRectFAboutCenter(scaledTargetRect,
|
|
mSourceRect.width() / mTargetRect.width());
|
|
scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
|
|
mSourceWindowClipInsets.set(
|
|
Math.max(scaledTargetRect.left, 0),
|
|
Math.max(scaledTargetRect.top, 0),
|
|
Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
|
|
Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
|
|
mSourceRect.set(scaledTargetRect);
|
|
|
|
Rect targetInsets = dp.getInsets();
|
|
if (dp.isVerticalBarLayout()) {
|
|
int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
|
|
mTransitionDragLength = dp.hotseatBarSizePx + dp.hotseatBarSidePaddingPx + hotseatInset;
|
|
} else {
|
|
mTransitionDragLength = dp.heightPx - tempRect.bottom;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean init(final Launcher launcher, boolean alreadyOnHome) {
|
|
if (launcher == mLauncher) {
|
|
return true;
|
|
}
|
|
if (mLauncher != null) {
|
|
// The launcher may have been recreated as a result of device rotation.
|
|
int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
|
|
initStateCallbacks();
|
|
mStateCallback.setState(oldState);
|
|
mLauncherLayoutListener.setHandler(null);
|
|
}
|
|
mWasLauncherAlreadyVisible = alreadyOnHome;
|
|
mLauncher = launcher;
|
|
|
|
// For the duration of the gesture, lock the screen orientation to ensure that we do not
|
|
// rotate mid-quickscrub
|
|
mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK);
|
|
|
|
mRecentsView = mLauncher.getOverviewPanel();
|
|
mQuickScrubController = mRecentsView.getQuickScrubController();
|
|
mLauncherLayoutListener = new LauncherLayoutListener(mLauncher);
|
|
|
|
mStateCallback.setState(STATE_LAUNCHER_PRESENT);
|
|
if (alreadyOnHome) {
|
|
onLauncherStart(launcher);
|
|
} else {
|
|
launcher.setOnStartCallback(this::onLauncherStart);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void onLauncherStart(final Launcher launcher) {
|
|
if (mLauncher != launcher) {
|
|
return;
|
|
}
|
|
if ((mStateCallback.getState() & STATE_HANDLER_INVALIDATED) != 0) {
|
|
return;
|
|
}
|
|
|
|
mStateCallback.setState(STATE_LAUNCHER_STARTED);
|
|
LauncherState startState = mLauncher.getStateManager().getState();
|
|
if (startState.disableRestore) {
|
|
startState = mLauncher.getStateManager().getRestState();
|
|
}
|
|
mLauncher.getStateManager().setRestState(startState);
|
|
|
|
AbstractFloatingView.closeAllOpenViews(mLauncher, mWasLauncherAlreadyVisible);
|
|
|
|
|
|
if (mWasLauncherAlreadyVisible && !mLauncher.getAppTransitionManager().isAnimating()) {
|
|
DeviceProfile dp = mLauncher.getDeviceProfile();
|
|
long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
|
|
mLauncherTransitionController = mLauncher.getStateManager()
|
|
.createAnimationToNewWorkspace(OVERVIEW, accuracy);
|
|
mLauncherTransitionController.dispatchOnStart();
|
|
mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
|
|
|
|
mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN);
|
|
} else {
|
|
TraceHelper.beginSection("WTS-init");
|
|
mLauncher.getStateManager().goToState(OVERVIEW, false);
|
|
TraceHelper.partitionSection("WTS-init", "State changed");
|
|
|
|
// TODO: Implement a better animation for fading in
|
|
View rootView = mLauncher.getRootView();
|
|
rootView.setAlpha(0);
|
|
rootView.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
|
|
|
|
@Override
|
|
public void onDraw() {
|
|
TraceHelper.endSection("WTS-init", "Launcher frame is drawn");
|
|
rootView.post(() ->
|
|
rootView.getViewTreeObserver().removeOnDrawListener(this));
|
|
if (launcher != mLauncher) {
|
|
return;
|
|
}
|
|
|
|
mStateCallback.setState(STATE_LAUNCHER_DRAWN);
|
|
}
|
|
});
|
|
|
|
// Optimization, hide the all apps view to prevent layout while initializing
|
|
mLauncher.getAppsView().setVisibility(View.GONE);
|
|
}
|
|
|
|
mRecentsView.showTask(mRunningTaskId);
|
|
mRecentsView.setFirstTaskIconScaledDown(true /* isScaledDown */, false /* animate */);
|
|
mLauncherLayoutListener.open();
|
|
}
|
|
|
|
public void setLauncherOnDrawCallback(Runnable callback) {
|
|
mLauncherDrawnCallback = callback;
|
|
}
|
|
|
|
private void launcherFrameDrawn() {
|
|
View rootView = mLauncher.getRootView();
|
|
if (rootView.getAlpha() < 1) {
|
|
if (mGestureStarted) {
|
|
final MultiStateCallback callback = mStateCallback;
|
|
rootView.animate().alpha(1)
|
|
.setDuration(getFadeInDuration())
|
|
.withEndAction(() -> callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE));
|
|
} else {
|
|
rootView.setAlpha(1);
|
|
mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE);
|
|
}
|
|
}
|
|
if (mLauncherDrawnCallback != null) {
|
|
mLauncherDrawnCallback.run();
|
|
}
|
|
mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
|
|
}
|
|
|
|
private void initializeLauncherAnimationController() {
|
|
mLauncherLayoutListener.setHandler(this);
|
|
onLauncherLayoutChanged();
|
|
|
|
final long transitionDelay = mLauncherFrameDrawnTime - mTouchTimeMs;
|
|
// Mimic ActivityMetricsLogger.logAppTransitionMultiEvents() logging for
|
|
// "Recents" activity for app transition tests for the app-to-recents case.
|
|
final LogMaker builder = new LogMaker(761/*APP_TRANSITION*/);
|
|
builder.setPackageName("com.android.systemui");
|
|
builder.addTaggedData(871/*FIELD_CLASS_NAME*/,
|
|
"com.android.systemui.recents.RecentsActivity");
|
|
builder.addTaggedData(319/*APP_TRANSITION_DELAY_MS*/,
|
|
transitionDelay);
|
|
mMetricsLogger.write(builder);
|
|
if (LatencyTrackerCompat.isEnabled(mContext)) {
|
|
LatencyTrackerCompat.logToggleRecents((int) transitionDelay);
|
|
}
|
|
}
|
|
|
|
public void updateInteractionType(@InteractionType int interactionType) {
|
|
if (mInteractionType != INTERACTION_NORMAL) {
|
|
throw new IllegalArgumentException(
|
|
"Can't change interaction type from " + mInteractionType);
|
|
}
|
|
if (!isInteractionQuick(interactionType)) {
|
|
throw new IllegalArgumentException(
|
|
"Can't change interaction type to " + interactionType);
|
|
}
|
|
mInteractionType = interactionType;
|
|
|
|
setStateOnUiThread(interactionType == INTERACTION_QUICK_SWITCH
|
|
? STATE_QUICK_SWITCH : STATE_QUICK_SCRUB_START);
|
|
|
|
// Start the window animation without waiting for launcher.
|
|
animateToProgress(1f, QUICK_SWITCH_START_DURATION);
|
|
}
|
|
|
|
private void onQuickInteractionStart() {
|
|
mLauncher.getStateManager().goToState(FAST_OVERVIEW,
|
|
mWasLauncherAlreadyVisible || mGestureStarted);
|
|
mQuickScrubController.onQuickScrubStart(false);
|
|
}
|
|
|
|
@WorkerThread
|
|
public void updateDisplacement(float displacement) {
|
|
mCurrentDisplacement = displacement;
|
|
|
|
float translation = Utilities.boundToRange(-mCurrentDisplacement, 0, mTransitionDragLength);
|
|
float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
|
|
mCurrentShift.updateValue(shift);
|
|
}
|
|
|
|
/**
|
|
* Called by {@link #mLauncherLayoutListener} when launcher layout changes
|
|
*/
|
|
public void onLauncherLayoutChanged() {
|
|
initTransitionEndpoints(mLauncher.getDeviceProfile());
|
|
|
|
if (!mWasLauncherAlreadyVisible) {
|
|
float startProgress;
|
|
AllAppsTransitionController controller = mLauncher.getAllAppsController();
|
|
|
|
if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
|
|
startProgress = 1;
|
|
} else {
|
|
float scrollRange = Math.max(controller.getShiftRange(), 1);
|
|
startProgress = (mTransitionDragLength / scrollRange) + 1;
|
|
}
|
|
AnimatorSet anim = new AnimatorSet();
|
|
ObjectAnimator shiftAnim = ObjectAnimator.ofFloat(controller, ALL_APPS_PROGRESS,
|
|
startProgress, OVERVIEW.getVerticalProgress(mLauncher));
|
|
shiftAnim.setInterpolator(LINEAR);
|
|
anim.play(shiftAnim);
|
|
|
|
// TODO: Link this animation to state animation, so that it is cancelled
|
|
// automatically on state change
|
|
anim.setDuration(mTransitionDragLength * 2);
|
|
mLauncherTransitionController =
|
|
AnimatorPlaybackController.wrap(anim, mTransitionDragLength * 2);
|
|
mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
|
|
}
|
|
}
|
|
|
|
@WorkerThread
|
|
private void updateFinalShift() {
|
|
float shift = mCurrentShift.value;
|
|
|
|
synchronized (mRecentsAnimationWrapper) {
|
|
if (mRecentsAnimationWrapper.controller != null) {
|
|
RectF currentRect;
|
|
synchronized (mTargetRect) {
|
|
currentRect = mRectFEvaluator.evaluate(shift, mSourceRect, mTargetRect);
|
|
}
|
|
|
|
mClipRect.left = (int) (mSourceWindowClipInsets.left * shift);
|
|
mClipRect.top = (int) (mSourceWindowClipInsets.top * shift);
|
|
mClipRect.right = (int)
|
|
(mSourceStackBounds.width() - (mSourceWindowClipInsets.right * shift));
|
|
mClipRect.bottom = (int)
|
|
(mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * shift));
|
|
|
|
mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL);
|
|
|
|
TransactionCompat transaction = new TransactionCompat();
|
|
for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
|
|
if (app.mode == MODE_CLOSING) {
|
|
transaction.setMatrix(app.leash, mTmpMatrix)
|
|
.setWindowCrop(app.leash, mClipRect);
|
|
|
|
if (app.isNotInRecents) {
|
|
transaction.setAlpha(app.leash, 1 - shift);
|
|
}
|
|
|
|
transaction.show(app.leash);
|
|
}
|
|
}
|
|
transaction.apply();
|
|
}
|
|
}
|
|
|
|
if (mLauncherTransitionController != null) {
|
|
Runnable runOnUi = () -> {
|
|
if (mLauncherTransitionController == null) {
|
|
return;
|
|
}
|
|
mLauncherTransitionController.setPlayFraction(shift);
|
|
|
|
// Make sure the window follows the first task if it moves, e.g. during quick scrub.
|
|
View firstTask = mRecentsView.getPageAt(0);
|
|
int scrollForFirstTask = mRecentsView.getScrollForPage(0);
|
|
int offsetFromFirstTask = (scrollForFirstTask - mRecentsView.getScrollX());
|
|
if (offsetFromFirstTask != 0) {
|
|
synchronized (mTargetRect) {
|
|
mTargetRect.set(mInitialTargetRect);
|
|
Utilities.scaleRectFAboutCenter(mTargetRect, firstTask.getScaleX());
|
|
float offsetX = offsetFromFirstTask + firstTask.getTranslationX();
|
|
mTargetRect.offset(offsetX, 0);
|
|
}
|
|
}
|
|
};
|
|
if (Looper.getMainLooper() == Looper.myLooper()) {
|
|
runOnUi.run();
|
|
} else {
|
|
// The fling operation completed even before the launcher was drawn
|
|
mMainExecutor.execute(runOnUi);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller,
|
|
RemoteAnimationTargetCompat[] apps, Rect homeContentInsets, Rect minimizedHomeBounds) {
|
|
if (apps != null) {
|
|
// Use the top closing app to determine the insets for the animation
|
|
for (RemoteAnimationTargetCompat target : apps) {
|
|
if (target.mode == MODE_CLOSING) {
|
|
DeviceProfile dp = LauncherAppState.getIDP(mContext).getDeviceProfile(mContext);
|
|
if (minimizedHomeBounds != null) {
|
|
mHomeStackBounds.set(minimizedHomeBounds);
|
|
dp = dp.getMultiWindowProfile(mContext,
|
|
new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height()));
|
|
dp.updateInsets(homeContentInsets);
|
|
} else {
|
|
mHomeStackBounds.set(new Rect(0, 0, dp.widthPx, dp.heightPx));
|
|
// TODO: Workaround for an existing issue where the home content insets are
|
|
// not valid immediately after rotation, just use the stable insets for now
|
|
Rect insets = new Rect();
|
|
WindowManagerWrapper.getInstance().getStableInsets(insets);
|
|
dp.updateInsets(insets);
|
|
}
|
|
|
|
// Initialize the start and end animation bounds
|
|
// TODO: Remove once platform is updated
|
|
try {
|
|
mSourceInsets.set(target.getContentInsets());
|
|
} catch (Error e) {
|
|
// TODO: Remove once platform is updated, use stable insets as fallback
|
|
WindowManagerWrapper.getInstance().getStableInsets(mSourceInsets);
|
|
}
|
|
mSourceStackBounds.set(target.sourceContainerBounds);
|
|
|
|
initTransitionEndpoints(dp);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
mRecentsAnimationWrapper.setController(controller, apps);
|
|
setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
|
|
}
|
|
|
|
public void onRecentsAnimationCanceled() {
|
|
mRecentsAnimationWrapper.setController(null, null);
|
|
clearReference();
|
|
setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
|
|
}
|
|
|
|
public void onGestureStarted() {
|
|
notifyGestureStarted();
|
|
setStateOnUiThread(STATE_GESTURE_STARTED);
|
|
mGestureStarted = true;
|
|
mRecentsAnimationWrapper.enableInputConsumer();
|
|
}
|
|
|
|
/**
|
|
* Notifies the launcher that the swipe gesture has started. This can be called multiple times
|
|
* on both background and UI threads
|
|
*/
|
|
private void notifyGestureStarted() {
|
|
final Launcher curLauncher = mLauncher;
|
|
if (curLauncher != null) {
|
|
curLauncher.onQuickstepGestureStarted(mWasLauncherAlreadyVisible);
|
|
}
|
|
}
|
|
|
|
@WorkerThread
|
|
public void onGestureEnded(float endVelocity) {
|
|
Resources res = mContext.getResources();
|
|
float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity);
|
|
boolean isFling = Math.abs(endVelocity) > flingThreshold;
|
|
|
|
long duration = MAX_SWIPE_DURATION;
|
|
final float endShift;
|
|
if (!isFling) {
|
|
endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
|
|
mLogAction = Touch.SWIPE;
|
|
} else {
|
|
endShift = endVelocity < 0 ? 1 : 0;
|
|
float minFlingVelocity = res.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. 5.
|
|
duration = 5 * Math.round(1000 * Math.abs(distanceToTravel / endVelocity));
|
|
}
|
|
mLogAction = Touch.FLING;
|
|
}
|
|
|
|
animateToProgress(endShift, duration);
|
|
}
|
|
|
|
private void doLogGesture(boolean toLauncher) {
|
|
final int direction;
|
|
if (mDp.isVerticalBarLayout()) {
|
|
direction = (mDp.isSeascape() ^ toLauncher) ? Direction.LEFT : Direction.RIGHT;
|
|
} else {
|
|
direction = toLauncher ? Direction.UP : Direction.DOWN;
|
|
}
|
|
|
|
int dstContainerType = toLauncher ? ContainerType.TASKSWITCHER : ContainerType.APP;
|
|
UserEventDispatcher.newInstance(mContext, mDp).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 progress, long duration) {
|
|
mIsGoingToHome = Float.compare(progress, 1) == 0;
|
|
ObjectAnimator anim = mCurrentShift.animateToValue(progress).setDuration(duration);
|
|
anim.setInterpolator(Interpolators.SCROLL);
|
|
anim.addListener(new AnimationSuccessListener() {
|
|
@Override
|
|
public void onAnimationSuccess(Animator animator) {
|
|
setStateOnUiThread(mIsGoingToHome ?
|
|
STATE_SCALED_CONTROLLER_RECENTS : STATE_SCALED_CONTROLLER_APP);
|
|
}
|
|
});
|
|
anim.start();
|
|
}
|
|
|
|
@UiThread
|
|
private void resumeLastTask() {
|
|
mRecentsAnimationWrapper.finish(false /* toHome */, null);
|
|
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();
|
|
}
|
|
|
|
clearReference();
|
|
mInputConsumer.unregisterInputConsumer();
|
|
}
|
|
|
|
private void invalidateHandlerWithLauncher() {
|
|
mLauncherTransitionController = null;
|
|
mLauncherLayoutListener.setHandler(null);
|
|
mLauncherLayoutListener.close(false);
|
|
|
|
// Restore the requested orientation to the user preference after the gesture has ended
|
|
mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_NONE);
|
|
|
|
mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, false /* animate */);
|
|
}
|
|
|
|
private void resetStateForAnimationCancel() {
|
|
LauncherState startState = mLauncher.getStateManager().getRestState();
|
|
boolean animate = mWasLauncherAlreadyVisible || mGestureStarted;
|
|
mLauncher.getStateManager().goToState(startState, animate);
|
|
}
|
|
|
|
public void layoutListenerClosed() {
|
|
if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) {
|
|
mLauncherTransitionController.setPlayFraction(1);
|
|
}
|
|
}
|
|
|
|
private void switchToScreenshot() {
|
|
boolean finishTransitionPosted = false;
|
|
final Runnable finishTransitionRunnable = () -> {
|
|
synchronized (mRecentsAnimationWrapper) {
|
|
mRecentsAnimationWrapper.finish(true /* toHome */,
|
|
() -> setStateOnUiThread(STATE_SWITCH_TO_SCREENSHOT_COMPLETE));
|
|
}
|
|
};
|
|
synchronized (mRecentsAnimationWrapper) {
|
|
if (mRecentsAnimationWrapper.controller != null) {
|
|
TransactionCompat transaction = new TransactionCompat();
|
|
for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
|
|
if (app.mode == MODE_CLOSING) {
|
|
// Update the screenshot of the task
|
|
ThumbnailData thumbnail =
|
|
mRecentsAnimationWrapper.controller.screenshotTask(app.taskId);
|
|
TaskView taskView = mRecentsView.updateThumbnail(app.taskId, thumbnail);
|
|
if (taskView != null) {
|
|
// Defer finishing the animation until the next launcher frame with the
|
|
// new thumbnail
|
|
ViewOnDrawExecutor executor = new ViewOnDrawExecutor() {
|
|
@Override
|
|
public void onViewDetachedFromWindow(View v) {
|
|
if (!isCompleted()) {
|
|
runAllTasks();
|
|
}
|
|
}
|
|
};
|
|
executor.attachTo(mLauncher, taskView,
|
|
false /* waitForLoadAnimation */);
|
|
executor.execute(finishTransitionRunnable);
|
|
finishTransitionPosted = true;
|
|
}
|
|
}
|
|
}
|
|
transaction.apply();
|
|
}
|
|
}
|
|
if (!finishTransitionPosted) {
|
|
// If we haven't posted the transition end runnable, run it now
|
|
finishTransitionRunnable.run();
|
|
}
|
|
doLogGesture(true /* toLauncher */);
|
|
}
|
|
|
|
private void setupLauncherUiAfterSwipeUpAnimation() {
|
|
// Re apply state in case we did something funky during the transition.
|
|
mLauncher.getStateManager().reapplyState();
|
|
|
|
// Animate the first icon.
|
|
mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, true /* animate */);
|
|
|
|
reset();
|
|
}
|
|
|
|
public void onQuickScrubEnd() {
|
|
setStateOnUiThread(STATE_QUICK_SCRUB_END);
|
|
}
|
|
|
|
private void switchToFinalAppAfterQuickSwitch() {
|
|
mQuickScrubController.onQuickSwitch();
|
|
}
|
|
|
|
private void switchToFinalAppAfterQuickScrub() {
|
|
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);
|
|
}
|
|
|
|
public void onQuickScrubProgress(float progress) {
|
|
if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null) {
|
|
// TODO: We can still get progress events while launcher is not ready on the worker
|
|
// thread. Keep track of last received progress and apply that progress when launcher
|
|
// is ready
|
|
return;
|
|
}
|
|
mQuickScrubController.onQuickScrubProgress(progress);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|