Merge "Play seekable animation for customize activity transition API." into udc-dev

This commit is contained in:
Wei Sheng Shih
2023-03-08 09:02:48 +00:00
committed by Android (Google) Code Review
10 changed files with 759 additions and 17 deletions

View File

@@ -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
+ '}';
}
@@ -222,6 +238,67 @@ public final class BackNavigationInfo implements Parcelable {
return String.valueOf(type);
}
/**
* 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
*/
@@ -233,6 +310,7 @@ public final class BackNavigationInfo implements Parcelable {
@Nullable
private IOnBackInvokedCallback mOnBackInvokedCallback = null;
private boolean mPrepareRemoteAnimation;
private CustomAnimationInfo mCustomAnimationInfo;
/**
* @see BackNavigationInfo#getType()
@@ -267,13 +345,23 @@ public final class BackNavigationInfo implements Parcelable {
return this;
}
/**
* 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);
}
}
}

View File

@@ -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,

View File

@@ -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.
*

View File

@@ -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();
}
});

View File

@@ -99,4 +99,8 @@ class BackAnimationRunner {
boolean isAnimationCancelled() {
return mAnimationCancelled;
}
void resetWaitingAnimation() {
mWaitingAnimation = false;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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;
@@ -148,6 +149,28 @@ public class BackNavigationControllerTests extends WindowTestsBase {
.isEqualTo(typeToString(BackNavigationInfo.TYPE_RETURN_TO_HOME));
}
@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();
@@ -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;
}
}