Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d8152bdd authored by Riddle Hsu's avatar Riddle Hsu
Browse files

Support independent seamless rotation with shell transition

With shell transition, the seamless windows will be unrotated with
the start transaction of transition. And restore the transformation
when the windows are drawn in new rotation after the start transaction.

Though the final decision of animation style is up to shell side,
it is fine to use seamless style for all target windows. Because if
seamless rotation is rejected, it usually falls back to use a
screenshot based animation. So if the target windows are not drawn
too slow, it has no difference to the normal rotation animation.
Just at least the condition in AsyncRotationController is more
aggressive to adopt seamless style.

The example of normal rotation:
Assume that StatusBar=S, NavigationBar=N,
ScreenDecorOverlay=O1, ScreenDecorOverlayBottom=O2
1. Transition is requested.
2. Select fade style for S and N.
   Select seamless style for O1 and O2.
3. Setup start transaction.
   Prepare alpha 0 for S and N.
   Prepare unrotate for O1 and O2.
4. O1 is redrawn, capture its draw transaction.
5. Shell receives onTransitionReady and applies start transaction.
6. AsyncRotationController receives the transaction committed callback.
   Restore the unrotate with the captured draw transaction.
7. O2 is redrawn, restore the unrotate directly.
8. S and N are redrawn, apply fade-in animation.
9. The rotation animation finishes.

For seamless rotation, just S and N also use seamless style.

Bug: 214324186
Test: atest TransitionTests#testDisplayRotationChange
            TransitionTests#testAppTransitionWithRotationChange
Test: adb shell setprop persist.debug.shell_transit 1; reboot
      - Rotate display with a simple activity.
      - Rotate display with a seamless activity, e.g. camera.
      - Rotate display with a seamless activity and app overlays.
      - Start a landscape activity from a portrait activity.

Change-Id: I1b35d280c1ef6d6c5fadc990ed5528d496d77d57
parent c271def0
Loading
Loading
Loading
Loading
+302 −164

File changed.

Preview size limit exceeded, changes collapsed.

+4 −8
Original line number Diff line number Diff line
@@ -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<WindowState> dispatchInsetsChanged) {
+15 −12
Original line number Diff line number Diff line
@@ -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;
+28 −9
Original line number Diff line number Diff line
@@ -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<SurfaceControl.TransactionCommittedListener> 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<SurfaceControl.TransactionCommittedListener> listenerCaptor =
                ArgumentCaptor.forClass(SurfaceControl.TransactionCommittedListener.class);
        player.onTransactionReady(startTransaction);
        verify(startTransaction).addTransactionCommittedListener(any(), listenerCaptor.capture());
        return listenerCaptor.getValue();
    }
}