diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 0990f1f7ff404..5c13e8192701c 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -16,10 +16,16 @@ package com.android.server.wm; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; +import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; + import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_FIXED_TRANSFORM; +import android.annotation.IntDef; import android.os.HandlerExecutor; import android.util.ArrayMap; +import android.util.Slog; import android.view.SurfaceControl; import android.view.WindowManager; import android.view.animation.AlphaAnimation; @@ -28,136 +34,197 @@ import android.view.animation.AnimationUtils; import com.android.internal.R; -import java.util.ArrayList; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.Consumer; /** - * Controller to fade out and in windows when the display is changing rotation. It can be used for - * both fixed rotation and normal rotation to hide some non-activity windows. The caller should show - * the windows until they are drawn with the new rotation. + * Controller to handle the appearance of non-activity windows which can update asynchronously when + * the display rotation is changing. This is an optimization to reduce the latency to start screen + * rotation or app transition animation. + *
The appearance:
+ * - Open app with rotation change: the target windows are faded out with open transition, and then
+ *   faded in after the transition when the windows are drawn with new rotation.
+ * - Normal rotation: the target windows are hidden by a parent leash with zero alpha after the
+ *   screenshot layer is shown, and will be faded in when they are drawn with new rotation.
+ * - Seamless rotation: Only shell transition uses this controller in this case. The target windows
+ *   will be requested to use sync transaction individually. Their window token will rotate to old
+ *   rotation. After the start transaction of transition is applied and the window is drawn in new
+ *   rotation, the old rotation transformation will be removed with applying the sync transaction.
+ * 
+ * For the windows which are forced to be seamless (e.g. screen decor overlay), the case is the + * same as above mentioned seamless rotation (only shell). Just the appearance may be mixed, e.g. + * 2 windows FADE and 2 windows SEAMLESS in normal rotation or app transition. And 4 (all) windows + * SEAMLESS in seamless rotation. */ -public class AsyncRotationController extends FadeAnimationController { +class AsyncRotationController extends FadeAnimationController implements Consumer { + private static final String TAG = "AsyncRotation"; + private static final boolean DEBUG = false; - /** The map of window token to its animation leash. */ - private final ArrayMap mTargetWindowTokens = new ArrayMap<>(); private final WindowManagerService mService; + /** The map of async windows to the operations of rotation appearance. */ + private final ArrayMap mTargetWindowTokens = new ArrayMap<>(); /** If non-null, it usually indicates that there will be a screen rotation animation. */ - private final Runnable mTimeoutRunnable; - private final WindowToken mNavBarToken; + private Runnable mTimeoutRunnable; + /** Non-null to indicate that the navigation bar is always handled by legacy seamless. */ + private WindowToken mNavBarToken; - /** A runnable which gets called when the {@link #show()} is called. */ + /** A runnable which gets called when the {@link #completeAll()} is called. */ private Runnable mOnShowRunnable; /** Whether to use constant zero alpha animation. */ private boolean mHideImmediately; - /** Whether this controller is triggered from shell transition with type CHANGE. */ - private final boolean mIsChangeTransition; + /** The case of legacy transition. */ + private static final int OP_LEGACY = 0; + /** It is usually OPEN/CLOSE/TO_FRONT/TO_BACK. */ + private static final int OP_APP_SWITCH = 1; + /** The normal display change transition which should have a screen rotation animation. */ + private static final int OP_CHANGE = 2; + /** The app requests seamless and the display supports. But the decision is still in shell. */ + private static final int OP_CHANGE_MAY_SEAMLESS = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { OP_LEGACY, OP_APP_SWITCH, OP_CHANGE, OP_CHANGE_MAY_SEAMLESS }) + @interface TransitionOp {} + + /** Non-zero if this controller is triggered by shell transition. */ + private final @TransitionOp int mTransitionOp; /** Whether the start transaction of the transition is committed (by shell). */ private boolean mIsStartTransactionCommitted; - /** The list to store the drawn tokens before the rotation animation starts. */ - private ArrayList mPendingShowTokens; - - /** - * The sync transactions of the target windows. It is used when the display has rotated but - * the windows need to fade out in previous rotation. These transactions will be applied with - * fade-in animation, so there won't be a flickering such as the windows have redrawn during - * fading out. - */ - private ArrayMap mCapturedDrawTransactions; + private SeamlessRotator mRotator; private final int mOriginalRotation; private final boolean mHasScreenRotationAnimation; - public AsyncRotationController(DisplayContent displayContent) { + AsyncRotationController(DisplayContent displayContent) { super(displayContent); mService = displayContent.mWmService; 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.inTransition() ? () -> { - synchronized (mService.mGlobalLock) { - displayContent.finishAsyncRotationIfPossible(); - mService.mWindowPlacerLocked.performSurfacePlacement(); + if (transitionType == WindowManager.TRANSIT_CHANGE) { + final DisplayRotation dr = displayContent.getDisplayRotation(); + final WindowState w = displayContent.getDisplayPolicy().getTopFullscreenOpaqueWindow(); + // A rough condition to check whether it may be seamless style. Though the final + // decision in shell may be different, it is fine because the jump cut can be covered + // by a screenshot if shell falls back to use normal rotation animation. + if (w != null && w.mAttrs.rotationAnimation == ROTATION_ANIMATION_SEAMLESS + && w.getTask() != null + && dr.canRotateSeamlessly(mOriginalRotation, dr.getRotation())) { + mTransitionOp = OP_CHANGE_MAY_SEAMLESS; + } else { + mTransitionOp = OP_CHANGE; } - } : null; + } else if (transitionType != WindowManager.TRANSIT_NONE) { + mTransitionOp = OP_APP_SWITCH; + } else { + mTransitionOp = OP_LEGACY; + } + + // Although OP_CHANGE_MAY_SEAMLESS may still play screen rotation animation because shell + // decides not to perform seamless rotation, it only affects whether to use fade animation + // when the windows are drawn. If the windows are not too slow (after rotation animation is + // done) to be drawn, the visual result can still look smooth. mHasScreenRotationAnimation = - displayContent.getRotationAnimation() != null || mIsChangeTransition; + displayContent.getRotationAnimation() != null || mTransitionOp == OP_CHANGE; if (mHasScreenRotationAnimation) { // Hide the windows immediately because screen should have been covered by screenshot. mHideImmediately = true; } - final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy(); - final WindowState navigationBar = displayPolicy.getNavigationBar(); - if (navigationBar != null) { - mNavBarToken = navigationBar.mToken; - final RecentsAnimationController controller = mService.getRecentsAnimationController(); - final boolean navBarControlledByRecents = - controller != null && controller.isNavigationBarAttachedToApp(); - // Do not animate movable navigation bar (e.g. non-gesture mode) or when the navigation - // bar is currently controlled by recents animation. - if (!displayPolicy.navigationBarCanMove() && !navBarControlledByRecents) { - mTargetWindowTokens.put(mNavBarToken, null); - } - } else { - mNavBarToken = null; - } - // Collect the target windows to fade out. The display won't wait for them to unfreeze. - final WindowState notificationShade = displayPolicy.getNotificationShade(); - displayContent.forAllWindows(w -> { - if (w.mActivityRecord == null && w.mHasSurface && !w.mForceSeamlesslyRotate - && !w.mIsWallpaper && !w.mIsImWindow && w != navigationBar - && w != notificationShade) { - mTargetWindowTokens.put(w.mToken, null); - } - }, true /* traverseTopToBottom */); + // Collect the windows which can rotate asynchronously without blocking the display. + displayContent.forAllWindows(this, true /* traverseTopToBottom */); + + // Legacy animation doesn't need to wait for the start transaction. + mIsStartTransactionCommitted = mTransitionOp == OP_LEGACY; + if (mIsStartTransactionCommitted) return; // The transition sync group may be finished earlier because it doesn't wait for these // target windows. But the windows still need to use sync transaction to keep the appearance // in previous rotation, so request a no-op sync to keep the state. - if (!mIsChangeTransition && transitionType != WindowManager.TRANSIT_NONE) { - for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { - final WindowToken token = mTargetWindowTokens.keyAt(i); - for (int j = token.getChildCount() - 1; j >= 0; j--) { - token.getChildAt(j).applyWithNextDraw(t -> {}); - } - } - } - } - - @Override - public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType) { - if (show) { - // The previous animation leash will be dropped when preparing fade-in animation, so - // simply remove it without restoring the transformation. - mTargetWindowTokens.remove(windowToken); - if (mCapturedDrawTransactions != null) { - // Unblock the window to draw its latest content with fade-in animation. - final SurfaceControl.Transaction t = mDisplayContent.getPendingTransaction(); - for (int i = windowToken.getChildCount() - 1; i >= 0; i--) { - final SurfaceControl.Transaction drawT = - mCapturedDrawTransactions.remove(windowToken.getChildAt(i)); - if (drawT != null) { - t.merge(drawT); - } - } - } - } - 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--) { - final WindowToken windowToken = mTargetWindowTokens.keyAt(i); + if (mHasScreenRotationAnimation + && mTargetWindowTokens.valueAt(i).mAction == Operation.ACTION_FADE) { + // The windows are hidden (leash is alpha 0) before finishing drawing so it is + // unnecessary to request sync. + continue; + } + final WindowToken token = mTargetWindowTokens.keyAt(i); + for (int j = token.getChildCount() - 1; j >= 0; j--) { + token.getChildAt(j).applyWithNextDraw(t -> {}); + } + } + } + + /** Assigns the operation for the window tokens which can update rotation asynchronously. */ + @Override + public void accept(WindowState w) { + if (w.mActivityRecord != null || !w.mHasSurface || w.mIsWallpaper || w.mIsImWindow + || w.mAttrs.type == TYPE_NOTIFICATION_SHADE) { + return; + } + if (mTransitionOp == OP_LEGACY && w.mForceSeamlesslyRotate) { + // Legacy transition already handles seamlessly windows. + return; + } + if (w.mAttrs.type == TYPE_NAVIGATION_BAR) { + int action = Operation.ACTION_FADE; + final boolean navigationBarCanMove = + mDisplayContent.getDisplayPolicy().navigationBarCanMove(); + if (mTransitionOp == OP_LEGACY) { + mNavBarToken = w.mToken; + // Do not animate movable navigation bar (e.g. 3-buttons mode). + if (navigationBarCanMove) return; + // Or when the navigation bar is currently controlled by recents animation. + final RecentsAnimationController recents = mService.getRecentsAnimationController(); + if (recents != null && recents.isNavigationBarAttachedToApp()) { + return; + } + } else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS) { + action = Operation.ACTION_SEAMLESS; + } + mTargetWindowTokens.put(w.mToken, new Operation(action)); + return; + } + + final int action = mTransitionOp == OP_CHANGE_MAY_SEAMLESS || w.mForceSeamlesslyRotate + ? Operation.ACTION_SEAMLESS : Operation.ACTION_FADE; + mTargetWindowTokens.put(w.mToken, new Operation(action)); + } + + /** Lets the window fit in new rotation naturally. */ + private void finishOp(WindowToken windowToken) { + final Operation op = mTargetWindowTokens.remove(windowToken); + if (op == null) return; + if (op.mCapturedDrawTransaction != null) { + // Unblock the window to show its latest content. + mDisplayContent.getPendingTransaction().merge(op.mCapturedDrawTransaction); + op.mCapturedDrawTransaction = null; + if (DEBUG) Slog.d(TAG, "finishOp merge transaction " + windowToken.getTopChild()); + } + if (op.mAction == Operation.ACTION_FADE) { + if (DEBUG) Slog.d(TAG, "finishOp fade-in " + windowToken.getTopChild()); + // The previous animation leash will be dropped when preparing fade-in animation, so + // simply apply new animation without restoring the transformation. fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_FIXED_TRANSFORM); + } else if (op.mAction == Operation.ACTION_SEAMLESS && mRotator != null + && op.mLeash != null && op.mLeash.isValid()) { + if (DEBUG) Slog.d(TAG, "finishOp undo seamless " + windowToken.getTopChild()); + mRotator.setIdentityMatrix(mDisplayContent.getPendingTransaction(), op.mLeash); + } + } + + /** + * Completes all operations such as applying fade-in animation on the previously hidden window + * tokens. This is called if all windows are ready in new rotation or timed out. + */ + void completeAll() { + for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { + finishOp(mTargetWindowTokens.keyAt(i)); } mTargetWindowTokens.clear(); - mPendingShowTokens = null; if (mTimeoutRunnable != null) { mService.mH.removeCallbacks(mTimeoutRunnable); } @@ -168,48 +235,81 @@ public class AsyncRotationController extends FadeAnimationController { } /** - * Returns {@code true} if all target windows are shown. It only takes effects if this - * controller is created for normal rotation. + * Notifies that the window is ready in new rotation. Returns {@code true} if all target + * windows have completed their rotation operations. */ - boolean show(WindowToken token) { - if (!isTargetToken(token)) return false; + boolean completeRotation(WindowToken token) { 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 (mPendingShowTokens == null) { - mPendingShowTokens = new ArrayList<>(); + final Operation op = mTargetWindowTokens.get(token); + // The animation or draw transaction should only start after the start transaction is + // applied by shell (e.g. show screenshot layer). 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 (op != null) { + if (DEBUG) Slog.d(TAG, "Complete set pending " + token.getTopChild()); + op.mIsCompletionPending = true; } - mPendingShowTokens.add(token); return false; } - if (!mHasScreenRotationAnimation && token.mTransitionController.inTransition()) { - // Defer showing to onTransitionFinished(). - return false; + if (mTransitionOp == OP_APP_SWITCH && token.mTransitionController.inTransition()) { + final Operation op = mTargetWindowTokens.get(token); + if (op != null && op.mAction == Operation.ACTION_FADE) { + // Defer showing to onTransitionFinished(). + if (DEBUG) Slog.d(TAG, "Defer completion " + token.getTopChild()); + 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 (!isTargetToken(token)) return false; + if (mHasScreenRotationAnimation || mTransitionOp != OP_LEGACY) { + if (DEBUG) Slog.d(TAG, "Complete directly " + token.getTopChild()); + finishOp(token); if (mTargetWindowTokens.isEmpty()) { - mService.mH.removeCallbacks(mTimeoutRunnable); + if (mTimeoutRunnable != null) mService.mH.removeCallbacks(mTimeoutRunnable); return true; } } + // The case (legacy fixed rotation) will be handled by completeAll() when all seamless + // windows are done. return false; } - /** Applies hide animation on the window tokens which may be seamlessly rotated later. */ - void hide() { + /** + * Prepares the corresponding operations (e.g. hide animation) for the window tokens which may + * be seamlessly rotated later. + */ + void start() { for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { final WindowToken windowToken = mTargetWindowTokens.keyAt(i); - fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_FIXED_TRANSFORM); + final Operation op = mTargetWindowTokens.valueAt(i); + if (op.mAction == Operation.ACTION_FADE) { + fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_FIXED_TRANSFORM); + op.mLeash = windowToken.getAnimationLeash(); + if (DEBUG) Slog.d(TAG, "Start fade-out " + windowToken.getTopChild()); + } else if (op.mAction == Operation.ACTION_SEAMLESS) { + op.mLeash = windowToken.mSurfaceControl; + if (DEBUG) Slog.d(TAG, "Start seamless " + windowToken.getTopChild()); + } } - if (mTimeoutRunnable != null) { - mService.mH.postDelayed(mTimeoutRunnable, - WindowManagerService.WINDOW_FREEZE_TIMEOUT_DURATION); + if (mHasScreenRotationAnimation) { + scheduleTimeout(); } } + private void scheduleTimeout() { + if (mTimeoutRunnable == null) { + mTimeoutRunnable = () -> { + synchronized (mService.mGlobalLock) { + Slog.i(TAG, "Async rotation timeout: " + mTargetWindowTokens); + mIsStartTransactionCommitted = true; + mDisplayContent.finishAsyncRotationIfPossible(); + mService.mWindowPlacerLocked.performSurfacePlacement(); + } + }; + } + mService.mH.postDelayed(mTimeoutRunnable, + WindowManagerService.WINDOW_FREEZE_TIMEOUT_DURATION); + } + /** Hides the window immediately until it is drawn in new rotation. */ void hideImmediately(WindowToken windowToken) { final boolean original = mHideImmediately; @@ -218,9 +318,11 @@ public class AsyncRotationController extends FadeAnimationController { mHideImmediately = original; } - /** Returns {@code true} if the window is handled by this controller. */ - boolean isHandledToken(WindowToken token) { - return token == mNavBarToken || isTargetToken(token); + /** Returns {@code true} if the window will rotate independently. */ + boolean isAsync(WindowState w) { + return w.mToken == mNavBarToken + || (w.mForceSeamlesslyRotate && mTransitionOp == OP_LEGACY) + || isTargetToken(w.mToken); } /** Returns {@code true} if the controller will run fade animations on the window. */ @@ -233,7 +335,7 @@ public class AsyncRotationController extends FadeAnimationController { * animation in rotated display. */ boolean shouldFreezeInsetsPosition(WindowState w) { - return !mHasScreenRotationAnimation && w.mTransitionController.inTransition() + return mTransitionOp == OP_APP_SWITCH && w.mTransitionController.inTransition() && isTargetToken(w.mToken); } @@ -246,54 +348,70 @@ public class AsyncRotationController 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. - final SeamlessRotator rotator = 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 && leash.isValid()) { - rotator.applyTransform(t, leash); + for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { + final Operation op = mTargetWindowTokens.valueAt(i); + final SurfaceControl leash = op.mLeash; + if (leash == null || !leash.isValid()) continue; + if (mHasScreenRotationAnimation && op.mAction == Operation.ACTION_FADE) { + // Hide the windows immediately because a screenshot layer should cover the screen. + t.setAlpha(leash, 0f); + if (DEBUG) { + Slog.d(TAG, "Setup alpha0 " + mTargetWindowTokens.keyAt(i).getTopChild()); + } + } else { + // 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 transformed with the start transaction. + if (mRotator == null) { + mRotator = new SeamlessRotator(mOriginalRotation, + mDisplayContent.getWindowConfiguration().getRotation(), + mDisplayContent.getDisplayInfo(), + false /* applyFixedTransformationHint */); + } + mRotator.applyTransform(t, leash); + if (DEBUG) { + Slog.d(TAG, "Setup unrotate " + mTargetWindowTokens.keyAt(i).getTopChild()); } } - 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); - if (leash != null && leash.isValid()) { - t.setAlpha(leash, 0f); - } } + + if (mIsStartTransactionCommitted) return; // If there are windows have redrawn in new rotation but the start transaction has not // been applied yet, the fade-in animation will be deferred. So once the transaction is // committed, the fade-in animation can run with screen rotation animation. t.addTransactionCommittedListener(new HandlerExecutor(mService.mH), () -> { synchronized (mService.mGlobalLock) { + if (DEBUG) Slog.d(TAG, "Start transaction is committed"); mIsStartTransactionCommitted = true; - if (mPendingShowTokens == null) return; - for (int i = mPendingShowTokens.size() - 1; i >= 0; i--) { - mDisplayContent.finishAsyncRotation(mPendingShowTokens.get(i)); + for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { + if (mTargetWindowTokens.valueAt(i).mIsCompletionPending) { + if (DEBUG) { + Slog.d(TAG, "Continue pending completion " + + mTargetWindowTokens.keyAt(i).getTopChild()); + } + mDisplayContent.finishAsyncRotation(mTargetWindowTokens.keyAt(i)); + } } - mPendingShowTokens = null; } }); } + /** Called when the transition by shell is done. */ void onTransitionFinished() { - if (mIsChangeTransition) { + if (mTransitionOp == OP_CHANGE) { // 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; } + if (DEBUG) Slog.d(TAG, "onTransitionFinished " + mTargetWindowTokens); // 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); + if (!token.isVisible()) { + mDisplayContent.finishAsyncRotation(token); + continue; + } 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. @@ -303,25 +421,35 @@ public class AsyncRotationController extends FadeAnimationController { } } } + if (!mTargetWindowTokens.isEmpty()) { + scheduleTimeout(); + } } - /** Captures the post draw transaction if the window should update with fade-in animation. */ + /** + * Captures the post draw transaction if the window should keep its appearance in previous + * rotation when running transition. Returns {@code true} if the draw transaction is handled + * by this controller. + */ boolean handleFinishDrawing(WindowState w, SurfaceControl.Transaction postDrawTransaction) { - if (mIsChangeTransition || !isTargetToken(w.mToken)) return false; - if (postDrawTransaction != null && w.mTransitionController.inTransition()) { - if (mCapturedDrawTransactions == null) { - mCapturedDrawTransactions = new ArrayMap<>(); - } - final SurfaceControl.Transaction t = mCapturedDrawTransactions.get(w); - if (t == null) { - mCapturedDrawTransactions.put(w, postDrawTransaction); - } else { - t.merge(postDrawTransaction); - } - return true; + if (mTransitionOp == OP_LEGACY || postDrawTransaction == null + || !w.mTransitionController.inTransition()) { + return false; } - mDisplayContent.finishAsyncRotation(w.mToken); - return false; + final Operation op = mTargetWindowTokens.get(w.mToken); + if (op == null) return false; + final boolean keepUntilTransitionFinish = + mTransitionOp == OP_APP_SWITCH && op.mAction == Operation.ACTION_FADE; + final boolean keepUntilStartTransaction = + !mIsStartTransactionCommitted && op.mAction == Operation.ACTION_SEAMLESS; + if (!keepUntilTransitionFinish && !keepUntilStartTransaction) return false; + if (op.mCapturedDrawTransaction == null) { + op.mCapturedDrawTransaction = postDrawTransaction; + } else { + op.mCapturedDrawTransaction.merge(postDrawTransaction); + } + if (DEBUG) Slog.d(TAG, "Capture draw transaction " + w); + return true; } @Override @@ -338,26 +466,36 @@ public class AsyncRotationController extends FadeAnimationController { if (mHideImmediately) { // For change transition, the hide transaction needs to be applied with sync transaction // (setupStartTransaction). So keep alpha 1 just to get the animation leash. - final float alpha = mIsChangeTransition ? 1 : 0; + final float alpha = mTransitionOp == OP_CHANGE ? 1 : 0; return new AlphaAnimation(alpha /* fromAlpha */, alpha /* toAlpha */); } return super.getFadeOutAnimation(); } - @Override - protected FadeAnimationAdapter createAdapter(LocalAnimationAdapter.AnimationSpec animationSpec, - boolean show, WindowToken windowToken) { - return new FadeAnimationAdapter(animationSpec, windowToken.getSurfaceAnimationRunner(), - show, windowToken) { - @Override - public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t, - int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) { - // The fade cycle is done when showing, so only need to store the leash when hiding. - if (!show) { - mTargetWindowTokens.put(mToken, animationLeash); - } - super.startAnimation(animationLeash, t, type, finishCallback); - } - }; + /** The operation to control the rotation appearance associated with window token. */ + private static class Operation { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { ACTION_SEAMLESS, ACTION_FADE }) + @interface Action {} + + static final int ACTION_SEAMLESS = 1; + static final int ACTION_FADE = 2; + final @Action int mAction; + /** The leash of window token. It can be animation leash or the token itself. */ + SurfaceControl mLeash; + /** Whether the window is drawn before the transition starts. */ + boolean mIsCompletionPending; + + /** + * The sync transaction of the target window. It is used when the display has rotated but + * the window needs to show in previous rotation. The transaction will be applied after the + * the start transaction of transition, so there won't be a flickering such as the window + * has redrawn during fading out. + */ + SurfaceControl.Transaction mCapturedDrawTransaction; + + Operation(@Action int action) { + mAction = action; + } } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index c87027dde3ad7..092cff384a805 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1880,7 +1880,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } if (mAsyncRotationController == null) { mAsyncRotationController = new AsyncRotationController(this); - mAsyncRotationController.hide(); + mAsyncRotationController.start(); return true; } return false; @@ -1890,7 +1890,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp void finishAsyncRotationIfPossible() { final AsyncRotationController controller = mAsyncRotationController; if (controller != null && !mDisplayRotation.hasSeamlessRotatingWindow()) { - controller.show(); + controller.completeAll(); mAsyncRotationController = null; } } @@ -1898,19 +1898,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** Shows the given window which may be hidden for screen rotation. */ void finishAsyncRotation(WindowToken windowToken) { final AsyncRotationController controller = mAsyncRotationController; - if (controller != null && controller.show(windowToken)) { + if (controller != null && controller.completeRotation(windowToken)) { mAsyncRotationController = null; } } /** Returns {@code true} if the screen rotation animation needs to wait for the window. */ boolean shouldSyncRotationChange(WindowState w) { - if (w.mForceSeamlesslyRotate) { - // The window should look no different before and after rotation. - return false; - } final AsyncRotationController controller = mAsyncRotationController; - return controller == null || !controller.isHandledToken(w.mToken); + return controller == null || !controller.isAsync(w); } void notifyInsetsChanged(Consumer dispatchInsetsChanged) { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 7387ea3703ee7..2a05d05d47863 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -675,18 +675,7 @@ public class DisplayRotation { return false; } - // For the upside down rotation we don't rotate seamlessly as the navigation bar moves - // position. Note most apps (using orientation:sensor or user as opposed to fullSensor) - // will not enter the reverse portrait orientation, so actually the orientation won't change - // at all. - if (oldRotation == mUpsideDownRotation || newRotation == mUpsideDownRotation) { - return false; - } - - // If the navigation bar can't change sides, then it will jump when we change orientations - // and we don't rotate seamlessly - unless that is allowed, eg. with gesture navigation - // where the navbar is low-profile enough that this isn't very noticeable. - if (!mAllowSeamlessRotationDespiteNavBarMoving && !mDisplayPolicy.navigationBarCanMove()) { + if (!canRotateSeamlessly(oldRotation, newRotation)) { return false; } @@ -713,6 +702,20 @@ public class DisplayRotation { return true; } + boolean canRotateSeamlessly(int oldRotation, int newRotation) { + // For the upside down rotation we don't rotate seamlessly as the navigation bar moves + // position. Note most apps (using orientation:sensor or user as opposed to fullSensor) + // will not enter the reverse portrait orientation, so actually the orientation won't change + // at all. + if (oldRotation == mUpsideDownRotation || newRotation == mUpsideDownRotation) { + return false; + } + // If the navigation bar can't change sides, then it will jump when we change orientations + // and we don't rotate seamlessly - unless that is allowed, eg. with gesture navigation + // where the navbar is low-profile enough that this isn't very noticeable. + return mAllowSeamlessRotationDespiteNavBarMoving || mDisplayPolicy.navigationBarCanMove(); + } + void markForSeamlessRotation(WindowState w, boolean seamlesslyRotated) { if (seamlesslyRotated == w.mSeamlesslyRotated || w.mForceSeamlesslyRotate) { return; 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 d8c653ce3c516..d74e1be88e0b4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.TRANSIT_CLOSE; @@ -504,7 +505,12 @@ public class TransitionTests extends WindowTestsBase { final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, "statusBar"); final WindowState navBar = createWindow(null, TYPE_NAVIGATION_BAR, "navBar"); final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); - final WindowState[] windows = { statusBar, navBar, ime }; + final WindowToken decorToken = new WindowToken.Builder(mWm, mock(IBinder.class), + TYPE_NAVIGATION_BAR_PANEL).setDisplayContent(mDisplayContent) + .setRoundedCornerOverlay(true).build(); + final WindowState screenDecor = + createWindow(null, decorToken.windowType, decorToken, "screenDecor"); + final WindowState[] windows = { statusBar, navBar, ime, screenDecor }; makeWindowVisible(windows); mDisplayContent.getDisplayPolicy().addWindowLw(statusBar, statusBar.mAttrs); mDisplayContent.getDisplayPolicy().addWindowLw(navBar, navBar.mAttrs); @@ -523,26 +529,27 @@ public class TransitionTests extends WindowTestsBase { player.startTransition(); assertFalse(statusBar.mToken.inTransition()); + assertFalse(decorToken.inTransition()); assertTrue(ime.mToken.inTransition()); assertTrue(task.inTransition()); + assertTrue(asyncRotationController.isTargetToken(decorToken)); + screenDecor.setOrientationChanging(false); // Status bar finishes drawing before the start transaction. Its fade-in animation will be // executed until the transaction is committed, so it is still in target tokens. statusBar.setOrientationChanging(false); assertTrue(asyncRotationController.isTargetToken(statusBar.mToken)); final SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); - final ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(SurfaceControl.TransactionCommittedListener.class); - player.onTransactionReady(startTransaction); + final SurfaceControl.TransactionCommittedListener transactionCommittedListener = + onRotationTransactionReady(player, startTransaction); - verify(startTransaction).addTransactionCommittedListener(any(), listenerCaptor.capture()); // The transaction is committed, so fade-in animation for status bar is consumed. - listenerCaptor.getValue().onTransactionCommitted(); + transactionCommittedListener.onTransactionCommitted(); assertFalse(asyncRotationController.isTargetToken(statusBar.mToken)); - // Status bar finishes drawing after the start transaction, so its fade-in animation can - // execute directly. + // Navigation bar finishes drawing after the start transaction, so its fade-in animation + // can execute directly. navBar.setOrientationChanging(false); assertFalse(asyncRotationController.isTargetToken(navBar.mToken)); assertNull(mDisplayContent.getAsyncRotationController()); @@ -577,7 +584,8 @@ public class TransitionTests extends WindowTestsBase { final SurfaceControl.Transaction startTransaction = mock(SurfaceControl.Transaction.class); final SurfaceControl leash = statusBar.mToken.getAnimationLeash(); doReturn(true).when(leash).isValid(); - player.onTransactionReady(startTransaction); + final SurfaceControl.TransactionCommittedListener transactionCommittedListener = + onRotationTransactionReady(player, startTransaction); // The leash should be unrotated. verify(startTransaction).setMatrix(eq(leash), any(), any()); @@ -588,6 +596,8 @@ public class TransitionTests extends WindowTestsBase { mock(SurfaceControl.Transaction.class); final boolean layoutNeeded = statusBar.finishDrawing(postDrawTransaction); assertFalse(layoutNeeded); + + transactionCommittedListener.onTransactionCommitted(); player.finish(); // The controller should capture the draw transaction and merge it when preparing to run // fade-in animation. @@ -750,4 +760,13 @@ public class TransitionTests extends WindowTestsBase { changes.put(curr, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); } } + + private static SurfaceControl.TransactionCommittedListener onRotationTransactionReady( + TestTransitionPlayer player, SurfaceControl.Transaction startTransaction) { + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(SurfaceControl.TransactionCommittedListener.class); + player.onTransactionReady(startTransaction); + verify(startTransaction).addTransactionCommittedListener(any(), listenerCaptor.capture()); + return listenerCaptor.getValue(); + } }