3f5d510b38
We will hide divider when isLikelyToStartNewTask become true, but this call sometime earlier than onRecentsAnimationStart then cause crash because mRecentsAnimationTargets is still null. Fix this by checking mRecentsAnimationTargets before set divider visibility. And also add new condition to hide divider to ensure it hidden if such case happened. Fix: 265238266 Test: manaul Test: pass existing tests Change-Id: I80b1294e69a52e7ac5255cd8e55e7c5e6a3dcbcb
2374 lines
108 KiB
Java
2374 lines
108 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.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
|
|
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_TASKBAR_REVISED_THRESHOLDS;
|
|
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.VibratorWrapper.OVERVIEW_HAPTIC;
|
|
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.EXPECTING_TASK_APPEARED;
|
|
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
|
|
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 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.content.res.Resources;
|
|
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.RemoteAnimationTarget;
|
|
import android.view.View;
|
|
import android.view.View.OnApplyWindowInsetsListener;
|
|
import android.view.ViewGroup;
|
|
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.dragndrop.DragView;
|
|
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.taskbar.TaskbarUIController;
|
|
import com.android.launcher3.tracing.InputConsumerProto;
|
|
import com.android.launcher3.tracing.SwipeHandlerProto;
|
|
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
|
|
import com.android.launcher3.util.DisplayController;
|
|
import com.android.launcher3.util.TraceHelper;
|
|
import com.android.launcher3.util.VibratorWrapper;
|
|
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.SurfaceTransaction;
|
|
import com.android.quickstep.util.SurfaceTransactionApplier;
|
|
import com.android.quickstep.util.SwipePipToHomeAnimator;
|
|
import com.android.quickstep.util.TaskViewSimulator;
|
|
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.TaskStackChangeListener;
|
|
import com.android.systemui.shared.system.TaskStackChangeListeners;
|
|
import com.android.wm.shell.common.TransactionPool;
|
|
import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.Optional;
|
|
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;
|
|
|
|
// Controls task thumbnail splash's reveal animation after landing on a task from quickswitch.
|
|
// These values match WindowManager/Shell starting_window_app_reveal_* config values.
|
|
private static final int SPLASH_FADE_OUT_DURATION = 133;
|
|
private static final int SPLASH_APP_REVEAL_DELAY = 83;
|
|
private static final int SPLASH_APP_REVEAL_DURATION = 266;
|
|
private static final int SPLASH_ANIMATION_DURATION = 349;
|
|
|
|
/**
|
|
* 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 mGestureStarted;
|
|
private boolean mLogDirectionUpOrLeft = true;
|
|
private PointF mDownPos;
|
|
private boolean mIsLikelyToStartNewTask;
|
|
|
|
private final long mTouchTimeMs;
|
|
private long mLauncherFrameDrawnTime;
|
|
|
|
private final int mSplashMainWindowShiftLength;
|
|
|
|
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;
|
|
|
|
private final int mTaskbarAppWindowThreshold;
|
|
private final int mTaskbarHomeOverviewThreshold;
|
|
private final int mTaskbarCatchUpThreshold;
|
|
private final boolean mTaskbarAlreadyOpen;
|
|
private final boolean mIsTaskbarAllAppsOpen;
|
|
private final boolean mIsTransientTaskbar;
|
|
// May be set to false when mIsTransientTaskbar is true.
|
|
private boolean mCanSlowSwipeGoHome = true;
|
|
private boolean mHasReachedOverviewThreshold = false;
|
|
private boolean mDividerHiddenBeforeAnimation = false;
|
|
|
|
@Nullable
|
|
private RemoteAnimationTargets.ReleaseCheck mSwipePipToHomeReleaseCheck = null;
|
|
|
|
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;
|
|
|
|
Resources res = context.getResources();
|
|
mQuickSwitchScaleScrollThreshold = res
|
|
.getDimension(R.dimen.quick_switch_scaling_scroll_threshold);
|
|
|
|
mSplashMainWindowShiftLength = -res
|
|
.getDimensionPixelSize(R.dimen.starting_surface_exit_animation_window_shift_length);
|
|
|
|
initTransitionEndpoints(mRemoteTargetHandles[0].getTaskViewSimulator()
|
|
.getOrientationState().getLauncherDeviceProfile());
|
|
initStateCallbacks();
|
|
|
|
mIsTransientTaskbar = mDp.isTaskbarPresent
|
|
&& DisplayController.isTransientTaskbar(mActivity);
|
|
TaskbarUIController controller = mActivityInterface.getTaskbarController();
|
|
mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed();
|
|
mIsTaskbarAllAppsOpen = controller != null && controller.isTaskbarAllAppsOpen();
|
|
mTaskbarAppWindowThreshold = res
|
|
.getDimensionPixelSize(ENABLE_TASKBAR_REVISED_THRESHOLDS.get()
|
|
? R.dimen.taskbar_app_window_threshold_v2
|
|
: R.dimen.taskbar_app_window_threshold);
|
|
boolean swipeWillNotShowTaskbar = mTaskbarAlreadyOpen;
|
|
mTaskbarHomeOverviewThreshold = swipeWillNotShowTaskbar
|
|
? 0
|
|
: res.getDimensionPixelSize(
|
|
ENABLE_TASKBAR_REVISED_THRESHOLDS.get()
|
|
? R.dimen.taskbar_home_overview_threshold_v2
|
|
: R.dimen.taskbar_home_overview_threshold);
|
|
mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold);
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
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() {
|
|
mActivityInterface.switchRunningTaskViewToScreenshot(
|
|
null, () -> {
|
|
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 */);
|
|
Optional.ofNullable(mActivityInterface.getTaskbarController())
|
|
.ifPresent(TaskbarUIController::startTranslationSpring);
|
|
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;
|
|
}
|
|
RemoteAnimationTarget 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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns threshold that needs to be met in order for motion pause to be allowed.
|
|
*/
|
|
public float getThresholdToAllowMotionPause() {
|
|
return mIsTransientTaskbar
|
|
? mTaskbarHomeOverviewThreshold
|
|
: 0;
|
|
}
|
|
|
|
public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
|
|
setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
|
|
}
|
|
|
|
private void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask, boolean animate) {
|
|
if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
|
|
if (isLikelyToStartNewTask && mIsTransientTaskbar) {
|
|
setDividerShown(false /* shown */, true /* immediate */);
|
|
}
|
|
|
|
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() {
|
|
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;
|
|
mSwipePipToHomeReleaseCheck = new RemoteAnimationTargets.ReleaseCheck();
|
|
mSwipePipToHomeReleaseCheck.setCanRelease(true);
|
|
mRecentsAnimationTargets.addReleaseCheck(mSwipePipToHomeReleaseCheck);
|
|
|
|
// 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) {
|
|
RemoteAnimationTarget 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);
|
|
}
|
|
|
|
@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);
|
|
InteractionJankMonitorWrapper.begin(mRecentsView,
|
|
InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS);
|
|
|
|
rv.post(() -> rv.getViewTreeObserver().removeOnDrawListener(this));
|
|
}
|
|
});
|
|
}
|
|
notifyGestureStartedAsync();
|
|
setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
|
|
|
|
if (mIsTransientTaskbar && !mTaskbarAlreadyOpen && !isLikelyToStartNewTask) {
|
|
setClampScrollOffset(true);
|
|
}
|
|
mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
|
|
mGestureStarted = true;
|
|
}
|
|
|
|
/**
|
|
* Sets whether or not we should clamp the scroll offset.
|
|
* This is used to avoid x-axis movement when swiping up transient taskbar.
|
|
* @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp threshold is
|
|
* met.
|
|
*/
|
|
private void setClampScrollOffset(boolean clampScrollOffset) {
|
|
if (!mIsTransientTaskbar) {
|
|
return;
|
|
}
|
|
if (mRecentsView == null) {
|
|
mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT,
|
|
() -> mRecentsView.setClampScrollOffset(clampScrollOffset));
|
|
return;
|
|
}
|
|
mRecentsView.setClampScrollOffset(clampScrollOffset);
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 endVelocityPxPerMs The velocity in the direction of the nav bar to the middle of the
|
|
* screen.
|
|
* @param velocityPxPerMs 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 endVelocityPxPerMs, PointF velocityPxPerMs, PointF downPos) {
|
|
float flingThreshold = mContext.getResources()
|
|
.getDimension(R.dimen.quickstep_fling_threshold_speed);
|
|
boolean isFling = mGestureStarted && !mIsMotionPaused
|
|
&& Math.abs(endVelocityPxPerMs) > flingThreshold;
|
|
mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
|
|
boolean isVelocityVertical = Math.abs(velocityPxPerMs.y) > Math.abs(velocityPxPerMs.x);
|
|
if (isVelocityVertical) {
|
|
mLogDirectionUpOrLeft = velocityPxPerMs.y < 0;
|
|
} else {
|
|
mLogDirectionUpOrLeft = velocityPxPerMs.x < 0;
|
|
}
|
|
mDownPos = downPos;
|
|
Runnable handleNormalGestureEndCallback = () -> handleNormalGestureEnd(
|
|
endVelocityPxPerMs, isFling, velocityPxPerMs, /* 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);
|
|
}
|
|
if (endTarget != RECENTS) {
|
|
InteractionJankMonitorWrapper.cancel(
|
|
InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS);
|
|
}
|
|
|
|
switch (endTarget) {
|
|
case HOME:
|
|
mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
|
|
// Notify the SysUI to use fade-in animation when entering PiP
|
|
SystemUiProxy.INSTANCE.get(mContext).setPipAnimationTypeToAlpha();
|
|
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(RemoteAnimationTarget[] 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 float dpiFromPx(float pixels) {
|
|
return Utilities.dpiFromPx(pixels, mContext.getResources().getDisplayMetrics().densityDpi);
|
|
}
|
|
|
|
private GestureEndTarget calculateEndTarget(
|
|
PointF velocityPxPerMs, float endVelocityPxPerMs, boolean isFlingY, boolean isCancel) {
|
|
ActiveGestureLog.INSTANCE.addLog(
|
|
new ActiveGestureLog.CompoundString("calculateEndTarget: velocities=(x=")
|
|
.append(Float.toString(dpiFromPx(velocityPxPerMs.x)))
|
|
.append("dp/ms, y=")
|
|
.append(Float.toString(dpiFromPx(velocityPxPerMs.y)))
|
|
.append("dp/ms), angle=")
|
|
.append(Double.toString(Math.toDegrees(Math.atan2(
|
|
-velocityPxPerMs.y, velocityPxPerMs.x)))));
|
|
|
|
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(velocityPxPerMs, endVelocityPxPerMs);
|
|
} else {
|
|
endTarget = calculateEndTargetForNonFling(velocityPxPerMs);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
return willGoToNewTask ? NEW_TASK : HOME;
|
|
}
|
|
|
|
private GestureEndTarget calculateEndTargetForNonFling(PointF velocity) {
|
|
final boolean isScrollingToNewTask = isScrollingToNewTask();
|
|
|
|
// 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;
|
|
}
|
|
return velocity.y < 0 && mCanSlowSwipeGoHome ? HOME : 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;
|
|
}
|
|
|
|
/**
|
|
* Sets whether a slow swipe can go to the HOME end target when the user lets go. A slow swipe
|
|
* for this purpose must meet two criteria:
|
|
* 1) y-velocity is less than quickstep_fling_threshold_speed
|
|
* AND
|
|
* 2) motion pause has not been detected (possibly because
|
|
* {@link MotionPauseDetector#setDisallowPause} has been called with disallowPause == true)
|
|
*/
|
|
public void setCanSlowSwipeGoHome(boolean canSlowSwipeGoHome) {
|
|
mCanSlowSwipeGoHome = canSlowSwipeGoHome;
|
|
}
|
|
|
|
@UiThread
|
|
private void handleNormalGestureEnd(
|
|
float endVelocityPxPerMs, boolean isFling, PointF velocityPxPerMs, boolean isCancel) {
|
|
long duration = MAX_SWIPE_DURATION;
|
|
float currentShift = mCurrentShift.value;
|
|
final GestureEndTarget endTarget = calculateEndTarget(
|
|
velocityPxPerMs, endVelocityPxPerMs, 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 - velocityPxPerMs.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 / velocityPxPerMs.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().
|
|
Runnable onPageTransitionEnd = () -> {
|
|
mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
|
|
setClampScrollOffset(false);
|
|
};
|
|
if (mRecentsView != null) {
|
|
ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
|
|
.SET_ON_PAGE_TRANSITION_END_CALLBACK);
|
|
mRecentsView.setOnPageTransitionEndCallback(onPageTransitionEnd);
|
|
} else {
|
|
onPageTransitionEnd.run();
|
|
}
|
|
|
|
animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
|
|
}
|
|
|
|
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, RemoteAnimationTarget 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) {
|
|
// This is also called when the launcher is resumed, in order to clear the pending
|
|
// widgets that have yet to be configured.
|
|
DragView.removeAllViews(mActivity);
|
|
|
|
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 RemoteAnimationTarget 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;
|
|
if (mSwipePipToHomeReleaseCheck != null) {
|
|
mSwipePipToHomeReleaseCheck.setCanRelease(false);
|
|
}
|
|
windowAnim = mSwipePipToHomeAnimators;
|
|
} else {
|
|
mSwipePipToHomeAnimator = null;
|
|
if (mSwipePipToHomeReleaseCheck != null) {
|
|
mSwipePipToHomeReleaseCheck.setCanRelease(true);
|
|
mSwipePipToHomeReleaseCheck = 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(RemoteAnimationTarget 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,
|
|
RemoteAnimationTarget 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
|
|
if (!mIsTransientTaskbar || mTaskbarAlreadyOpen || mIsTaskbarAllAppsOpen
|
|
|| mDividerHiddenBeforeAnimation) {
|
|
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);
|
|
}
|
|
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 (!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() {
|
|
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();
|
|
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);
|
|
// If split case, we should update all split tasks snapshot
|
|
if (mIsSwipeForSplit) {
|
|
int[] splitTaskIds = TopTaskTracker.INSTANCE.get(
|
|
mContext).getRunningSplitTaskIds();
|
|
for (int i = 0; i < splitTaskIds.length; i++) {
|
|
// Skip running one because done above.
|
|
if (splitTaskIds[i] == runningTaskId) continue;
|
|
|
|
mRecentsAnimationController.screenshotTask(splitTaskIds[i]);
|
|
}
|
|
}
|
|
MAIN_EXECUTOR.execute(() -> {
|
|
mTaskSnapshot = taskSnapshot;
|
|
if (!updateThumbnail(runningTaskId, false /* refreshView */)) {
|
|
setScreenshotCapturedState();
|
|
}
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
finishTransitionPosted = updateThumbnail(runningTaskId, false /* 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 (mRecentsView != null
|
|
&& mActivityInterface.getDesktopVisibilityController() != null
|
|
&& mActivityInterface.getDesktopVisibilityController().areFreeformTasksVisible()) {
|
|
mRecentsView.switchToScreenshot(() -> {
|
|
mRecentsView.finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
|
|
() -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
|
|
});
|
|
} else {
|
|
mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.detachNavigationBarFromApp(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
if (mSwipePipToHomeReleaseCheck != null) {
|
|
mSwipePipToHomeReleaseCheck.setCanRelease(true);
|
|
mSwipePipToHomeReleaseCheck = null;
|
|
}
|
|
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();
|
|
mTaskAnimationManager.setLiveTileCleanUpHandler(() -> {
|
|
mRecentsView.cleanupRemoteTargets();
|
|
mInputConsumerProxy.destroy();
|
|
});
|
|
mTaskAnimationManager.enableLiveTileRestartListener();
|
|
|
|
SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG);
|
|
doLogGesture(RECENTS, mRecentsView.getCurrentPageTaskView());
|
|
reset();
|
|
}
|
|
|
|
private static boolean isNotInRecents(RemoteAnimationTarget app) {
|
|
return app.isNotInRecents
|
|
|| app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME;
|
|
}
|
|
|
|
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);
|
|
if (!hasTaskPreviouslyAppeared) {
|
|
ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
|
|
}
|
|
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(RemoteAnimationTarget[] appearedTaskTargets) {
|
|
if (mRecentsAnimationController != null) {
|
|
if (handleTaskAppeared(appearedTaskTargets)) {
|
|
Optional<RemoteAnimationTarget> taskTargetOptional =
|
|
Arrays.stream(appearedTaskTargets)
|
|
.filter(targetCompat ->
|
|
targetCompat.taskId == mGestureState.getLastStartedTaskId())
|
|
.findFirst();
|
|
if (!taskTargetOptional.isPresent()) {
|
|
finishRecentsAnimationOnTasksAppeared();
|
|
return;
|
|
}
|
|
RemoteAnimationTarget taskTarget = taskTargetOptional.get();
|
|
TaskView taskView = mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
|
|
if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) {
|
|
finishRecentsAnimationOnTasksAppeared();
|
|
return;
|
|
}
|
|
|
|
ViewGroup splashView = mActivity.getDragLayer();
|
|
|
|
// When revealing the app with launcher splash screen, make the app visible
|
|
// and behind the splash view before the splash is animated away.
|
|
SurfaceTransactionApplier surfaceApplier =
|
|
new SurfaceTransactionApplier(splashView);
|
|
SurfaceTransaction transaction = new SurfaceTransaction();
|
|
for (RemoteAnimationTarget target : appearedTaskTargets) {
|
|
transaction.forSurface(target.leash).setAlpha(1).setLayer(-1);
|
|
}
|
|
surfaceApplier.scheduleApply(transaction);
|
|
|
|
SplashScreenExitAnimationUtils.startAnimations(splashView, taskTarget.leash,
|
|
mSplashMainWindowShiftLength, new TransactionPool(), new Rect(),
|
|
SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
|
|
/* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
|
|
SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
|
|
new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
finishRecentsAnimationOnTasksAppeared();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void finishRecentsAnimationOnTasksAppeared() {
|
|
if (mRecentsAnimationController != null) {
|
|
mRecentsAnimationController.finish(false /* toRecents */, null /* onFinishComplete */);
|
|
}
|
|
ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* Overrides the gesture displacement to keep the app window at the bottom of the screen while
|
|
* the transient taskbar is being swiped in.
|
|
*
|
|
* There is also a catch up period so that the window can start moving 1:1 with the swipe.
|
|
*/
|
|
@Override
|
|
protected float overrideDisplacementForTransientTaskbar(float displacement) {
|
|
if (!mIsTransientTaskbar) {
|
|
return displacement;
|
|
}
|
|
|
|
if (mTaskbarAlreadyOpen || mIsTaskbarAllAppsOpen) {
|
|
return displacement;
|
|
}
|
|
|
|
if (displacement < mTaskbarAppWindowThreshold) {
|
|
return 0;
|
|
}
|
|
|
|
// "Catch up" with the displacement at mTaskbarCatchUpThreshold.
|
|
if (displacement < mTaskbarCatchUpThreshold) {
|
|
if (!mHasReachedOverviewThreshold) {
|
|
setDividerShown(false /* shown */, true /* immediate */);
|
|
mHasReachedOverviewThreshold = true;
|
|
}
|
|
return Utilities.mapToRange(displacement, mTaskbarAppWindowThreshold,
|
|
mTaskbarCatchUpThreshold, 0, mTaskbarCatchUpThreshold, ACCEL_DEACCEL);
|
|
}
|
|
|
|
return displacement;
|
|
}
|
|
|
|
private void setDividerShown(boolean shown, boolean immediate) {
|
|
if (mRecentsAnimationTargets == null) {
|
|
if (!shown) {
|
|
mDividerHiddenBeforeAnimation = true;
|
|
}
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
}
|