Schedule PIP mode changes at the beginning/end of the transitions.
- When transitioning from fullscreen to PiP, ensure all PiP/MW/Config
changes come after the transition completes, and inversely, from PiP to
fullscreen, ensure that all changes come before the transition up starts
- Add a series of tests to verify the callback state when the animation
is canceled
- Also fixes an issue where the surface is preserved when we don't want
Bug: 37169080
Bug: 37103000
Test: bit FrameworksServicesTests:com.android.server.wm.BoundsAnimationControllerTests
Test: android.server.cts.ActivityManagerPinnedStackTests
Change-Id: I6425c95df358358ed76d9cc8a130606c2124062e
diff --git a/services/core/java/com/android/server/wm/BoundsAnimationController.java b/services/core/java/com/android/server/wm/BoundsAnimationController.java
index f41eed5..e634552 100644
--- a/services/core/java/com/android/server/wm/BoundsAnimationController.java
+++ b/services/core/java/com/android/server/wm/BoundsAnimationController.java
@@ -22,6 +22,7 @@
import android.animation.Animator;
import android.animation.ValueAnimator;
+import android.annotation.IntDef;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
@@ -35,6 +36,9 @@
import com.android.internal.annotations.VisibleForTesting;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Enables animating bounds of objects.
*
@@ -43,7 +47,7 @@
* relaunching it would cause poorer experience), these class provides a way to directly animate
* the bounds of the resized object.
*
- * The object that is resized needs to implement {@link AnimateBoundsUser} interface.
+ * The object that is resized needs to implement {@link BoundsAnimationTarget} interface.
*
* NOTE: All calls to methods in this class should be done on the UI thread
*/
@@ -56,8 +60,19 @@
private static final int DEFAULT_TRANSITION_DURATION = 425;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({NO_PIP_MODE_CHANGED_CALLBACKS, SCHEDULE_PIP_MODE_CHANGED_ON_START,
+ SCHEDULE_PIP_MODE_CHANGED_ON_END})
+ public @interface SchedulePipModeChangedState {}
+ /** Do not schedule any PiP mode changed callbacks as a part of this animation. */
+ public static final int NO_PIP_MODE_CHANGED_CALLBACKS = 0;
+ /** Schedule a PiP mode changed callback when this animation starts. */
+ public static final int SCHEDULE_PIP_MODE_CHANGED_ON_START = 1;
+ /** Schedule a PiP mode changed callback when this animation ends. */
+ public static final int SCHEDULE_PIP_MODE_CHANGED_ON_END = 2;
+
// Only accessed on UI thread.
- private ArrayMap<AnimateBoundsUser, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
+ private ArrayMap<BoundsAnimationTarget, BoundsAnimator> mRunningAnimations = new ArrayMap<>();
private final class AppTransitionNotifier
extends WindowManagerInternal.AppTransitionListener implements Runnable {
@@ -108,40 +123,42 @@
@VisibleForTesting
final class BoundsAnimator extends ValueAnimator
implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
- private final AnimateBoundsUser mTarget;
+ private final BoundsAnimationTarget mTarget;
private final Rect mFrom = new Rect();
private final Rect mTo = new Rect();
private final Rect mTmpRect = new Rect();
private final Rect mTmpTaskBounds = new Rect();
- private final boolean mMoveToFullScreen;
- // True if this this animation was cancelled and will be replaced the another animation from
- // the same {@link #AnimateBoundsUser} target.
+
+ // True if this this animation was canceled and will be replaced the another animation from
+ // the same {@link #BoundsAnimationTarget} target.
private boolean mSkipFinalResize;
// True if this animation replaced a previous animation of the same
- // {@link #AnimateBoundsUser} target.
+ // {@link #BoundsAnimationTarget} target.
private final boolean mSkipAnimationStart;
- // True if this animation was cancelled by the user, not as a part of a replacing animation
+ // True if this animation was canceled by the user, not as a part of a replacing animation
private boolean mSkipAnimationEnd;
- // True if this animation is not replacing a previous animation, or if the previous
- // animation is animating to a different fullscreen state than the current animation.
- // We use this to ensure that we always provide a consistent set/order of callbacks when we
- // transition to/from PiP.
- private final boolean mAnimatingToNewFullscreenState;
+ // True if the animation target should be moved to the fullscreen stack at the end of this
+ // animation
+ private boolean mMoveToFullscreen;
+
+ // Whether to schedule PiP mode changes on animation start/end
+ private @SchedulePipModeChangedState int mSchedulePipModeChangedState;
// Depending on whether we are animating from
// a smaller to a larger size
private final int mFrozenTaskWidth;
private final int mFrozenTaskHeight;
- BoundsAnimator(AnimateBoundsUser target, Rect from, Rect to, boolean moveToFullScreen,
- boolean replacingExistingAnimation, boolean animatingToNewFullscreenState) {
+ BoundsAnimator(BoundsAnimationTarget target, Rect from, Rect to,
+ @SchedulePipModeChangedState int schedulePipModeChangedState,
+ boolean moveToFullscreen, boolean replacingExistingAnimation) {
super();
mTarget = target;
mFrom.set(from);
mTo.set(to);
- mMoveToFullScreen = moveToFullScreen;
mSkipAnimationStart = replacingExistingAnimation;
- mAnimatingToNewFullscreenState = animatingToNewFullscreenState;
+ mSchedulePipModeChangedState = schedulePipModeChangedState;
+ mMoveToFullscreen = moveToFullscreen;
addUpdateListener(this);
addListener(this);
@@ -161,7 +178,8 @@
@Override
public void onAnimationStart(Animator animation) {
if (DEBUG) Slog.d(TAG, "onAnimationStart: mTarget=" + mTarget
- + " mSkipAnimationStart=" + mSkipAnimationStart);
+ + " mSkipAnimationStart=" + mSkipAnimationStart
+ + " mSchedulePipModeChangedState=" + mSchedulePipModeChangedState);
mFinishAnimationAfterTransition = false;
mTmpRect.set(mFrom.left, mFrom.top, mFrom.left + mFrozenTaskWidth,
mFrom.top + mFrozenTaskHeight);
@@ -170,13 +188,8 @@
// we trigger any size changes, so it can swap surfaces
// in to appropriate modes, or do as it wishes otherwise.
if (!mSkipAnimationStart) {
- mTarget.onAnimationStart(mMoveToFullScreen);
- }
-
- // If we are animating to a new fullscreen state (either to/from fullscreen), then
- // notify the target of the change with the new frozen task bounds
- if (mAnimatingToNewFullscreenState && mMoveToFullScreen) {
- mTarget.updatePictureInPictureMode(null);
+ mTarget.onAnimationStart(mSchedulePipModeChangedState ==
+ SCHEDULE_PIP_MODE_CHANGED_ON_START);
}
// Immediately update the task bounds if they have to become larger, but preserve
@@ -206,20 +219,26 @@
// any further animation.
if (DEBUG) Slog.d(TAG, "animateUpdate: cancelled");
+ // If we have already scheduled a PiP mode changed at the start of the animation,
+ // then we need to clean up and schedule one at the end, since we have canceled the
+ // animation to the final state.
+ if (mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+ mSchedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
+ }
+
// Since we are cancelling immediately without a replacement animation, send the
// animation end to maintain callback parity, but also skip any further resizes
- prepareCancel(false /* skipAnimationEnd */, true /* skipFinalResize */);
- cancel();
+ cancelAndCallAnimationEnd();
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
- + " mMoveToFullScreen=" + mMoveToFullScreen
+ " mSkipFinalResize=" + mSkipFinalResize
+ " mFinishAnimationAfterTransition=" + mFinishAnimationAfterTransition
- + " mAppTransitionIsRunning=" + mAppTransition.isRunning());
+ + " mAppTransitionIsRunning=" + mAppTransition.isRunning()
+ + " callers=" + Debug.getCallers(2));
// There could be another animation running. For example in the
// move to fullscreen case, recents will also be closing while the
@@ -231,58 +250,57 @@
return;
}
- if (!mSkipFinalResize) {
- // If not cancelled, resize the pinned stack to the final size. All calls to
- // setPinnedStackSize() must be done between onAnimationStart() and onAnimationEnd()
- mTarget.setPinnedStackSize(mTo, null);
+ if (!mSkipAnimationEnd) {
+ // If this animation has already scheduled the picture-in-picture mode on start, and
+ // we are not skipping the final resize due to being canceled, then move the PiP to
+ // fullscreen once the animation ends
+ if (DEBUG) Slog.d(TAG, "onAnimationEnd: mTarget=" + mTarget
+ + " moveToFullscreen=" + mMoveToFullscreen);
+ mTarget.onAnimationEnd(mSchedulePipModeChangedState ==
+ SCHEDULE_PIP_MODE_CHANGED_ON_END, !mSkipFinalResize ? mTo : null,
+ mMoveToFullscreen);
}
- finishAnimation();
-
- if (mMoveToFullScreen && !mSkipFinalResize) {
- mTarget.moveToFullscreen();
- }
+ // Clean up this animation
+ removeListener(this);
+ removeUpdateListener(this);
+ mRunningAnimations.remove(mTarget);
}
@Override
public void onAnimationCancel(Animator animation) {
- finishAnimation();
+ // Always skip the final resize when the animation is canceled
+ mSkipFinalResize = true;
+ mMoveToFullscreen = false;
}
- public void prepareCancel(boolean skipAnimationEnd, boolean skipFinalResize) {
- if (DEBUG) Slog.d(TAG, "prepareCancel: skipAnimationEnd=" + skipAnimationEnd
- + " skipFinalResize=" + skipFinalResize);
- mSkipAnimationEnd = skipAnimationEnd;
- mSkipFinalResize = skipFinalResize;
+ private void cancelAndCallAnimationEnd() {
+ if (DEBUG) Slog.d(TAG, "cancelAndCallAnimationEnd: mTarget=" + mTarget);
+ mSkipAnimationEnd = false;
+ super.cancel();
}
@Override
public void cancel() {
if (DEBUG) Slog.d(TAG, "cancel: mTarget=" + mTarget);
+ mSkipAnimationEnd = true;
super.cancel();
}
- /** Returns true if the animation target is the same as the input bounds. */
+ /**
+ * @return true if the animation target is the same as the input bounds.
+ */
boolean isAnimatingTo(Rect bounds) {
return mTo.equals(bounds);
}
- private boolean animatingToLargerSize() {
- if (mFrom.width() * mFrom.height() > mTo.width() * mTo.height()) {
- return false;
- }
- return true;
- }
-
- private void finishAnimation() {
- if (DEBUG) Slog.d(TAG, "finishAnimation: mTarget=" + mTarget
- + " callers" + Debug.getCallers(2));
- if (!mSkipAnimationEnd) {
- mTarget.onAnimationEnd();
- }
- removeListener(this);
- removeUpdateListener(this);
- mRunningAnimations.remove(mTarget);
+ /**
+ * @return true if we are animating to a larger surface size
+ */
+ @VisibleForTesting
+ boolean animatingToLargerSize() {
+ // TODO: Fix this check for aspect ratio changes
+ return (mFrom.width() * mFrom.height() <= mTo.width() * mTo.height());
}
@Override
@@ -291,63 +309,23 @@
}
}
- public interface AnimateBoundsUser {
- /**
- * Sets the size of the target (without any intermediate steps, like scheduling animation)
- * but freezes the bounds of any tasks in the target at taskBounds,
- * to allow for more flexibility during resizing. Only works for the pinned stack at the
- * moment.
- *
- * @return Whether the target should continue to be animated and this call was successful.
- * If false, the animation will be cancelled because the user has determined that the
- * animation is now invalid and not required. In such a case, the cancel will trigger the
- * animation end callback as well, but will not send any further size changes.
- */
- boolean setPinnedStackSize(Rect bounds, Rect taskBounds);
-
- /**
- * Callback for the target to inform it that the animation has started, so it can do some
- * necessary preparation.
- */
- void onAnimationStart(boolean toFullscreen);
-
- /**
- * Callback for the target to inform it that the animation is going to a new fullscreen
- * state and should update the picture-in-picture mode accordingly.
- *
- * @param targetStackBounds the target stack bounds we are animating to, can be null if
- * we are animating to fullscreen
- */
- void updatePictureInPictureMode(Rect targetStackBounds);
-
- /**
- * Callback for the target to inform it that the animation has ended, so it can do some
- * necessary cleanup.
- */
- void onAnimationEnd();
-
- /**
- * Callback for the target to inform it to reparent to the fullscreen stack.
- */
- void moveToFullscreen();
- }
-
- public void animateBounds(final AnimateBoundsUser target, Rect from, Rect to,
- int animationDuration, boolean moveToFullscreen) {
- animateBoundsImpl(target, from, to, animationDuration, moveToFullscreen);
+ public void animateBounds(final BoundsAnimationTarget target, Rect from, Rect to,
+ int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
+ boolean moveToFullscreen) {
+ animateBoundsImpl(target, from, to, animationDuration, schedulePipModeChangedState,
+ moveToFullscreen);
}
@VisibleForTesting
- BoundsAnimator animateBoundsImpl(final AnimateBoundsUser target, Rect from, Rect to,
- int animationDuration, boolean moveToFullscreen) {
+ BoundsAnimator animateBoundsImpl(final BoundsAnimationTarget target, Rect from, Rect to,
+ int animationDuration, @SchedulePipModeChangedState int schedulePipModeChangedState,
+ boolean moveToFullscreen) {
final BoundsAnimator existing = mRunningAnimations.get(target);
final boolean replacing = existing != null;
- final boolean animatingToNewFullscreenState = (existing == null) ||
- (existing.mMoveToFullScreen != moveToFullscreen);
if (DEBUG) Slog.d(TAG, "animateBounds: target=" + target + " from=" + from + " to=" + to
- + " moveToFullscreen=" + moveToFullscreen + " replacing=" + replacing
- + " animatingToNewFullscreenState=" + animatingToNewFullscreenState);
+ + " schedulePipModeChangedState=" + schedulePipModeChangedState
+ + " replacing=" + replacing);
if (replacing) {
if (existing.isAnimatingTo(to)) {
@@ -355,15 +333,36 @@
// one we are trying to start.
if (DEBUG) Slog.d(TAG, "animateBounds: same destination as existing=" + existing
+ " ignoring...");
+
return existing;
}
- // Since we are replacing, we skip both animation start and end callbacks, and don't
- // animate to the final bounds when cancelling
- existing.prepareCancel(true /* skipAnimationEnd */, true /* skipFinalResize */);
+
+ // Update the PiP callback states if we are replacing the animation
+ if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+ if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+ if (DEBUG) Slog.d(TAG, "animateBounds: still animating to fullscreen, keep"
+ + " existing deferred state");
+ } else {
+ if (DEBUG) Slog.d(TAG, "animateBounds: fullscreen animation canceled, callback"
+ + " on start already processed, schedule deferred update on end");
+ schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
+ }
+ } else if (existing.mSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END) {
+ if (schedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_START) {
+ if (DEBUG) Slog.d(TAG, "animateBounds: non-fullscreen animation canceled,"
+ + " callback on start will be processed");
+ } else {
+ if (DEBUG) Slog.d(TAG, "animateBounds: still animating from fullscreen, keep"
+ + " existing deferred state");
+ schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END;
+ }
+ }
+
+ // Since we are replacing, we skip both animation start and end callbacks
existing.cancel();
}
- final BoundsAnimator animator = new BoundsAnimator(target, from, to, moveToFullscreen,
- replacing, animatingToNewFullscreenState);
+ final BoundsAnimator animator = new BoundsAnimator(target, from, to,
+ schedulePipModeChangedState, moveToFullscreen, replacing);
mRunningAnimations.put(target, animator);
animator.setFloatValues(0f, 1f);
animator.setDuration((animationDuration != -1 ? animationDuration