blob: 6ec1da0c48b75074d81dd27b3370c333430da82c [file] [log] [blame]
/*
* Copyright (C) 2019 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.inputconsumers;
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
import static com.android.quickstep.RecentsActivity.EXTRA_TASK_ID;
import static com.android.quickstep.RecentsActivity.EXTRA_THUMBNAIL;
import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.HOME;
import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.LAST_TASK;
import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.NEW_TASK;
import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.RECENTS;
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Bundle;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
import com.android.quickstep.AnimatedFloat;
import com.android.quickstep.BaseSwipeUpHandler;
import com.android.quickstep.MultiStateCallback;
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SwipeSharedState;
import com.android.quickstep.fallback.FallbackRecentsView;
import com.android.quickstep.util.ObjectWrapper;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.SwipeAnimationTargetSet;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.InputConsumerController;
public class FallbackNoButtonInputConsumer extends
BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
private static final String[] STATE_NAMES = DEBUG_STATES ? new String[5] : null;
private static int getFlagForIndex(int index, String name) {
if (DEBUG_STATES) {
STATE_NAMES[index] = name;
}
return 1 << index;
}
private static final int STATE_RECENTS_PRESENT =
getFlagForIndex(0, "STATE_RECENTS_PRESENT");
private static final int STATE_HANDLER_INVALIDATED =
getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
private static final int STATE_GESTURE_CANCELLED =
getFlagForIndex(2, "STATE_GESTURE_CANCELLED");
private static final int STATE_GESTURE_COMPLETED =
getFlagForIndex(3, "STATE_GESTURE_COMPLETED");
private static final int STATE_APP_CONTROLLER_RECEIVED =
getFlagForIndex(4, "STATE_APP_CONTROLLER_RECEIVED");
public enum GestureEndTarget {
HOME(3, 100, 1),
RECENTS(1, 300, 0),
LAST_TASK(0, 150, 1),
NEW_TASK(0, 150, 1);
private final float mEndProgress;
private final long mDurationMultiplier;
private final float mLauncherAlpha;
GestureEndTarget(float endProgress, long durationMultiplier, float launcherAlpha) {
mEndProgress = endProgress;
mDurationMultiplier = durationMultiplier;
mLauncherAlpha = launcherAlpha;
}
}
private final AnimatedFloat mLauncherAlpha = new AnimatedFloat(this::onLauncherAlphaChanged);
private boolean mIsMotionPaused = false;
private GestureEndTarget mEndTarget;
private final boolean mInQuickSwitchMode;
private final boolean mContinuingLastGesture;
private final boolean mRunningOverHome;
private final boolean mSwipeUpOverHome;
private final RunningTaskInfo mRunningTaskInfo;
private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
private RunningWindowAnim mFinishAnimation;
public FallbackNoButtonInputConsumer(Context context,
OverviewComponentObserver overviewComponentObserver,
RunningTaskInfo runningTaskInfo, RecentsModel recentsModel,
InputConsumerController inputConsumer,
boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
super(context, overviewComponentObserver, recentsModel, inputConsumer, runningTaskInfo.id);
mLauncherAlpha.value = 1;
mRunningTaskInfo = runningTaskInfo;
mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
mContinuingLastGesture = continuingLastGesture;
mRunningOverHome = ActivityManagerWrapper.isHomeTask(runningTaskInfo);
mSwipeUpOverHome = mRunningOverHome && !mInQuickSwitchMode;
if (mSwipeUpOverHome) {
mClipAnimationHelper.setBaseAlphaCallback((t, a) -> 1 - mLauncherAlpha.value);
} else {
mClipAnimationHelper.setBaseAlphaCallback((t, a) -> mLauncherAlpha.value);
}
initStateCallbacks();
}
private void initStateCallbacks() {
mStateCallback = new MultiStateCallback(STATE_NAMES);
mStateCallback.addCallback(STATE_HANDLER_INVALIDATED,
this::onHandlerInvalidated);
mStateCallback.addCallback(STATE_RECENTS_PRESENT | STATE_HANDLER_INVALIDATED,
this::onHandlerInvalidatedWithRecents);
mStateCallback.addCallback(STATE_GESTURE_CANCELLED | STATE_APP_CONTROLLER_RECEIVED,
this::finishAnimationTargetSetAnimationComplete);
if (mInQuickSwitchMode) {
mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED
| STATE_RECENTS_PRESENT,
this::finishAnimationTargetSet);
} else {
mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED,
this::finishAnimationTargetSet);
}
}
private void onLauncherAlphaChanged() {
if (mRecentsAnimationWrapper.targetSet != null && mEndTarget == null) {
applyTransformUnchecked();
}
}
@Override
protected boolean onActivityInit(final RecentsActivity activity, Boolean alreadyOnHome) {
mActivity = activity;
mRecentsView = activity.getOverviewPanel();
linkRecentsViewScroll();
mRecentsView.setDisallowScrollToClearAll(true);
mRecentsView.getClearAllButton().setVisibilityAlpha(0);
mRecentsView.setZoomProgress(1);
if (!mContinuingLastGesture) {
if (mRunningOverHome) {
mRecentsView.onGestureAnimationStart(mRunningTaskInfo);
} else {
mRecentsView.onGestureAnimationStart(mRunningTaskId);
}
}
setStateOnUiThread(STATE_RECENTS_PRESENT);
return true;
}
@Override
protected boolean moveWindowWithRecentsScroll() {
return mInQuickSwitchMode;
}
@Override
public void initWhenReady() {
if (mInQuickSwitchMode) {
// Only init if we are in quickswitch mode
super.initWhenReady();
}
}
@Override
public void updateDisplacement(float displacement) {
if (!mInQuickSwitchMode) {
super.updateDisplacement(displacement);
}
}
@Override
protected InputConsumer createNewInputProxyHandler() {
// Just consume all input on the active task
return InputConsumer.NO_OP;
}
@Override
public void onMotionPauseChanged(boolean isPaused) {
if (!mInQuickSwitchMode) {
mIsMotionPaused = isPaused;
mLauncherAlpha.animateToValue(mLauncherAlpha.value, isPaused ? 0 : 1)
.setDuration(150).start();
performHapticFeedback();
}
}
@Override
public Intent getLaunchIntent() {
if (mInQuickSwitchMode || mSwipeUpOverHome) {
return mOverviewComponentObserver.getOverviewIntent();
} else {
return mOverviewComponentObserver.getHomeIntent();
}
}
@Override
public void updateFinalShift() {
mTransformParams.setProgress(mCurrentShift.value);
mRecentsAnimationWrapper.setWindowThresholdCrossed(!mInQuickSwitchMode
&& (mCurrentShift.value > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD));
if (mRecentsAnimationWrapper.targetSet != null) {
applyTransformUnchecked();
}
}
@Override
public void onGestureCancelled() {
updateDisplacement(0);
mEndTarget = LAST_TASK;
setStateOnUiThread(STATE_GESTURE_CANCELLED);
}
@Override
public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
mEndVelocityPxPerMs.set(0, velocity.y / 1000);
if (mInQuickSwitchMode) {
// For now set it to non-null, it will be reset before starting the animation
mEndTarget = LAST_TASK;
} else {
float flingThreshold = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_velocity);
boolean isFling = Math.abs(endVelocity) > flingThreshold;
if (isFling) {
mEndTarget = endVelocity < 0 ? HOME : LAST_TASK;
} else if (mIsMotionPaused) {
mEndTarget = RECENTS;
} else {
mEndTarget = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? HOME : LAST_TASK;
}
}
setStateOnUiThread(STATE_GESTURE_COMPLETED);
}
@Override
public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) {
if (mInQuickSwitchMode && mEndTarget != null) {
sharedState.canGestureBeContinued = true;
sharedState.goingToLauncher = false;
mCanceled = true;
mCurrentShift.cancelAnimation();
if (mFinishAnimation != null) {
mFinishAnimation.cancel();
}
if (mRecentsView != null) {
if (mFinishingRecentsAnimationForNewTaskId != -1) {
TaskView newRunningTaskView = mRecentsView.getTaskView(
mFinishingRecentsAnimationForNewTaskId);
int newRunningTaskId = newRunningTaskView != null
? newRunningTaskView.getTask().key.id
: -1;
mRecentsView.setCurrentTask(newRunningTaskId);
sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId);
}
mRecentsView.setOnScrollChangeListener(null);
}
} else {
setStateOnUiThread(STATE_HANDLER_INVALIDATED);
}
}
private void onHandlerInvalidated() {
mActivityInitListener.unregister();
if (mGestureEndCallback != null) {
mGestureEndCallback.run();
}
if (mFinishAnimation != null) {
mFinishAnimation.end();
}
}
private void onHandlerInvalidatedWithRecents() {
mRecentsView.onGestureAnimationEnd();
mRecentsView.setDisallowScrollToClearAll(false);
mRecentsView.getClearAllButton().setVisibilityAlpha(1);
}
private void finishAnimationTargetSetAnimationComplete() {
switch (mEndTarget) {
case HOME: {
if (mSwipeUpOverHome) {
mRecentsAnimationWrapper.finish(false, null, false);
// Send a home intent to clear the task stack
mContext.startActivity(mOverviewComponentObserver.getHomeIntent());
} else {
mRecentsAnimationWrapper.finish(true, null, true);
}
break;
}
case LAST_TASK:
mRecentsAnimationWrapper.finish(false, null, false);
break;
case RECENTS: {
if (mSwipeUpOverHome) {
mRecentsAnimationWrapper.finish(true, null, true);
break;
}
ThumbnailData thumbnail =
mRecentsAnimationWrapper.targetSet.controller.screenshotTask(mRunningTaskId);
mRecentsAnimationWrapper.setDeferCancelUntilNextTransition(true /* defer */,
false /* screenshot */);
ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
ActivityOptionsCompat.setFreezeRecentTasksList(options);
Bundle extras = new Bundle();
extras.putBinder(EXTRA_THUMBNAIL, new ObjectWrapper<>(thumbnail));
extras.putInt(EXTRA_TASK_ID, mRunningTaskId);
Intent intent = new Intent(mOverviewComponentObserver.getOverviewIntent())
.putExtras(extras);
mContext.startActivity(intent, options.toBundle());
mRecentsAnimationWrapper.targetSet.controller.cleanupScreenshot();
break;
}
case NEW_TASK: {
startNewTask(STATE_HANDLER_INVALIDATED, b -> {});
break;
}
}
setStateOnUiThread(STATE_HANDLER_INVALIDATED);
}
private void finishAnimationTargetSet() {
if (mInQuickSwitchMode) {
// Recalculate the end target, some views might have been initialized after
// gesture has ended.
if (mRecentsView == null || !mRecentsAnimationWrapper.hasTargets()) {
mEndTarget = LAST_TASK;
} else {
final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
final int taskToLaunch = mRecentsView.getNextPage();
mEndTarget = (runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex)
? NEW_TASK : LAST_TASK;
}
}
float endProgress = mEndTarget.mEndProgress;
long duration = (long) (mEndTarget.mDurationMultiplier *
Math.abs(endProgress - mCurrentShift.value));
if (mRecentsView != null) {
duration = Math.max(duration, mRecentsView.getScroller().getDuration());
}
if (mCurrentShift.value != endProgress || mInQuickSwitchMode) {
AnimationSuccessListener endListener = new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
finishAnimationTargetSetAnimationComplete();
mFinishAnimation = null;
}
};
if (mEndTarget == HOME && !mRunningOverHome) {
RectFSpringAnim anim = createWindowAnimationToHome(mCurrentShift.value, duration);
anim.addAnimatorListener(endListener);
anim.start(mEndVelocityPxPerMs);
mFinishAnimation = RunningWindowAnim.wrap(anim);
} else {
AnimatorSet anim = new AnimatorSet();
anim.play(mLauncherAlpha.animateToValue(
mLauncherAlpha.value, mEndTarget.mLauncherAlpha));
anim.play(mCurrentShift.animateToValue(mCurrentShift.value, endProgress));
anim.setDuration(duration);
anim.addListener(endListener);
anim.start();
mFinishAnimation = RunningWindowAnim.wrap(anim);
}
} else {
finishAnimationTargetSetAnimationComplete();
}
}
@Override
public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
super.onRecentsAnimationStart(targetSet);
mRecentsAnimationWrapper.enableInputConsumer();
if (mRunningOverHome) {
mClipAnimationHelper.prepareAnimation(mDp, true);
}
applyTransformUnchecked();
setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
}
@Override
public void onRecentsAnimationCanceled() {
mRecentsAnimationWrapper.setController(null);
setStateOnUiThread(STATE_HANDLER_INVALIDATED);
}
/**
* 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.
*/
private RectFSpringAnim createWindowAnimationToHome(float startProgress, long duration) {
HomeAnimationFactory factory = new HomeAnimationFactory() {
@Override
public RectF getWindowTargetRect() {
return HomeAnimationFactory.getDefaultWindowTargetRect(mDp);
}
@Override
public AnimatorPlaybackController createActivityAnimationToHome() {
AnimatorSet anim = new AnimatorSet();
anim.play(mLauncherAlpha.animateToValue(mLauncherAlpha.value, 1));
anim.setDuration(duration);
return AnimatorPlaybackController.wrap(anim, duration);
}
};
return createWindowAnimationToHome(startProgress, factory);
}
}