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