From 2c90f3b13f36536eaa58edac06e900d60eff280a Mon Sep 17 00:00:00 2001 From: Riddle Hsu Date: Wed, 15 Dec 2021 20:51:59 -0600 Subject: [PATCH] Handle non-app windows in app rotation transition asynchronously When starting activity in different orientation with shell transition, the display will be rotated immediately. To avoid jump cut of non-app windows by the change, those windows will be faded out in old rotation. And when the app transition is done, the windows will be faded in with current rotation. This also reduces transition latency because the transition can start once the activities are ready, without waiting for those non-activity window to complete redraw. Bug: 210839369 Test: atest TransitionTests#testAppTransitionWithRotationChange Test: adb shell setprop persist.debug.shell_transit 1; reboot Launch landscape app from portrait home. Change-Id: I0afc3fcdb3b2ee9d23d5d4addc488ae085a2afd8 --- .../com/android/server/wm/DisplayContent.java | 21 ++-- .../server/wm/FadeAnimationController.java | 25 ++--- .../wm/FadeRotationAnimationController.java | 100 +++++++++++++++--- .../server/wm/InsetsSourceProvider.java | 8 ++ .../wm/NavBarFadeAnimationController.java | 8 -- .../android/server/wm/SeamlessRotator.java | 14 ++- .../com/android/server/wm/Transition.java | 10 ++ .../android/server/wm/TransitionTests.java | 37 +++++++ 8 files changed, 172 insertions(+), 51 deletions(-) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 3167437720b9c..e94e06168cacc 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1843,6 +1843,17 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } + /** Returns {@code true} if the decided new rotation has not applied to configuration yet. */ + private boolean isRotationChanging() { + return mDisplayRotation.getRotation() != getWindowConfiguration().getRotation(); + } + + private void startFadeRotationAnimationIfNeeded() { + if (isRotationChanging()) { + startFadeRotationAnimation(false /* shouldDebounce */); + } + } + /** * Starts the hide animation for the windows which will be rotated seamlessly. * @@ -3202,11 +3213,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Hide the windows which are not significant in rotation animation. So that the windows // don't need to block the unfreeze time. - if (screenRotationAnimation != null && screenRotationAnimation.hasScreenshot() - // Do not fade for freezing without rotation change. - && mDisplayRotation.getRotation() != getWindowConfiguration().getRotation() - && mFadeRotationAnimationController == null) { - startFadeRotationAnimation(false /* shouldDebounce */); + if (screenRotationAnimation != null && screenRotationAnimation.hasScreenshot()) { + startFadeRotationAnimationIfNeeded(); } } @@ -3227,6 +3235,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } if (!controller.isCollecting(this)) { controller.collect(this); + startFadeRotationAnimationIfNeeded(); } return; } @@ -3234,7 +3243,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp this, this, null /* remoteTransition */, displayChange); if (t != null) { mAtmService.startLaunchPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY); - if (getRotation() != getWindowConfiguration().getRotation()) { + if (isRotationChanging()) { mWmService.mLatencyTracker.onActionStart(ACTION_ROTATE_SCREEN); controller.mTransitionMetricsReporter.associate(t, startTime -> mWmService.mLatencyTracker.onActionEnd(ACTION_ROTATE_SCREEN)); diff --git a/services/core/java/com/android/server/wm/FadeAnimationController.java b/services/core/java/com/android/server/wm/FadeAnimationController.java index 817b27a55c1b8..561a07061bb45 100644 --- a/services/core/java/com/android/server/wm/FadeAnimationController.java +++ b/services/core/java/com/android/server/wm/FadeAnimationController.java @@ -21,7 +21,6 @@ import static com.android.server.wm.WindowAnimationSpecProto.ANIMATION; import android.annotation.NonNull; import android.content.Context; -import android.util.ArrayMap; import android.util.proto.ProtoOutputStream; import android.view.SurfaceControl; import android.view.animation.Animation; @@ -38,7 +37,6 @@ import java.io.PrintWriter; public class FadeAnimationController { protected final DisplayContent mDisplayContent; protected final Context mContext; - protected final ArrayMap mDeferredFinishCallbacks = new ArrayMap<>(); public FadeAnimationController(DisplayContent displayContent) { mDisplayContent = displayContent; @@ -78,16 +76,8 @@ public class FadeAnimationController { return; } - // We deferred the end of the animation when hiding the token, so we need to end it now that - // it's shown again. - final SurfaceAnimator.OnAnimationFinishedCallback finishedCallback = show ? (t, r) -> { - final Runnable runnable = mDeferredFinishCallbacks.remove(windowToken); - if (runnable != null) { - runnable.run(); - } - } : null; windowToken.startAnimation(windowToken.getPendingTransaction(), animationAdapter, - show /* hidden */, animationType, finishedCallback); + show /* hidden */, animationType, null /* finishedCallback */); } protected FadeAnimationAdapter createAdapter(LocalAnimationAdapter.AnimationSpec animationSpec, @@ -135,7 +125,7 @@ public class FadeAnimationController { }; } - protected class FadeAnimationAdapter extends LocalAnimationAdapter { + protected static class FadeAnimationAdapter extends LocalAnimationAdapter { protected final boolean mShow; protected final WindowToken mToken; @@ -149,13 +139,10 @@ public class FadeAnimationController { @Override public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) { - // We defer the end of the hide animation to ensure the tokens stay hidden until - // we show them again. - if (!mShow) { - mDeferredFinishCallbacks.put(mToken, endDeferFinishCallback); - return true; - } - return false; + // Defer the finish callback (restore leash) of the hide animation to ensure the token + // stay hidden until it needs to show again. Besides, when starting the show animation, + // the previous hide animation will be cancelled, so the callback can be ignored. + return !mShow; } } } diff --git a/services/core/java/com/android/server/wm/FadeRotationAnimationController.java b/services/core/java/com/android/server/wm/FadeRotationAnimationController.java index cf36c85ebabf7..bbda577e9c572 100644 --- a/services/core/java/com/android/server/wm/FadeRotationAnimationController.java +++ b/services/core/java/com/android/server/wm/FadeRotationAnimationController.java @@ -50,7 +50,7 @@ public class FadeRotationAnimationController extends FadeAnimationController { /** Whether to use constant zero alpha animation. */ private boolean mHideImmediately; - /** Whether this controller is triggered from shell transition. */ + /** Whether this controller is triggered from shell transition with type CHANGE. */ private final boolean mIsChangeTransition; /** Whether the start transaction of the transition is committed (by shell). */ @@ -59,21 +59,30 @@ public class FadeRotationAnimationController extends FadeAnimationController { /** The list to store the drawn tokens before the rotation animation starts. */ private ArrayList mPendingShowTokens; + /** It is used when the display has rotated, but some windows fade out in old rotation. */ + private SeamlessRotator mRotator; + + private final int mOriginalRotation; + private final boolean mHasScreenRotationAnimation; + public FadeRotationAnimationController(DisplayContent displayContent) { super(displayContent); mService = displayContent.mWmService; - mIsChangeTransition = displayContent.inTransition() - && displayContent.mTransitionController.getCollectingTransitionType() - == WindowManager.TRANSIT_CHANGE; + mOriginalRotation = displayContent.getWindowConfiguration().getRotation(); + final int transitionType = + displayContent.mTransitionController.getCollectingTransitionType(); + mIsChangeTransition = transitionType == WindowManager.TRANSIT_CHANGE; + // Only CHANGE type (rotation animation) needs to wait for the start transaction. mIsStartTransactionCommitted = !mIsChangeTransition; - mTimeoutRunnable = displayContent.getRotationAnimation() != null - || mIsChangeTransition ? () -> { + mTimeoutRunnable = displayContent.inTransition() ? () -> { synchronized (mService.mGlobalLock) { displayContent.finishFadeRotationAnimationIfPossible(); mService.mWindowPlacerLocked.performSurfacePlacement(); } } : null; - if (mTimeoutRunnable != null) { + mHasScreenRotationAnimation = + displayContent.getRotationAnimation() != null || mIsChangeTransition; + if (mHasScreenRotationAnimation) { // Hide the windows immediately because screen should have been covered by screenshot. mHideImmediately = true; } @@ -103,6 +112,19 @@ public class FadeRotationAnimationController extends FadeAnimationController { }, true /* traverseTopToBottom */); } + @Override + public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType) { + if (show) { + final SurfaceControl leash = mTargetWindowTokens.remove(windowToken); + if (leash != null && mRotator != null) { + // The leash was unrotated by start transaction of transition. Clear the transform + // to reshow the window in current rotation. + mRotator.setIdentityMatrix(mDisplayContent.getPendingTransaction(), leash); + } + } + super.fadeWindowToken(show, windowToken, animationType); + } + /** Applies show animation on the previously hidden window tokens. */ void show() { for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { @@ -125,19 +147,23 @@ public class FadeRotationAnimationController extends FadeAnimationController { * controller is created for normal rotation. */ boolean show(WindowToken token) { + if (!isTargetToken(token)) return false; if (!mIsStartTransactionCommitted) { // The fade-in animation should only start after the screenshot layer is shown by shell. // Otherwise the window will be blinking before the rotation animation starts. So store // to a pending list and animate them until the transaction is committed. - if (mTargetWindowTokens.containsKey(token)) { - if (mPendingShowTokens == null) { - mPendingShowTokens = new ArrayList<>(); - } - mPendingShowTokens.add(token); + if (mPendingShowTokens == null) { + mPendingShowTokens = new ArrayList<>(); } + mPendingShowTokens.add(token); return false; } - if (mTimeoutRunnable != null && mTargetWindowTokens.remove(token) != null) { + if (!mHasScreenRotationAnimation && token.mTransitionController.inTransition()) { + // Defer showing to onTransitionFinished(). + return false; + } + // If the timeout runnable is null (fixed rotation), the case will be handled by show(). + if (mTimeoutRunnable != null) { fadeWindowToken(true /* show */, token, ANIMATION_TYPE_FIXED_TRANSFORM); if (mTargetWindowTokens.isEmpty()) { mService.mH.removeCallbacks(mTimeoutRunnable); @@ -177,6 +203,15 @@ public class FadeRotationAnimationController extends FadeAnimationController { return mTargetWindowTokens.containsKey(token); } + /** + * Whether the insets animation leash should use previous position when running fade out + * animation in rotated display. + */ + boolean shouldFreezeInsetsPosition(WindowState w) { + return !mHasScreenRotationAnimation && w.mTransitionController.inTransition() + && isTargetToken(w.mToken); + } + void setOnShowRunnable(Runnable onShowRunnable) { mOnShowRunnable = onShowRunnable; } @@ -186,6 +221,22 @@ public class FadeRotationAnimationController extends FadeAnimationController { * transition starts. And associate transaction callback to consume pending animations. */ void setupStartTransaction(SurfaceControl.Transaction t) { + if (!mIsChangeTransition) { + // Take OPEN/CLOSE transition type as the example, the non-activity windows need to + // fade out in previous rotation while display has rotated to the new rotation, so + // their leashes are unrotated with the start transaction. + mRotator = new SeamlessRotator(mOriginalRotation, + mDisplayContent.getWindowConfiguration().getRotation(), + mDisplayContent.getDisplayInfo(), + false /* applyFixedTransformationHint */); + for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { + final SurfaceControl leash = mTargetWindowTokens.valueAt(i); + if (leash != null) { + mRotator.applyTransform(t, leash); + } + } + return; + } // Hide the windows immediately because a screenshot layer should cover the screen. for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { final SurfaceControl leash = mTargetWindowTokens.valueAt(i); @@ -208,9 +259,30 @@ public class FadeRotationAnimationController extends FadeAnimationController { }); } + void onTransitionFinished() { + if (mIsChangeTransition) { + // With screen rotation animation, the windows are always faded in when they are drawn. + // Because if they are drawn fast enough, the fade animation should not be observable. + return; + } + // For other transition types, the fade-in animation runs after the transition to make the + // transition animation (e.g. launch activity) look cleaner. + for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { + final WindowToken token = mTargetWindowTokens.keyAt(i); + for (int j = token.getChildCount() - 1; j >= 0; j--) { + // Only fade in the drawn windows. If the remaining windows are drawn later, + // show(WindowToken) will be called to fade in them. + if (token.getChildAt(j).isDrawFinishedLw()) { + mDisplayContent.finishFadeRotationAnimation(token); + break; + } + } + } + } + @Override public Animation getFadeInAnimation() { - if (mTimeoutRunnable != null) { + if (mHasScreenRotationAnimation) { // Use a shorter animation so it is easier to align with screen rotation animation. return AnimationUtils.loadAnimation(mContext, R.anim.screen_rotate_0_enter); } diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index af917264698e9..a8a923140a411 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -297,6 +297,14 @@ class InsetsSourceProvider { } private Point getWindowFrameSurfacePosition() { + if (mControl != null) { + final FadeRotationAnimationController fadeController = + mWin.mDisplayContent.getFadeRotationAnimationController(); + if (fadeController != null && fadeController.shouldFreezeInsetsPosition(mWin)) { + // Use previous position because the fade-out animation runs in old rotation. + return mControl.getSurfacePosition(); + } + } final Rect frame = mWin.getFrame(); final Point position = new Point(); mWin.transformFrameToSurfacePosition(frame.left, frame.top, position); diff --git a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java index af8293aab977d..80f2ab62f120e 100644 --- a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java +++ b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java @@ -95,14 +95,6 @@ public class NavBarFadeAnimationController extends FadeAnimationController{ } else { fadeAnim.run(); } - } else { - // If fade rotation animation is running and controlling the nav bar, make sure we empty - // the mDeferredFinishCallbacks and defer the runnable until fade rotation animation - // finishes. - final Runnable runnable = mDeferredFinishCallbacks.remove(mNavigationBar.mToken); - if (runnable != null) { - controller.setOnShowRunnable(runnable); - } } } diff --git a/services/core/java/com/android/server/wm/SeamlessRotator.java b/services/core/java/com/android/server/wm/SeamlessRotator.java index 4cc369f0a187b..c20b85858c443 100644 --- a/services/core/java/com/android/server/wm/SeamlessRotator.java +++ b/services/core/java/com/android/server/wm/SeamlessRotator.java @@ -20,7 +20,6 @@ import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import android.graphics.Matrix; -import android.os.IBinder; import android.view.DisplayInfo; import android.view.Surface.Rotation; import android.view.SurfaceControl; @@ -73,7 +72,7 @@ public class SeamlessRotator { * global display rotation. */ public void unrotate(Transaction transaction, WindowContainer win) { - transaction.setMatrix(win.getSurfaceControl(), mTransform, mFloat9); + applyTransform(transaction, win.getSurfaceControl()); // WindowState sets the position of the window so transform the position and update it. final float[] winSurfacePos = {win.mLastSurfacePosition.x, win.mLastSurfacePosition.y}; mTransform.mapPoints(winSurfacePos); @@ -83,6 +82,10 @@ public class SeamlessRotator { } } + void applyTransform(Transaction t, SurfaceControl sc) { + t.setMatrix(sc, mTransform, mFloat9); + } + /** * Returns the rotation of the display before it started rotating. * @@ -106,14 +109,17 @@ public class SeamlessRotator { return; } - mTransform.reset(); - t.setMatrix(win.mSurfaceControl, mTransform, mFloat9); + setIdentityMatrix(t, win.mSurfaceControl); t.setPosition(win.mSurfaceControl, win.mLastSurfacePosition.x, win.mLastSurfacePosition.y); if (mApplyFixedTransformHint) { t.unsetFixedTransformHint(win.mSurfaceControl); } } + void setIdentityMatrix(Transaction t, SurfaceControl sc) { + t.setMatrix(sc, Matrix.IDENTITY_MATRIX, mFloat9); + } + public void dump(PrintWriter pw) { pw.print("{old="); pw.print(mOldRotation); pw.print(", new="); pw.print(mNewRotation); pw.print("}"); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index dae004dd5f6f0..c7c3bb6b31e26 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -143,6 +143,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe /** The final animation targets derived from participants after promotion. */ private ArraySet mTargets = null; + /** The main display running this transition. */ + private DisplayContent mTargetDisplay; + /** * Set of participating windowtokens (activity/wallpaper) which are visible at the end of * the transition animation. @@ -473,6 +476,12 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId); dc.getInputMonitor().setActiveRecents(null /* activity */, null /* layer */); } + + final FadeRotationAnimationController fadeRotationController = + mTargetDisplay.getFadeRotationAnimationController(); + if (fadeRotationController != null) { + fadeRotationController.onTransitionFinished(); + } } void abort() { @@ -514,6 +523,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } } if (dc == null) dc = mController.mAtm.mRootWindowContainer.getDefaultDisplay(); + mTargetDisplay = dc; if (mState == STATE_ABORT) { mController.abort(this); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index a97c0571257ed..ec6cd92493172 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -524,6 +524,43 @@ public class TransitionTests extends WindowTestsBase { assertNull(mDisplayContent.getFadeRotationAnimationController()); } + @Test + public void testAppTransitionWithRotationChange() { + final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, "statusBar"); + makeWindowVisible(statusBar); + mDisplayContent.getDisplayPolicy().addWindowLw(statusBar, statusBar.mAttrs); + final ActivityRecord app = createActivityRecord(mDisplayContent); + final TestTransitionPlayer player = registerTestTransitionPlayer(); + final Transition transition = app.mTransitionController.createTransition(TRANSIT_OPEN); + app.mTransitionController.requestStartTransition(transition, app.getTask(), + null /* remoteTransition */, null /* displayChange */); + mDisplayContent.getDisplayRotation().setRotation(mDisplayContent.getRotation() + 1); + final int anyChanges = 1; + mDisplayContent.requestChangeTransitionIfNeeded(anyChanges, null /* displayChange */); + transition.setKnownConfigChanges(mDisplayContent, anyChanges); + final FadeRotationAnimationController fadeController = + mDisplayContent.getFadeRotationAnimationController(); + assertNotNull(fadeController); + assertTrue(fadeController.shouldFreezeInsetsPosition(statusBar)); + + statusBar.setOrientationChanging(true); + player.startTransition(); + // Non-app windows should not be collected. + assertFalse(statusBar.mToken.inTransition()); + assertTrue(app.getTask().inTransition()); + + final SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); + player.onTransactionReady(startTransaction); + // The leash should be unrotated. + verify(startTransaction).setMatrix(eq(statusBar.mToken.getAnimationLeash()), any(), any()); + + // The redrawn window will be faded in when the transition finishes. And because this test + // only use one non-activity window, the fade rotation controller should also be cleared. + statusBar.mWinAnimator.mDrawState = WindowStateAnimator.HAS_DRAWN; + player.finish(); + assertNull(mDisplayContent.getFadeRotationAnimationController()); + } + @Test public void testIntermediateVisibility() { final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class);