Play seekable animation for customize activity transition API.
For cross-activity animation, support seekable animation if app
has customize activity exit transition by Window#setWindowAnimations or
android:windowAnimationStyle.
Because there could fall back to default cross-activity animation when
the customized animation was not able to load, defer assigning the
mActiveCallback when received onAnimationStart.
Bug: 259427810
Test: verify customized animation can play.
Test: atest BackNavigationControllerTests BackAnimationControllerTest\
CustomizeActivityAnimationTest
Change-Id: I59dc6ef75a226c634b06f483aeed7aec03087d18
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java
index d7bca30..5140594 100644
--- a/core/java/android/window/BackNavigationInfo.java
+++ b/core/java/android/window/BackNavigationInfo.java
@@ -92,6 +92,8 @@
@Nullable
private final IOnBackInvokedCallback mOnBackInvokedCallback;
private final boolean mPrepareRemoteAnimation;
+ @Nullable
+ private final CustomAnimationInfo mCustomAnimationInfo;
/**
* Create a new {@link BackNavigationInfo} instance.
@@ -104,11 +106,13 @@
private BackNavigationInfo(@BackTargetType int type,
@Nullable RemoteCallback onBackNavigationDone,
@Nullable IOnBackInvokedCallback onBackInvokedCallback,
- boolean isPrepareRemoteAnimation) {
+ boolean isPrepareRemoteAnimation,
+ @Nullable CustomAnimationInfo customAnimationInfo) {
mType = type;
mOnBackNavigationDone = onBackNavigationDone;
mOnBackInvokedCallback = onBackInvokedCallback;
mPrepareRemoteAnimation = isPrepareRemoteAnimation;
+ mCustomAnimationInfo = customAnimationInfo;
}
private BackNavigationInfo(@NonNull Parcel in) {
@@ -116,6 +120,7 @@
mOnBackNavigationDone = in.readTypedObject(RemoteCallback.CREATOR);
mOnBackInvokedCallback = IOnBackInvokedCallback.Stub.asInterface(in.readStrongBinder());
mPrepareRemoteAnimation = in.readBoolean();
+ mCustomAnimationInfo = in.readTypedObject(CustomAnimationInfo.CREATOR);
}
/** @hide */
@@ -125,6 +130,7 @@
dest.writeTypedObject(mOnBackNavigationDone, flags);
dest.writeStrongInterface(mOnBackInvokedCallback);
dest.writeBoolean(mPrepareRemoteAnimation);
+ dest.writeTypedObject(mCustomAnimationInfo, flags);
}
/**
@@ -172,6 +178,15 @@
}
}
+ /**
+ * Get customize animation info.
+ * @hide
+ */
+ @Nullable
+ public CustomAnimationInfo getCustomAnimationInfo() {
+ return mCustomAnimationInfo;
+ }
+
/** @hide */
@Override
public int describeContents() {
@@ -197,6 +212,7 @@
+ "mType=" + typeToString(mType) + " (" + mType + ")"
+ ", mOnBackNavigationDone=" + mOnBackNavigationDone
+ ", mOnBackInvokedCallback=" + mOnBackInvokedCallback
+ + ", mCustomizeAnimationInfo=" + mCustomAnimationInfo
+ '}';
}
@@ -223,6 +239,67 @@
}
/**
+ * Information for customize back animation.
+ * @hide
+ */
+ public static final class CustomAnimationInfo implements Parcelable {
+ private final String mPackageName;
+ private int mWindowAnimations;
+
+ /**
+ * The package name of the windowAnimations.
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * The resource Id of window animations.
+ */
+ public int getWindowAnimations() {
+ return mWindowAnimations;
+ }
+
+ public CustomAnimationInfo(@NonNull String packageName) {
+ this.mPackageName = packageName;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(mPackageName);
+ dest.writeInt(mWindowAnimations);
+ }
+
+ private CustomAnimationInfo(@NonNull Parcel in) {
+ mPackageName = in.readString8();
+ mWindowAnimations = in.readInt();
+ }
+
+ @Override
+ public String toString() {
+ return "CustomAnimationInfo, package name= " + mPackageName;
+ }
+
+ @NonNull
+ public static final Creator<CustomAnimationInfo> CREATOR = new Creator<>() {
+ @Override
+ public CustomAnimationInfo createFromParcel(Parcel in) {
+ return new CustomAnimationInfo(in);
+ }
+
+ @Override
+ public CustomAnimationInfo[] newArray(int size) {
+ return new CustomAnimationInfo[size];
+ }
+ };
+ }
+ /**
* @hide
*/
@SuppressWarnings("UnusedReturnValue") // Builder pattern
@@ -233,6 +310,7 @@
@Nullable
private IOnBackInvokedCallback mOnBackInvokedCallback = null;
private boolean mPrepareRemoteAnimation;
+ private CustomAnimationInfo mCustomAnimationInfo;
/**
* @see BackNavigationInfo#getType()
@@ -268,12 +346,22 @@
}
/**
+ * Set windowAnimations for customize animation.
+ */
+ public Builder setWindowAnimations(String packageName, int windowAnimations) {
+ mCustomAnimationInfo = new CustomAnimationInfo(packageName);
+ mCustomAnimationInfo.mWindowAnimations = windowAnimations;
+ return this;
+ }
+
+ /**
* Builds and returns an instance of {@link BackNavigationInfo}
*/
public BackNavigationInfo build() {
return new BackNavigationInfo(mType, mOnBackNavigationDone,
mOnBackInvokedCallback,
- mPrepareRemoteAnimation);
+ mPrepareRemoteAnimation,
+ mCustomAnimationInfo);
}
}
}
diff --git a/core/java/android/window/IBackAnimationRunner.aidl b/core/java/android/window/IBackAnimationRunner.aidl
index 1c67789..b1d7582 100644
--- a/core/java/android/window/IBackAnimationRunner.aidl
+++ b/core/java/android/window/IBackAnimationRunner.aidl
@@ -37,14 +37,13 @@
/**
* Called when the system is ready for the handler to start animating all the visible tasks.
- * @param type The back navigation type.
* @param apps The list of departing (type=MODE_CLOSING) and entering (type=MODE_OPENING)
windows to animate,
* @param wallpapers The list of wallpapers to animate.
* @param nonApps The list of non-app windows such as Bubbles to animate.
* @param finishedCallback The callback to invoke when the animation is finished.
*/
- void onAnimationStart(in int type,
+ void onAnimationStart(
in RemoteAnimationTarget[] apps,
in RemoteAnimationTarget[] wallpapers,
in RemoteAnimationTarget[] nonApps,
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 600ae50..5cab674 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -265,6 +265,34 @@
}
return null;
}
+
+ /** Get animation resId by attribute Id from specific LayoutParams */
+ public int getAnimationResId(LayoutParams lp, int animAttr, int transit) {
+ int resId = Resources.ID_NULL;
+ if (animAttr >= 0) {
+ AttributeCache.Entry ent = getCachedAnimations(lp);
+ if (ent != null) {
+ resId = ent.array.getResourceId(animAttr, 0);
+ }
+ }
+ resId = updateToTranslucentAnimIfNeeded(resId, transit);
+ return resId;
+ }
+
+ /** Get default animation resId */
+ public int getDefaultAnimationResId(int animAttr, int transit) {
+ int resId = Resources.ID_NULL;
+ if (animAttr >= 0) {
+ AttributeCache.Entry ent = getCachedAnimations(DEFAULT_PACKAGE,
+ mDefaultWindowAnimationStyleResId);
+ if (ent != null) {
+ resId = ent.array.getResourceId(animAttr, 0);
+ }
+ }
+ resId = updateToTranslucentAnimIfNeeded(resId, transit);
+ return resId;
+ }
+
/**
* Load animation by attribute Id from a specific AnimationStyle resource.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 0b87598..349ff36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -120,6 +120,9 @@
@Nullable
private IOnBackInvokedCallback mActiveCallback;
+ private CrossActivityAnimation mDefaultActivityAnimation;
+ private CustomizeActivityAnimation mCustomizeActivityAnimation;
+
@VisibleForTesting
final RemoteCallback mNavigationObserver = new RemoteCallback(
new RemoteCallback.OnResultListener() {
@@ -194,10 +197,12 @@
new CrossTaskBackAnimation(mContext, mAnimationBackground);
mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
crossTaskAnimation.mBackAnimationRunner);
- final CrossActivityAnimation crossActivityAnimation =
+ mDefaultActivityAnimation =
new CrossActivityAnimation(mContext, mAnimationBackground);
mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
- crossActivityAnimation.mBackAnimationRunner);
+ mDefaultActivityAnimation.mBackAnimationRunner);
+ mCustomizeActivityAnimation =
+ new CustomizeActivityAnimation(mContext, mAnimationBackground);
// TODO (236760237): register dialog close animation when it's completed.
}
@@ -368,7 +373,6 @@
final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
if (shouldDispatchToAnimator) {
if (mAnimationDefinition.contains(backType)) {
- mActiveCallback = mAnimationDefinition.get(backType).getCallback();
mAnimationDefinition.get(backType).startGesture();
} else {
mActiveCallback = null;
@@ -542,13 +546,12 @@
}
final int backType = mBackNavigationInfo.getType();
+ final BackAnimationRunner runner = mAnimationDefinition.get(backType);
// Simply trigger and finish back navigation when no animator defined.
- if (!shouldDispatchToAnimator() || mActiveCallback == null) {
+ if (!shouldDispatchToAnimator() || runner == null) {
invokeOrCancelBack();
return;
}
-
- final BackAnimationRunner runner = mAnimationDefinition.get(backType);
if (runner.isWaitingAnimation()) {
ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
return;
@@ -607,6 +610,12 @@
mShouldStartOnNextMoveEvent = false;
mTouchTracker.reset();
mActiveCallback = null;
+ // reset to default
+ if (mDefaultActivityAnimation != null
+ && mAnimationDefinition.contains(BackNavigationInfo.TYPE_CROSS_ACTIVITY)) {
+ mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ mDefaultActivityAnimation.mBackAnimationRunner);
+ }
if (mBackNavigationInfo != null) {
mBackNavigationInfo.onBackNavigationFinished(mTriggerBack);
mBackNavigationInfo = null;
@@ -614,14 +623,35 @@
mTriggerBack = false;
}
+ private BackAnimationRunner getAnimationRunnerAndInit() {
+ int type = mBackNavigationInfo.getType();
+ // Initiate customized cross-activity animation, or fall back to cross activity animation
+ if (type == BackNavigationInfo.TYPE_CROSS_ACTIVITY && mAnimationDefinition.contains(type)) {
+ final BackNavigationInfo.CustomAnimationInfo animationInfo =
+ mBackNavigationInfo.getCustomAnimationInfo();
+ if (animationInfo != null && mCustomizeActivityAnimation != null
+ && mCustomizeActivityAnimation.prepareNextAnimation(animationInfo)) {
+ mAnimationDefinition.get(type).resetWaitingAnimation();
+ mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ mCustomizeActivityAnimation.mBackAnimationRunner);
+ }
+ }
+ return mAnimationDefinition.get(type);
+ }
+
private void createAdapter() {
IBackAnimationRunner runner = new IBackAnimationRunner.Stub() {
@Override
- public void onAnimationStart(int type, RemoteAnimationTarget[] apps,
+ public void onAnimationStart(RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
IBackAnimationFinishedCallback finishedCallback) {
mShellExecutor.execute(() -> {
- final BackAnimationRunner runner = mAnimationDefinition.get(type);
+ if (mBackNavigationInfo == null) {
+ Log.e(TAG, "Lack of navigation info to start animation.");
+ return;
+ }
+ final int type = mBackNavigationInfo.getType();
+ final BackAnimationRunner runner = getAnimationRunnerAndInit();
if (runner == null) {
Log.e(TAG, "Animation didn't be defined for type "
+ BackNavigationInfo.typeToString(type));
@@ -634,6 +664,7 @@
}
return;
}
+ mActiveCallback = runner.getCallback();
mBackAnimationFinishedCallback = finishedCallback;
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
@@ -645,11 +676,13 @@
mActiveCallback, mTouchTracker.createStartEvent(apps[0]));
}
+ // Dispatch the first progress after animation start for smoothing the initial
+ // animation, instead of waiting for next onMove.
+ final BackMotionEvent backFinish = mTouchTracker.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.
- final BackMotionEvent backFinish = mTouchTracker.createProgressEvent();
- dispatchOnBackProgressed(mActiveCallback, backFinish);
startPostCommitAnimation();
}
});
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index 82c523f..22b841a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -99,4 +99,8 @@
boolean isAnimationCancelled() {
return mAnimationCancelled;
}
+
+ void resetWaitingAnimation() {
+ mWaitingAnimation = false;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
new file mode 100644
index 0000000..ae33b94
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2023 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 android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.util.FloatProperty;
+import android.view.Choreographer;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Transformation;
+import android.window.BackEvent;
+import android.window.BackMotionEvent;
+import android.window.BackNavigationInfo;
+import android.window.BackProgressAnimator;
+import android.window.IOnBackInvokedCallback;
+
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.policy.TransitionAnimation;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+/**
+ * Class that handle customized close activity transition animation.
+ */
+@ShellMainThread
+class CustomizeActivityAnimation {
+ private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+ final BackAnimationRunner mBackAnimationRunner;
+ private final float mCornerRadius;
+ private final SurfaceControl.Transaction mTransaction;
+ private final BackAnimationBackground mBackground;
+ private RemoteAnimationTarget mEnteringTarget;
+ private RemoteAnimationTarget mClosingTarget;
+ private IRemoteAnimationFinishedCallback mFinishCallback;
+ /** Duration of post animation after gesture committed. */
+ private static final int POST_ANIMATION_DURATION = 250;
+
+ private static final int SCALE_FACTOR = 1000;
+ private final SpringAnimation mProgressSpring;
+ private float mLatestProgress = 0.0f;
+
+ private static final float TARGET_COMMIT_PROGRESS = 0.5f;
+
+ private final float[] mTmpFloat9 = new float[9];
+ private final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
+
+ final CustomAnimationLoader mCustomAnimationLoader;
+ private Animation mEnterAnimation;
+ private Animation mCloseAnimation;
+ final Transformation mTransformation = new Transformation();
+
+ private final Choreographer mChoreographer;
+
+ CustomizeActivityAnimation(Context context, BackAnimationBackground background) {
+ this(context, background, new SurfaceControl.Transaction(), null);
+ }
+
+ CustomizeActivityAnimation(Context context, BackAnimationBackground background,
+ SurfaceControl.Transaction transaction, Choreographer choreographer) {
+ mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+ mBackground = background;
+ mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
+ mCustomAnimationLoader = new CustomAnimationLoader(context);
+
+ mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
+ mProgressSpring.setSpring(new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+ mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction;
+ mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance();
+ }
+
+ private float getLatestProgress() {
+ return mLatestProgress * SCALE_FACTOR;
+ }
+ private void setLatestProgress(float value) {
+ mLatestProgress = value / SCALE_FACTOR;
+ applyTransformTransaction(mLatestProgress);
+ }
+
+ private static final FloatProperty<CustomizeActivityAnimation> ENTER_PROGRESS_PROP =
+ new FloatProperty<>("enter") {
+ @Override
+ public void setValue(CustomizeActivityAnimation anim, float value) {
+ anim.setLatestProgress(value);
+ }
+
+ @Override
+ public Float get(CustomizeActivityAnimation object) {
+ return object.getLatestProgress();
+ }
+ };
+
+ // The target will lose focus when alpha == 0, so keep a minimum value for it.
+ private static float keepMinimumAlpha(float transAlpha) {
+ return Math.max(transAlpha, 0.005f);
+ }
+
+ private static void initializeAnimation(Animation animation, Rect bounds) {
+ final int width = bounds.width();
+ final int height = bounds.height();
+ animation.initialize(width, height, width, height);
+ }
+
+ private void startBackAnimation() {
+ if (mEnteringTarget == null || mClosingTarget == null
+ || mCloseAnimation == null || mEnterAnimation == null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
+ return;
+ }
+ initializeAnimation(mCloseAnimation, mClosingTarget.localBounds);
+ initializeAnimation(mEnterAnimation, mEnteringTarget.localBounds);
+
+ // Draw background with task background color.
+ if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) {
+ mBackground.ensureBackground(
+ mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
+ }
+ }
+
+ private void applyTransformTransaction(float progress) {
+ if (mClosingTarget == null || mEnteringTarget == null) {
+ return;
+ }
+ applyTransform(mClosingTarget.leash, progress, mCloseAnimation);
+ applyTransform(mEnteringTarget.leash, progress, mEnterAnimation);
+ mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+ mTransaction.apply();
+ }
+
+ private void applyTransform(SurfaceControl leash, float progress, Animation animation) {
+ mTransformation.clear();
+ animation.getTransformationAt(progress, mTransformation);
+ mTransaction.setMatrix(leash, mTransformation.getMatrix(), mTmpFloat9);
+ mTransaction.setAlpha(leash, keepMinimumAlpha(mTransformation.getAlpha()));
+ mTransaction.setCornerRadius(leash, mCornerRadius);
+ }
+
+ void finishAnimation() {
+ if (mCloseAnimation != null) {
+ mCloseAnimation.reset();
+ mCloseAnimation = null;
+ }
+ if (mEnterAnimation != null) {
+ mEnterAnimation.reset();
+ mEnterAnimation = null;
+ }
+ if (mEnteringTarget != null) {
+ mEnteringTarget.leash.release();
+ mEnteringTarget = null;
+ }
+ if (mClosingTarget != null) {
+ mClosingTarget.leash.release();
+ mClosingTarget = null;
+ }
+ if (mBackground != null) {
+ mBackground.removeBackground(mTransaction);
+ }
+ mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+ mTransaction.apply();
+ mTransformation.clear();
+ mLatestProgress = 0;
+ if (mFinishCallback != null) {
+ try {
+ mFinishCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ mFinishCallback = null;
+ }
+ mProgressSpring.animateToFinalPosition(0);
+ mProgressSpring.skipToEnd();
+ }
+
+ void onGestureProgress(@NonNull BackEvent backEvent) {
+ if (mEnteringTarget == null || mClosingTarget == null
+ || mCloseAnimation == null || mEnterAnimation == null) {
+ return;
+ }
+
+ final float progress = backEvent.getProgress();
+
+ float springProgress = (progress > 0.1f
+ ? mapLinear(progress, 0.1f, 1f, TARGET_COMMIT_PROGRESS, 1f)
+ : mapLinear(progress, 0, 1f, 0f, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
+
+ mProgressSpring.animateToFinalPosition(springProgress);
+ }
+
+ static float mapLinear(float x, float a1, float a2, float b1, float b2) {
+ return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
+ }
+
+ void onGestureCommitted() {
+ if (mEnteringTarget == null || mClosingTarget == null
+ || mCloseAnimation == null || mEnterAnimation == null) {
+ finishAnimation();
+ return;
+ }
+ mProgressSpring.cancel();
+
+ // Enter phase 2 of the animation
+ final ValueAnimator valueAnimator = ValueAnimator.ofFloat(mLatestProgress, 1f)
+ .setDuration(POST_ANIMATION_DURATION);
+ valueAnimator.setInterpolator(mDecelerateInterpolator);
+ valueAnimator.addUpdateListener(animation -> {
+ float progress = (float) animation.getAnimatedValue();
+ applyTransformTransaction(progress);
+ });
+
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finishAnimation();
+ }
+ });
+ valueAnimator.start();
+ }
+
+ /**
+ * Load customize animation before animation start.
+ */
+ boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) {
+ mCloseAnimation = mCustomAnimationLoader.load(
+ animationInfo, false /* enterAnimation */);
+ if (mCloseAnimation != null) {
+ mEnterAnimation = mCustomAnimationLoader.load(
+ animationInfo, true /* enterAnimation */);
+ return true;
+ }
+ return false;
+ }
+
+ private final class Callback extends IOnBackInvokedCallback.Default {
+ @Override
+ public void onBackStarted(BackMotionEvent backEvent) {
+ mProgressAnimator.onBackStarted(backEvent,
+ CustomizeActivityAnimation.this::onGestureProgress);
+ }
+
+ @Override
+ public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
+ mProgressAnimator.onBackProgressed(backEvent);
+ }
+
+ @Override
+ public void onBackCancelled() {
+ mProgressAnimator.onBackCancelled(CustomizeActivityAnimation.this::finishAnimation);
+ }
+
+ @Override
+ public void onBackInvoked() {
+ mProgressAnimator.reset();
+ onGestureCommitted();
+ }
+ }
+
+ private final class Runner extends IRemoteAnimationRunner.Default {
+ @Override
+ public void onAnimationStart(
+ int transit,
+ RemoteAnimationTarget[] apps,
+ RemoteAnimationTarget[] wallpapers,
+ RemoteAnimationTarget[] nonApps,
+ IRemoteAnimationFinishedCallback finishedCallback) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to customize animation.");
+ for (RemoteAnimationTarget a : apps) {
+ if (a.mode == MODE_CLOSING) {
+ mClosingTarget = a;
+ }
+ if (a.mode == MODE_OPENING) {
+ mEnteringTarget = a;
+ }
+ }
+ if (mCloseAnimation == null || mEnterAnimation == null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW,
+ "No animation loaded, should choose cross-activity animation?");
+ }
+
+ startBackAnimation();
+ mFinishCallback = finishedCallback;
+ }
+
+ @Override
+ public void onAnimationCancelled(boolean isKeyguardOccluded) {
+ finishAnimation();
+ }
+ }
+
+ /**
+ * Helper class to load custom animation.
+ */
+ static class CustomAnimationLoader {
+ private final TransitionAnimation mTransitionAnimation;
+
+ CustomAnimationLoader(Context context) {
+ mTransitionAnimation = new TransitionAnimation(
+ context, false /* debug */, "CustomizeBackAnimation");
+ }
+
+ Animation load(BackNavigationInfo.CustomAnimationInfo animationInfo,
+ boolean enterAnimation) {
+ final String packageName = animationInfo.getPackageName();
+ if (packageName.isEmpty()) {
+ return null;
+ }
+ final int windowAnimations = animationInfo.getWindowAnimations();
+ if (windowAnimations == 0) {
+ return null;
+ }
+ final int attrs = enterAnimation
+ ? com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation
+ : com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation;
+ Animation a = mTransitionAnimation.loadAnimationAttr(packageName, windowAnimations,
+ attrs, false /* translucent */);
+ // Only allow to load default animation for opening target.
+ if (a == null && enterAnimation) {
+ a = mTransitionAnimation.loadDefaultAnimationAttr(attrs, false /* translucent */);
+ }
+ if (a != null) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a);
+ } else {
+ ProtoLog.e(WM_SHELL_BACK_PREVIEW, "No custom animation loaded");
+ }
+ return a;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 5a4a44f..6dae479 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -422,7 +422,7 @@
RemoteAnimationTarget animationTarget = createAnimationTarget();
RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget};
if (mController.mBackAnimationAdapter != null) {
- mController.mBackAnimationAdapter.getRunner().onAnimationStart(type,
+ mController.mBackAnimationAdapter.getRunner().onAnimationStart(
targets, null, null, mBackAnimationFinishedCallback);
mShellExecutor.flushAll();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
new file mode 100644
index 0000000..2814ef9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2023 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.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.WindowConfiguration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Choreographer;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.BackNavigationInfo;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class CustomizeActivityAnimationTest extends ShellTestCase {
+ private static final int BOUND_SIZE = 100;
+ @Mock
+ private BackAnimationBackground mBackAnimationBackground;
+ @Mock
+ private Animation mMockCloseAnimation;
+ @Mock
+ private Animation mMockOpenAnimation;
+
+ private CustomizeActivityAnimation mCustomizeActivityAnimation;
+
+ @Before
+ public void setUp() throws Exception {
+ mCustomizeActivityAnimation = new CustomizeActivityAnimation(mContext,
+ mBackAnimationBackground, mock(SurfaceControl.Transaction.class),
+ mock(Choreographer.class));
+ spyOn(mCustomizeActivityAnimation);
+ spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader);
+ doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+ .load(any(), eq(false));
+ doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
+ .load(any(), eq(true));
+ }
+
+ RemoteAnimationTarget createAnimationTarget(boolean open) {
+ SurfaceControl topWindowLeash = new SurfaceControl();
+ return new RemoteAnimationTarget(1,
+ open ? RemoteAnimationTarget.MODE_OPENING : RemoteAnimationTarget.MODE_CLOSING,
+ topWindowLeash, false, new Rect(), new Rect(), -1,
+ new Point(0, 0), new Rect(0, 0, BOUND_SIZE, BOUND_SIZE), new Rect(),
+ new WindowConfiguration(), true, null, null, null, false, -1);
+ }
+
+ @Test
+ public void receiveFinishAfterInvoke() throws InterruptedException {
+ mCustomizeActivityAnimation.prepareNextAnimation(
+ new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+ final RemoteAnimationTarget close = createAnimationTarget(false);
+ final RemoteAnimationTarget open = createAnimationTarget(true);
+ // start animation with remote animation targets
+ final CountDownLatch finishCalled = new CountDownLatch(1);
+ final Runnable finishCallback = finishCalled::countDown;
+ mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+ new RemoteAnimationTarget[]{close, open}, null, null, finishCallback);
+ verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+ verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+
+ try {
+ mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked();
+ } catch (RemoteException r) {
+ fail("onBackInvoked throw remote exception");
+ }
+ verify(mCustomizeActivityAnimation).onGestureCommitted();
+ finishCalled.await(1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void receiveFinishAfterCancel() throws InterruptedException {
+ mCustomizeActivityAnimation.prepareNextAnimation(
+ new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+ final RemoteAnimationTarget close = createAnimationTarget(false);
+ final RemoteAnimationTarget open = createAnimationTarget(true);
+ // start animation with remote animation targets
+ final CountDownLatch finishCalled = new CountDownLatch(1);
+ final Runnable finishCallback = finishCalled::countDown;
+ mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+ new RemoteAnimationTarget[]{close, open}, null, null, finishCallback);
+ verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+ verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
+ eq(BOUND_SIZE), eq(BOUND_SIZE));
+
+ try {
+ mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackCancelled();
+ } catch (RemoteException r) {
+ fail("onBackCancelled throw remote exception");
+ }
+ finishCalled.await(1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void receiveFinishWithoutAnimationAfterInvoke() throws InterruptedException {
+ mCustomizeActivityAnimation.prepareNextAnimation(
+ new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
+ // start animation without any remote animation targets
+ final CountDownLatch finishCalled = new CountDownLatch(1);
+ final Runnable finishCallback = finishCalled::countDown;
+ mCustomizeActivityAnimation.mBackAnimationRunner.startAnimation(
+ new RemoteAnimationTarget[]{}, null, null, finishCallback);
+
+ try {
+ mCustomizeActivityAnimation.mBackAnimationRunner.getCallback().onBackInvoked();
+ } catch (RemoteException r) {
+ fail("onBackInvoked throw remote exception");
+ }
+ verify(mCustomizeActivityAnimation).onGestureCommitted();
+ finishCalled.await(1, TimeUnit.SECONDS);
+ }
+}
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 2d45dc2..f9f972c 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -21,6 +21,7 @@
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_NONE;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
@@ -31,6 +32,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.res.ResourceId;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
@@ -51,12 +53,14 @@
import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.LocalServices;
import com.android.server.wm.utils.InsetUtils;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Objects;
import java.util.function.Consumer;
/**
@@ -80,6 +84,8 @@
// re-parenting leashes and set launch behind, etc. Will be handled when transition finished.
private AnimationHandler.ScheduleAnimationBuilder mPendingAnimationBuilder;
+ private static int sDefaultAnimationResId;
+
/**
* true if the back predictability feature is enabled
*/
@@ -255,9 +261,19 @@
} else if (prevActivity != null) {
if (!isOccluded || prevActivity.canShowWhenLocked()) {
// We have another Activity in the same currentTask to go to
- backType = BackNavigationInfo.TYPE_CROSS_ACTIVITY;
+ final WindowContainer parent = currentActivity.getParent();
+ final boolean isCustomize = parent != null
+ && (parent.asTask() != null
+ || (parent.asTaskFragment() != null
+ && parent.canCustomizeAppTransition()))
+ && isCustomizeExitAnimation(window);
+ if (isCustomize) {
+ infoBuilder.setWindowAnimations(
+ window.mAttrs.packageName, window.mAttrs.windowAnimations);
+ }
removedWindowContainer = currentActivity;
prevTask = prevActivity.getTask();
+ backType = BackNavigationInfo.TYPE_CROSS_ACTIVITY;
} else {
backType = BackNavigationInfo.TYPE_CALLBACK;
}
@@ -370,6 +386,37 @@
return kc.isKeyguardLocked(displayId) && kc.isDisplayOccluded(displayId);
}
+ /**
+ * There are two ways to customize activity exit animation, one is to provide the
+ * windowAnimationStyle by Activity#setTheme, another one is to set resId by
+ * Window#setWindowAnimations.
+ * Not all run-time customization methods can be checked from here, such as
+ * overridePendingTransition, which the animation resource will be set just before the
+ * transition is about to happen.
+ */
+ private static boolean isCustomizeExitAnimation(WindowState window) {
+ // The default animation ResId is loaded from system package, so the result must match.
+ if (Objects.equals(window.mAttrs.packageName, "android")) {
+ return false;
+ }
+ if (window.mAttrs.windowAnimations != 0) {
+ final TransitionAnimation transitionAnimation = window.getDisplayContent()
+ .mAppTransition.mTransitionAnimation;
+ final int attr = com.android.internal.R.styleable
+ .WindowAnimation_activityCloseExitAnimation;
+ final int appResId = transitionAnimation.getAnimationResId(
+ window.mAttrs, attr, TRANSIT_OLD_NONE);
+ if (ResourceId.isValid(appResId)) {
+ if (sDefaultAnimationResId == 0) {
+ sDefaultAnimationResId = transitionAnimation.getDefaultAnimationResId(attr,
+ TRANSIT_OLD_NONE);
+ }
+ return sDefaultAnimationResId != appResId;
+ }
+ }
+ return false;
+ }
+
// For legacy transition.
/**
* Once we find the transition targets match back animation targets, remove the target from
@@ -997,7 +1044,7 @@
return () -> {
try {
- mBackAnimationAdapter.getRunner().onAnimationStart(mType,
+ mBackAnimationAdapter.getRunner().onAnimationStart(
targets, null, null, callback);
} catch (RemoteException e) {
e.printStackTrace();
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 56461f0..b80c3e8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -26,6 +26,7 @@
import static android.window.BackNavigationInfo.typeToString;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -149,6 +150,28 @@
}
@Test
+ public void backTypeCrossActivityWithCustomizeExitAnimation() {
+ CrossActivityTestCase testCase = createTopTaskWithTwoActivities();
+ IOnBackInvokedCallback callback = withSystemCallback(testCase.task);
+ testCase.windowFront.mAttrs.windowAnimations = 0x10;
+ spyOn(mDisplayContent.mAppTransition.mTransitionAnimation);
+ doReturn(0xffff00AB).when(mDisplayContent.mAppTransition.mTransitionAnimation)
+ .getAnimationResId(any(), anyInt(), anyInt());
+ doReturn(0xffff00CD).when(mDisplayContent.mAppTransition.mTransitionAnimation)
+ .getDefaultAnimationResId(anyInt(), anyInt());
+
+ BackNavigationInfo backNavigationInfo = startBackNavigation();
+ assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull();
+ assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback);
+ assertThat(backNavigationInfo.getCustomAnimationInfo().getWindowAnimations())
+ .isEqualTo(testCase.windowFront.mAttrs.windowAnimations);
+ assertThat(typeToString(backNavigationInfo.getType()))
+ .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_ACTIVITY));
+ // verify if back animation would start.
+ assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation());
+ }
+
+ @Test
public void backTypeCrossActivityWhenBackToPreviousActivity() {
CrossActivityTestCase testCase = createTopTaskWithTwoActivities();
IOnBackInvokedCallback callback = withSystemCallback(testCase.task);
@@ -158,6 +181,8 @@
assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback);
assertThat(typeToString(backNavigationInfo.getType()))
.isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_ACTIVITY));
+ // verify if back animation would start.
+ assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation());
// reset drawing status
testCase.recordFront.forAllWindows(w -> {
@@ -510,6 +535,8 @@
testCase.task = task;
testCase.recordBack = record1;
testCase.recordFront = record2;
+ testCase.windowBack = window1;
+ testCase.windowFront = window2;
return testCase;
}
@@ -525,6 +552,8 @@
private class CrossActivityTestCase {
public Task task;
public ActivityRecord recordBack;
+ public WindowState windowBack;
public ActivityRecord recordFront;
+ public WindowState windowFront;
}
}