Hook up fixed-rotation logic to shell transitions

This works by separating an app launching to 2 phases
(similar to legacy fixed rotation). First it launches
the app into a different rotation. Once the animation
finishes it creates a new seamless display-rotation
transition.

Because we have displayRotation in windowconfiguration,
we can directly rotate a window's surface instead of
checking explicitly for fixedRotationState. Since the
second-phase is a transition, we don't (ab)use
seamlessRotator. Once the display lines-up, the
surfaces and configurations automatically update.

Since we still want to explore shell-transitions style
rotation (single-transition), add a system property
to turn it off and on.

Bug: 217560545
Test: existing tests and manual tests
Change-Id: I0b34b32ff5b2650a519d3c195df452de446cf515
diff --git a/core/java/android/util/RotationUtils.java b/core/java/android/util/RotationUtils.java
index 0ac2c9c..cebdbf6 100644
--- a/core/java/android/util/RotationUtils.java
+++ b/core/java/android/util/RotationUtils.java
@@ -24,8 +24,10 @@
 import android.annotation.Dimension;
 import android.graphics.Insets;
 import android.graphics.Matrix;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.view.Surface.Rotation;
+import android.view.SurfaceControl;
 
 /**
  * A class containing utility methods related to rotation.
@@ -121,13 +123,64 @@
 
     /** @return the rotation needed to rotate from oldRotation to newRotation. */
     @Rotation
-    public static int deltaRotation(int oldRotation, int newRotation) {
+    public static int deltaRotation(@Rotation int oldRotation, @Rotation int newRotation) {
         int delta = newRotation - oldRotation;
         if (delta < 0) delta += 4;
         return delta;
     }
 
     /**
+     * Rotates a surface CCW around the origin (eg. a 90-degree rotation will result in the
+     * bottom-left being at the origin). Use {@link #rotatePoint} to transform the top-left
+     * corner appropriately.
+     */
+    public static void rotateSurface(SurfaceControl.Transaction t, SurfaceControl sc,
+            @Rotation int rotation) {
+        // Note: the matrix values look inverted, but they aren't because our coordinate-space
+        // is actually left-handed.
+        // Note: setMatrix expects values in column-major order.
+        switch (rotation) {
+            case ROTATION_0:
+                t.setMatrix(sc, 1.f, 0.f, 0.f, 1.f);
+                break;
+            case ROTATION_90:
+                t.setMatrix(sc, 0.f, -1.f, 1.f, 0.f);
+                break;
+            case ROTATION_180:
+                t.setMatrix(sc, -1.f, 0.f, 0.f, -1.f);
+                break;
+            case ROTATION_270:
+                t.setMatrix(sc, 0.f, 1.f, -1.f, 0.f);
+                break;
+        }
+    }
+
+    /**
+     * Rotates a point CCW within a rectangle of size parentW x parentH with top/left at the
+     * origin as if the point is stuck to the rectangle. The rectangle is transformed such that
+     * it's top/left remains at the origin after the rotation.
+     */
+    public static void rotatePoint(Point inOutPoint, @Rotation int rotation,
+            int parentW, int parentH) {
+        int origX = inOutPoint.x;
+        switch (rotation) {
+            case ROTATION_0:
+                return;
+            case ROTATION_90:
+                inOutPoint.x = inOutPoint.y;
+                inOutPoint.y = parentW - origX;
+                return;
+            case ROTATION_180:
+                inOutPoint.x = parentW - inOutPoint.x;
+                inOutPoint.y = parentH - inOutPoint.y;
+                return;
+            case ROTATION_270:
+                inOutPoint.x = parentH - inOutPoint.y;
+                inOutPoint.y = origX;
+        }
+    }
+
+    /**
      * Sets a matrix such that given a rotation, it transforms physical display
      * coordinates to that rotation's logical coordinates.
      *
diff --git a/core/tests/coretests/src/android/util/RotationUtilsTest.java b/core/tests/coretests/src/android/util/RotationUtilsTest.java
index 5dbe03e..826eb30 100644
--- a/core/tests/coretests/src/android/util/RotationUtilsTest.java
+++ b/core/tests/coretests/src/android/util/RotationUtilsTest.java
@@ -17,12 +17,14 @@
 package android.util;
 
 import static android.util.RotationUtils.rotateBounds;
+import static android.util.RotationUtils.rotatePoint;
 import static android.view.Surface.ROTATION_180;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 
 import static org.junit.Assert.assertEquals;
 
+import android.graphics.Point;
 import android.graphics.Rect;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -58,4 +60,23 @@
         rotateBounds(testResult, testParent, ROTATION_270);
         assertEquals(new Rect(520, 40, 580, 120), testResult);
     }
+
+    @Test
+    public void testRotatePoint() {
+        int parentW = 1000;
+        int parentH = 600;
+        Point testPt = new Point(60, 40);
+
+        Point testResult = new Point(testPt);
+        rotatePoint(testResult, ROTATION_90, parentW, parentH);
+        assertEquals(new Point(40, 940), testResult);
+
+        testResult.set(testPt.x, testPt.y);
+        rotatePoint(testResult, ROTATION_180, parentW, parentH);
+        assertEquals(new Point(940, 560), testResult);
+
+        testResult.set(testPt.x, testPt.y);
+        rotatePoint(testResult, ROTATION_270, parentW, parentH);
+        assertEquals(new Point(560, 60), testResult);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index ddf01a8..34d98ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -201,6 +201,7 @@
                 "Display is changing, check if it should be seamless.");
         boolean checkedDisplayLayout = false;
         boolean hasTask = false;
+        boolean displayExplicitSeamless = false;
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
 
@@ -209,7 +210,6 @@
 
             // This container isn't rotating, so we can ignore it.
             if (change.getEndRotation() == change.getStartRotation()) continue;
-
             if ((change.getFlags() & FLAG_IS_DISPLAY) != 0) {
                 // In the presence of System Alert windows we can not seamlessly rotate.
                 if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) {
@@ -217,6 +217,8 @@
                             "  display has system alert windows, so not seamless.");
                     return false;
                 }
+                displayExplicitSeamless =
+                        change.getRotationAnimation() == ROTATION_ANIMATION_SEAMLESS;
             } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
                 if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
@@ -268,8 +270,8 @@
             }
         }
 
-        // ROTATION_ANIMATION_SEAMLESS can only be requested by task.
-        if (hasTask) {
+        // ROTATION_ANIMATION_SEAMLESS can only be requested by task or display.
+        if (hasTask || displayExplicitSeamless) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Rotation IS seamless.");
             return true;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
index 7f8eaf1..7e95814 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
@@ -16,10 +16,14 @@
 
 package com.android.wm.shell.util;
 
+import android.graphics.Point;
+import android.util.RotationUtils;
 import android.view.SurfaceControl;
 
 /**
- * Utility class that takes care of counter-rotating surfaces during a transition animation.
+ * Utility class that takes care of rotating unchanging child-surfaces to match the parent rotation
+ * during a transition animation. This gives the illusion that the child surfaces haven't rotated
+ * relative to the screen.
  */
 public class CounterRotator {
     private SurfaceControl mSurface = null;
@@ -33,29 +37,30 @@
      * Sets up this rotator.
      *
      * @param rotateDelta is the forward rotation change (the rotation the display is making).
-     * @param displayW (and H) Is the size of the rotating display.
+     * @param parentW (and H) Is the size of the rotating parent after the rotation.
      */
     public void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta,
-            float displayW, float displayH) {
+            float parentW, float parentH) {
         if (rotateDelta == 0) return;
-        // We want to counter-rotate, so subtract from 4
-        rotateDelta = 4 - (rotateDelta + 4) % 4;
         mSurface = new SurfaceControl.Builder()
                 .setName("Transition Unrotate")
                 .setContainerLayer()
                 .setParent(parent)
                 .build();
-        // column-major
-        if (rotateDelta == 1) {
-            t.setMatrix(mSurface, 0, 1, -1, 0);
-            t.setPosition(mSurface, displayW, 0);
-        } else if (rotateDelta == 2) {
-            t.setMatrix(mSurface, -1, 0, 0, -1);
-            t.setPosition(mSurface, displayW, displayH);
-        } else if (rotateDelta == 3) {
-            t.setMatrix(mSurface, 0, -1, 1, 0);
-            t.setPosition(mSurface, 0, displayH);
+        // Rotate forward to match the new rotation (rotateDelta is the forward rotation the parent
+        // already took). Child surfaces will be in the old rotation relative to the new parent
+        // rotation, so we need to forward-rotate the child surfaces to match.
+        RotationUtils.rotateSurface(t, mSurface, rotateDelta);
+        final Point tmpPt = new Point(0, 0);
+        // parentW/H are the size in the END rotation, the rotation utilities expect the starting
+        // size. So swap them if necessary
+        if ((rotateDelta % 2) != 0) {
+            final float w = parentW;
+            parentW = parentH;
+            parentH = w;
         }
+        RotationUtils.rotatePoint(tmpPt, rotateDelta, (int) parentW, (int) parentH);
+        t.setPosition(mSurface, tmpPt.x, tmpPt.y);
         t.show(mSurface);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 0f4a06f..dbf93b4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -591,6 +591,13 @@
                         .setRotate().build())
                 .build();
         assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays));
+
+        // Seamless if display is explicitly seamless.
+        final TransitionInfo seamlessDisplay = new TransitionInfoBuilder(TRANSIT_CHANGE)
+                .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
+                        .setRotate(ROTATION_ANIMATION_SEAMLESS).build())
+                .build();
+        assertTrue(DefaultTransitionHandler.isRotationSeamless(seamlessDisplay, displays));
     }
 
     class TransitionInfoBuilder {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
index 7ef0901..2e9a16f 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt
@@ -24,6 +24,7 @@
 import android.os.RemoteException
 import android.util.ArrayMap
 import android.util.Log
+import android.util.RotationUtils
 import android.view.IRemoteAnimationFinishedCallback
 import android.view.IRemoteAnimationRunner
 import android.view.RemoteAnimationAdapter
@@ -345,39 +346,33 @@
          * Sets up this rotator.
          *
          * @param rotateDelta is the forward rotation change (the rotation the display is making).
-         * @param displayW (and H) Is the size of the rotating display.
+         * @param parentW (and H) Is the size of the rotating parent.
          */
         fun setup(
             t: SurfaceControl.Transaction,
             parent: SurfaceControl,
             rotateDelta: Int,
-            displayW: Float,
-            displayH: Float
+            parentW: Float,
+            parentH: Float
         ) {
-            var rotateDelta = rotateDelta
             if (rotateDelta == 0) return
-            // We want to counter-rotate, so subtract from 4
-            rotateDelta = 4 - (rotateDelta + 4) % 4
-            surface = SurfaceControl.Builder()
+            val surface = SurfaceControl.Builder()
                     .setName("Transition Unrotate")
                     .setContainerLayer()
                     .setParent(parent)
                     .build()
-            // column-major
-            when (rotateDelta) {
-                1 -> {
-                    t.setMatrix(surface, 0f, 1f, -1f, 0f)
-                    t.setPosition(surface!!, displayW, 0f)
-                }
-                2 -> {
-                    t.setMatrix(surface, -1f, 0f, 0f, -1f)
-                    t.setPosition(surface!!, displayW, displayH)
-                }
-                3 -> {
-                    t.setMatrix(surface, 0f, -1f, 1f, 0f)
-                    t.setPosition(surface!!, 0f, displayH)
-                }
-            }
+            // Rotate forward to match the new rotation (rotateDelta is the forward rotation the
+            // parent already took). Child surfaces will be in the old rotation relative to the new
+            // parent rotation, so we need to forward-rotate the child surfaces to match.
+            RotationUtils.rotateSurface(t, surface, rotateDelta)
+            val tmpPt = Point(0, 0)
+            // parentW/H are the size in the END rotation, the rotation utilities expect the
+            // starting size. So swap them if necessary
+            val flipped = rotateDelta % 2 != 0
+            val pw = if (flipped) parentH else parentW
+            val ph = if (flipped) parentW else parentH
+            RotationUtils.rotatePoint(tmpPt, rotateDelta, pw.toInt(), ph.toInt())
+            t.setPosition(surface, tmpPt.x.toFloat(), tmpPt.y.toFloat())
             t.show(surface)
         }
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 092cff3..95f46b15 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1054,6 +1054,7 @@
         mAppTransition.registerListenerLocked(mWmService.mActivityManagerAppTransitionNotifier);
         mAppTransition.registerListenerLocked(mFixedRotationTransitionListener);
         mAppTransitionController = new AppTransitionController(mWmService, this);
+        mTransitionController.registerLegacyListener(mFixedRotationTransitionListener);
         mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this);
 
         final InputChannel inputChannel = mWmService.mInputManager.monitorInput(
@@ -1111,6 +1112,7 @@
         t.remove(mSurfaceControl);
 
         mLastSurfacePosition.set(0, 0);
+        mLastDeltaRotation = Surface.ROTATION_0;
 
         configureSurfaces(t);
 
@@ -1604,7 +1606,7 @@
      */
     @Rotation
     int rotationForActivityInDifferentOrientation(@NonNull ActivityRecord r) {
-        if (mTransitionController.isShellTransitionsEnabled()) {
+        if (mTransitionController.useShellTransitionsRotation()) {
             return ROTATION_UNDEFINED;
         }
         if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM) {
@@ -1645,18 +1647,30 @@
             // It has been set and not yet finished.
             return true;
         }
-        if (!r.occludesParent() || r.isVisible()) {
+        if (!r.occludesParent()) {
             // While entering or leaving a translucent or floating activity (e.g. dialog style),
             // there is a visible activity in the background. Then it still needs rotation animation
             // to cover the activity configuration change.
             return false;
         }
+        if (mTransitionController.isShellTransitionsEnabled()
+                ? mTransitionController.wasVisibleAtStart(r) : r.isVisible()) {
+            // If activity is already visible, then it's not "launching". However, shell-transitions
+            // will make it visible immediately.
+            return false;
+        }
         if (checkOpening) {
-            if (!mAppTransition.isTransitionSet() || !mOpeningApps.contains(r)) {
-                // Apply normal rotation animation in case of the activity set different requested
-                // orientation without activity switch, or the transition is unset due to starting
-                // window was transferred ({@link #mSkipAppTransitionAnimation}).
-                return false;
+            if (mTransitionController.isShellTransitionsEnabled()) {
+                if (!mTransitionController.isCollecting(r)) {
+                    return false;
+                }
+            } else {
+                if (!mAppTransition.isTransitionSet() || !mOpeningApps.contains(r)) {
+                    // Apply normal rotation animation in case of the activity set different
+                    // requested orientation without activity switch, or the transition is unset due
+                    // to starting window was transferred ({@link #mSkipAppTransitionAnimation}).
+                    return false;
+                }
             }
             if (r.isState(RESUMED) && !r.getRootTask().mInResumeTopActivity) {
                 // If the activity is executing or has done the lifecycle callback, use normal
@@ -1733,15 +1747,19 @@
     }
 
     void setFixedRotationLaunchingAppUnchecked(@Nullable ActivityRecord r, int rotation) {
+        final boolean useAsyncRotation = !mTransitionController.isShellTransitionsEnabled();
         if (mFixedRotationLaunchingApp == null && r != null) {
-            mWmService.mDisplayNotificationController.dispatchFixedRotationStarted(this, rotation);
-            startAsyncRotation(
-                    // Delay the hide animation to avoid blinking by clicking navigation bar that
-                    // may toggle fixed rotation in a short time.
-                    r == mFixedRotationTransitionListener.mAnimatingRecents /* shouldDebounce */);
+            mWmService.mDisplayNotificationController.dispatchFixedRotationStarted(this,
+                    rotation);
+            if (useAsyncRotation) {
+                startAsyncRotation(
+                        // Delay the hide animation to avoid blinking by clicking navigation bar
+                        // that may toggle fixed rotation in a short time.
+                        r == mFixedRotationTransitionListener.mAnimatingRecents);
+            }
         } else if (mFixedRotationLaunchingApp != null && r == null) {
             mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this);
-            finishAsyncRotationIfPossible();
+            if (useAsyncRotation) finishAsyncRotationIfPossible();
         }
         mFixedRotationLaunchingApp = r;
     }
@@ -1760,7 +1778,8 @@
         if (prevRotatedLaunchingApp != null
                 && prevRotatedLaunchingApp.getWindowConfiguration().getRotation() == rotation
                 // It is animating so we can expect there will have a transition callback.
-                && prevRotatedLaunchingApp.isAnimating(TRANSITION | PARENTS)) {
+                && (prevRotatedLaunchingApp.isAnimating(TRANSITION | PARENTS)
+                        || mTransitionController.inTransition(prevRotatedLaunchingApp))) {
             // It may be the case that multiple activities launch consecutively. Because their
             // rotation are the same, the transformed state can be shared to avoid duplicating
             // the heavy operations. This also benefits that the states of multiple activities
@@ -1798,6 +1817,7 @@
         }
         // Update directly because the app which will change the orientation of display is ready.
         if (mDisplayRotation.updateOrientation(getOrientation(), false /* forceUpdate */)) {
+            mTransitionController.setSeamlessRotation(this);
             sendNewConfiguration();
             return;
         }
@@ -3129,6 +3149,7 @@
             mChangingContainers.clear();
             mUnknownAppVisibilityController.clear();
             mAppTransition.removeAppTransitionTimeoutCallbacks();
+            mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
             handleAnimatingStoppedAndTransition();
             mWmService.stopFreezingDisplayLocked();
             super.removeImmediately();
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index d86382d..26871c3 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -209,6 +209,12 @@
         return mTransientLaunches != null && mTransientLaunches.contains(activity);
     }
 
+    void setSeamlessRotation(@NonNull WindowContainer wc) {
+        final ChangeInfo info = mChanges.get(wc);
+        if (info == null) return;
+        info.mFlags = info.mFlags | ChangeInfo.FLAG_SEAMLESS_ROTATION;
+    }
+
     @VisibleForTesting
     int getSyncId() {
         return mSyncId;
@@ -1122,6 +1128,15 @@
             // hardware-screen-level surfaces.
             return asDC.getWindowingLayer();
         }
+        if (!wc.mTransitionController.useShellTransitionsRotation()) {
+            final WindowToken asToken = wc.asWindowToken();
+            if (asToken != null) {
+                // WindowTokens can have a fixed-rotation applied to them. In the current
+                // implementation this fact is hidden from the player, so we must create a leash.
+                final SurfaceControl leash = asToken.getOrCreateFixedRotationLeash();
+                if (leash != null) return leash;
+            }
+        }
         return wc.getSurfaceControl();
     }
 
@@ -1224,6 +1239,8 @@
                 final ActivityRecord topMostActivity = task.getTopMostActivity();
                 change.setAllowEnterPip(topMostActivity != null
                         && topMostActivity.checkEnterPictureInPictureAppOpsState());
+            } else if ((info.mFlags & ChangeInfo.FLAG_SEAMLESS_ROTATION) != 0) {
+                change.setRotationAnimation(ROTATION_ANIMATION_SEAMLESS);
             }
             final ActivityRecord activityRecord = target.asActivityRecord();
             if (activityRecord != null) {
@@ -1337,6 +1354,21 @@
 
     @VisibleForTesting
     static class ChangeInfo {
+        private static final int FLAG_NONE = 0;
+
+        /**
+         * When set, the associated WindowContainer has been explicitly requested to be a
+         * seamless rotation. This is currently only used by DisplayContent during fixed-rotation.
+         */
+        private static final int FLAG_SEAMLESS_ROTATION = 1;
+
+        @IntDef(prefix = { "FLAG_" }, value = {
+                FLAG_NONE,
+                FLAG_SEAMLESS_ROTATION
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        @interface Flag {}
+
         // Usually "post" change state.
         WindowContainer mParent;
 
@@ -1350,6 +1382,9 @@
         int mRotation = ROTATION_UNDEFINED;
         @ActivityInfo.Config int mKnownConfigChanges;
 
+        /** These are just extra info. They aren't used for change-detection. */
+        @Flag int mFlags = FLAG_NONE;
+
         ChangeInfo(@NonNull WindowContainer origState) {
             mVisible = origState.isVisibleRequested();
             mWindowingMode = origState.getWindowingMode();
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 60307ce..3d9d824 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -34,6 +34,7 @@
 import android.os.IRemoteCallback;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
@@ -58,6 +59,10 @@
 class TransitionController {
     private static final String TAG = "TransitionController";
 
+    /** Whether to use shell-transitions rotation instead of fixed-rotation. */
+    private static final boolean SHELL_TRANSITIONS_ROTATION =
+            SystemProperties.getBoolean("persist.debug.shell_transit_rotate", false);
+
     /** The same as legacy APP_TRANSITION_TIMEOUT_MS. */
     private static final int DEFAULT_TIMEOUT_MS = 5000;
     /** Less duration for CHANGE type because it does not involve app startup. */
@@ -203,6 +208,11 @@
         return mTransitionPlayer != null;
     }
 
+    /** @return {@code true} if using shell-transitions rotation instead of fixed-rotation. */
+    boolean useShellTransitionsRotation() {
+        return isShellTransitionsEnabled() && SHELL_TRANSITIONS_ROTATION;
+    }
+
     /**
      * @return {@code true} if transition is actively collecting changes. This is {@code false}
      * once a transition is playing
@@ -260,6 +270,21 @@
         return false;
     }
 
+    /**
+     * Temporary work-around to deal with integration of legacy fixed-rotation. Returns whether
+     * the activity was visible before the collecting transition.
+     * TODO: at-least replace the polling mechanism.
+     */
+    boolean wasVisibleAtStart(@NonNull ActivityRecord ar) {
+        if (mCollectingTransition == null) return ar.isVisible();
+        final Transition.ChangeInfo ci = mCollectingTransition.mChanges.get(ar);
+        if (ci == null) {
+            // not part of transition, so use current state.
+            return ar.isVisible();
+        }
+        return ci.mVisible;
+    }
+
     @WindowManager.TransitionType
     int getCollectingTransitionType() {
         return mCollectingTransition != null ? mCollectingTransition.mType : TRANSIT_NONE;
@@ -484,6 +509,11 @@
         }
     }
 
+    void setSeamlessRotation(@NonNull WindowContainer wc) {
+        if (mCollectingTransition == null) return;
+        mCollectingTransition.setSeamlessRotation(wc);
+    }
+
     void legacyDetachNavigationBarFromApp(@NonNull IBinder token) {
         final Transition transition = Transition.fromBinder(token);
         if (transition == null || !mPlayingTransitions.contains(transition)) {
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 36bb55e..6ee30bb 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -111,6 +111,14 @@
             changed = true;
         }
         if (mTransitionController.isShellTransitionsEnabled()) {
+            // Apply legacy fixed rotation to wallpaper if it is becoming visible
+            if (!mTransitionController.useShellTransitionsRotation() && changed && visible) {
+                final WindowState wallpaperTarget =
+                        mDisplayContent.mWallpaperController.getWallpaperTarget();
+                if (wallpaperTarget != null && wallpaperTarget.mToken.hasFixedRotationTransform()) {
+                    linkFixedRotationTransform(wallpaperTarget.mToken);
+                }
+            }
             return changed;
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 8a373bf..1bd305e 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -78,13 +78,14 @@
 import android.util.ArraySet;
 import android.util.Pair;
 import android.util.Pools;
+import android.util.RotationUtils;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
-import android.view.InsetsState;
 import android.view.MagnificationSpec;
 import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationTarget;
+import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Builder;
 import android.view.SurfaceControlViewHost;
@@ -197,6 +198,7 @@
 
     private final Point mTmpPos = new Point();
     protected final Point mLastSurfacePosition = new Point();
+    protected @Surface.Rotation int mLastDeltaRotation = Surface.ROTATION_0;
 
     /** Total number of elements in this subtree, including our own hierarchy element. */
     private int mTreeWeight = 1;
@@ -473,6 +475,7 @@
         t.remove(mSurfaceControl);
         // Clear the last position so the new SurfaceControl will get correct position
         mLastSurfacePosition.set(0, 0);
+        mLastDeltaRotation = Surface.ROTATION_0;
 
         final SurfaceControl.Builder b = mWmService.makeSurfaceBuilder(null)
                 .setContainerLayer()
@@ -644,6 +647,7 @@
             getSyncTransaction().remove(mSurfaceControl);
             setSurfaceControl(null);
             mLastSurfacePosition.set(0, 0);
+            mLastDeltaRotation = Surface.ROTATION_0;
             scheduleAnimation();
         }
         if (mOverlayHost != null) {
@@ -3127,12 +3131,43 @@
         }
 
         getRelativePosition(mTmpPos);
-        if (mTmpPos.equals(mLastSurfacePosition)) {
+        final int deltaRotation = getRelativeDisplayRotation();
+        if (mTmpPos.equals(mLastSurfacePosition) && deltaRotation == mLastDeltaRotation) {
             return;
         }
 
         t.setPosition(mSurfaceControl, mTmpPos.x, mTmpPos.y);
+        // set first, since we don't want rotation included in this (for now).
         mLastSurfacePosition.set(mTmpPos.x, mTmpPos.y);
+
+        if (mTransitionController.isShellTransitionsEnabled()
+                && !mTransitionController.useShellTransitionsRotation()) {
+            if (deltaRotation != Surface.ROTATION_0) {
+                updateSurfaceRotation(t, deltaRotation, null /* positionLeash */);
+            } else if (deltaRotation != mLastDeltaRotation) {
+                t.setMatrix(mSurfaceControl, 1, 0, 0, 1);
+            }
+        }
+        mLastDeltaRotation = deltaRotation;
+    }
+
+    /**
+     * Updates the surface transform based on a difference in displayed-rotation from its parent.
+     * @param positionLeash If non-null, the rotated position will be set on this surface instead
+     *                      of the window surface. {@see WindowToken#getOrCreateFixedRotationLeash}.
+     */
+    protected void updateSurfaceRotation(Transaction t, @Surface.Rotation int deltaRotation,
+            @Nullable SurfaceControl positionLeash) {
+        // parent must be non-null otherwise deltaRotation would be 0.
+        RotationUtils.rotateSurface(t, mSurfaceControl, deltaRotation);
+        mTmpPos.set(mLastSurfacePosition.x, mLastSurfacePosition.y);
+        final Rect parentBounds = getParent().getBounds();
+        final boolean flipped = (deltaRotation % 2) != 0;
+        RotationUtils.rotatePoint(mTmpPos, deltaRotation,
+                flipped ? parentBounds.height() : parentBounds.width(),
+                flipped ? parentBounds.width() : parentBounds.height());
+        t.setPosition(positionLeash != null ? positionLeash : mSurfaceControl,
+                mTmpPos.x, mTmpPos.y);
     }
 
     @VisibleForTesting
@@ -3170,6 +3205,16 @@
         }
     }
 
+    /** @return the difference in displayed-rotation from parent. */
+    @Surface.Rotation
+    int getRelativeDisplayRotation() {
+        final WindowContainer parent = getParent();
+        if (parent == null) return Surface.ROTATION_0;
+        final int rotation = getWindowConfiguration().getDisplayRotation();
+        final int parentRotation = parent.getWindowConfiguration().getDisplayRotation();
+        return RotationUtils.deltaRotation(rotation, parentRotation);
+    }
+
     void waitForAllWindowsDrawn() {
         forAllWindows(w -> {
             w.requestDrawIfNeeded(mWaitingForDrawn);
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index db231f6..f398034 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -44,6 +44,7 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
 import android.view.InsetsState;
+import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager.LayoutParams.WindowType;
 import android.window.WindowContext;
@@ -99,6 +100,7 @@
     final boolean mOwnerCanManageAppTokens;
 
     private FixedRotationTransformState mFixedRotationTransformState;
+    private SurfaceControl mFixedRotationTransformLeash;
 
     /**
      * When set to {@code true}, this window token is created from {@link WindowContext}
@@ -521,8 +523,14 @@
         if (state == null) {
             return;
         }
-
-        state.resetTransform();
+        if (!mTransitionController.isShellTransitionsEnabled()) {
+            state.resetTransform();
+        } else {
+            // Remove all the leashes
+            for (int i = state.mAssociatedTokens.size() - 1; i >= 0; --i) {
+                state.mAssociatedTokens.get(i).removeFixedRotationLeash();
+            }
+        }
         // Clear the flag so if the display will be updated to the same orientation, the transform
         // won't take effect.
         state.mIsTransforming = false;
@@ -554,6 +562,43 @@
     }
 
     /**
+     * Gets or creates a leash which can be treated as if this window is not-rotated. This is
+     * used to adapt mismatched-rotation surfaces into code that expects all windows to share
+     * the same rotation.
+     */
+    @Nullable
+    SurfaceControl getOrCreateFixedRotationLeash() {
+        if (!mTransitionController.isShellTransitionsEnabled()) return null;
+        final int rotation = getRelativeDisplayRotation();
+        if (rotation == Surface.ROTATION_0) return mFixedRotationTransformLeash;
+        if (mFixedRotationTransformLeash != null) return mFixedRotationTransformLeash;
+
+        final SurfaceControl.Transaction t = getSyncTransaction();
+        final SurfaceControl leash = makeSurface().setContainerLayer()
+                .setParent(getParentSurfaceControl())
+                .setName(getSurfaceControl() + " - rotation-leash")
+                .setHidden(false)
+                .setEffectLayer()
+                .setCallsite("WindowToken.getOrCreateFixedRotationLeash")
+                .build();
+        t.setPosition(leash, mLastSurfacePosition.x, mLastSurfacePosition.y);
+        t.show(leash);
+        t.reparent(getSurfaceControl(), leash);
+        t.setAlpha(getSurfaceControl(), 1.f);
+        mFixedRotationTransformLeash = leash;
+        updateSurfaceRotation(t, rotation, mFixedRotationTransformLeash);
+        return mFixedRotationTransformLeash;
+    }
+
+    void removeFixedRotationLeash() {
+        if (mFixedRotationTransformLeash == null) return;
+        final SurfaceControl.Transaction t = getSyncTransaction();
+        t.reparent(getSurfaceControl(), getParentSurfaceControl());
+        t.remove(mFixedRotationTransformLeash);
+        mFixedRotationTransformLeash = null;
+    }
+
+    /**
      * It is called when the window is using fixed rotation transform, and before display applies
      * the same rotation, the rotation change for display is canceled, e.g. the orientation from
      * sensor is updated to previous direction.
@@ -575,7 +620,7 @@
     @Override
     void updateSurfacePosition(SurfaceControl.Transaction t) {
         super.updateSurfacePosition(t);
-        if (isFixedRotationTransforming()) {
+        if (!mTransitionController.isShellTransitionsEnabled() && isFixedRotationTransforming()) {
             final ActivityRecord r = asActivityRecord();
             final Task rootTask = r != null ? r.getRootTask() : null;
             // Don't transform the activity in PiP because the PiP task organizer will handle it.
@@ -588,6 +633,20 @@
     }
 
     @Override
+    protected void updateSurfaceRotation(SurfaceControl.Transaction t,
+            @Surface.Rotation int deltaRotation, SurfaceControl positionLeash) {
+        final ActivityRecord r = asActivityRecord();
+        if (r != null) {
+            final Task rootTask = r.getRootTask();
+            // Don't transform the activity in PiP because the PiP task organizer will handle it.
+            if (rootTask != null && rootTask.inPinnedWindowingMode()) {
+                return;
+            }
+        }
+        super.updateSurfaceRotation(t, deltaRotation, positionLeash);
+    }
+
+    @Override
     void resetSurfacePositionForAnimationLeash(SurfaceControl.Transaction t) {
         // Keep the transformed position to animate because the surface will show in different
         // rotation than the animator of leash.