blob: cf858dcb0637f5dcd8d192f8d6e6455e93c893a6 [file] [log] [blame]
/*
* Copyright (C) 2021 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.wm.shell.back;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
import static com.android.window.flags.Flags.predictiveBackSystemAnimations;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings.Global;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.view.IRemoteAnimationRunner;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import android.window.BackAnimationAdapter;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
import android.window.IBackAnimationFinishedCallback;
import android.window.IBackAnimationRunner;
import android.window.IOnBackInvokedCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.LatencyTracker;
import com.android.internal.view.AppearanceRegion;
import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Controls the window animation run when a user initiates a back gesture.
*/
public class BackAnimationController implements RemoteCallable<BackAnimationController> {
private static final String TAG = "ShellBackPreview";
private static final int SETTING_VALUE_OFF = 0;
private static final int SETTING_VALUE_ON = 1;
public static final boolean IS_ENABLED =
SystemProperties.getInt("persist.wm.debug.predictive_back",
SETTING_VALUE_ON) == SETTING_VALUE_ON;
public static final float FLING_MAX_LENGTH_SECONDS = 0.1f; // 100ms
public static final float FLING_SPEED_UP_FACTOR = 0.6f;
/**
* The maximum additional progress in case of fling gesture.
* The end animation starts after the user lifts the finger from the screen, we continue to
* fire {@link BackEvent}s until the velocity reaches 0.
*/
private static final float MAX_FLING_PROGRESS = 0.3f; /* 30% of the screen */
/** Predictive back animation developer option */
private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
/**
* Max duration to wait for an animation to finish before triggering the real back.
*/
private static final long MAX_ANIMATION_DURATION = 2000;
private final LatencyTracker mLatencyTracker;
/** True when a back gesture is ongoing */
private boolean mBackGestureStarted = false;
/** Tracks if an uninterruptible animation is in progress */
private boolean mPostCommitAnimationInProgress = false;
/** Tracks if we should start the back gesture on the next motion move event */
private boolean mShouldStartOnNextMoveEvent = false;
private final FlingAnimationUtils mFlingAnimationUtils;
/** Registry for the back animations */
private final ShellBackAnimationRegistry mShellBackAnimationRegistry;
@Nullable
private BackNavigationInfo mBackNavigationInfo;
private final IActivityTaskManager mActivityTaskManager;
private final Context mContext;
private final ContentResolver mContentResolver;
private final ShellController mShellController;
private final ShellExecutor mShellExecutor;
private final Handler mBgHandler;
/**
* Tracks the current user back gesture.
*/
private TouchTracker mCurrentTracker = new TouchTracker();
/**
* Tracks the next back gesture in case a new user gesture has started while the back animation
* (and navigation) associated with {@link #mCurrentTracker} have not yet finished.
*/
private TouchTracker mQueuedTracker = new TouchTracker();
private final Runnable mAnimationTimeoutRunnable = () -> {
ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
MAX_ANIMATION_DURATION);
onBackAnimationFinished();
};
private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
@VisibleForTesting
BackAnimationAdapter mBackAnimationAdapter;
@Nullable
private IOnBackInvokedCallback mActiveCallback;
@VisibleForTesting
final RemoteCallback mNavigationObserver = new RemoteCallback(
new RemoteCallback.OnResultListener() {
@Override
public void onResult(@Nullable Bundle result) {
mShellExecutor.execute(() -> {
if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
// If an uninterruptible animation is already in progress, we should
// ignore this due to it may cause focus lost. (alpha = 0)
return;
}
ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone.");
setTriggerBack(false);
resetTouchTracker();
});
}
});
private final BackAnimationBackground mAnimationBackground;
private StatusBarCustomizer mCustomizer;
private boolean mTrackingLatency;
public BackAnimationController(
@NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@NonNull @ShellMainThread ShellExecutor shellExecutor,
@NonNull @ShellBackgroundThread Handler backgroundHandler,
Context context,
@NonNull BackAnimationBackground backAnimationBackground,
ShellBackAnimationRegistry shellBackAnimationRegistry) {
this(
shellInit,
shellController,
shellExecutor,
backgroundHandler,
ActivityTaskManager.getService(),
context,
context.getContentResolver(),
backAnimationBackground,
shellBackAnimationRegistry);
}
@VisibleForTesting
BackAnimationController(
@NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@NonNull @ShellMainThread ShellExecutor shellExecutor,
@NonNull @ShellBackgroundThread Handler bgHandler,
@NonNull IActivityTaskManager activityTaskManager,
Context context,
ContentResolver contentResolver,
@NonNull BackAnimationBackground backAnimationBackground,
ShellBackAnimationRegistry shellBackAnimationRegistry) {
mShellController = shellController;
mShellExecutor = shellExecutor;
mActivityTaskManager = activityTaskManager;
mContext = context;
mContentResolver = contentResolver;
mBgHandler = bgHandler;
shellInit.addInitCallback(this::onInit, this);
mAnimationBackground = backAnimationBackground;
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
mFlingAnimationUtils = new FlingAnimationUtils.Builder(displayMetrics)
.setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
.setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
.build();
mShellBackAnimationRegistry = shellBackAnimationRegistry;
mLatencyTracker = LatencyTracker.getInstance(mContext);
}
private void onInit() {
setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
updateEnableAnimationFromFlags();
createAdapter();
mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
this::createExternalInterface, this);
}
private void setupAnimationDeveloperSettingsObserver(
@NonNull ContentResolver contentResolver,
@NonNull @ShellBackgroundThread final Handler backgroundHandler) {
if (predictiveBackSystemAnimations()) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation aconfig flag is enabled, therefore "
+ "developer settings flag is ignored and no content observer registered");
return;
}
ContentObserver settingsObserver = new ContentObserver(backgroundHandler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
updateEnableAnimationFromFlags();
}
};
contentResolver.registerContentObserver(
Global.getUriFor(Global.ENABLE_BACK_ANIMATION),
false, settingsObserver, UserHandle.USER_SYSTEM
);
}
/**
* Updates {@link BackAnimationController#mEnableAnimations} based on the current values of the
* aconfig flag and the developer settings flag
*/
@ShellBackgroundThread
private void updateEnableAnimationFromFlags() {
boolean isEnabled = predictiveBackSystemAnimations() || isDeveloperSettingEnabled();
mEnableAnimations.set(isEnabled);
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
}
private boolean isDeveloperSettingEnabled() {
return Global.getInt(mContext.getContentResolver(),
Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF) == SETTING_VALUE_ON;
}
public BackAnimation getBackAnimationImpl() {
return mBackAnimation;
}
private ExternalInterfaceBinder createExternalInterface() {
return new IBackAnimationImpl(this);
}
private final BackAnimationImpl mBackAnimation = new BackAnimationImpl();
@Override
public Context getContext() {
return mContext;
}
@Override
public ShellExecutor getRemoteCallExecutor() {
return mShellExecutor;
}
private class BackAnimationImpl implements BackAnimation {
@Override
public void onBackMotion(
float touchX,
float touchY,
float velocityX,
float velocityY,
int keyAction,
@BackEvent.SwipeEdge int swipeEdge
) {
mShellExecutor.execute(() -> onMotionEvent(
/* touchX = */ touchX,
/* touchY = */ touchY,
/* velocityX = */ velocityX,
/* velocityY = */ velocityY,
/* keyAction = */ keyAction,
/* swipeEdge = */ swipeEdge));
}
@Override
public void setTriggerBack(boolean triggerBack) {
mShellExecutor.execute(() -> BackAnimationController.this.setTriggerBack(triggerBack));
}
@Override
public void setSwipeThresholds(
float linearDistance,
float maxDistance,
float nonLinearFactor) {
mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds(
linearDistance, maxDistance, nonLinearFactor));
}
@Override
public void setStatusBarCustomizer(StatusBarCustomizer customizer) {
mCustomizer = customizer;
mAnimationBackground.setStatusBarCustomizer(customizer);
}
}
private static class IBackAnimationImpl extends IBackAnimation.Stub
implements ExternalInterfaceBinder {
private BackAnimationController mController;
IBackAnimationImpl(BackAnimationController controller) {
mController = controller;
}
@Override
public void setBackToLauncherCallback(IOnBackInvokedCallback callback,
IRemoteAnimationRunner runner) {
executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
(controller) -> controller.registerAnimation(
BackNavigationInfo.TYPE_RETURN_TO_HOME,
new BackAnimationRunner(
callback,
runner,
controller.mContext,
CUJ_PREDICTIVE_BACK_HOME)));
}
@Override
public void clearBackToLauncherCallback() {
executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback",
(controller) -> controller.unregisterAnimation(
BackNavigationInfo.TYPE_RETURN_TO_HOME));
}
public void customizeStatusBarAppearance(AppearanceRegion appearance) {
executeRemoteCallWithTaskPermission(mController, "useLauncherSysBarFlags",
(controller) -> controller.customizeStatusBarAppearance(appearance));
}
@Override
public void invalidate() {
mController = null;
}
}
private void customizeStatusBarAppearance(AppearanceRegion appearance) {
if (mCustomizer != null) {
mCustomizer.customizeStatusBarAppearance(appearance);
}
}
void registerAnimation(@BackNavigationInfo.BackTargetType int type,
@NonNull BackAnimationRunner runner) {
mShellBackAnimationRegistry.registerAnimation(type, runner);
}
void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) {
mShellBackAnimationRegistry.unregisterAnimation(type);
}
private TouchTracker getActiveTracker() {
if (mCurrentTracker.isActive()) return mCurrentTracker;
if (mQueuedTracker.isActive()) return mQueuedTracker;
return null;
}
/**
* Called when a new motion event needs to be transferred to this
* {@link BackAnimationController}
*/
public void onMotionEvent(
float touchX,
float touchY,
float velocityX,
float velocityY,
int keyAction,
@BackEvent.SwipeEdge int swipeEdge) {
TouchTracker activeTouchTracker = getActiveTracker();
if (activeTouchTracker != null) {
activeTouchTracker.update(touchX, touchY, velocityX, velocityY);
}
// two gestures are waiting to be processed at the moment, skip any further user touches
if (mCurrentTracker.isFinished() && mQueuedTracker.isFinished()) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"Ignoring MotionEvent because two gestures are already being queued.");
return;
}
if (keyAction == MotionEvent.ACTION_DOWN) {
if (!mBackGestureStarted) {
mShouldStartOnNextMoveEvent = true;
}
} else if (keyAction == MotionEvent.ACTION_MOVE) {
if (!mBackGestureStarted && mShouldStartOnNextMoveEvent) {
// Let the animation initialized here to make sure the onPointerDownOutsideFocus
// could be happened when ACTION_DOWN, it may change the current focus that we
// would access it when startBackNavigation.
onGestureStarted(touchX, touchY, swipeEdge);
mShouldStartOnNextMoveEvent = false;
}
onMove();
} else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"Finishing gesture with event action: %d", keyAction);
if (keyAction == MotionEvent.ACTION_CANCEL) {
setTriggerBack(false);
}
onGestureFinished();
}
}
private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
TouchTracker touchTracker;
if (mCurrentTracker.isInInitialState()) {
touchTracker = mCurrentTracker;
} else if (mQueuedTracker.isInInitialState()) {
touchTracker = mQueuedTracker;
} else {
ProtoLog.w(WM_SHELL_BACK_PREVIEW,
"Cannot start tracking new gesture with neither tracker in initial state.");
return;
}
touchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
touchTracker.setState(TouchTracker.TouchTrackerState.ACTIVE);
mBackGestureStarted = true;
if (touchTracker == mCurrentTracker) {
// Only start the back navigation if no other gesture is being processed. Otherwise,
// the back navigation will be started once the current gesture has finished.
startBackNavigation(mCurrentTracker);
}
}
private void startBackNavigation(@NonNull TouchTracker touchTracker) {
try {
startLatencyTracking();
mBackNavigationInfo = mActivityTaskManager.startBackNavigation(
mNavigationObserver, mEnableAnimations.get() ? mBackAnimationAdapter : null);
onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker);
} catch (RemoteException remoteException) {
Log.e(TAG, "Failed to initAnimation", remoteException);
finishBackNavigation(touchTracker.getTriggerBack());
}
}
private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo,
@NonNull TouchTracker touchTracker) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
if (backNavigationInfo == null) {
ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null.");
cancelLatencyTracking();
return;
}
final int backType = backNavigationInfo.getType();
final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
if (shouldDispatchToAnimator) {
if (!mShellBackAnimationRegistry.startGesture(backType)) {
mActiveCallback = null;
}
} else {
mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
// App is handling back animation. Cancel system animation latency tracking.
cancelLatencyTracking();
dispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
}
}
private void onMove() {
if (!mBackGestureStarted || mBackNavigationInfo == null || mActiveCallback == null) {
return;
}
// Skip dispatching if the move corresponds to the queued instead of the current gesture
if (mQueuedTracker.isActive()) return;
final BackMotionEvent backEvent = mCurrentTracker.createProgressEvent();
dispatchOnBackProgressed(mActiveCallback, backEvent);
}
private void injectBackKey() {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "injectBackKey");
sendBackEvent(KeyEvent.ACTION_DOWN);
sendBackEvent(KeyEvent.ACTION_UP);
}
@SuppressLint("MissingPermission")
private void sendBackEvent(int action) {
final long when = SystemClock.uptimeMillis();
final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */,
0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
InputDevice.SOURCE_KEYBOARD);
ev.setDisplayId(mContext.getDisplay().getDisplayId());
if (!mContext.getSystemService(InputManager.class)
.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Inject input event fail");
}
}
private boolean shouldDispatchToAnimator() {
return mEnableAnimations.get()
&& mBackNavigationInfo != null
&& mBackNavigationInfo.isPrepareRemoteAnimation();
}
private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
BackMotionEvent backEvent) {
if (callback == null) {
return;
}
try {
callback.onBackStarted(backEvent);
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackStarted error: ", e);
}
}
/**
* Allows us to manage the fling gesture, it smoothly animates the current progress value to
* the final position, calculated based on the current velocity.
*
* @param callback the callback to be invoked when the animation ends.
*/
private void dispatchOrAnimateOnBackInvoked(IOnBackInvokedCallback callback,
@NonNull TouchTracker touchTracker) {
if (callback == null) {
return;
}
boolean animationStarted = false;
if (mBackNavigationInfo != null && mBackNavigationInfo.isAnimationCallback()) {
final BackMotionEvent backMotionEvent = touchTracker.createProgressEvent();
if (backMotionEvent != null) {
// Constraints - absolute values
float minVelocity = mFlingAnimationUtils.getMinVelocityPxPerSecond();
float maxVelocity = mFlingAnimationUtils.getHighVelocityPxPerSecond();
float maxX = touchTracker.getMaxDistance(); // px
float maxFlingDistance = maxX * MAX_FLING_PROGRESS; // px
// Current state
float currentX = backMotionEvent.getTouchX();
float velocity = MathUtils.constrain(backMotionEvent.getVelocityX(),
-maxVelocity, maxVelocity);
// Target state
float animationFaction = velocity / maxVelocity; // value between -1 and 1
float flingDistance = animationFaction * maxFlingDistance; // px
float endX = MathUtils.constrain(currentX + flingDistance, 0f, maxX);
if (!Float.isNaN(endX)
&& currentX != endX
&& Math.abs(velocity) >= minVelocity) {
ValueAnimator animator = ValueAnimator.ofFloat(currentX, endX);
mFlingAnimationUtils.apply(
/* animator = */ animator,
/* currValue = */ currentX,
/* endValue = */ endX,
/* velocity = */ velocity,
/* maxDistance = */ maxFlingDistance
);
animator.addUpdateListener(animation -> {
Float animatedValue = (Float) animation.getAnimatedValue();
float progress = touchTracker.getProgress(animatedValue);
final BackMotionEvent backEvent = touchTracker.createProgressEvent(
progress);
dispatchOnBackProgressed(mActiveCallback, backEvent);
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchOnBackInvoked(callback);
}
});
animator.start();
animationStarted = true;
}
}
}
if (!animationStarted) {
dispatchOnBackInvoked(callback);
}
}
private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
if (callback == null) {
return;
}
try {
callback.onBackInvoked();
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackInvoked error: ", e);
}
}
private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
if (callback == null) {
return;
}
try {
callback.onBackCancelled();
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackCancelled error: ", e);
}
}
private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
BackMotionEvent backEvent) {
if (callback == null) {
return;
}
try {
callback.onBackProgressed(backEvent);
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackProgressed error: ", e);
}
}
/**
* Sets to true when the back gesture has passed the triggering threshold, false otherwise.
*/
public void setTriggerBack(boolean triggerBack) {
TouchTracker activeBackGestureInfo = getActiveTracker();
if (activeBackGestureInfo != null) {
activeBackGestureInfo.setTriggerBack(triggerBack);
}
}
private void setSwipeThresholds(
float linearDistance,
float maxDistance,
float nonLinearFactor) {
mCurrentTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor);
mQueuedTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor);
}
private void invokeOrCancelBack(@NonNull TouchTracker touchTracker) {
// Make a synchronized call to core before dispatch back event to client side.
// If the close transition happens before the core receives onAnimationFinished, there will
// play a second close animation for that transition.
if (mBackAnimationFinishedCallback != null) {
try {
mBackAnimationFinishedCallback.onAnimationFinished(touchTracker.getTriggerBack());
} catch (RemoteException e) {
Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e);
}
mBackAnimationFinishedCallback = null;
}
if (mBackNavigationInfo != null) {
final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
if (touchTracker.getTriggerBack()) {
dispatchOrAnimateOnBackInvoked(callback, touchTracker);
} else {
dispatchOnBackCancelled(callback);
}
}
finishBackNavigation(touchTracker.getTriggerBack());
}
/**
* Called when the gesture is released, then it could start the post commit animation.
*/
private void onGestureFinished() {
TouchTracker activeTouchTracker = getActiveTracker();
if (!mBackGestureStarted || activeTouchTracker == null) {
// This can happen when an unfinished gesture has been reset in resetTouchTracker
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"onGestureFinished called while no gesture is started");
return;
}
boolean triggerBack = activeTouchTracker.getTriggerBack();
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack);
mBackGestureStarted = false;
activeTouchTracker.setState(TouchTracker.TouchTrackerState.FINISHED);
if (mPostCommitAnimationInProgress) {
ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
return;
}
if (mBackNavigationInfo == null) {
// No focus window found or core are running recents animation, inject back key as
// legacy behavior, or new back gesture was started while previous has not finished yet
if (!mQueuedTracker.isInInitialState()) {
ProtoLog.e(WM_SHELL_BACK_PREVIEW, "mBackNavigationInfo is null AND there is "
+ "another back animation in progress");
}
mCurrentTracker.reset();
if (triggerBack) {
injectBackKey();
}
finishBackNavigation(triggerBack);
return;
}
final int backType = mBackNavigationInfo.getType();
// Simply trigger and finish back navigation when no animator defined.
if (!shouldDispatchToAnimator()
|| mShellBackAnimationRegistry.isAnimationCancelledOrNull(backType)) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Trigger back without dispatching to animator.");
invokeOrCancelBack(mCurrentTracker);
mCurrentTracker.reset();
return;
} else if (mShellBackAnimationRegistry.isWaitingAnimation(backType)) {
ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
// Supposed it is in post commit animation state, and start the timeout to watch
// if the animation is ready.
mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
return;
}
startPostCommitAnimation();
}
/**
* Start the phase 2 animation when gesture is released.
* Callback to {@link #onBackAnimationFinished} when it is finished or timeout.
*/
private void startPostCommitAnimation() {
if (mPostCommitAnimationInProgress) {
return;
}
mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()");
mPostCommitAnimationInProgress = true;
mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
// The next callback should be {@link #onBackAnimationFinished}.
if (mCurrentTracker.getTriggerBack()) {
dispatchOrAnimateOnBackInvoked(mActiveCallback, mCurrentTracker);
} else {
dispatchOnBackCancelled(mActiveCallback);
}
}
/**
* Called when the post commit animation is completed or timeout.
* This will trigger the real {@link IOnBackInvokedCallback} behavior.
*/
@VisibleForTesting
void onBackAnimationFinished() {
// Stop timeout runner.
mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
mPostCommitAnimationInProgress = false;
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
if (mCurrentTracker.isActive() || mCurrentTracker.isFinished()) {
// Trigger the real back.
invokeOrCancelBack(mCurrentTracker);
} else {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"mCurrentBackGestureInfo was null when back animation finished");
}
resetTouchTracker();
}
/**
* Resets the TouchTracker and potentially starts a new back navigation in case one is queued
*/
private void resetTouchTracker() {
TouchTracker temp = mCurrentTracker;
mCurrentTracker = mQueuedTracker;
temp.reset();
mQueuedTracker = temp;
if (mCurrentTracker.isInInitialState()) {
if (mBackGestureStarted) {
mBackGestureStarted = false;
dispatchOnBackCancelled(mActiveCallback);
finishBackNavigation(false);
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"resetTouchTracker -> reset an unfinished gesture");
} else {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> no queued gesture");
}
return;
}
if (mCurrentTracker.isFinished() && mCurrentTracker.getTriggerBack()) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> start queued back navigation "
+ "AND post commit animation");
injectBackKey();
finishBackNavigation(true);
mCurrentTracker.reset();
} else if (!mCurrentTracker.isFinished()) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"resetTouchTracker -> queued gesture not finished; do nothing");
} else {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> reset queued gesture");
mCurrentTracker.reset();
}
}
/**
* This should be called after the whole back navigation is completed.
*/
@VisibleForTesting
void finishBackNavigation(boolean triggerBack) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
mActiveCallback = null;
mShellBackAnimationRegistry.resetDefaultCrossActivity();
cancelLatencyTracking();
if (mBackNavigationInfo != null) {
mBackNavigationInfo.onBackNavigationFinished(triggerBack);
mBackNavigationInfo = null;
}
}
private void startLatencyTracking() {
if (mTrackingLatency) {
cancelLatencyTracking();
}
mLatencyTracker.onActionStart(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
mTrackingLatency = true;
}
private void cancelLatencyTracking() {
if (!mTrackingLatency) {
return;
}
mLatencyTracker.onActionCancel(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
mTrackingLatency = false;
}
private void endLatencyTracking() {
if (!mTrackingLatency) {
return;
}
mLatencyTracker.onActionEnd(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
mTrackingLatency = false;
}
private void createAdapter() {
IBackAnimationRunner runner =
new IBackAnimationRunner.Stub() {
@Override
public void onAnimationStart(
RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers,
RemoteAnimationTarget[] nonApps,
IBackAnimationFinishedCallback finishedCallback) {
mShellExecutor.execute(
() -> {
endLatencyTracking();
if (mBackNavigationInfo == null) {
ProtoLog.e(WM_SHELL_BACK_PREVIEW,
"Lack of navigation info to start animation.");
return;
}
final BackAnimationRunner runner =
mShellBackAnimationRegistry.getAnimationRunnerAndInit(
mBackNavigationInfo);
if (runner == null) {
if (finishedCallback != null) {
try {
finishedCallback.onAnimationFinished(false);
} catch (RemoteException e) {
Log.w(
TAG,
"Failed call IBackNaviAnimationController",
e);
}
}
return;
}
mActiveCallback = runner.getCallback();
mBackAnimationFinishedCallback = finishedCallback;
ProtoLog.d(
WM_SHELL_BACK_PREVIEW,
"BackAnimationController: startAnimation()");
runner.startAnimation(
apps,
wallpapers,
nonApps,
() ->
mShellExecutor.execute(
BackAnimationController.this
::onBackAnimationFinished));
if (apps.length >= 1) {
dispatchOnBackStarted(
mActiveCallback,
mCurrentTracker.createStartEvent(apps[0]));
}
// Dispatch the first progress after animation start for
// smoothing the initial animation, instead of waiting for next
// onMove.
final BackMotionEvent backFinish = mCurrentTracker
.createProgressEvent();
dispatchOnBackProgressed(mActiveCallback, backFinish);
if (!mBackGestureStarted) {
// if the down -> up gesture happened before animation
// start, we have to trigger the uninterruptible transition
// to finish the back animation.
startPostCommitAnimation();
}
});
}
@Override
public void onAnimationCancelled() {
mShellExecutor.execute(
() -> {
if (!mShellBackAnimationRegistry.cancel(
mBackNavigationInfo.getType())) {
return;
}
if (!mBackGestureStarted) {
invokeOrCancelBack(mCurrentTracker);
}
});
}
};
mBackAnimationAdapter = new BackAnimationAdapter(runner);
}
}