diff options
11 files changed, 705 insertions, 350 deletions
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 6b0a73b25e71..1c2a6aa2bbdf 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -10535,8 +10535,9 @@ public class ActivityManagerService extends IActivityManager.Stub final PinnedActivityStack pinnedStack = mStackSupervisor.getStack(PINNED_STACK_ID); if (pinnedStack != null) { - pinnedStack.animateResizePinnedStack(null /* sourceBounds */, - destBounds, animationDuration); + pinnedStack.animateResizePinnedStack(null /* sourceHintBounds */, + destBounds, animationDuration, + false /* schedulePipModeChangedOnAnimationEnd */); } } else { throw new IllegalArgumentException("Stack: " + stackId diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index 4c84d98d468a..825e8ac0a698 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -589,6 +589,13 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai } /** + * Returns whether to defer the scheduling of the multi-window mode. + */ + boolean deferScheduleMultiWindowModeChanged() { + return false; + } + + /** * Defers updating the bounds of the stack. If the stack was resized/repositioned while * deferring, the bounds will update in {@link #continueUpdateBounds()}. */ diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 4d16e33ad81c..43ae4b22ba63 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -2504,7 +2504,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D // incorrect if AMS.resizeStackWithBoundsFromWindowManager() is already called while waiting // for the AMS lock to be freed. So check and make sure these bounds are still good. final PinnedStackWindowController stackController = stack.getWindowContainerController(); - if (stackController.pinnedStackResizeAllowed()) { + if (stackController.pinnedStackResizeDisallowed()) { return; } @@ -2873,7 +2873,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D return true; } - void moveActivityToPinnedStackLocked(ActivityRecord r, Rect sourceBounds, float aspectRatio, + void moveActivityToPinnedStackLocked(ActivityRecord r, Rect sourceHintBounds, float aspectRatio, boolean moveHomeStackToFront, String reason) { mWindowManager.deferSurfaceLayout(); @@ -2948,11 +2948,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D final Rect destBounds = mWindowManager.getPictureInPictureBounds(DEFAULT_DISPLAY, aspectRatio, false /* useExistingStackBounds */); - // TODO(b/36099777): Schedule the PiP mode change here immediately until we can defer all - // callbacks until after the bounds animation - scheduleUpdatePictureInPictureModeIfNeeded(r.getTask(), destBounds, true /* immediate */); - - stack.animateResizePinnedStack(sourceBounds, destBounds, -1 /* animationDuration */); + stack.animateResizePinnedStack(sourceHintBounds, destBounds, -1 /* animationDuration */, + true /* schedulePipModeChangedOnAnimationEnd */); mService.mTaskChangeNotificationController.notifyActivityPinned(r.packageName); } @@ -4179,6 +4176,12 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D } void scheduleUpdateMultiWindowMode(TaskRecord task) { + // If the stack is animating in a way where we will be forcing a multi-mode change at the + // end, then ensure that we defer all in between multi-window mode changes + if (task.getStack().deferScheduleMultiWindowModeChanged()) { + return; + } + for (int i = task.mActivities.size() - 1; i >= 0; i--) { final ActivityRecord r = task.mActivities.get(i); if (r.app != null && r.app.thread != null) { diff --git a/services/core/java/com/android/server/am/PinnedActivityStack.java b/services/core/java/com/android/server/am/PinnedActivityStack.java index cd9c42c7c643..a4932bbdd732 100644 --- a/services/core/java/com/android/server/am/PinnedActivityStack.java +++ b/services/core/java/com/android/server/am/PinnedActivityStack.java @@ -43,9 +43,10 @@ class PinnedActivityStack extends ActivityStack<PinnedStackWindowController> return new PinnedStackWindowController(mStackId, this, displayId, onTop, outBounds); } - void animateResizePinnedStack(Rect sourceBounds, Rect destBounds, int animationDuration) { - getWindowContainerController().animateResizePinnedStack(sourceBounds, destBounds, - animationDuration); + void animateResizePinnedStack(Rect sourceHintBounds, Rect toBounds, int animationDuration, + boolean schedulePipModeChangedOnAnimationEnd) { + getWindowContainerController().animateResizePinnedStack(toBounds, sourceHintBounds, + animationDuration, schedulePipModeChangedOnAnimationEnd); } void setPictureInPictureAspectRatio(float aspectRatio) { @@ -60,7 +61,18 @@ class PinnedActivityStack extends ActivityStack<PinnedStackWindowController> return getWindowContainerController().isAnimatingBoundsToFullscreen(); } - @Override + /** + * Returns whether to defer the scheduling of the multi-window mode. + */ + boolean deferScheduleMultiWindowModeChanged() { + // For the pinned stack, the deferring of the multi-window mode changed is tied to the + // transition animation into picture-in-picture, and is called once the animation completes, + // or is interrupted in a way that would leave the stack in a non-fullscreen state. + // @see BoundsAnimationController + // @see BoundsAnimationControllerTests + return mWindowContainerController.deferScheduleMultiWindowModeChanged(); + } + public void updatePictureInPictureModeForPinnedStackAnimation(Rect targetStackBounds) { // It is guaranteed that the activities requiring the update will be in the pinned stack at // this point (either reparented before the animation into PiP, or before reparenting after diff --git a/services/core/java/com/android/server/wm/BoundsAnimationController.java b/services/core/java/com/android/server/wm/BoundsAnimationController.java index f41eed53cbca..e6345523bd14 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 static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; 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 android.view.WindowManagerInternal; 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 @@ import com.android.internal.annotations.VisibleForTesting; * 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 @@ public class BoundsAnimationController { 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 @@ public class BoundsAnimationController { @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 @@ public class BoundsAnimationController { @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 @@ public class BoundsAnimationController { // 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 @@ public class BoundsAnimationController { // 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 @@ public class BoundsAnimationController { 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 class BoundsAnimationController { } } - 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 @@ public class BoundsAnimationController { // 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 diff --git a/services/core/java/com/android/server/wm/BoundsAnimationTarget.java b/services/core/java/com/android/server/wm/BoundsAnimationTarget.java new file mode 100644 index 000000000000..8b1bf7bf77dc --- /dev/null +++ b/services/core/java/com/android/server/wm/BoundsAnimationTarget.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 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.server.wm; + +import android.graphics.Rect; + +/** + * The target for a BoundsAnimation. + * @see BoundsAnimationController + */ +interface BoundsAnimationTarget { + + /** + * Callback for the target to inform it that the animation has started, so it can do some + * necessary preparation. + * + * @param schedulePipModeChangedCallback whether or not to schedule the PiP mode changed + * callbacks + */ + void onAnimationStart(boolean schedulePipModeChangedCallback); + + /** + * 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. This will + * only be called between onAnimationStart() and onAnimationEnd(). + * + * @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 stackBounds, Rect taskBounds); + + /** + * Callback for the target to inform it that the animation has ended, so it can do some + * necessary cleanup. + * + * @param schedulePipModeChangedCallback whether or not to schedule the PiP mode changed + * callbacks + * @param finalStackSize the final stack bounds to set on the target (can be to indicate that + * the animation was cancelled and the target does not need to update to the final stack bounds) + * @param moveToFullscreen whether or the target should reparent itself to the fullscreen stack + * when the animation completes + */ + void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackSize, + boolean moveToFullscreen); +} diff --git a/services/core/java/com/android/server/wm/PinnedStackWindowController.java b/services/core/java/com/android/server/wm/PinnedStackWindowController.java index 135832e619a4..5b9c16ce60c9 100644 --- a/services/core/java/com/android/server/wm/PinnedStackWindowController.java +++ b/services/core/java/com/android/server/wm/PinnedStackWindowController.java @@ -17,6 +17,10 @@ package com.android.server.wm; import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; +import static com.android.server.wm.BoundsAnimationController.NO_PIP_MODE_CHANGED_CALLBACKS; +import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_END; +import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_START; +import static com.android.server.wm.BoundsAnimationController.SchedulePipModeChangedState; import android.app.RemoteAction; import android.graphics.Rect; @@ -40,36 +44,53 @@ public class PinnedStackWindowController extends StackWindowController { /** * Animates the pinned stack. */ - public void animateResizePinnedStack(Rect sourceBounds, Rect destBounds, - int animationDuration) { + public void animateResizePinnedStack(Rect toBounds, Rect sourceHintBounds, + int animationDuration, boolean schedulePipModeChangedOnAnimationEnd) { synchronized (mWindowMap) { if (mContainer == null) { throw new IllegalArgumentException("Pinned stack container not found :("); } - // Get non-null fullscreen bounds if the bounds are null - final boolean moveToFullscreen = destBounds == null; - destBounds = getPinnedStackAnimationBounds(destBounds); - - // If the bounds are truly null, then there was no fullscreen stack at this time, so - // animate this to the full display bounds - final Rect toBounds; - if (destBounds == null) { - toBounds = new Rect(); - mContainer.getDisplayContent().getLogicalDisplayRect(toBounds); - } else { - toBounds = destBounds; + // Get the from-bounds + final Rect fromBounds = new Rect(); + mContainer.getBounds(fromBounds); + + // Get non-null fullscreen to-bounds for animating if the bounds are null + @SchedulePipModeChangedState int schedulePipModeChangedState = + NO_PIP_MODE_CHANGED_CALLBACKS; + final boolean toFullscreen = toBounds == null; + if (toFullscreen) { + if (schedulePipModeChangedOnAnimationEnd) { + throw new IllegalArgumentException("Should not defer scheduling PiP mode" + + " change on animation to fullscreen."); + } + schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_START; + + mService.getStackBounds(FULLSCREEN_WORKSPACE_STACK_ID, mTmpBoundsRect); + if (!mTmpBoundsRect.isEmpty()) { + // If there is a fullscreen bounds, use that + toBounds = new Rect(mTmpBoundsRect); + } else { + // Otherwise, use the display bounds + toBounds = new Rect(); + mContainer.getDisplayContent().getLogicalDisplayRect(toBounds); + } + } else if (schedulePipModeChangedOnAnimationEnd) { + schedulePipModeChangedState = SCHEDULE_PIP_MODE_CHANGED_ON_END; } - final Rect originalBounds = new Rect(); - mContainer.getBounds(originalBounds); - mContainer.setAnimationFinalBounds(sourceBounds, toBounds); + mContainer.setAnimationFinalBounds(sourceHintBounds, toBounds, toFullscreen); + + final Rect finalToBounds = toBounds; + final @SchedulePipModeChangedState int finalSchedulePipModeChangedState = + schedulePipModeChangedState; UiThread.getHandler().post(() -> { if (mContainer == null) { return; } - mService.mBoundsAnimationController.animateBounds(mContainer, originalBounds, - toBounds, animationDuration, moveToFullscreen); + mService.mBoundsAnimationController.animateBounds(mContainer, fromBounds, + finalToBounds, animationDuration, finalSchedulePipModeChangedState, + toFullscreen); }); } } @@ -93,7 +114,8 @@ public class PinnedStackWindowController extends StackWindowController { if (Float.compare(aspectRatio, pinnedStackController.getAspectRatio()) != 0) { if (!toBounds.equals(targetBounds)) { - animateResizePinnedStack(null /* sourceBounds */, toBounds, -1 /* duration */); + animateResizePinnedStack(toBounds, null /* sourceHintBounds */, + -1 /* duration */, false /* schedulePipModeChangedOnAnimationEnd */); } pinnedStackController.setAspectRatio( pinnedStackController.isValidPictureInPictureAspectRatio(aspectRatio) @@ -116,26 +138,31 @@ public class PinnedStackWindowController extends StackWindowController { } /** - * @return whether the bounds are currently animating to fullscreen. + * @return whether the multi-window mode change should be deferred as a part of a transition + * from fullscreen to non-fullscreen bounds. */ - public boolean isAnimatingBoundsToFullscreen() { - return mContainer.isAnimatingBoundsToFullscreen(); + public boolean deferScheduleMultiWindowModeChanged() { + synchronized(mWindowMap) { + return mContainer.deferScheduleMultiWindowModeChanged(); + } } - public boolean pinnedStackResizeAllowed() { - return mContainer.pinnedStackResizeAllowed(); + /** + * @return whether the bounds are currently animating to fullscreen. + */ + public boolean isAnimatingBoundsToFullscreen() { + synchronized (mWindowMap) { + return mContainer.isAnimatingBoundsToFullscreen(); + } } /** - * Checks the {@param bounds} and retirms non-null fullscreen bounds for the pinned stack - * animation if necessary. + * @return whether the stack can be resized from the bounds animation. */ - private Rect getPinnedStackAnimationBounds(Rect bounds) { - mService.getStackBounds(FULLSCREEN_WORKSPACE_STACK_ID, mTmpBoundsRect); - if (bounds == null && !mTmpBoundsRect.isEmpty()) { - bounds = new Rect(mTmpBoundsRect); + public boolean pinnedStackResizeDisallowed() { + synchronized (mWindowMap) { + return mContainer.pinnedStackResizeDisallowed(); } - return bounds; } /** diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java index d7c41d335b72..d141f7cb2eb3 100644 --- a/services/core/java/com/android/server/wm/TaskStack.java +++ b/services/core/java/com/android/server/wm/TaskStack.java @@ -56,7 +56,7 @@ import com.android.server.EventLogTags; import java.io.PrintWriter; public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLayerUser, - BoundsAnimationController.AnimateBoundsUser { + BoundsAnimationTarget { /** Minimum size of an adjusted stack bounds relative to original stack bounds. Used to * restrict IME adjustment so that a min portion of top stack remains visible.*/ private static final float ADJUSTED_STACK_FRACTION_MIN = 0.3f; @@ -132,7 +132,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye private boolean mBoundsAnimatingToFullscreen = false; private boolean mCancelCurrentBoundsAnimation = false; private Rect mBoundsAnimationTarget = new Rect(); - private Rect mBoundsAnimationSourceBounds = new Rect(); + private Rect mBoundsAnimationSourceHintBounds = new Rect(); // Temporary storage for the new bounds that should be used after the configuration change. // Will be cleared once the client retrieves the new bounds via getBoundsForNewConfiguration(). @@ -322,18 +322,19 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye * Sets the bounds animation target bounds ahead of an animation. This can't currently be done * in onAnimationStart() since that is started on the UiThread. */ - void setAnimationFinalBounds(Rect sourceBounds, Rect destBounds) { + void setAnimationFinalBounds(Rect sourceHintBounds, Rect destBounds, boolean toFullscreen) { mBoundsAnimatingRequested = true; - if (sourceBounds != null) { - mBoundsAnimationSourceBounds.set(sourceBounds); - } else { - mBoundsAnimationSourceBounds.setEmpty(); - } + mBoundsAnimatingToFullscreen = toFullscreen; if (destBounds != null) { mBoundsAnimationTarget.set(destBounds); } else { mBoundsAnimationTarget.setEmpty(); } + if (sourceHintBounds != null) { + mBoundsAnimationSourceHintBounds.set(sourceHintBounds); + } else { + mBoundsAnimationSourceHintBounds.setEmpty(); + } } /** @@ -346,8 +347,8 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye /** * @return the final source bounds for the bounds animation. */ - void getFinalAnimationSourceBounds(Rect outBounds) { - outBounds.set(mBoundsAnimationSourceBounds); + void getFinalAnimationSourceHintBounds(Rect outBounds) { + outBounds.set(mBoundsAnimationSourceHintBounds); } /** @@ -413,7 +414,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye // orientation, clear the animation target bounds since they are obsolete, and // cancel any currently running animations mBoundsAnimationTarget.setEmpty(); - mBoundsAnimationSourceBounds.setEmpty(); + mBoundsAnimationSourceHintBounds.setEmpty(); mCancelCurrentBoundsAnimation = true; return true; } @@ -1458,13 +1459,16 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye } } - public boolean setPinnedStackSize(Rect bounds, Rect tempTaskBounds) { - if (mCancelCurrentBoundsAnimation) { - return false; + public boolean setPinnedStackSize(Rect stackBounds, Rect tempTaskBounds) { + // Hold the lock since this is called from the BoundsAnimator running on the UiThread + synchronized (mService.mWindowMap) { + if (mCancelCurrentBoundsAnimation) { + return false; + } } try { - mService.mActivityManager.resizePinnedStack(bounds, tempTaskBounds); + mService.mActivityManager.resizePinnedStack(stackBounds, tempTaskBounds); } catch (RemoteException e) { // I don't believe you. } @@ -1472,11 +1476,11 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye } @Override // AnimatesBounds - public void onAnimationStart(boolean toFullscreen) { + public void onAnimationStart(boolean schedulePipModeChangedCallback) { + // Hold the lock since this is called from the BoundsAnimator running on the UiThread synchronized (mService.mWindowMap) { mBoundsAnimatingRequested = false; mBoundsAnimating = true; - mBoundsAnimatingToFullscreen = toFullscreen; mCancelCurrentBoundsAnimation = false; } @@ -1486,41 +1490,63 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye } catch (RemoteException e) { // I don't believe you... } - } - } - @Override // AnimatesBounds - public void updatePictureInPictureMode(Rect targetStackBounds) { - final PinnedStackWindowController controller = - (PinnedStackWindowController) getController(); - if (controller != null) { - controller.updatePictureInPictureModeForPinnedStackAnimation(targetStackBounds); + final PinnedStackWindowController controller = + (PinnedStackWindowController) getController(); + if (schedulePipModeChangedCallback && controller != null) { + // We need to schedule the PiP mode change after the animation down, so use the + // final bounds + controller.updatePictureInPictureModeForPinnedStackAnimation(null); + } } } @Override // AnimatesBounds - public void onAnimationEnd() { + public void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackSize, + boolean moveToFullscreen) { + // Hold the lock since this is called from the BoundsAnimator running on the UiThread synchronized (mService.mWindowMap) { mBoundsAnimating = false; mService.requestTraversal(); } if (mStackId == PINNED_STACK_ID) { + final PinnedStackWindowController controller = + (PinnedStackWindowController) getController(); + if (schedulePipModeChangedCallback && controller != null) { + // We need to schedule the PiP mode change after the animation down, so use the + // final bounds + controller.updatePictureInPictureModeForPinnedStackAnimation( + mBoundsAnimationTarget); + } + + // Update to the final bounds if requested. This is done here instead of in the bounds + // animator to allow us to coordinate this after we notify the PiP mode changed + if (finalStackSize != null) { + setPinnedStackSize(finalStackSize, null); + } + try { mService.mActivityManager.notifyPinnedStackAnimationEnded(); + if (moveToFullscreen) { + mService.mActivityManager.moveTasksToFullscreenStack(mStackId, + true /* onTop */); + } } catch (RemoteException e) { // I don't believe you... } } } - @Override - public void moveToFullscreen() { - try { - mService.mActivityManager.moveTasksToFullscreenStack(mStackId, true); - } catch (RemoteException e) { - e.printStackTrace(); + /** + * @return True if we are currently animating the pinned stack from fullscreen to non-fullscreen + * bounds and we have a deferred PiP mode changed callback set with the animation. + */ + public boolean deferScheduleMultiWindowModeChanged() { + if (mStackId == PINNED_STACK_ID) { + return (mBoundsAnimatingRequested || mBoundsAnimating); } + return false; } public boolean hasMovementAnimations() { @@ -1539,7 +1565,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye return mBoundsAnimating && mBoundsAnimatingToFullscreen; } - public boolean pinnedStackResizeAllowed() { + public boolean pinnedStackResizeDisallowed() { if (mBoundsAnimating && mCancelCurrentBoundsAnimation) { return true; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 0b96f3fbc1fc..9555c8dff03c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -4365,9 +4365,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // When we change the Surface size, in scenarios which may require changing // the surface position in sync with the resize, we use a preserved surface // so we can freeze it while waiting for the client to report draw on the newly - // sized surface. + // sized surface. Don't preserve surfaces if the insets change while animating the pinned + // stack since it can lead to issues if a new surface is created while calculating the + // scale for the animation using the source hint rect + // (see WindowStateAnimator#setSurfaceBoundariesLocked()). if (isDragResizeChanged() || isResizedWhileNotDragResizing() - || surfaceInsetsChanging()) { + || (surfaceInsetsChanging() && !inPinnedWorkspace())) { mLastSurfaceInsets.set(mAttrs.surfaceInsets); setDragResizing(); diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index fa353367c6e4..a2889b145f16 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -1360,7 +1360,7 @@ class WindowStateAnimator { int posX = mTmpSize.left; int posY = mTmpSize.top; task.mStack.getDimBounds(mTmpStackBounds); - task.mStack.getFinalAnimationSourceBounds(mTmpSourceBounds); + task.mStack.getFinalAnimationSourceHintBounds(mTmpSourceBounds); if (!mTmpSourceBounds.isEmpty()) { // Get the final target stack bounds, if we are not animating, this is just the // current stack bounds diff --git a/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java index 85dc7125e3cc..7f150a21a20e 100644 --- a/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/BoundsAnimationControllerTests.java @@ -16,6 +16,11 @@ package com.android.server.wm; +import static com.android.server.wm.BoundsAnimationController.NO_PIP_MODE_CHANGED_CALLBACKS; +import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_END; +import static com.android.server.wm.BoundsAnimationController.SCHEDULE_PIP_MODE_CHANGED_ON_START; +import static com.android.server.wm.BoundsAnimationController.SchedulePipModeChangedState; + import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Rect; @@ -47,6 +52,12 @@ import com.android.server.wm.BoundsAnimationController.BoundsAnimator; * Test class for {@link BoundsAnimationController} to ensure that it sends the right callbacks * depending on the various interactions. * + * We are really concerned about only three of the transition states [F = fullscreen, !F = floating] + * F->!F, !F->!F, and !F->F. Each animation can only be cancelled from the target mid-transition, + * or if a new animation starts on the same target. The tests below verifies that the target is + * notified of all the cases where it is animating and cancelled so that it can respond + * appropriately. + * * Build/Install/Run: * bit FrameworksServicesTests:com.android.server.wm.BoundsAnimationControllerTests */ @@ -109,47 +120,46 @@ public class BoundsAnimationControllerTests extends WindowTestsBase { /** * A test animate bounds user to track callbacks from the bounds animation. */ - private class TestAnimateBoundsUser implements BoundsAnimationController.AnimateBoundsUser { + private class TestBoundsAnimationTarget implements BoundsAnimationTarget { + boolean mAwaitingAnimationStart; boolean mMovedToFullscreen; boolean mAnimationStarted; - boolean mAnimationStartedToFullscreen; + boolean mSchedulePipModeChangedOnStart; boolean mAnimationEnded; - boolean mUpdatedPictureInPictureModeWithBounds; + Rect mAnimationEndFinalStackBounds; + boolean mSchedulePipModeChangedOnEnd; boolean mBoundsUpdated; + boolean mCancelRequested; Rect mStackBounds; Rect mTaskBounds; - boolean mRequestCancelAnimation = false; - - void reinitialize(Rect stackBounds, Rect taskBounds) { + void initialize(Rect from) { + mAwaitingAnimationStart = true; mMovedToFullscreen = false; mAnimationStarted = false; - mAnimationStartedToFullscreen = false; mAnimationEnded = false; - mUpdatedPictureInPictureModeWithBounds = false; - mStackBounds = stackBounds; - mTaskBounds = taskBounds; + mAnimationEndFinalStackBounds = null; + mSchedulePipModeChangedOnStart = false; + mSchedulePipModeChangedOnEnd = false; + mStackBounds = from; + mTaskBounds = null; mBoundsUpdated = false; - mRequestCancelAnimation = false; } @Override - public void onAnimationStart(boolean toFullscreen) { + public void onAnimationStart(boolean schedulePipModeChangedCallback) { + mAwaitingAnimationStart = false; mAnimationStarted = true; - mAnimationStartedToFullscreen = toFullscreen; - } - - @Override - public void updatePictureInPictureMode(Rect targetStackBounds) { - mUpdatedPictureInPictureModeWithBounds = true; + mSchedulePipModeChangedOnStart = schedulePipModeChangedCallback; } @Override public boolean setPinnedStackSize(Rect stackBounds, Rect taskBounds) { // TODO: Once we break the runs apart, we should fail() here if this is called outside // of onAnimationStart() and onAnimationEnd() - if (mRequestCancelAnimation) { + if (mCancelRequested) { + mCancelRequested = false; return false; } else { mBoundsUpdated = true; @@ -160,32 +170,206 @@ public class BoundsAnimationControllerTests extends WindowTestsBase { } @Override - public void onAnimationEnd() { + public void onAnimationEnd(boolean schedulePipModeChangedCallback, Rect finalStackBounds, + boolean moveToFullscreen) { mAnimationEnded = true; + mAnimationEndFinalStackBounds = finalStackBounds; + mSchedulePipModeChangedOnEnd = schedulePipModeChangedCallback; + mMovedToFullscreen = moveToFullscreen; + mTaskBounds = null; } + } - @Override - public void moveToFullscreen() { - mMovedToFullscreen = true; + /** + * Drives the animations, makes common assertions along the way. + */ + private class BoundsAnimationDriver { + + private BoundsAnimationController mController; + private TestBoundsAnimationTarget mTarget; + private BoundsAnimator mAnimator; + + private Rect mFrom; + private Rect mTo; + private Rect mLargerBounds; + private Rect mExpectedFinalBounds; + + BoundsAnimationDriver(BoundsAnimationController controller, + TestBoundsAnimationTarget target) { + mController = controller; + mTarget = target; + } + + BoundsAnimationDriver start(Rect from, Rect to) { + if (mAnimator != null) { + throw new IllegalArgumentException("Call restart() to restart an animation"); + } + + mTarget.initialize(from); + + // Started, not running + assertTrue(mTarget.mAwaitingAnimationStart); + assertTrue(!mTarget.mAnimationStarted); + + startImpl(from, to); + + // Started and running + assertTrue(!mTarget.mAwaitingAnimationStart); + assertTrue(mTarget.mAnimationStarted); + + return this; + } + + BoundsAnimationDriver restart(Rect to) { + if (mAnimator == null) { + throw new IllegalArgumentException("Call start() to start a new animation"); + } + + BoundsAnimator oldAnimator = mAnimator; + boolean toSameBounds = mAnimator.isStarted() && to.equals(mTo); + + // Reset the animation start state + mTarget.mAnimationStarted = false; + + // Start animation + startImpl(mTarget.mStackBounds, to); + + if (toSameBounds) { + // Same animator if same final bounds + assertSame(oldAnimator, mAnimator); + } + + // No animation start for replacing animation + assertTrue(!mTarget.mAnimationStarted); + mTarget.mAnimationStarted = true; + return this; + } + + private BoundsAnimationDriver startImpl(Rect from, Rect to) { + boolean fromFullscreen = from.equals(BOUNDS_FULL); + boolean toFullscreen = to.equals(BOUNDS_FULL); + mFrom = new Rect(from); + mTo = new Rect(to); + mExpectedFinalBounds = new Rect(to); + mLargerBounds = getLargerBounds(mFrom, mTo); + + // Start animation + final @SchedulePipModeChangedState int schedulePipModeChangedState = toFullscreen + ? SCHEDULE_PIP_MODE_CHANGED_ON_START + : fromFullscreen + ? SCHEDULE_PIP_MODE_CHANGED_ON_END + : NO_PIP_MODE_CHANGED_CALLBACKS; + mAnimator = mController.animateBoundsImpl(mTarget, from, to, DURATION, + schedulePipModeChangedState, toFullscreen); + + // Original stack bounds, frozen task bounds + assertEquals(mFrom, mTarget.mStackBounds); + assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds); + + // Animating to larger size + if (mFrom.equals(mLargerBounds)) { + assertTrue(!mAnimator.animatingToLargerSize()); + } else if (mTo.equals(mLargerBounds)) { + assertTrue(mAnimator.animatingToLargerSize()); + } + + return this; + } + + BoundsAnimationDriver expectStarted(boolean schedulePipModeChanged) { + // Callback made + assertTrue(mTarget.mAnimationStarted); + + assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnStart); + return this; + } + + BoundsAnimationDriver update(float t) { + mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(t)); + + // Temporary stack bounds, frozen task bounds + if (t == 0f) { + assertEquals(mFrom, mTarget.mStackBounds); + } else if (t == 1f) { + assertEquals(mTo, mTarget.mStackBounds); + } else { + assertNotEquals(mFrom, mTarget.mStackBounds); + assertNotEquals(mTo, mTarget.mStackBounds); + } + assertEqualSizeAtOffset(mLargerBounds, mTarget.mTaskBounds); + return this; + } + + BoundsAnimationDriver cancel() { + // Cancel + mTarget.mCancelRequested = true; + mTarget.mBoundsUpdated = false; + mExpectedFinalBounds = null; + + // Update + mAnimator.onAnimationUpdate(mMockAnimator.getWithValue(0.5f)); + + // Not started, not running, cancel reset + assertTrue(!mTarget.mCancelRequested); + + // Stack/task bounds not updated + assertTrue(!mTarget.mBoundsUpdated); + + // Callback made + assertTrue(mTarget.mAnimationEnded); + assertNull(mTarget.mAnimationEndFinalStackBounds); + + return this; + } + + BoundsAnimationDriver end() { + mAnimator.end(); + + // Final stack bounds + assertEquals(mTo, mTarget.mStackBounds); + assertEquals(mExpectedFinalBounds, mTarget.mAnimationEndFinalStackBounds); + assertNull(mTarget.mTaskBounds); + + return this; + } + + BoundsAnimationDriver expectEnded(boolean schedulePipModeChanged, + boolean moveToFullscreen) { + // Callback made + assertTrue(mTarget.mAnimationEnded); + + assertEquals(schedulePipModeChanged, mTarget.mSchedulePipModeChangedOnEnd); + assertEquals(moveToFullscreen, mTarget.mMovedToFullscreen); + return this; + } + + private Rect getLargerBounds(Rect r1, Rect r2) { + int r1Area = r1.width() * r1.height(); + int r2Area = r2.width() * r2.height(); + if (r1Area <= r2Area) { + return r2; + } else { + return r1; + } } } // Constants + private static final boolean SCHEDULE_PIP_MODE_CHANGED = true; private static final boolean MOVE_TO_FULLSCREEN = true; + private static final int DURATION = 100; // Some dummy bounds to represent fullscreen and floating bounds private static final Rect BOUNDS_FULL = new Rect(0, 0, 100, 100); - private static final Rect BOUNDS_FLOATING = new Rect(80, 80, 95, 95); - private static final Rect BOUNDS_ALT_FLOATING = new Rect(60, 60, 95, 95); - - // Some dummy duration - private static final int DURATION = 100; + private static final Rect BOUNDS_FLOATING = new Rect(60, 60, 95, 95); + private static final Rect BOUNDS_SMALLER_FLOATING = new Rect(80, 80, 95, 95); // Common - private MockAppTransition mAppTransition; - private MockValueAnimator mAnimator; - private TestAnimateBoundsUser mTarget; + private MockAppTransition mMockAppTransition; + private MockValueAnimator mMockAnimator; + private TestBoundsAnimationTarget mTarget; private BoundsAnimationController mController; + private BoundsAnimationDriver mDriver; // Temp private Rect mTmpRect = new Rect(); @@ -196,153 +380,184 @@ public class BoundsAnimationControllerTests extends WindowTestsBase { final Context context = InstrumentationRegistry.getTargetContext(); final Handler handler = new Handler(Looper.getMainLooper()); - mAppTransition = new MockAppTransition(context); - mAnimator = new MockValueAnimator(); - mTarget = new TestAnimateBoundsUser(); - mController = new BoundsAnimationController(context, mAppTransition, handler); + mMockAppTransition = new MockAppTransition(context); + mMockAnimator = new MockValueAnimator(); + mTarget = new TestBoundsAnimationTarget(); + mController = new BoundsAnimationController(context, mMockAppTransition, handler); + mDriver = new BoundsAnimationDriver(mController, mTarget); } + /** BASE TRANSITIONS **/ + @UiThreadTest @Test public void testFullscreenToFloatingTransition() throws Exception { - // Create and start the animation - mTarget.reinitialize(BOUNDS_FULL, null); - final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL, - BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN); - - // Assert that when we are started, and that we are not going to fullscreen - assertTrue(mTarget.mAnimationStarted); - assertFalse(mTarget.mAnimationStartedToFullscreen); - // Ensure we are not triggering a PiP mode change - assertFalse(mTarget.mUpdatedPictureInPictureModeWithBounds); - // Ensure that the task stack bounds are already frozen to the larger source stack bounds - assertEquals(BOUNDS_FULL, mTarget.mStackBounds); - assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds)); - - // Drive some animation updates, ensure that only the stack bounds change and the task - // bounds are frozen to the original stack bounds (adjusted for the offset) - boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f)); - assertNotEquals(BOUNDS_FULL, mTarget.mStackBounds); - assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds)); - boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(1f)); - assertNotEquals(BOUNDS_FULL, mTarget.mStackBounds); - assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds)); - - // Finish the animation, ensure that it reaches the final bounds with the given state - boundsAnimator.end(); - assertTrue(mTarget.mAnimationEnded); - assertEquals(BOUNDS_FLOATING, mTarget.mStackBounds); - assertNull(mTarget.mTaskBounds); + mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0f) + .update(0.5f) + .update(1f) + .end() + .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test public void testFloatingToFullscreenTransition() throws Exception { - // Create and start the animation - mTarget.reinitialize(BOUNDS_FULL, null); - final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FLOATING, - BOUNDS_FULL, DURATION, MOVE_TO_FULLSCREEN); - - // Assert that when we are started, and that we are going to fullscreen - assertTrue(mTarget.mAnimationStarted); - assertTrue(mTarget.mAnimationStartedToFullscreen); - // Ensure that we update the PiP mode change with the new fullscreen bounds - assertTrue(mTarget.mUpdatedPictureInPictureModeWithBounds); - // Ensure that the task stack bounds are already frozen to the larger target stack bounds - assertEquals(BOUNDS_FLOATING, mTarget.mStackBounds); - assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds)); - - // Drive some animation updates, ensure that only the stack bounds change and the task - // bounds are frozen to the original stack bounds (adjusted for the offset) - boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f)); - assertNotEquals(BOUNDS_FLOATING, mTarget.mStackBounds); - assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds)); - boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(1f)); - assertNotEquals(BOUNDS_FLOATING, mTarget.mStackBounds); - assertEquals(BOUNDS_FULL, offsetToZero(mTarget.mTaskBounds)); - - // Finish the animation, ensure that it reaches the final bounds with the given state - boundsAnimator.end(); - assertTrue(mTarget.mAnimationEnded); - assertEquals(BOUNDS_FULL, mTarget.mStackBounds); - assertNull(mTarget.mTaskBounds); + mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) + .expectStarted(SCHEDULE_PIP_MODE_CHANGED) + .update(0f) + .update(0.5f) + .update(1f) + .end() + .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN); + } + + @UiThreadTest + @Test + public void testFloatingToSmallerFloatingTransition() throws Exception { + mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0f) + .update(0.5f) + .update(1f) + .end() + .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); + } + + @UiThreadTest + @Test + public void testFloatingToLargerFloatingTransition() throws Exception { + mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0f) + .update(0.5f) + .update(1f) + .end() + .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); + } + + /** F->!F w/ CANCEL **/ + + @UiThreadTest + @Test + public void testFullscreenToFloatingCancelFromTarget() throws Exception { + mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .cancel() + .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); + } + + @UiThreadTest + @Test + public void testFullscreenToFloatingCancelFromAnimationToSameBounds() throws Exception { + mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .restart(BOUNDS_FLOATING) + .end() + .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); + } + + @UiThreadTest + @Test + public void testFullscreenToFloatingCancelFromAnimationToFloatingBounds() throws Exception { + mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .restart(BOUNDS_SMALLER_FLOATING) + .end() + .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); + } + + @UiThreadTest + @Test + public void testFullscreenToFloatingCancelFromAnimationToFullscreenBounds() throws Exception { + mDriver.start(BOUNDS_FULL, BOUNDS_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .restart(BOUNDS_FULL) + .end() + .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN); + } + + /** !F->F w/ CANCEL **/ + + @UiThreadTest + @Test + public void testFloatingToFullscreenCancelFromTarget() throws Exception { + mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) + .expectStarted(SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .cancel() + .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); + } + + @UiThreadTest + @Test + public void testFloatingToFullscreenCancelFromAnimationToSameBounds() throws Exception { + mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) + .expectStarted(SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .restart(BOUNDS_FULL) + .end() + .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, MOVE_TO_FULLSCREEN); } @UiThreadTest @Test - public void testInterruptAnimationFromUser() throws Exception { - // Create and start the animation - mTarget.reinitialize(BOUNDS_FULL, null); - final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL, - BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN); - - // Cancel the animation on the next update from the user - mTarget.mRequestCancelAnimation = true; - mTarget.mBoundsUpdated = false; - boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f)); - // Ensure that we got no more updates after returning false and the bounds are not updated - // to the end value - assertFalse(mTarget.mBoundsUpdated); - assertNotEquals(BOUNDS_FLOATING, mTarget.mStackBounds); - assertNotEquals(BOUNDS_FLOATING, mTarget.mTaskBounds); - // Ensure that we received the animation end call - assertTrue(mTarget.mAnimationEnded); + public void testFloatingToFullscreenCancelFromAnimationToFloatingBounds() throws Exception { + mDriver.start(BOUNDS_FLOATING, BOUNDS_FULL) + .expectStarted(SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .restart(BOUNDS_SMALLER_FLOATING) + .end() + .expectEnded(SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } + /** !F->!F w/ CANCEL **/ + @UiThreadTest @Test - public void testCancelAnimationFromNewAnimationToExistingBounds() throws Exception { - // Create and start the animation - mTarget.reinitialize(BOUNDS_FULL, null); - final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL, - BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN); - - // Drive some animation updates - boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f)); - - // Cancel the animation as a restart to the same bounds - mTarget.reinitialize(null, null); - final BoundsAnimator altBoundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL, - BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN); - // Ensure the animator is the same - assertSame(boundsAnimator, altBoundsAnimator); - // Ensure we haven't restarted or finished the animation - assertFalse(mTarget.mAnimationStarted); - assertFalse(mTarget.mAnimationEnded); - // Ensure that we haven't tried to update the PiP mode - assertFalse(mTarget.mUpdatedPictureInPictureModeWithBounds); + public void testFloatingToSmallerFloatingCancelFromTarget() throws Exception { + mDriver.start(BOUNDS_FLOATING, BOUNDS_SMALLER_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .cancel() + .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); } @UiThreadTest @Test - public void testCancelAnimationFromNewAnimationToNewBounds() throws Exception { - // Create and start the animation - mTarget.reinitialize(BOUNDS_FULL, null); - final BoundsAnimator boundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL, - BOUNDS_FLOATING, DURATION, !MOVE_TO_FULLSCREEN); - - // Drive some animation updates - boundsAnimator.onAnimationUpdate(mAnimator.getWithValue(0.5f)); - - // Cancel the animation as a restart to new bounds - mTarget.reinitialize(null, null); - final BoundsAnimator altBoundsAnimator = mController.animateBoundsImpl(mTarget, BOUNDS_FULL, - BOUNDS_ALT_FLOATING, DURATION, !MOVE_TO_FULLSCREEN); - // Ensure the animator is not the same - assertNotSame(boundsAnimator, altBoundsAnimator); - // Ensure that we did not get an animation start/end callback - assertFalse(mTarget.mAnimationStarted); - assertFalse(mTarget.mAnimationEnded); - // Ensure that we haven't tried to update the PiP mode - assertFalse(mTarget.mUpdatedPictureInPictureModeWithBounds); + public void testFloatingToLargerFloatingCancelFromTarget() throws Exception { + mDriver.start(BOUNDS_SMALLER_FLOATING, BOUNDS_FLOATING) + .expectStarted(!SCHEDULE_PIP_MODE_CHANGED) + .update(0.25f) + .cancel() + .expectEnded(!SCHEDULE_PIP_MODE_CHANGED, !MOVE_TO_FULLSCREEN); + } + + /** MISC **/ + + @UiThreadTest + @Test + public void testBoundsAreCopied() throws Exception { + Rect from = new Rect(0, 0, 100, 100); + Rect to = new Rect(25, 25, 75, 75); + mDriver.start(from, to) + .update(0.25f) + .end(); + assertEquals(new Rect(0, 0, 100, 100), from); + assertEquals(new Rect(25, 25, 75, 75), to); } /** - * @return the bounds offset to zero/zero. + * @return whether the task and stack bounds would be the same if they were at the same offset. */ - private Rect offsetToZero(Rect bounds) { - mTmpRect.set(bounds); - mTmpRect.offsetTo(0, 0); - return mTmpRect; + private boolean assertEqualSizeAtOffset(Rect stackBounds, Rect taskBounds) { + mTmpRect.set(taskBounds); + mTmpRect.offsetTo(stackBounds.left, stackBounds.top); + return stackBounds.equals(mTmpRect); } } |