mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-06 02:46:57 +00:00
2197 lines
100 KiB
Java
2197 lines
100 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 android.view.Surface.ROTATION_0;
|
|
import static android.view.Surface.ROTATION_270;
|
|
import static android.view.Surface.ROTATION_90;
|
|
import static android.widget.Toast.LENGTH_SHORT;
|
|
|
|
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.PagedView.INVALID_PAGE;
|
|
import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
|
|
import static com.android.launcher3.anim.Interpolators.DEACCEL;
|
|
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.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
|
|
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
|
|
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
|
|
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
|
|
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
|
|
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
|
|
import static com.android.launcher3.uioverrides.QuickstepLauncher.ENABLE_PIP_KEEP_CLEAR_ALGORITHM;
|
|
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
|
|
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
|
|
import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
|
|
import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
|
|
import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
|
|
import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
|
|
import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
|
|
import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
|
|
import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
|
|
import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
|
|
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_CANCELED;
|
|
import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
|
|
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
|
|
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
|
|
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
|
|
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
|
|
import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC;
|
|
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
|
|
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
|
|
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.TargetApi;
|
|
import android.app.Activity;
|
|
import android.app.ActivityManager;
|
|
import android.app.WindowConfiguration;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.graphics.Matrix;
|
|
import android.graphics.PointF;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.os.Build;
|
|
import android.os.IBinder;
|
|
import android.os.SystemClock;
|
|
import android.util.Log;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.View.OnApplyWindowInsetsListener;
|
|
import android.view.ViewTreeObserver.OnDrawListener;
|
|
import android.view.ViewTreeObserver.OnScrollChangedListener;
|
|
import android.view.WindowInsets;
|
|
import android.view.animation.Interpolator;
|
|
import android.widget.Toast;
|
|
import android.window.PictureInPictureSurfaceTransaction;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.UiThread;
|
|
|
|
import com.android.internal.util.LatencyTracker;
|
|
import com.android.launcher3.AbstractFloatingView;
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.anim.AnimationSuccessListener;
|
|
import com.android.launcher3.anim.AnimatorPlaybackController;
|
|
import com.android.launcher3.logging.StatsLogManager;
|
|
import com.android.launcher3.logging.StatsLogManager.StatsLogger;
|
|
import com.android.launcher3.statemanager.BaseState;
|
|
import com.android.launcher3.statemanager.StatefulActivity;
|
|
import com.android.launcher3.tracing.InputConsumerProto;
|
|
import com.android.launcher3.tracing.SwipeHandlerProto;
|
|
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
|
|
import com.android.launcher3.util.TraceHelper;
|
|
import com.android.launcher3.util.WindowBounds;
|
|
import com.android.quickstep.BaseActivityInterface.AnimationFactory;
|
|
import com.android.quickstep.GestureState.GestureEndTarget;
|
|
import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
|
|
import com.android.quickstep.util.ActiveGestureErrorDetector;
|
|
import com.android.quickstep.util.ActiveGestureLog;
|
|
import com.android.quickstep.util.ActivityInitListener;
|
|
import com.android.quickstep.util.AnimatorControllerWithResistance;
|
|
import com.android.quickstep.util.InputConsumerProxy;
|
|
import com.android.quickstep.util.InputProxyHandlerFactory;
|
|
import com.android.quickstep.util.MotionPauseDetector;
|
|
import com.android.quickstep.util.ProtoTracer;
|
|
import com.android.quickstep.util.RecentsOrientedState;
|
|
import com.android.quickstep.util.RectFSpringAnim;
|
|
import com.android.quickstep.util.StaggeredWorkspaceAnim;
|
|
import com.android.quickstep.util.SurfaceTransactionApplier;
|
|
import com.android.quickstep.util.SwipePipToHomeAnimator;
|
|
import com.android.quickstep.util.TaskViewSimulator;
|
|
import com.android.quickstep.util.VibratorWrapper;
|
|
import com.android.quickstep.views.RecentsView;
|
|
import com.android.quickstep.views.TaskView;
|
|
import com.android.systemui.shared.recents.model.Task;
|
|
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.InteractionJankMonitorWrapper;
|
|
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
|
|
import com.android.systemui.shared.system.TaskStackChangeListener;
|
|
import com.android.systemui.shared.system.TaskStackChangeListeners;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* Handles the navigation gestures when Launcher is the default home activity.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.R)
|
|
public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
|
|
Q extends RecentsView, S extends BaseState<S>>
|
|
extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener,
|
|
RecentsAnimationCallbacks.RecentsAnimationListener {
|
|
private static final String TAG = "AbsSwipeUpHandler";
|
|
|
|
private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
|
|
|
|
protected final BaseActivityInterface<S, T> mActivityInterface;
|
|
protected final InputConsumerProxy mInputConsumerProxy;
|
|
protected final ActivityInitListener mActivityInitListener;
|
|
// Callbacks to be made once the recents animation starts
|
|
private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
|
|
private final OnScrollChangedListener mOnRecentsScrollListener = this::onRecentsViewScroll;
|
|
|
|
// Null if the recents animation hasn't started yet or has been canceled or finished.
|
|
protected @Nullable RecentsAnimationController mRecentsAnimationController;
|
|
protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController;
|
|
protected RecentsAnimationTargets mRecentsAnimationTargets;
|
|
protected T mActivity;
|
|
protected Q mRecentsView;
|
|
protected Runnable mGestureEndCallback;
|
|
protected MultiStateCallback mStateCallback;
|
|
protected boolean mCanceled;
|
|
private boolean mRecentsViewScrollLinked = false;
|
|
private final ActivityLifecycleCallbacksAdapter mLifecycleCallbacks =
|
|
new ActivityLifecycleCallbacksAdapter() {
|
|
@Override
|
|
public void onActivityDestroyed(Activity activity) {
|
|
if (mActivity != activity) {
|
|
return;
|
|
}
|
|
mRecentsView = null;
|
|
mActivity = null;
|
|
}
|
|
};
|
|
|
|
private static int FLAG_COUNT = 0;
|
|
private static int getNextStateFlag(String name) {
|
|
if (DEBUG_STATES) {
|
|
STATE_NAMES.add(name);
|
|
}
|
|
int index = 1 << FLAG_COUNT;
|
|
FLAG_COUNT++;
|
|
return index;
|
|
}
|
|
|
|
// Launcher UI related states
|
|
protected static final int STATE_LAUNCHER_PRESENT =
|
|
getNextStateFlag("STATE_LAUNCHER_PRESENT");
|
|
protected static final int STATE_LAUNCHER_STARTED =
|
|
getNextStateFlag("STATE_LAUNCHER_STARTED");
|
|
protected static final int STATE_LAUNCHER_DRAWN =
|
|
getNextStateFlag("STATE_LAUNCHER_DRAWN");
|
|
// Called when the Launcher has connected to the touch interaction service (and the taskbar
|
|
// ui controller is initialized)
|
|
protected static final int STATE_LAUNCHER_BIND_TO_SERVICE =
|
|
getNextStateFlag("STATE_LAUNCHER_BIND_TO_SERVICE");
|
|
|
|
// Internal initialization states
|
|
private static final int STATE_APP_CONTROLLER_RECEIVED =
|
|
getNextStateFlag("STATE_APP_CONTROLLER_RECEIVED");
|
|
|
|
// Interaction finish states
|
|
private static final int STATE_SCALED_CONTROLLER_HOME =
|
|
getNextStateFlag("STATE_SCALED_CONTROLLER_HOME");
|
|
private static final int STATE_SCALED_CONTROLLER_RECENTS =
|
|
getNextStateFlag("STATE_SCALED_CONTROLLER_RECENTS");
|
|
|
|
protected static final int STATE_HANDLER_INVALIDATED =
|
|
getNextStateFlag("STATE_HANDLER_INVALIDATED");
|
|
private static final int STATE_GESTURE_STARTED =
|
|
getNextStateFlag("STATE_GESTURE_STARTED");
|
|
private static final int STATE_GESTURE_CANCELLED =
|
|
getNextStateFlag("STATE_GESTURE_CANCELLED");
|
|
private static final int STATE_GESTURE_COMPLETED =
|
|
getNextStateFlag("STATE_GESTURE_COMPLETED");
|
|
|
|
private static final int STATE_CAPTURE_SCREENSHOT =
|
|
getNextStateFlag("STATE_CAPTURE_SCREENSHOT");
|
|
protected static final int STATE_SCREENSHOT_CAPTURED =
|
|
getNextStateFlag("STATE_SCREENSHOT_CAPTURED");
|
|
private static final int STATE_SCREENSHOT_VIEW_SHOWN =
|
|
getNextStateFlag("STATE_SCREENSHOT_VIEW_SHOWN");
|
|
|
|
private static final int STATE_RESUME_LAST_TASK =
|
|
getNextStateFlag("STATE_RESUME_LAST_TASK");
|
|
private static final int STATE_START_NEW_TASK =
|
|
getNextStateFlag("STATE_START_NEW_TASK");
|
|
private static final int STATE_CURRENT_TASK_FINISHED =
|
|
getNextStateFlag("STATE_CURRENT_TASK_FINISHED");
|
|
private static final int STATE_FINISH_WITH_NO_END =
|
|
getNextStateFlag("STATE_FINISH_WITH_NO_END");
|
|
|
|
private static final int LAUNCHER_UI_STATES =
|
|
STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED |
|
|
STATE_LAUNCHER_BIND_TO_SERVICE;
|
|
|
|
public static final long MAX_SWIPE_DURATION = 350;
|
|
|
|
public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
|
|
private static final float SWIPE_DURATION_MULTIPLIER =
|
|
Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
|
|
private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
|
|
|
|
public static final long RECENTS_ATTACH_DURATION = 300;
|
|
|
|
private static final float MAX_QUICK_SWITCH_RECENTS_SCALE_PROGRESS = 0.07f;
|
|
|
|
/**
|
|
* Used as the page index for logging when we return to the last task at the end of the gesture.
|
|
*/
|
|
private static final int LOG_NO_OP_PAGE_INDEX = -1;
|
|
|
|
protected final TaskAnimationManager mTaskAnimationManager;
|
|
|
|
// Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
|
|
private RunningWindowAnim[] mRunningWindowAnim;
|
|
// Possible second animation running at the same time as mRunningWindowAnim
|
|
private Animator mParallelRunningAnim;
|
|
// Current running divider animation
|
|
private ValueAnimator mDividerAnimator;
|
|
private boolean mIsMotionPaused;
|
|
private boolean mHasMotionEverBeenPaused;
|
|
|
|
private boolean mContinuingLastGesture;
|
|
|
|
private ThumbnailData mTaskSnapshot;
|
|
|
|
// Used to control launcher components throughout the swipe gesture.
|
|
private AnimatorControllerWithResistance mLauncherTransitionController;
|
|
private boolean mHasEndedLauncherTransition;
|
|
|
|
private AnimationFactory mAnimationFactory = (t) -> { };
|
|
|
|
private boolean mWasLauncherAlreadyVisible;
|
|
|
|
private boolean mPassedOverviewThreshold;
|
|
private boolean mGestureStarted;
|
|
private boolean mLogDirectionUpOrLeft = true;
|
|
private PointF mDownPos;
|
|
private boolean mIsLikelyToStartNewTask;
|
|
|
|
private final long mTouchTimeMs;
|
|
private long mLauncherFrameDrawnTime;
|
|
|
|
private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
|
|
|
|
private SwipePipToHomeAnimator mSwipePipToHomeAnimator;
|
|
protected boolean mIsSwipingPipToHome;
|
|
// TODO(b/195473090) no split PIP for now, remove once we have more clarity
|
|
// can try to have RectFSpringAnim evaluate multiple rects at once
|
|
private final SwipePipToHomeAnimator[] mSwipePipToHomeAnimators =
|
|
new SwipePipToHomeAnimator[2];
|
|
|
|
// Interpolate RecentsView scale from start of quick switch scroll until this scroll threshold
|
|
private final float mQuickSwitchScaleScrollThreshold;
|
|
|
|
public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
|
|
TaskAnimationManager taskAnimationManager, GestureState gestureState,
|
|
long touchTimeMs, boolean continuingLastGesture,
|
|
InputConsumerController inputConsumer) {
|
|
super(context, deviceState, gestureState);
|
|
mActivityInterface = gestureState.getActivityInterface();
|
|
mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
|
|
mInputConsumerProxy =
|
|
new InputConsumerProxy(context, /* rotationSupplier = */ () -> {
|
|
if (mRecentsView == null) {
|
|
return ROTATION_0;
|
|
}
|
|
return mRecentsView.getPagedViewOrientedState().getRecentsActivityRotation();
|
|
}, inputConsumer, /* callback = */ () -> {
|
|
endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
|
|
endLauncherTransitionController();
|
|
}, new InputProxyHandlerFactory(mActivityInterface, mGestureState));
|
|
mTaskAnimationManager = taskAnimationManager;
|
|
mTouchTimeMs = touchTimeMs;
|
|
mContinuingLastGesture = continuingLastGesture;
|
|
mQuickSwitchScaleScrollThreshold = context.getResources().getDimension(
|
|
R.dimen.quick_switch_scaling_scroll_threshold);
|
|
|
|
initAfterSubclassConstructor();
|
|
initStateCallbacks();
|
|
}
|
|
|
|
@Nullable
|
|
private static ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateFlag) {
|
|
if (stateFlag == STATE_GESTURE_STARTED) {
|
|
return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_STARTED;
|
|
} else if (stateFlag == STATE_GESTURE_COMPLETED) {
|
|
return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_COMPLETED;
|
|
} else if (stateFlag == STATE_GESTURE_CANCELLED) {
|
|
return ActiveGestureErrorDetector.GestureEvent.STATE_GESTURE_CANCELLED;
|
|
} else if (stateFlag == STATE_SCREENSHOT_CAPTURED) {
|
|
return ActiveGestureErrorDetector.GestureEvent.STATE_SCREENSHOT_CAPTURED;
|
|
} else if (stateFlag == STATE_CAPTURE_SCREENSHOT) {
|
|
return ActiveGestureErrorDetector.GestureEvent.STATE_CAPTURE_SCREENSHOT;
|
|
} else if (stateFlag == STATE_HANDLER_INVALIDATED) {
|
|
return ActiveGestureErrorDetector.GestureEvent.STATE_HANDLER_INVALIDATED;
|
|
} else if (stateFlag == STATE_LAUNCHER_DRAWN) {
|
|
return ActiveGestureErrorDetector.GestureEvent.STATE_LAUNCHER_DRAWN;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void initStateCallbacks() {
|
|
mStateCallback = new MultiStateCallback(
|
|
STATE_NAMES.toArray(new String[0]), AbsSwipeUpHandler::getTrackedEventForState);
|
|
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
|
|
this::onLauncherPresentAndGestureStarted);
|
|
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
|
|
this::initializeLauncherAnimationController);
|
|
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
|
|
this::launcherFrameDrawn);
|
|
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
|
|
| STATE_GESTURE_CANCELLED,
|
|
this::resetStateForAnimationCancel);
|
|
|
|
mStateCallback.runOnceAtState(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
|
|
this::resumeLastTask);
|
|
mStateCallback.runOnceAtState(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
|
|
this::startNewTask);
|
|
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
|
|
| STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT,
|
|
this::switchToScreenshot);
|
|
|
|
mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
|
|
| STATE_SCALED_CONTROLLER_RECENTS,
|
|
this::finishCurrentTransitionToRecents);
|
|
|
|
mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
|
|
| STATE_SCALED_CONTROLLER_HOME,
|
|
this::finishCurrentTransitionToHome);
|
|
mStateCallback.runOnceAtState(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
|
|
this::reset);
|
|
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
|
|
| STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
|
|
| STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
|
|
| STATE_GESTURE_STARTED,
|
|
this::setupLauncherUiAfterSwipeUpToRecentsAnimation);
|
|
|
|
mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED,
|
|
this::continueComputingRecentsScrollIfNecessary);
|
|
mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED
|
|
| STATE_RECENTS_SCROLLING_FINISHED,
|
|
this::onSettledOnEndTarget);
|
|
|
|
mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
|
|
this::invalidateHandlerWithLauncher);
|
|
mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
|
|
this::resetStateForAnimationCancel);
|
|
mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_FINISH_WITH_NO_END,
|
|
this::resetStateForAnimationCancel);
|
|
|
|
if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
|
|
mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
|
|
| STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
|
|
(b) -> mRecentsView.setRunningTaskHidden(!b));
|
|
}
|
|
}
|
|
|
|
protected boolean onActivityInit(Boolean alreadyOnHome) {
|
|
if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
|
|
return false;
|
|
}
|
|
|
|
T createdActivity = mActivityInterface.getCreatedActivity();
|
|
if (createdActivity != null) {
|
|
initTransitionEndpoints(createdActivity.getDeviceProfile());
|
|
}
|
|
final T activity = mActivityInterface.getCreatedActivity();
|
|
if (mActivity == activity) {
|
|
return true;
|
|
}
|
|
|
|
if (mActivity != null) {
|
|
if (mStateCallback.hasStates(STATE_GESTURE_COMPLETED)) {
|
|
// If the activity has restarted between setting the page scroll settling callback
|
|
// and actually receiving the callback, just mark the gesture completed
|
|
mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
|
|
return true;
|
|
}
|
|
|
|
// The launcher may have been recreated as a result of device rotation.
|
|
int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
|
|
initStateCallbacks();
|
|
mStateCallback.setState(oldState);
|
|
}
|
|
mWasLauncherAlreadyVisible = alreadyOnHome;
|
|
mActivity = activity;
|
|
// Override the visibility of the activity until the gesture actually starts and we swipe
|
|
// up, or until we transition home and the home animation is composed
|
|
if (alreadyOnHome) {
|
|
mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
|
|
} else {
|
|
mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
|
|
}
|
|
|
|
mRecentsView = activity.getOverviewPanel();
|
|
mRecentsView.setOnPageTransitionEndCallback(null);
|
|
|
|
mStateCallback.setState(STATE_LAUNCHER_PRESENT);
|
|
if (alreadyOnHome) {
|
|
onLauncherStart();
|
|
} else {
|
|
activity.runOnceOnStart(this::onLauncherStart);
|
|
}
|
|
|
|
// Set up a entire animation lifecycle callback to notify the current recents view when
|
|
// the animation is canceled
|
|
mGestureState.runOnceAtState(STATE_RECENTS_ANIMATION_CANCELED, () -> {
|
|
HashMap<Integer, ThumbnailData> snapshots =
|
|
mGestureState.consumeRecentsAnimationCanceledSnapshot();
|
|
if (snapshots != null) {
|
|
mRecentsView.switchToScreenshot(snapshots, () -> {
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.cleanupScreenshot();
|
|
} else if (mDeferredCleanupRecentsAnimationController != null) {
|
|
mDeferredCleanupRecentsAnimationController.cleanupScreenshot();
|
|
mDeferredCleanupRecentsAnimationController = null;
|
|
}
|
|
});
|
|
mRecentsView.onRecentsAnimationComplete();
|
|
}
|
|
});
|
|
|
|
setupRecentsViewUi();
|
|
mRecentsView.runOnPageScrollsInitialized(this::linkRecentsViewScroll);
|
|
activity.runOnBindToTouchInteractionService(this::onLauncherBindToService);
|
|
|
|
mActivity.registerActivityLifecycleCallbacks(mLifecycleCallbacks);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return true if the window should be translated horizontally if the recents view scrolls
|
|
*/
|
|
protected boolean moveWindowWithRecentsScroll() {
|
|
return mGestureState.getEndTarget() != HOME;
|
|
}
|
|
|
|
private void onLauncherStart() {
|
|
final T activity = mActivityInterface.getCreatedActivity();
|
|
if (mActivity != activity) {
|
|
return;
|
|
}
|
|
if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
|
|
return;
|
|
}
|
|
// RecentsView never updates the display rotation until swipe-up, force update
|
|
// RecentsOrientedState before passing to TaskViewSimulator.
|
|
mRecentsView.updateRecentsRotation();
|
|
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
|
|
.setOrientationState(mRecentsView.getPagedViewOrientedState()));
|
|
|
|
// If we've already ended the gesture and are going home, don't prepare recents UI,
|
|
// as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
|
|
if (mGestureState.getEndTarget() != HOME) {
|
|
Runnable initAnimFactory = () -> {
|
|
mAnimationFactory = mActivityInterface.prepareRecentsUI(mDeviceState,
|
|
mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated);
|
|
maybeUpdateRecentsAttachedState(false /* animate */);
|
|
if (mGestureState.getEndTarget() != null) {
|
|
// Update the end target in case the gesture ended before we init.
|
|
mAnimationFactory.setEndTarget(mGestureState.getEndTarget());
|
|
}
|
|
};
|
|
if (mWasLauncherAlreadyVisible) {
|
|
// Launcher is visible, but might be about to stop. Thus, if we prepare recents
|
|
// now, it might get overridden by moveToRestState() in onStop(). To avoid this,
|
|
// wait until the next gesture (and possibly launcher) starts.
|
|
mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, initAnimFactory);
|
|
} else {
|
|
initAnimFactory.run();
|
|
}
|
|
}
|
|
AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible,
|
|
AbstractFloatingView.TYPE_LISTENER);
|
|
|
|
if (mWasLauncherAlreadyVisible) {
|
|
mStateCallback.setState(STATE_LAUNCHER_DRAWN);
|
|
} else {
|
|
Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init");
|
|
View dragLayer = activity.getDragLayer();
|
|
dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
|
|
boolean mHandled = false;
|
|
|
|
@Override
|
|
public void onDraw() {
|
|
if (mHandled) {
|
|
return;
|
|
}
|
|
mHandled = true;
|
|
|
|
TraceHelper.INSTANCE.endSection(traceToken);
|
|
dragLayer.post(() ->
|
|
dragLayer.getViewTreeObserver().removeOnDrawListener(this));
|
|
if (activity != mActivity) {
|
|
return;
|
|
}
|
|
|
|
mStateCallback.setState(STATE_LAUNCHER_DRAWN);
|
|
}
|
|
});
|
|
}
|
|
|
|
activity.getRootView().setOnApplyWindowInsetsListener(this);
|
|
mStateCallback.setState(STATE_LAUNCHER_STARTED);
|
|
}
|
|
|
|
private void onLauncherBindToService() {
|
|
mStateCallback.setState(STATE_LAUNCHER_BIND_TO_SERVICE);
|
|
flushOnRecentsAnimationAndLauncherBound();
|
|
}
|
|
|
|
private void onLauncherPresentAndGestureStarted() {
|
|
// Re-setup the recents UI when gesture starts, as the state could have been changed during
|
|
// that time by a previous window transition.
|
|
setupRecentsViewUi();
|
|
|
|
// For the duration of the gesture, in cases where an activity is launched while the
|
|
// activity is not yet resumed, finish the animation to ensure we get resumed
|
|
mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback(
|
|
mOnDeferredActivityLaunch);
|
|
|
|
mGestureState.runOnceAtState(STATE_END_TARGET_SET,
|
|
() -> {
|
|
mDeviceState.getRotationTouchHelper()
|
|
.onEndTargetCalculated(mGestureState.getEndTarget(),
|
|
mActivityInterface);
|
|
});
|
|
|
|
notifyGestureStartedAsync();
|
|
}
|
|
|
|
private void onDeferredActivityLaunch() {
|
|
if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
|
|
mActivityInterface.switchRunningTaskViewToScreenshot(
|
|
null, () -> {
|
|
mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
|
|
});
|
|
} else {
|
|
mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
|
|
}
|
|
}
|
|
|
|
private void setupRecentsViewUi() {
|
|
if (mContinuingLastGesture) {
|
|
updateSysUiFlags(mCurrentShift.value);
|
|
return;
|
|
}
|
|
notifyGestureAnimationStartToRecents();
|
|
}
|
|
|
|
protected void notifyGestureAnimationStartToRecents() {
|
|
Task[] runningTasks;
|
|
if (mIsSwipeForSplit) {
|
|
int[] splitTaskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds();
|
|
runningTasks = mGestureState.getRunningTask().getPlaceholderTasks(splitTaskIds);
|
|
} else {
|
|
runningTasks = mGestureState.getRunningTask().getPlaceholderTasks();
|
|
}
|
|
mRecentsView.onGestureAnimationStart(runningTasks, mDeviceState.getRotationTouchHelper());
|
|
}
|
|
|
|
private void launcherFrameDrawn() {
|
|
mLauncherFrameDrawnTime = SystemClock.uptimeMillis();
|
|
}
|
|
|
|
private void initializeLauncherAnimationController() {
|
|
buildAnimationController();
|
|
|
|
Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents",
|
|
TraceHelper.FLAG_IGNORE_BINDERS);
|
|
LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS,
|
|
(int) (mLauncherFrameDrawnTime - mTouchTimeMs));
|
|
TraceHelper.INSTANCE.endSection(traceToken);
|
|
|
|
// This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
|
|
// high-res thumbnail loader here once we are sure that we will end up in an overview state
|
|
RecentsModel.INSTANCE.get(mContext).getThumbnailCache()
|
|
.getHighResLoadingState().setVisible(true);
|
|
}
|
|
|
|
public MotionPauseDetector.OnMotionPauseListener getMotionPauseListener() {
|
|
return new MotionPauseDetector.OnMotionPauseListener() {
|
|
@Override
|
|
public void onMotionPauseDetected() {
|
|
mHasMotionEverBeenPaused = true;
|
|
maybeUpdateRecentsAttachedState(true/* animate */, true/* moveFocusedTask */);
|
|
performHapticFeedback();
|
|
}
|
|
|
|
@Override
|
|
public void onMotionPauseChanged(boolean isPaused) {
|
|
mIsMotionPaused = isPaused;
|
|
}
|
|
};
|
|
}
|
|
|
|
private void maybeUpdateRecentsAttachedState() {
|
|
maybeUpdateRecentsAttachedState(true /* animate */);
|
|
}
|
|
|
|
private void maybeUpdateRecentsAttachedState(boolean animate) {
|
|
maybeUpdateRecentsAttachedState(animate, false /* moveFocusedTask */);
|
|
}
|
|
|
|
/**
|
|
* Determines whether to show or hide RecentsView. The window is always
|
|
* synchronized with its corresponding TaskView in RecentsView, so if
|
|
* RecentsView is shown, it will appear to be attached to the window.
|
|
*
|
|
* Note this method has no effect unless the navigation mode is NO_BUTTON.
|
|
* @param animate whether to animate when attaching RecentsView
|
|
* @param moveFocusedTask whether to move focused task to front when attaching
|
|
*/
|
|
private void maybeUpdateRecentsAttachedState(boolean animate, boolean moveFocusedTask) {
|
|
if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
|
|
return;
|
|
}
|
|
RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
|
|
? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
|
|
: null;
|
|
final boolean recentsAttachedToAppWindow;
|
|
if (mGestureState.getEndTarget() != null) {
|
|
recentsAttachedToAppWindow = mGestureState.getEndTarget().recentsAttachedToAppWindow;
|
|
} else if (mContinuingLastGesture
|
|
&& mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
|
|
recentsAttachedToAppWindow = true;
|
|
} else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
|
|
// The window is going away so make sure recents is always visible in this case.
|
|
recentsAttachedToAppWindow = true;
|
|
} else {
|
|
recentsAttachedToAppWindow = mHasMotionEverBeenPaused || mIsLikelyToStartNewTask;
|
|
}
|
|
if (moveFocusedTask && !mAnimationFactory.hasRecentsEverAttachedToAppWindow()
|
|
&& recentsAttachedToAppWindow) {
|
|
// Only move focused task if RecentsView has never been attached before, to avoid
|
|
// TaskView jumping to new position as we move the tasks.
|
|
mRecentsView.moveFocusedTaskToFront();
|
|
}
|
|
mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
|
|
|
|
// Reapply window transform throughout the attach animation, as the animation affects how
|
|
// much the window is bound by overscroll (vs moving freely).
|
|
if (animate) {
|
|
ValueAnimator reapplyWindowTransformAnim = ValueAnimator.ofFloat(0, 1);
|
|
reapplyWindowTransformAnim.addUpdateListener(anim -> {
|
|
if (mRunningWindowAnim == null || mRunningWindowAnim.length == 0) {
|
|
applyScrollAndTransform();
|
|
}
|
|
});
|
|
reapplyWindowTransformAnim.setDuration(RECENTS_ATTACH_DURATION).start();
|
|
mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED,
|
|
reapplyWindowTransformAnim::cancel);
|
|
} else {
|
|
applyScrollAndTransform();
|
|
}
|
|
}
|
|
|
|
public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
|
|
setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
|
|
}
|
|
|
|
private void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask, boolean animate) {
|
|
if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
|
|
mIsLikelyToStartNewTask = isLikelyToStartNewTask;
|
|
maybeUpdateRecentsAttachedState(animate);
|
|
}
|
|
}
|
|
|
|
private void buildAnimationController() {
|
|
if (!canCreateNewOrUpdateExistingLauncherTransitionController()) {
|
|
return;
|
|
}
|
|
initTransitionEndpoints(mActivity.getDeviceProfile());
|
|
mAnimationFactory.createActivityInterface(mTransitionDragLength);
|
|
}
|
|
|
|
/**
|
|
* We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
|
|
* (it has its own animation) or if we explicitly ended the controller already.
|
|
* @return Whether we can create the launcher controller or update its progress.
|
|
*/
|
|
private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
|
|
return mGestureState.getEndTarget() != HOME && !mHasEndedLauncherTransition;
|
|
}
|
|
|
|
@Override
|
|
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
|
|
WindowInsets result = view.onApplyWindowInsets(windowInsets);
|
|
buildAnimationController();
|
|
// Reapply the current shift to ensure it takes new insets into account, e.g. when long
|
|
// pressing to stash taskbar without moving the finger.
|
|
updateFinalShift();
|
|
return result;
|
|
}
|
|
|
|
private void onAnimatorPlaybackControllerCreated(AnimatorControllerWithResistance anim) {
|
|
boolean isFirstCreation = mLauncherTransitionController == null;
|
|
mLauncherTransitionController = anim;
|
|
if (isFirstCreation) {
|
|
mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, () -> {
|
|
// Wait until the gesture is started (touch slop was passed) to start in sync with
|
|
// mWindowTransitionController. This ensures we don't hide the taskbar background
|
|
// when long pressing to stash it, for instance.
|
|
mLauncherTransitionController.getNormalController().dispatchOnStart();
|
|
updateLauncherTransitionProgress();
|
|
});
|
|
}
|
|
}
|
|
|
|
public Intent getLaunchIntent() {
|
|
return mGestureState.getOverviewIntent();
|
|
}
|
|
|
|
/**
|
|
* Called when the value of {@link #mCurrentShift} changes
|
|
*/
|
|
@UiThread
|
|
@Override
|
|
public void updateFinalShift() {
|
|
final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
|
|
if (passed != mPassedOverviewThreshold) {
|
|
mPassedOverviewThreshold = passed;
|
|
if (mDeviceState.isTwoButtonNavMode() && !mGestureState.isHandlingAtomicEvent()) {
|
|
performHapticFeedback();
|
|
}
|
|
}
|
|
|
|
updateSysUiFlags(mCurrentShift.value);
|
|
applyScrollAndTransform();
|
|
|
|
updateLauncherTransitionProgress();
|
|
}
|
|
|
|
private void updateLauncherTransitionProgress() {
|
|
if (mLauncherTransitionController == null
|
|
|| !canCreateNewOrUpdateExistingLauncherTransitionController()) {
|
|
return;
|
|
}
|
|
mLauncherTransitionController.setProgress(
|
|
Math.max(mCurrentShift.value, getScaleProgressDueToScroll()), mDragLengthFactor);
|
|
}
|
|
|
|
/**
|
|
* @param windowProgress 0 == app, 1 == overview
|
|
*/
|
|
private void updateSysUiFlags(float windowProgress) {
|
|
if (mRecentsAnimationController != null && mRecentsView != null) {
|
|
TaskView runningTask = mRecentsView.getRunningTaskView();
|
|
TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
|
|
int centermostTaskFlags = centermostTask == null ? 0
|
|
: centermostTask.getThumbnail().getSysUiStatusNavFlags();
|
|
boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
|
|
boolean quickswitchThresholdPassed = centermostTask != runningTask;
|
|
|
|
// We will handle the sysui flags based on the centermost task view.
|
|
mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed
|
|
|| (quickswitchThresholdPassed && centermostTaskFlags != 0));
|
|
mRecentsAnimationController.setSplitScreenMinimized(mContext, swipeUpThresholdPassed);
|
|
// Provide a hint to WM the direction that we will be settling in case the animation
|
|
// needs to be canceled
|
|
mRecentsAnimationController.setWillFinishToHome(swipeUpThresholdPassed);
|
|
|
|
if (swipeUpThresholdPassed) {
|
|
mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
|
|
} else {
|
|
mActivity.getSystemUiController().updateUiState(
|
|
UI_STATE_FULLSCREEN_TASK, centermostTaskFlags);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRecentsAnimationStart(RecentsAnimationController controller,
|
|
RecentsAnimationTargets targets) {
|
|
super.onRecentsAnimationStart(controller, targets);
|
|
mRemoteTargetHandles = mTargetGluer.assignTargetsForSplitScreen(mContext, targets);
|
|
mRecentsAnimationController = controller;
|
|
mRecentsAnimationTargets = targets;
|
|
|
|
// Only initialize the device profile, if it has not been initialized before, as in some
|
|
// configurations targets.homeContentInsets may not be correct.
|
|
if (mActivity == null) {
|
|
RemoteAnimationTargetCompat primaryTaskTarget = targets.apps[0];
|
|
// orientation state is independent of which remote target handle we use since both
|
|
// should be pointing to the same one. Just choose index 0 for now since that works for
|
|
// both split and non-split
|
|
RecentsOrientedState orientationState = mRemoteTargetHandles[0].getTaskViewSimulator()
|
|
.getOrientationState();
|
|
DeviceProfile dp = orientationState.getLauncherDeviceProfile();
|
|
if (targets.minimizedHomeBounds != null && primaryTaskTarget != null) {
|
|
Rect overviewStackBounds = mActivityInterface
|
|
.getOverviewWindowBounds(targets.minimizedHomeBounds, primaryTaskTarget);
|
|
dp = dp.getMultiWindowProfile(mContext,
|
|
new WindowBounds(overviewStackBounds, targets.homeContentInsets));
|
|
} else {
|
|
// If we are not in multi-window mode, home insets should be same as system insets.
|
|
dp = dp.copy(mContext);
|
|
}
|
|
dp.updateInsets(targets.homeContentInsets);
|
|
dp.updateIsSeascape(mContext);
|
|
initTransitionEndpoints(dp);
|
|
orientationState.setMultiWindowMode(dp.isMultiWindowMode);
|
|
}
|
|
|
|
// Notify when the animation starts
|
|
flushOnRecentsAnimationAndLauncherBound();
|
|
|
|
// Only add the callback to enable the input consumer after we actually have the controller
|
|
mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
|
|
this::startInterceptingTouchesForGesture);
|
|
mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
|
|
|
|
mPassedOverviewThreshold = false;
|
|
}
|
|
|
|
@Override
|
|
public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
/* event= */ "cancelRecentsAnimation",
|
|
/* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
|
|
mActivityInitListener.unregister();
|
|
// Cache the recents animation controller so we can defer its cleanup to after having
|
|
// properly cleaned up the screenshot without accidentally using it.
|
|
mDeferredCleanupRecentsAnimationController = mRecentsAnimationController;
|
|
mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
|
|
// Defer clearing the controller and the targets until after we've updated the state
|
|
mRecentsAnimationController = null;
|
|
mRecentsAnimationTargets = null;
|
|
if (mRecentsView != null) {
|
|
mRecentsView.setRecentsAnimationTargets(null, null);
|
|
}
|
|
}
|
|
|
|
@UiThread
|
|
public void onGestureStarted(boolean isLikelyToStartNewTask) {
|
|
mActivityInterface.closeOverlay();
|
|
TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
|
|
|
|
if (mRecentsView != null) {
|
|
final View rv = mRecentsView;
|
|
mRecentsView.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
|
|
boolean mHandled = false;
|
|
|
|
@Override
|
|
public void onDraw() {
|
|
if (mHandled) {
|
|
return;
|
|
}
|
|
mHandled = true;
|
|
|
|
InteractionJankMonitorWrapper.begin(mRecentsView,
|
|
InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH, 2000 /* ms timeout */);
|
|
InteractionJankMonitorWrapper.begin(mRecentsView,
|
|
InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
|
|
|
|
rv.post(() -> rv.getViewTreeObserver().removeOnDrawListener(this));
|
|
}
|
|
});
|
|
}
|
|
notifyGestureStartedAsync();
|
|
setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
|
|
mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
|
|
mGestureStarted = true;
|
|
SystemUiProxy.INSTANCE.get(mContext).notifySwipeUpGestureStarted();
|
|
}
|
|
|
|
/**
|
|
* Notifies the launcher that the swipe gesture has started. This can be called multiple times.
|
|
*/
|
|
@UiThread
|
|
private void notifyGestureStartedAsync() {
|
|
final T curActivity = mActivity;
|
|
if (curActivity != null) {
|
|
// Once the gesture starts, we can no longer transition home through the button, so
|
|
// reset the force override of the activity visibility
|
|
mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called as a result on ACTION_CANCEL to return the UI to the start state.
|
|
*/
|
|
@UiThread
|
|
public void onGestureCancelled() {
|
|
updateDisplacement(0);
|
|
mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
|
|
handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */);
|
|
}
|
|
|
|
/**
|
|
* @param endVelocity The velocity in the direction of the nav bar to the middle of the screen.
|
|
* @param velocity The x and y components of the velocity when the gesture ends.
|
|
* @param downPos The x and y value of where the gesture started.
|
|
*/
|
|
@UiThread
|
|
public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
|
|
float flingThreshold = mContext.getResources()
|
|
.getDimension(R.dimen.quickstep_fling_threshold_speed);
|
|
boolean isFling = mGestureStarted && !mIsMotionPaused
|
|
&& Math.abs(endVelocity) > flingThreshold;
|
|
mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
|
|
boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
|
|
if (isVelocityVertical) {
|
|
mLogDirectionUpOrLeft = velocity.y < 0;
|
|
} else {
|
|
mLogDirectionUpOrLeft = velocity.x < 0;
|
|
}
|
|
mDownPos = downPos;
|
|
Runnable handleNormalGestureEndCallback = () ->
|
|
handleNormalGestureEnd(endVelocity, isFling, velocity, /* isCancel= */ false);
|
|
if (mRecentsView != null) {
|
|
mRecentsView.runOnPageScrollsInitialized(handleNormalGestureEndCallback);
|
|
} else {
|
|
handleNormalGestureEndCallback.run();
|
|
}
|
|
}
|
|
|
|
private void endRunningWindowAnim(boolean cancel) {
|
|
if (mRunningWindowAnim != null) {
|
|
if (cancel) {
|
|
for (RunningWindowAnim r : mRunningWindowAnim) {
|
|
if (r != null) {
|
|
r.cancel();
|
|
}
|
|
}
|
|
} else {
|
|
for (RunningWindowAnim r : mRunningWindowAnim) {
|
|
if (r != null) {
|
|
r.end();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (mParallelRunningAnim != null) {
|
|
// Unlike the above animation, the parallel animation won't have anything to take up
|
|
// the work if it's canceled, so just end it instead.
|
|
mParallelRunningAnim.end();
|
|
}
|
|
}
|
|
|
|
private void onSettledOnEndTarget() {
|
|
// Fast-finish the attaching animation if it's still running.
|
|
maybeUpdateRecentsAttachedState(false);
|
|
final GestureEndTarget endTarget = mGestureState.getEndTarget();
|
|
// Wait until the given View (if supplied) draws before resuming the last task.
|
|
View postResumeLastTask = mActivityInterface.onSettledOnEndTarget(endTarget);
|
|
|
|
if (endTarget != NEW_TASK) {
|
|
InteractionJankMonitorWrapper.cancel(
|
|
InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
|
|
}
|
|
if (endTarget != HOME) {
|
|
InteractionJankMonitorWrapper.cancel(
|
|
InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
|
|
}
|
|
|
|
switch (endTarget) {
|
|
case HOME:
|
|
mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
|
|
// Notify swipe-to-home (recents animation) is finished
|
|
SystemUiProxy.INSTANCE.get(mContext).notifySwipeToHomeFinished();
|
|
break;
|
|
case RECENTS:
|
|
mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
|
|
| STATE_SCREENSHOT_VIEW_SHOWN);
|
|
break;
|
|
case NEW_TASK:
|
|
mStateCallback.setState(STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT);
|
|
break;
|
|
case LAST_TASK:
|
|
if (postResumeLastTask != null) {
|
|
ViewUtils.postFrameDrawn(postResumeLastTask,
|
|
() -> mStateCallback.setState(STATE_RESUME_LAST_TASK));
|
|
} else {
|
|
mStateCallback.setState(STATE_RESUME_LAST_TASK);
|
|
}
|
|
if (mRecentsAnimationTargets != null) {
|
|
setDividerShown(true /* shown */, true /* immediate */);
|
|
}
|
|
break;
|
|
}
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
/* event= */ "onSettledOnEndTarget " + endTarget,
|
|
/* gestureEvent= */ ON_SETTLED_ON_END_TARGET);
|
|
}
|
|
|
|
/** @return Whether this was the task we were waiting to appear, and thus handled it. */
|
|
protected boolean handleTaskAppeared(RemoteAnimationTargetCompat[] appearedTaskTarget) {
|
|
if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
|
|
return false;
|
|
}
|
|
boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTarget).anyMatch(
|
|
targetCompat -> targetCompat.taskId == mGestureState.getLastStartedTaskId());
|
|
if (mStateCallback.hasStates(STATE_START_NEW_TASK) && hasStartedTaskBefore) {
|
|
reset();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private GestureEndTarget calculateEndTarget(
|
|
PointF velocity, float endVelocity, boolean isFlingY, boolean isCancel) {
|
|
|
|
if (mGestureState.isHandlingAtomicEvent()) {
|
|
// Button mode, this is only used to go to recents.
|
|
return RECENTS;
|
|
}
|
|
|
|
GestureEndTarget endTarget;
|
|
if (isCancel) {
|
|
endTarget = LAST_TASK;
|
|
} else if (isFlingY) {
|
|
endTarget = calculateEndTargetForFlingY(velocity, endVelocity);
|
|
} else {
|
|
endTarget = calculateEndTargetForNonFling(velocity);
|
|
}
|
|
|
|
if (mDeviceState.isOverviewDisabled() && endTarget == RECENTS) {
|
|
return LAST_TASK;
|
|
}
|
|
|
|
return endTarget;
|
|
}
|
|
|
|
private GestureEndTarget calculateEndTargetForFlingY(PointF velocity, float endVelocity) {
|
|
// If swiping at a diagonal, base end target on the faster velocity direction.
|
|
final boolean willGoToNewTask =
|
|
isScrollingToNewTask() && Math.abs(velocity.x) > Math.abs(endVelocity);
|
|
final boolean isSwipeUp = endVelocity < 0;
|
|
if (!isSwipeUp) {
|
|
final boolean isCenteredOnNewTask =
|
|
mRecentsView.getDestinationPage() != mRecentsView.getRunningTaskIndex();
|
|
return willGoToNewTask || isCenteredOnNewTask ? NEW_TASK : LAST_TASK;
|
|
}
|
|
|
|
if (!mDeviceState.isFullyGesturalNavMode()) {
|
|
return (!hasReachedOverviewThreshold() && willGoToNewTask) ? NEW_TASK : RECENTS;
|
|
}
|
|
return willGoToNewTask ? NEW_TASK : HOME;
|
|
}
|
|
|
|
private GestureEndTarget calculateEndTargetForNonFling(PointF velocity) {
|
|
final boolean isScrollingToNewTask = isScrollingToNewTask();
|
|
final boolean reachedOverviewThreshold = hasReachedOverviewThreshold();
|
|
if (!mDeviceState.isFullyGesturalNavMode()) {
|
|
return reachedOverviewThreshold && mGestureStarted
|
|
? RECENTS
|
|
: (isScrollingToNewTask ? NEW_TASK : LAST_TASK);
|
|
}
|
|
|
|
// Fully gestural mode.
|
|
final boolean isFlingX = Math.abs(velocity.x) > mContext.getResources()
|
|
.getDimension(R.dimen.quickstep_fling_threshold_speed);
|
|
if (isScrollingToNewTask && isFlingX) {
|
|
// Flinging towards new task takes precedence over mIsMotionPaused (which only
|
|
// checks y-velocity).
|
|
return NEW_TASK;
|
|
} else if (mIsMotionPaused) {
|
|
return RECENTS;
|
|
} else if (isScrollingToNewTask) {
|
|
return NEW_TASK;
|
|
} else if (reachedOverviewThreshold) {
|
|
return HOME;
|
|
}
|
|
return LAST_TASK;
|
|
}
|
|
|
|
private boolean isScrollingToNewTask() {
|
|
if (mRecentsView == null) {
|
|
return false;
|
|
}
|
|
if (!hasTargets()) {
|
|
// If there are no running tasks, then we can assume that this is a continuation of
|
|
// the last gesture, but after the recents animation has finished.
|
|
return true;
|
|
}
|
|
int runningTaskIndex = mRecentsView.getRunningTaskIndex();
|
|
return runningTaskIndex >= 0 && mRecentsView.getNextPage() != runningTaskIndex;
|
|
}
|
|
|
|
private boolean hasReachedOverviewThreshold() {
|
|
return mCurrentShift.value > MIN_PROGRESS_FOR_OVERVIEW;
|
|
}
|
|
|
|
@UiThread
|
|
private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity,
|
|
boolean isCancel) {
|
|
long duration = MAX_SWIPE_DURATION;
|
|
float currentShift = mCurrentShift.value;
|
|
final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
|
|
isFling, isCancel);
|
|
// Set the state, but don't notify until the animation completes
|
|
mGestureState.setEndTarget(endTarget, false /* isAtomic */);
|
|
mAnimationFactory.setEndTarget(endTarget);
|
|
|
|
float endShift = endTarget.isLauncher ? 1 : 0;
|
|
final float startShift;
|
|
if (!isFling) {
|
|
long expectedDuration = Math.abs(Math.round((endShift - currentShift)
|
|
* MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
|
|
duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
|
|
startShift = currentShift;
|
|
} else {
|
|
startShift = Utilities.boundToRange(currentShift - velocity.y
|
|
* getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
|
|
if (mTransitionDragLength > 0) {
|
|
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 / velocity.y));
|
|
duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
|
|
}
|
|
}
|
|
Interpolator interpolator;
|
|
S state = mActivityInterface.stateFromGestureEndTarget(endTarget);
|
|
if (state.displayOverviewTasksAsGrid(mDp)) {
|
|
interpolator = ACCEL_DEACCEL;
|
|
} else if (endTarget == RECENTS) {
|
|
interpolator = OVERSHOOT_1_2;
|
|
} else {
|
|
interpolator = DEACCEL;
|
|
}
|
|
|
|
if (endTarget.isLauncher) {
|
|
mInputConsumerProxy.enable();
|
|
}
|
|
if (endTarget == HOME) {
|
|
duration = mActivity != null && mActivity.getDeviceProfile().isTaskbarPresent
|
|
? StaggeredWorkspaceAnim.DURATION_TASKBAR_MS
|
|
: StaggeredWorkspaceAnim.DURATION_MS;
|
|
// Early detach the nav bar once the endTarget is determined as HOME
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.detachNavigationBarFromApp(true);
|
|
}
|
|
} else if (endTarget == RECENTS) {
|
|
if (mRecentsView != null) {
|
|
int nearestPage = mRecentsView.getDestinationPage();
|
|
if (nearestPage == INVALID_PAGE) {
|
|
// Allow the snap to invalid page to catch future error cases.
|
|
Log.e(TAG,
|
|
"RecentsView destination page is invalid",
|
|
new IllegalStateException());
|
|
}
|
|
|
|
boolean isScrolling = false;
|
|
if (mRecentsView.getNextPage() != nearestPage) {
|
|
// We shouldn't really scroll to the next page when swiping up to recents.
|
|
// Only allow settling on the next page if it's nearest to the center.
|
|
mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration));
|
|
isScrolling = true;
|
|
}
|
|
if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) {
|
|
mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION);
|
|
isScrolling = true;
|
|
}
|
|
if (!mGestureState.isHandlingAtomicEvent() || isScrolling) {
|
|
duration = Math.max(duration, mRecentsView.getScroller().getDuration());
|
|
}
|
|
}
|
|
} else if (endTarget == LAST_TASK && mRecentsView != null
|
|
&& mRecentsView.getNextPage() != mRecentsView.getRunningTaskIndex()) {
|
|
mRecentsView.snapToPage(mRecentsView.getRunningTaskIndex(), Math.toIntExact(duration));
|
|
}
|
|
|
|
// Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
|
|
// or resumeLastTask().
|
|
if (mRecentsView != null) {
|
|
ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
|
|
.SET_ON_PAGE_TRANSITION_END_CALLBACK);
|
|
mRecentsView.setOnPageTransitionEndCallback(
|
|
() -> mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED));
|
|
} else {
|
|
mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
|
|
}
|
|
|
|
animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocity);
|
|
}
|
|
|
|
private void doLogGesture(GestureEndTarget endTarget, @Nullable TaskView targetTask) {
|
|
if (mDp == null || !mDp.isGestureMode || mDownPos == null) {
|
|
// We probably never received an animation controller, skip logging.
|
|
return;
|
|
}
|
|
|
|
StatsLogManager.EventEnum event;
|
|
switch (endTarget) {
|
|
case HOME:
|
|
event = LAUNCHER_HOME_GESTURE;
|
|
break;
|
|
case RECENTS:
|
|
event = LAUNCHER_OVERVIEW_GESTURE;
|
|
break;
|
|
case LAST_TASK:
|
|
case NEW_TASK:
|
|
event = mLogDirectionUpOrLeft ? LAUNCHER_QUICKSWITCH_LEFT
|
|
: LAUNCHER_QUICKSWITCH_RIGHT;
|
|
break;
|
|
default:
|
|
event = IGNORE;
|
|
}
|
|
StatsLogger logger = StatsLogManager.newInstance(mContext).logger()
|
|
.withSrcState(LAUNCHER_STATE_BACKGROUND)
|
|
.withDstState(endTarget.containerType);
|
|
if (targetTask != null) {
|
|
logger.withItemInfo(targetTask.getItemInfo());
|
|
}
|
|
|
|
int pageIndex = endTarget == LAST_TASK || mRecentsView == null
|
|
? LOG_NO_OP_PAGE_INDEX
|
|
: mRecentsView.getNextPage();
|
|
logger.withRank(pageIndex);
|
|
logger.log(event);
|
|
}
|
|
|
|
/** Animates to the given progress, where 0 is the current app and 1 is overview. */
|
|
@UiThread
|
|
private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
|
|
GestureEndTarget target, PointF velocityPxPerMs) {
|
|
runOnRecentsAnimationAndLauncherBound(() -> animateToProgressInternal(start, end, duration,
|
|
interpolator, target, velocityPxPerMs));
|
|
}
|
|
|
|
protected abstract HomeAnimationFactory createHomeAnimationFactory(
|
|
ArrayList<IBinder> launchCookies, long duration, boolean isTargetTranslucent,
|
|
boolean appCanEnterPip, RemoteAnimationTargetCompat runningTaskTarget);
|
|
|
|
private final TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() {
|
|
@Override
|
|
public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
|
|
boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
|
|
if (task.taskId == mGestureState.getRunningTaskId()
|
|
&& task.configuration.windowConfiguration.getActivityType()
|
|
!= ACTIVITY_TYPE_HOME) {
|
|
// Since this is an edge case, just cancel and relaunch with default activity
|
|
// options (since we don't know if there's an associated app icon to launch from)
|
|
endRunningWindowAnim(true /* cancel */);
|
|
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
|
|
mActivityRestartListener);
|
|
ActivityManagerWrapper.getInstance().startActivityFromRecents(task.taskId, null);
|
|
}
|
|
}
|
|
};
|
|
|
|
@UiThread
|
|
private void animateToProgressInternal(float start, float end, long duration,
|
|
Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
|
|
maybeUpdateRecentsAttachedState();
|
|
|
|
// If we are transitioning to launcher, then listen for the activity to be restarted while
|
|
// the transition is in progress
|
|
if (mGestureState.getEndTarget().isLauncher) {
|
|
TaskStackChangeListeners.getInstance().registerTaskStackListener(
|
|
mActivityRestartListener);
|
|
|
|
mParallelRunningAnim = mActivityInterface.getParallelAnimationToLauncher(
|
|
mGestureState.getEndTarget(), duration,
|
|
mTaskAnimationManager.getCurrentCallbacks());
|
|
if (mParallelRunningAnim != null) {
|
|
mParallelRunningAnim.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mParallelRunningAnim = null;
|
|
}
|
|
});
|
|
mParallelRunningAnim.start();
|
|
}
|
|
}
|
|
|
|
if (mGestureState.getEndTarget() == HOME) {
|
|
getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
|
|
final RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
|
|
? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
|
|
: null;
|
|
final ArrayList<IBinder> cookies = runningTaskTarget != null
|
|
? runningTaskTarget.taskInfo.launchCookies
|
|
: new ArrayList<>();
|
|
boolean isTranslucent = runningTaskTarget != null && runningTaskTarget.isTranslucent;
|
|
boolean hasValidLeash = runningTaskTarget != null
|
|
&& runningTaskTarget.leash != null
|
|
&& runningTaskTarget.leash.isValid();
|
|
boolean appCanEnterPip = !mDeviceState.isPipActive()
|
|
&& hasValidLeash
|
|
&& runningTaskTarget.allowEnterPip
|
|
&& runningTaskTarget.taskInfo.pictureInPictureParams != null
|
|
&& runningTaskTarget.taskInfo.pictureInPictureParams.isAutoEnterEnabled();
|
|
HomeAnimationFactory homeAnimFactory =
|
|
createHomeAnimationFactory(cookies, duration, isTranslucent, appCanEnterPip,
|
|
runningTaskTarget);
|
|
mIsSwipingPipToHome = !mIsSwipeForSplit && appCanEnterPip;
|
|
final RectFSpringAnim[] windowAnim;
|
|
if (mIsSwipingPipToHome) {
|
|
mSwipePipToHomeAnimator = createWindowAnimationToPip(
|
|
homeAnimFactory, runningTaskTarget, start);
|
|
mSwipePipToHomeAnimators[0] = mSwipePipToHomeAnimator;
|
|
windowAnim = mSwipePipToHomeAnimators;
|
|
} else {
|
|
mSwipePipToHomeAnimator = null;
|
|
windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
|
|
|
|
windowAnim[0].addAnimatorListener(new AnimationSuccessListener() {
|
|
@Override
|
|
public void onAnimationSuccess(Animator animator) {
|
|
if (mRecentsAnimationController == null) {
|
|
// If the recents animation is interrupted, we still end the running
|
|
// animation (not canceled) so this is still called. In that case,
|
|
// we can skip doing any future work here for the current gesture.
|
|
return;
|
|
}
|
|
// Finalize the state and notify of the change
|
|
mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
|
|
}
|
|
});
|
|
}
|
|
mRunningWindowAnim = new RunningWindowAnim[windowAnim.length];
|
|
for (int i = 0, windowAnimLength = windowAnim.length; i < windowAnimLength; i++) {
|
|
RectFSpringAnim windowAnimation = windowAnim[i];
|
|
if (windowAnimation == null) {
|
|
continue;
|
|
}
|
|
DeviceProfile dp = mActivity == null ? null : mActivity.getDeviceProfile();
|
|
windowAnimation.start(mContext, dp, velocityPxPerMs);
|
|
mRunningWindowAnim[i] = RunningWindowAnim.wrap(windowAnimation);
|
|
}
|
|
homeAnimFactory.setSwipeVelocity(velocityPxPerMs.y);
|
|
homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
|
|
mLauncherTransitionController = null;
|
|
|
|
if (mRecentsView != null) {
|
|
mRecentsView.onPrepareGestureEndAnimation(null, mGestureState.getEndTarget(),
|
|
getRemoteTaskViewSimulators());
|
|
}
|
|
} else {
|
|
AnimatorSet animatorSet = new AnimatorSet();
|
|
ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
|
|
windowAnim.addUpdateListener(valueAnimator -> {
|
|
computeRecentsScrollIfInvisible();
|
|
});
|
|
windowAnim.addListener(new AnimationSuccessListener() {
|
|
@Override
|
|
public void onAnimationSuccess(Animator animator) {
|
|
if (mRecentsAnimationController == null) {
|
|
// If the recents animation is interrupted, we still end the running
|
|
// animation (not canceled) so this is still called. In that case, we can
|
|
// skip doing any future work here for the current gesture.
|
|
return;
|
|
}
|
|
if (mRecentsView != null) {
|
|
int taskToLaunch = mRecentsView.getNextPage();
|
|
int runningTask = getLastAppearedTaskIndex();
|
|
boolean hasStartedNewTask = hasStartedNewTask();
|
|
if (target == NEW_TASK && taskToLaunch == runningTask
|
|
&& !hasStartedNewTask) {
|
|
// We are about to launch the current running task, so use LAST_TASK
|
|
// state instead of NEW_TASK. This could happen, for example, if our
|
|
// scroll is aborted after we determined the target to be NEW_TASK.
|
|
mGestureState.setEndTarget(LAST_TASK);
|
|
} else if (target == LAST_TASK && hasStartedNewTask) {
|
|
// We are about to re-launch the previously running task, but we can't
|
|
// just finish the controller like we normally would because that would
|
|
// instead resume the last task that appeared, and not ensure that this
|
|
// task is restored to the top. To address this, re-launch the task as
|
|
// if it were a new task.
|
|
mGestureState.setEndTarget(NEW_TASK);
|
|
}
|
|
}
|
|
mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
|
|
}
|
|
});
|
|
animatorSet.play(windowAnim);
|
|
if (mRecentsView != null) {
|
|
mRecentsView.onPrepareGestureEndAnimation(
|
|
animatorSet, mGestureState.getEndTarget(),
|
|
getRemoteTaskViewSimulators());
|
|
}
|
|
animatorSet.setDuration(duration).setInterpolator(interpolator);
|
|
animatorSet.start();
|
|
mRunningWindowAnim = new RunningWindowAnim[]{RunningWindowAnim.wrap(animatorSet)};
|
|
}
|
|
}
|
|
|
|
private int calculateWindowRotation(RemoteAnimationTargetCompat runningTaskTarget,
|
|
RecentsOrientedState orientationState) {
|
|
if (runningTaskTarget.rotationChange != 0
|
|
&& TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
|
|
return Math.abs(runningTaskTarget.rotationChange) == ROTATION_90
|
|
? ROTATION_270 : ROTATION_90;
|
|
} else {
|
|
return orientationState.getDisplayRotation();
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private SwipePipToHomeAnimator createWindowAnimationToPip(HomeAnimationFactory homeAnimFactory,
|
|
RemoteAnimationTargetCompat runningTaskTarget, float startProgress) {
|
|
// Directly animate the app to PiP (picture-in-picture) mode
|
|
final ActivityManager.RunningTaskInfo taskInfo = runningTaskTarget.taskInfo;
|
|
final RecentsOrientedState orientationState = mRemoteTargetHandles[0].getTaskViewSimulator()
|
|
.getOrientationState();
|
|
final int windowRotation = calculateWindowRotation(runningTaskTarget, orientationState);
|
|
final int homeRotation = orientationState.getRecentsActivityRotation();
|
|
|
|
final Matrix[] homeToWindowPositionMaps = new Matrix[mRemoteTargetHandles.length];
|
|
final RectF startRect = updateProgressForStartRect(homeToWindowPositionMaps,
|
|
startProgress)[0];
|
|
final Matrix homeToWindowPositionMap = homeToWindowPositionMaps[0];
|
|
// Move the startRect to Launcher space as floatingIconView runs in Launcher
|
|
final Matrix windowToHomePositionMap = new Matrix();
|
|
homeToWindowPositionMap.invert(windowToHomePositionMap);
|
|
windowToHomePositionMap.mapRect(startRect);
|
|
|
|
final Rect hotseatKeepClearArea = getKeepClearAreaForHotseat();
|
|
final Rect destinationBounds = SystemUiProxy.INSTANCE.get(mContext)
|
|
.startSwipePipToHome(taskInfo.topActivity,
|
|
taskInfo.topActivityInfo,
|
|
runningTaskTarget.taskInfo.pictureInPictureParams,
|
|
homeRotation,
|
|
hotseatKeepClearArea);
|
|
if (destinationBounds == null) {
|
|
// No destination bounds returned from SystemUI, bail early.
|
|
return null;
|
|
}
|
|
final Rect appBounds = new Rect();
|
|
final WindowConfiguration winConfig = taskInfo.configuration.windowConfiguration;
|
|
// Adjust the appBounds for TaskBar by using the calculated window crop Rect
|
|
// from TaskViewSimulator and fallback to the bounds in TaskInfo when it's originated
|
|
// from windowing modes other than full-screen.
|
|
if (winConfig.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) {
|
|
mRemoteTargetHandles[0].getTaskViewSimulator().getCurrentCropRect().round(appBounds);
|
|
} else {
|
|
appBounds.set(winConfig.getBounds());
|
|
}
|
|
final SwipePipToHomeAnimator.Builder builder = new SwipePipToHomeAnimator.Builder()
|
|
.setContext(mContext)
|
|
.setTaskId(runningTaskTarget.taskId)
|
|
.setComponentName(taskInfo.topActivity)
|
|
.setLeash(runningTaskTarget.leash)
|
|
.setSourceRectHint(
|
|
runningTaskTarget.taskInfo.pictureInPictureParams.getSourceRectHint())
|
|
.setAppBounds(appBounds)
|
|
.setHomeToWindowPositionMap(homeToWindowPositionMap)
|
|
.setStartBounds(startRect)
|
|
.setDestinationBounds(destinationBounds)
|
|
.setCornerRadius(mRecentsView.getPipCornerRadius())
|
|
.setShadowRadius(mRecentsView.getPipShadowRadius())
|
|
.setAttachedView(mRecentsView);
|
|
// We would assume home and app window always in the same rotation While homeRotation
|
|
// is not ROTATION_0 (which implies the rotation is turned on in launcher settings).
|
|
if (homeRotation == ROTATION_0
|
|
&& (windowRotation == ROTATION_90 || windowRotation == ROTATION_270)) {
|
|
builder.setFromRotation(mRemoteTargetHandles[0].getTaskViewSimulator(), windowRotation,
|
|
taskInfo.displayCutoutInsets);
|
|
}
|
|
final SwipePipToHomeAnimator swipePipToHomeAnimator = builder.build();
|
|
AnimatorPlaybackController activityAnimationToHome =
|
|
homeAnimFactory.createActivityAnimationToHome();
|
|
swipePipToHomeAnimator.addAnimatorListener(new AnimatorListenerAdapter() {
|
|
private boolean mHasAnimationEnded;
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
if (mHasAnimationEnded) return;
|
|
// Ensure Launcher ends in NORMAL state
|
|
activityAnimationToHome.dispatchOnStart();
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
if (mHasAnimationEnded) return;
|
|
mHasAnimationEnded = true;
|
|
activityAnimationToHome.getAnimationPlayer().end();
|
|
if (mRecentsAnimationController == null) {
|
|
// If the recents animation is interrupted, we still end the running
|
|
// animation (not canceled) so this is still called. In that case, we can
|
|
// skip doing any future work here for the current gesture.
|
|
return;
|
|
}
|
|
// Finalize the state and notify of the change
|
|
mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
|
|
}
|
|
});
|
|
setupWindowAnimation(new RectFSpringAnim[]{swipePipToHomeAnimator});
|
|
return swipePipToHomeAnimator;
|
|
}
|
|
|
|
private Rect getKeepClearAreaForHotseat() {
|
|
Rect keepClearArea;
|
|
if (!ENABLE_PIP_KEEP_CLEAR_ALGORITHM) {
|
|
// make the height equal to hotseatBarSizePx only
|
|
keepClearArea = new Rect(0, 0, 0, mDp.hotseatBarSizePx);
|
|
return keepClearArea;
|
|
}
|
|
// the keep clear area in global screen coordinates, in pixels
|
|
if (mDp.isPhone) {
|
|
if (mDp.isSeascape()) {
|
|
// in seascape the Hotseat is on the left edge of the screen
|
|
keepClearArea = new Rect(0, 0, mDp.hotseatBarSizePx, mDp.heightPx);
|
|
} else if (mDp.isLandscape) {
|
|
// in landscape the Hotseat is on the right edge of the screen
|
|
keepClearArea = new Rect(mDp.widthPx - mDp.hotseatBarSizePx, 0,
|
|
mDp.widthPx, mDp.heightPx);
|
|
} else {
|
|
// in portrait mode the Hotseat is at the bottom of the screen
|
|
keepClearArea = new Rect(0, mDp.heightPx - mDp.hotseatBarSizePx,
|
|
mDp.widthPx, mDp.heightPx);
|
|
}
|
|
} else {
|
|
// large screens have Hotseat always at the bottom of the screen
|
|
keepClearArea = new Rect(0, mDp.heightPx - mDp.hotseatBarSizePx,
|
|
mDp.widthPx, mDp.heightPx);
|
|
}
|
|
return keepClearArea;
|
|
}
|
|
|
|
private void startInterceptingTouchesForGesture() {
|
|
if (mRecentsAnimationController == null) {
|
|
return;
|
|
}
|
|
|
|
mRecentsAnimationController.enableInputConsumer();
|
|
|
|
// Start hiding the divider
|
|
setDividerShown(false /* shown */, true /* immediate */);
|
|
}
|
|
|
|
private void computeRecentsScrollIfInvisible() {
|
|
if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
|
|
// Views typically don't compute scroll when invisible as an optimization,
|
|
// but in our case we need to since the window offset depends on the scroll.
|
|
mRecentsView.computeScroll();
|
|
}
|
|
}
|
|
|
|
private void continueComputingRecentsScrollIfNecessary() {
|
|
if (!mGestureState.hasState(STATE_RECENTS_SCROLLING_FINISHED)
|
|
&& !mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)
|
|
&& !mCanceled) {
|
|
computeRecentsScrollIfInvisible();
|
|
mRecentsView.postOnAnimation(this::continueComputingRecentsScrollIfNecessary);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an animation that transforms the current app window into the home app.
|
|
* @param startProgress The progress of {@link #mCurrentShift} to start the window from.
|
|
* @param homeAnimationFactory The home animation factory.
|
|
*/
|
|
@Override
|
|
protected RectFSpringAnim[] createWindowAnimationToHome(float startProgress,
|
|
HomeAnimationFactory homeAnimationFactory) {
|
|
RectFSpringAnim[] anim =
|
|
super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
|
|
setupWindowAnimation(anim);
|
|
return anim;
|
|
}
|
|
|
|
private void setupWindowAnimation(RectFSpringAnim[] anims) {
|
|
anims[0].addOnUpdateListener((r, p) -> {
|
|
updateSysUiFlags(Math.max(p, mCurrentShift.value));
|
|
});
|
|
anims[0].addAnimatorListener(new AnimationSuccessListener() {
|
|
@Override
|
|
public void onAnimationSuccess(Animator animator) {
|
|
if (mRecentsView != null) {
|
|
mRecentsView.post(mRecentsView::resetTaskVisuals);
|
|
}
|
|
// Make sure recents is in its final state
|
|
maybeUpdateRecentsAttachedState(false);
|
|
mActivityInterface.onSwipeUpToHomeComplete(mDeviceState);
|
|
}
|
|
});
|
|
if (mRecentsAnimationTargets != null) {
|
|
mRecentsAnimationTargets.addReleaseCheck(anims[0]);
|
|
}
|
|
}
|
|
|
|
public void onConsumerAboutToBeSwitched() {
|
|
if (mActivity != null) {
|
|
// In the off chance that the gesture ends before Launcher is started, we should clear
|
|
// the callback here so that it doesn't update with the wrong state
|
|
mActivity.clearRunOnceOnStartCallback();
|
|
resetLauncherListeners();
|
|
}
|
|
if (mGestureState.isRecentsAnimationRunning() && mGestureState.getEndTarget() != null
|
|
&& !mGestureState.getEndTarget().isLauncher) {
|
|
// Continued quick switch.
|
|
cancelCurrentAnimation();
|
|
} else {
|
|
mStateCallback.setStateOnUiThread(STATE_FINISH_WITH_NO_END);
|
|
reset();
|
|
}
|
|
}
|
|
|
|
public boolean isCanceled() {
|
|
return mCanceled;
|
|
}
|
|
|
|
@UiThread
|
|
private void resumeLastTask() {
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.finish(false /* toRecents */, null);
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
/* event= */ "finishRecentsAnimation",
|
|
/* extras= */ false,
|
|
/* gestureEvent= */ FINISH_RECENTS_ANIMATION);
|
|
}
|
|
doLogGesture(LAST_TASK, null);
|
|
reset();
|
|
}
|
|
|
|
@UiThread
|
|
private void startNewTask() {
|
|
TaskView taskToLaunch = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
|
|
startNewTask(success -> {
|
|
if (!success) {
|
|
reset();
|
|
// We couldn't launch the task, so take user to overview so they can
|
|
// decide what to do instead of staying in this broken state.
|
|
endLauncherTransitionController();
|
|
updateSysUiFlags(1 /* windowProgress == overview */);
|
|
}
|
|
doLogGesture(NEW_TASK, taskToLaunch);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called when we successfully startNewTask() on the task that was previously running. Normally
|
|
* we call resumeLastTask() when returning to the previously running task, but this handles a
|
|
* specific edge case: if we switch from A to B, and back to A before B appears, we need to
|
|
* start A again to ensure it stays on top.
|
|
*/
|
|
@androidx.annotation.CallSuper
|
|
protected void onRestartPreviouslyAppearedTask() {
|
|
// Finish the controller here, since we won't get onTaskAppeared() for a task that already
|
|
// appeared.
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.finish(false, null);
|
|
}
|
|
reset();
|
|
}
|
|
|
|
private void reset() {
|
|
mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
|
|
if (mActivity != null) {
|
|
mActivity.unregisterActivityLifecycleCallbacks(mLifecycleCallbacks);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels any running animation so that the active target can be overriden by a new swipe
|
|
* handler (in case of quick switch).
|
|
*/
|
|
private void cancelCurrentAnimation() {
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
"AbsSwipeUpHandler.cancelCurrentAnimation",
|
|
ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION);
|
|
mCanceled = true;
|
|
mCurrentShift.cancelAnimation();
|
|
|
|
// Cleanup when switching handlers
|
|
mInputConsumerProxy.unregisterCallback();
|
|
mActivityInitListener.unregister();
|
|
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
|
|
mActivityRestartListener);
|
|
mTaskSnapshot = null;
|
|
}
|
|
|
|
private void invalidateHandler() {
|
|
if (!ENABLE_QUICKSTEP_LIVE_TILE.get() || !mActivityInterface.isInLiveTileMode()
|
|
|| mGestureState.getEndTarget() != RECENTS) {
|
|
mInputConsumerProxy.destroy();
|
|
mTaskAnimationManager.setLiveTileCleanUpHandler(null);
|
|
}
|
|
mInputConsumerProxy.unregisterCallback();
|
|
endRunningWindowAnim(false /* cancel */);
|
|
|
|
if (mGestureEndCallback != null) {
|
|
mGestureEndCallback.run();
|
|
}
|
|
|
|
mActivityInitListener.unregister();
|
|
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
|
|
mActivityRestartListener);
|
|
mTaskSnapshot = null;
|
|
}
|
|
|
|
private void invalidateHandlerWithLauncher() {
|
|
endLauncherTransitionController();
|
|
|
|
mRecentsView.onGestureAnimationEnd();
|
|
resetLauncherListeners();
|
|
}
|
|
|
|
private void endLauncherTransitionController() {
|
|
mHasEndedLauncherTransition = true;
|
|
|
|
if (mLauncherTransitionController != null) {
|
|
// End the animation, but stay at the same visual progress.
|
|
mLauncherTransitionController.getNormalController().dispatchSetInterpolator(
|
|
t -> Utilities.boundToRange(mCurrentShift.value, 0, 1));
|
|
mLauncherTransitionController.getNormalController().getAnimationPlayer().end();
|
|
mLauncherTransitionController = null;
|
|
}
|
|
|
|
if (mRecentsView != null) {
|
|
mRecentsView.abortScrollerAnimation();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unlike invalidateHandlerWithLauncher, this is called even when switching consumers, e.g. on
|
|
* continued quick switch gesture, which cancels the previous handler but doesn't invalidate it.
|
|
*/
|
|
private void resetLauncherListeners() {
|
|
// Reset the callback for deferred activity launches
|
|
if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
|
|
mActivityInterface.setOnDeferredActivityLaunchCallback(null);
|
|
}
|
|
mActivity.getRootView().setOnApplyWindowInsetsListener(null);
|
|
|
|
mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
|
|
}
|
|
|
|
private void resetStateForAnimationCancel() {
|
|
boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
|
|
mActivityInterface.onTransitionCancelled(wasVisible, mGestureState.getEndTarget());
|
|
|
|
// Leave the pending invisible flag, as it may be used by wallpaper open animation.
|
|
if (mActivity != null) {
|
|
mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
|
|
}
|
|
}
|
|
|
|
protected void switchToScreenshot() {
|
|
if (!hasTargets()) {
|
|
// If there are no targets, then we don't need to capture anything
|
|
mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
|
|
} else {
|
|
final int runningTaskId = mGestureState.getRunningTaskId();
|
|
final boolean refreshView = !ENABLE_QUICKSTEP_LIVE_TILE.get() /* refreshView */;
|
|
boolean finishTransitionPosted = false;
|
|
if (mRecentsAnimationController != null) {
|
|
// Update the screenshot of the task
|
|
if (mTaskSnapshot == null) {
|
|
UI_HELPER_EXECUTOR.execute(() -> {
|
|
if (mRecentsAnimationController == null) return;
|
|
final ThumbnailData taskSnapshot =
|
|
mRecentsAnimationController.screenshotTask(runningTaskId);
|
|
MAIN_EXECUTOR.execute(() -> {
|
|
mTaskSnapshot = taskSnapshot;
|
|
if (!updateThumbnail(runningTaskId, refreshView)) {
|
|
setScreenshotCapturedState();
|
|
}
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
finishTransitionPosted = updateThumbnail(runningTaskId, refreshView);
|
|
}
|
|
if (!finishTransitionPosted) {
|
|
setScreenshotCapturedState();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns whether finish transition was posted.
|
|
private boolean updateThumbnail(int runningTaskId, boolean refreshView) {
|
|
boolean finishTransitionPosted = false;
|
|
final TaskView taskView;
|
|
if (mGestureState.getEndTarget() == HOME || mGestureState.getEndTarget() == NEW_TASK) {
|
|
// Capture the screenshot before finishing the transition to home or quickswitching to
|
|
// ensure it's taken in the correct orientation, but no need to update the thumbnail.
|
|
taskView = null;
|
|
} else {
|
|
taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, refreshView);
|
|
}
|
|
if (taskView != null && refreshView && !mCanceled) {
|
|
// Defer finishing the animation until the next launcher frame with the
|
|
// new thumbnail
|
|
finishTransitionPosted = ViewUtils.postFrameDrawn(taskView,
|
|
() -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
|
|
this::isCanceled);
|
|
}
|
|
return finishTransitionPosted;
|
|
}
|
|
|
|
private void setScreenshotCapturedState() {
|
|
// If we haven't posted a draw callback, set the state immediately.
|
|
Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
|
|
TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
|
|
mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
|
|
TraceHelper.INSTANCE.endSection(traceToken);
|
|
}
|
|
|
|
private void finishCurrentTransitionToRecents() {
|
|
if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
|
|
mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.detachNavigationBarFromApp(true);
|
|
}
|
|
} else if (!hasTargets() || mRecentsAnimationController == null) {
|
|
// If there are no targets or the animation not started, then there is nothing to finish
|
|
mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
|
|
} else {
|
|
mRecentsAnimationController.finish(true /* toRecents */,
|
|
() -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
|
|
}
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
/* event= */ "finishRecentsAnimation",
|
|
/* extras= */ true,
|
|
/* gestureEvent= */ FINISH_RECENTS_ANIMATION);
|
|
}
|
|
|
|
private void finishCurrentTransitionToHome() {
|
|
if (!hasTargets() || mRecentsAnimationController == null) {
|
|
// If there are no targets or the animation not started, then there is nothing to finish
|
|
mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
|
|
} else {
|
|
maybeFinishSwipeToHome();
|
|
finishRecentsControllerToHome(
|
|
() -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
|
|
}
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
/* event= */ "finishRecentsAnimation",
|
|
/* extras= */ true,
|
|
/* gestureEvent= */ FINISH_RECENTS_ANIMATION);
|
|
doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView());
|
|
}
|
|
|
|
/**
|
|
* Notifies SysUI that transition is finished if applicable and also pass leash transactions
|
|
* from Launcher to WM.
|
|
* This should happen before {@link #finishRecentsControllerToHome(Runnable)}.
|
|
*/
|
|
private void maybeFinishSwipeToHome() {
|
|
if (mIsSwipingPipToHome && mSwipePipToHomeAnimators[0] != null) {
|
|
SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
|
|
mSwipePipToHomeAnimator.getTaskId(),
|
|
mSwipePipToHomeAnimator.getComponentName(),
|
|
mSwipePipToHomeAnimator.getDestinationBounds(),
|
|
mSwipePipToHomeAnimator.getContentOverlay());
|
|
mRecentsAnimationController.setFinishTaskTransaction(
|
|
mSwipePipToHomeAnimator.getTaskId(),
|
|
mSwipePipToHomeAnimator.getFinishTransaction(),
|
|
mSwipePipToHomeAnimator.getContentOverlay());
|
|
mIsSwipingPipToHome = false;
|
|
} else if (mIsSwipeForSplit) {
|
|
// Transaction to hide the task to avoid flicker for entering PiP from split-screen.
|
|
PictureInPictureSurfaceTransaction tx =
|
|
new PictureInPictureSurfaceTransaction.Builder()
|
|
.setAlpha(0f)
|
|
.build();
|
|
tx.setShouldDisableCanAffectSystemUiFlags(false);
|
|
int[] taskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds();
|
|
for (int taskId : taskIds) {
|
|
mRecentsAnimationController.setFinishTaskTransaction(taskId,
|
|
tx, null /* overlay */);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected abstract void finishRecentsControllerToHome(Runnable callback);
|
|
|
|
private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
|
|
if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
|
|
return;
|
|
}
|
|
endLauncherTransitionController();
|
|
mRecentsView.onSwipeUpAnimationSuccess();
|
|
if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
|
|
mTaskAnimationManager.setLiveTileCleanUpHandler(() -> {
|
|
mRecentsView.cleanupRemoteTargets();
|
|
mInputConsumerProxy.destroy();
|
|
});
|
|
mTaskAnimationManager.enableLiveTileRestartListener();
|
|
}
|
|
|
|
SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG);
|
|
doLogGesture(RECENTS, mRecentsView.getCurrentPageTaskView());
|
|
reset();
|
|
}
|
|
|
|
private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
|
|
return app.isNotInRecents
|
|
|| app.activityType == ACTIVITY_TYPE_HOME;
|
|
}
|
|
|
|
/**
|
|
* To be called at the end of constructor of subclasses. This calls various methods which can
|
|
* depend on proper class initialization.
|
|
*/
|
|
protected void initAfterSubclassConstructor() {
|
|
initTransitionEndpoints(mRemoteTargetHandles[0].getTaskViewSimulator()
|
|
.getOrientationState().getLauncherDeviceProfile());
|
|
}
|
|
|
|
protected void performHapticFeedback() {
|
|
VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
|
|
}
|
|
|
|
public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
|
|
return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
|
|
}
|
|
|
|
public void setGestureEndCallback(Runnable gestureEndCallback) {
|
|
mGestureEndCallback = gestureEndCallback;
|
|
}
|
|
|
|
protected void linkRecentsViewScroll() {
|
|
SurfaceTransactionApplier.create(mRecentsView, applier -> {
|
|
runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
|
|
.setSyncTransactionApplier(applier));
|
|
runOnRecentsAnimationAndLauncherBound(() ->
|
|
mRecentsAnimationTargets.addReleaseCheck(applier));
|
|
});
|
|
|
|
mRecentsView.addOnScrollChangedListener(mOnRecentsScrollListener);
|
|
runOnRecentsAnimationAndLauncherBound(() ->
|
|
mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
|
|
mRecentsAnimationTargets));
|
|
mRecentsViewScrollLinked = true;
|
|
}
|
|
|
|
private void onRecentsViewScroll() {
|
|
if (moveWindowWithRecentsScroll()) {
|
|
updateFinalShift();
|
|
}
|
|
}
|
|
|
|
protected void startNewTask(Consumer<Boolean> resultCallback) {
|
|
// Launch the task user scrolled to (mRecentsView.getNextPage()).
|
|
if (!mCanceled) {
|
|
TaskView nextTask = mRecentsView.getNextPageTaskView();
|
|
if (nextTask != null) {
|
|
int taskId = nextTask.getTask().key.id;
|
|
mGestureState.updateLastStartedTaskId(taskId);
|
|
boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
|
|
.contains(taskId);
|
|
nextTask.launchTask(success -> {
|
|
resultCallback.accept(success);
|
|
if (success) {
|
|
if (hasTaskPreviouslyAppeared) {
|
|
onRestartPreviouslyAppearedTask();
|
|
}
|
|
} else {
|
|
mActivityInterface.onLaunchTaskFailed();
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.finish(true /* toRecents */, null);
|
|
}
|
|
}
|
|
}, true /* freezeTaskList */);
|
|
} else {
|
|
mActivityInterface.onLaunchTaskFailed();
|
|
Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.finish(true /* toRecents */, null);
|
|
}
|
|
}
|
|
}
|
|
mCanceled = false;
|
|
}
|
|
|
|
/**
|
|
* Runs the given {@param action} if the recents animation has already started and Launcher has
|
|
* been created and bound to the TouchInteractionService, or queues it to be run when it this
|
|
* next happens.
|
|
*/
|
|
private void runOnRecentsAnimationAndLauncherBound(Runnable action) {
|
|
mRecentsAnimationStartCallbacks.add(action);
|
|
flushOnRecentsAnimationAndLauncherBound();
|
|
}
|
|
|
|
private void flushOnRecentsAnimationAndLauncherBound() {
|
|
if (mRecentsAnimationTargets == null ||
|
|
!mStateCallback.hasStates(STATE_LAUNCHER_BIND_TO_SERVICE)) {
|
|
return;
|
|
}
|
|
|
|
if (!mRecentsAnimationStartCallbacks.isEmpty()) {
|
|
for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
|
|
action.run();
|
|
}
|
|
mRecentsAnimationStartCallbacks.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
|
|
* @return whether the recents animation has started and there are valid app targets.
|
|
*/
|
|
protected boolean hasTargets() {
|
|
return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
|
|
}
|
|
|
|
@Override
|
|
public void onRecentsAnimationFinished(RecentsAnimationController controller) {
|
|
if (!controller.getFinishTargetIsLauncher()) {
|
|
setDividerShown(true /* shown */, false /* immediate */);
|
|
}
|
|
mRecentsAnimationController = null;
|
|
mRecentsAnimationTargets = null;
|
|
if (mRecentsView != null) {
|
|
mRecentsView.setRecentsAnimationTargets(null, null);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTasksAppeared(RemoteAnimationTargetCompat[] appearedTaskTargets) {
|
|
if (mRecentsAnimationController != null) {
|
|
if (handleTaskAppeared(appearedTaskTargets)) {
|
|
mRecentsAnimationController.finish(false /* toRecents */,
|
|
null /* onFinishComplete */);
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
/* event= */ "finishRecentsAnimation",
|
|
/* extras= */ false,
|
|
/* gestureEvent= */ FINISH_RECENTS_ANIMATION);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return The index of the TaskView in RecentsView whose taskId matches the task that will
|
|
* resume if we finish the controller.
|
|
*/
|
|
protected int getLastAppearedTaskIndex() {
|
|
return mGestureState.getLastAppearedTaskId() != -1
|
|
? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
|
|
: mRecentsView.getRunningTaskIndex();
|
|
}
|
|
|
|
/**
|
|
* @return Whether we are continuing a gesture that already landed on a new task,
|
|
* but before that task appeared.
|
|
*/
|
|
protected boolean hasStartedNewTask() {
|
|
return mGestureState.getLastStartedTaskId() != -1;
|
|
}
|
|
|
|
/**
|
|
* Registers a callback to run when the activity is ready.
|
|
*/
|
|
public void initWhenReady() {
|
|
// Preload the plan
|
|
RecentsModel.INSTANCE.get(mContext).getTasks(null);
|
|
|
|
mActivityInitListener.register();
|
|
}
|
|
|
|
/**
|
|
* Applies the transform on the recents animation
|
|
*/
|
|
protected void applyScrollAndTransform() {
|
|
// No need to apply any transform if there is ongoing swipe-to-home animator
|
|
// swipe-to-pip handles the leash solely
|
|
// swipe-to-icon animation is handled by RectFSpringAnim anim
|
|
boolean notSwipingToHome = mRecentsAnimationTargets != null
|
|
&& mGestureState.getEndTarget() != HOME;
|
|
boolean setRecentsScroll = mRecentsViewScrollLinked && mRecentsView != null;
|
|
for (RemoteTargetHandle remoteHandle : mRemoteTargetHandles) {
|
|
AnimatorControllerWithResistance playbackController =
|
|
remoteHandle.getPlaybackController();
|
|
if (playbackController != null) {
|
|
playbackController.setProgress(Math.max(mCurrentShift.value,
|
|
getScaleProgressDueToScroll()), mDragLengthFactor);
|
|
}
|
|
|
|
if (notSwipingToHome) {
|
|
TaskViewSimulator taskViewSimulator = remoteHandle.getTaskViewSimulator();
|
|
if (setRecentsScroll) {
|
|
taskViewSimulator.setScroll(mRecentsView.getScrollOffset());
|
|
}
|
|
taskViewSimulator.apply(remoteHandle.getTransformParams());
|
|
}
|
|
}
|
|
ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
|
|
}
|
|
|
|
// Scaling of RecentsView during quick switch based on amount of recents scroll
|
|
private float getScaleProgressDueToScroll() {
|
|
if (mActivity == null || !mActivity.getDeviceProfile().isTablet || mRecentsView == null
|
|
|| !mRecentsViewScrollLinked) {
|
|
return 0;
|
|
}
|
|
|
|
float scrollOffset = Math.abs(mRecentsView.getScrollOffset(mRecentsView.getCurrentPage()));
|
|
int maxScrollOffset = mRecentsView.getPagedOrientationHandler().getPrimaryValue(
|
|
mRecentsView.getLastComputedTaskSize().width(),
|
|
mRecentsView.getLastComputedTaskSize().height());
|
|
maxScrollOffset += mRecentsView.getPageSpacing();
|
|
|
|
float maxScaleProgress =
|
|
MAX_QUICK_SWITCH_RECENTS_SCALE_PROGRESS * mRecentsView.getMaxScaleForFullScreen();
|
|
float scaleProgress = maxScaleProgress;
|
|
|
|
if (scrollOffset < mQuickSwitchScaleScrollThreshold) {
|
|
scaleProgress = Utilities.mapToRange(scrollOffset, 0, mQuickSwitchScaleScrollThreshold,
|
|
0, maxScaleProgress, ACCEL_DEACCEL);
|
|
} else if (scrollOffset > (maxScrollOffset - mQuickSwitchScaleScrollThreshold)) {
|
|
scaleProgress = Utilities.mapToRange(scrollOffset,
|
|
(maxScrollOffset - mQuickSwitchScaleScrollThreshold), maxScrollOffset,
|
|
maxScaleProgress, 0, ACCEL_DEACCEL);
|
|
}
|
|
|
|
return scaleProgress;
|
|
}
|
|
|
|
private void setDividerShown(boolean shown, boolean immediate) {
|
|
if (mDividerAnimator != null) {
|
|
mDividerAnimator.cancel();
|
|
}
|
|
mDividerAnimator = TaskViewUtils.createSplitAuxiliarySurfacesAnimator(
|
|
mRecentsAnimationTargets.nonApps, shown, (dividerAnimator) -> {
|
|
dividerAnimator.start();
|
|
if (immediate) {
|
|
dividerAnimator.end();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Used for winscope tracing, see launcher_trace.proto
|
|
* @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
|
|
* @param inputConsumerProto The parent of this proto message.
|
|
*/
|
|
public void writeToProto(InputConsumerProto.Builder inputConsumerProto) {
|
|
SwipeHandlerProto.Builder swipeHandlerProto = SwipeHandlerProto.newBuilder();
|
|
|
|
mGestureState.writeToProto(swipeHandlerProto);
|
|
|
|
swipeHandlerProto.setIsRecentsAttachedToAppWindow(
|
|
mAnimationFactory.isRecentsAttachedToAppWindow());
|
|
swipeHandlerProto.setScrollOffset(mRecentsView == null
|
|
? 0
|
|
: mRecentsView.getScrollOffset());
|
|
swipeHandlerProto.setAppToOverviewProgress(mCurrentShift.value);
|
|
|
|
inputConsumerProto.setSwipeHandler(swipeHandlerProto);
|
|
}
|
|
|
|
public interface Factory {
|
|
AbsSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs);
|
|
}
|
|
}
|