diff options
10 files changed, 759 insertions, 17 deletions
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java index d7bca30209e6..514059456279 100644 --- a/core/java/android/window/BackNavigationInfo.java +++ b/core/java/android/window/BackNavigationInfo.java @@ -92,6 +92,8 @@ public final class BackNavigationInfo implements Parcelable { @Nullable private final IOnBackInvokedCallback mOnBackInvokedCallback; private final boolean mPrepareRemoteAnimation; + @Nullable + private final CustomAnimationInfo mCustomAnimationInfo; /** * Create a new {@link BackNavigationInfo} instance. @@ -104,11 +106,13 @@ public final class BackNavigationInfo implements Parcelable { 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 @@ public final class BackNavigationInfo implements Parcelable { mOnBackNavigationDone = in.readTypedObject(RemoteCallback.CREATOR); mOnBackInvokedCallback = IOnBackInvokedCallback.Stub.asInterface(in.readStrongBinder()); mPrepareRemoteAnimation = in.readBoolean(); + mCustomAnimationInfo = in.readTypedObject(CustomAnimationInfo.CREATOR); } /** @hide */ @@ -125,6 +130,7 @@ public final class BackNavigationInfo implements Parcelable { dest.writeTypedObject(mOnBackNavigationDone, flags); dest.writeStrongInterface(mOnBackInvokedCallback); dest.writeBoolean(mPrepareRemoteAnimation); + dest.writeTypedObject(mCustomAnimationInfo, flags); } /** @@ -172,6 +178,15 @@ public final class BackNavigationInfo implements Parcelable { } } + /** + * Get customize animation info. + * @hide + */ + @Nullable + public CustomAnimationInfo getCustomAnimationInfo() { + return mCustomAnimationInfo; + } + /** @hide */ @Override public int describeContents() { @@ -197,6 +212,7 @@ public final class BackNavigationInfo implements Parcelable { + "mType=" + typeToString(mType) + " (" + mType + ")" + ", mOnBackNavigationDone=" + mOnBackNavigationDone + ", mOnBackInvokedCallback=" + mOnBackInvokedCallback + + ", mCustomizeAnimationInfo=" + mCustomAnimationInfo + '}'; } @@ -223,6 +239,67 @@ public final class BackNavigationInfo implements Parcelable { } /** + * 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 @@ public final class BackNavigationInfo implements Parcelable { @Nullable private IOnBackInvokedCallback mOnBackInvokedCallback = null; private boolean mPrepareRemoteAnimation; + private CustomAnimationInfo mCustomAnimationInfo; /** * @see BackNavigationInfo#getType() @@ -268,12 +346,22 @@ public final class BackNavigationInfo implements Parcelable { } /** + * 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 1c677896dbd9..b1d75826a948 100644 --- a/core/java/android/window/IBackAnimationRunner.aidl +++ b/core/java/android/window/IBackAnimationRunner.aidl @@ -37,14 +37,13 @@ oneway interface IBackAnimationRunner { /** * 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 600ae50e8d02..5cab674eab05 100644 --- a/core/java/com/android/internal/policy/TransitionAnimation.java +++ b/core/java/com/android/internal/policy/TransitionAnimation.java @@ -265,6 +265,34 @@ public class TransitionAnimation { } 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 0b8759890359..349ff36ce8ce 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Nullable private IOnBackInvokedCallback mActiveCallback; + private CrossActivityAnimation mDefaultActivityAnimation; + private CustomizeActivityAnimation mCustomizeActivityAnimation; + @VisibleForTesting final RemoteCallback mNavigationObserver = new RemoteCallback( new RemoteCallback.OnResultListener() { @@ -194,10 +197,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } return; } + mActiveCallback = runner.getCallback(); mBackAnimationFinishedCallback = finishedCallback; ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()"); @@ -645,11 +676,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 82c523f337db..22b841a338c1 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 @@ class BackAnimationRunner { 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 000000000000..ae33b9445acd --- /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 5a4a44fde58d..6dae479ae7a7 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 @@ public class BackAnimationControllerTest extends ShellTestCase { 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 000000000000..2814ef9e26cc --- /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 2d45dc20c963..f9f972c20ac6 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_CLOSING; 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 static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; 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.OnBackInvokedCallbackInfo; 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 @@ class BackNavigationController { // 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 @@ class BackNavigationController { } 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 @@ class BackNavigationController { 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 @@ class BackNavigationController { 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 56461f007351..b80c3e84198b 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.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; 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 @@ public class BackNavigationControllerTests extends WindowTestsBase { } @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 @@ public class BackNavigationControllerTests extends WindowTestsBase { 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 @@ public class BackNavigationControllerTests extends WindowTestsBase { testCase.task = task; testCase.recordBack = record1; testCase.recordFront = record2; + testCase.windowBack = window1; + testCase.windowFront = window2; return testCase; } @@ -525,6 +552,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { private class CrossActivityTestCase { public Task task; public ActivityRecord recordBack; + public WindowState windowBack; public ActivityRecord recordFront; + public WindowState windowFront; } } |