Merge "[1/n] Create AppCompatLetterboxPolicyState abstraction" into main
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
index afc6506..4e390df 100644
--- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
@@ -22,6 +22,9 @@
 
 import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
 import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString;
+import static com.android.server.wm.AppCompatLetterboxUtils.calculateLetterboxInnerBounds;
+import static com.android.server.wm.AppCompatLetterboxUtils.calculateLetterboxOuterBounds;
+import static com.android.server.wm.AppCompatLetterboxUtils.calculateLetterboxPosition;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -32,6 +35,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.LetterboxDetails;
 import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 
@@ -43,7 +47,7 @@
     @NonNull
     private final ActivityRecord mActivityRecord;
     @NonNull
-    private final LetterboxPolicyState mLetterboxPolicyState;
+    private final AppCompatLetterboxPolicyState mLetterboxPolicyState;
     @NonNull
     private final AppCompatRoundedCorners mAppCompatRoundedCorners;
     @NonNull
@@ -54,7 +58,8 @@
     AppCompatLetterboxPolicy(@NonNull ActivityRecord  activityRecord,
             @NonNull AppCompatConfiguration appCompatConfiguration) {
         mActivityRecord = activityRecord;
-        mLetterboxPolicyState = new LetterboxPolicyState();
+        mLetterboxPolicyState = Flags.appCompatRefactoring() ? new ShellLetterboxPolicyState()
+                : new LegacyLetterboxPolicyState();
         // TODO (b/358334569) Improve cutout logic dependency on app compat.
         mAppCompatRoundedCorners = new AppCompatRoundedCorners(mActivityRecord,
                 this::isLetterboxedNotForDisplayCutout);
@@ -88,7 +93,24 @@
 
     @Nullable
     LetterboxDetails getLetterboxDetails() {
-        return mLetterboxPolicyState.getLetterboxDetails();
+        final WindowState w = mActivityRecord.findMainWindow();
+        if (!isRunning() || w == null || w.isLetterboxedForDisplayCutout()) {
+            return null;
+        }
+        final Rect letterboxInnerBounds = new Rect();
+        final Rect letterboxOuterBounds = new Rect();
+        mLetterboxPolicyState.getLetterboxInnerBounds(letterboxInnerBounds);
+        mLetterboxPolicyState.getLetterboxOuterBounds(letterboxOuterBounds);
+
+        if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) {
+            return null;
+        }
+
+        return new LetterboxDetails(
+                letterboxInnerBounds,
+                letterboxOuterBounds,
+                w.mAttrs.insetsFlags.appearance
+        );
     }
 
     /**
@@ -99,6 +121,13 @@
         return mLetterboxPolicyState.isFullyTransparentBarAllowed(rect);
     }
 
+    /**
+     * Updates the letterbox surfaces in case this is needed.
+     *
+     * @param winHint   The WindowState for the letterboxed Activity.
+     * @param t         The current Transaction.
+     * @param inputT    The pending transaction used for the input surface.
+     */
     void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint,
             @NonNull SurfaceControl.Transaction t,
             @NonNull SurfaceControl.Transaction inputT) {
@@ -232,12 +261,17 @@
                 || w.mAnimatingExit;
     }
 
-    private class LetterboxPolicyState {
+    /**
+     * Existing {@link AppCompatLetterboxPolicyState} implementation.
+     * TODO(b/375339716): Clean code for legacy implementation.
+     */
+    private class LegacyLetterboxPolicyState implements AppCompatLetterboxPolicyState {
 
         @Nullable
         private Letterbox mLetterbox;
 
-        void layoutLetterboxIfNeeded(@NonNull WindowState w) {
+        @Override
+        public void layoutLetterboxIfNeeded(@NonNull WindowState w) {
             if (!isRunning()) {
                 final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord
                         .mAppCompatController.getAppCompatLetterboxOverrides();
@@ -252,41 +286,11 @@
                         .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame);
             }
             final Point letterboxPosition = new Point();
-            if (mActivityRecord.isInLetterboxAnimation()) {
-                // In this case we attach the letterbox to the task instead of the activity.
-                mActivityRecord.getTask().getPosition(letterboxPosition);
-            } else {
-                mActivityRecord.getPosition(letterboxPosition);
-            }
-
-            // Get the bounds of the "space-to-fill". The transformed bounds have the highest
-            // priority because the activity is launched in a rotated environment. In multi-window
-            // mode, the taskFragment-level represents this for both split-screen
-            // and activity-embedding. In fullscreen-mode, the task container does
-            // (since the orientation letterbox is also applied to the task).
-            final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds();
-            final Rect spaceToFill = transformedBounds != null
-                    ? transformedBounds
-                    : mActivityRecord.inMultiWindowMode()
-                            ? mActivityRecord.getTaskFragment().getBounds()
-                            : mActivityRecord.getRootTask().getParent().getBounds();
-            // In case of translucent activities an option is to use the WindowState#getFrame() of
-            // the first opaque activity beneath. In some cases (e.g. an opaque activity is using
-            // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct
-            // information and in particular it might provide a value for a smaller area making
-            // the letterbox overlap with the translucent activity's frame.
-            // If we use WindowState#getFrame() for the translucent activity's letterbox inner
-            // frame, the letterbox will then be overlapped with the translucent activity's frame.
-            // Because the surface layer of letterbox is lower than an activity window, this
-            // won't crop the content, but it may affect other features that rely on values stored
-            // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher
-            // For this reason we use ActivityRecord#getBounds() that the translucent activity
-            // inherits from the first opaque activity beneath and also takes care of the scaling
-            // in case of activities in size compat mode.
-            final TransparentPolicy transparentPolicy =
-                    mActivityRecord.mAppCompatController.getTransparentPolicy();
-            final Rect innerFrame =
-                    transparentPolicy.isRunning() ? mActivityRecord.getBounds() : w.getFrame();
+            calculateLetterboxPosition(mActivityRecord, letterboxPosition);
+            final Rect spaceToFill = new Rect();
+            calculateLetterboxOuterBounds(mActivityRecord, spaceToFill);
+            final Rect innerFrame = new Rect();
+            calculateLetterboxInnerBounds(mActivityRecord, w, innerFrame);
             mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition);
             if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides()
                     .isDoubleTapEvent()) {
@@ -299,18 +303,21 @@
          * @return  {@code true} if the policy is running and so if the current activity is
          *          letterboxed.
          */
-        boolean isRunning() {
+        @Override
+        public boolean isRunning() {
             return mLetterbox != null;
         }
 
-        void onMovedToDisplay(int displayId) {
+        @Override
+        public void onMovedToDisplay(int displayId) {
             if (isRunning()) {
                 mLetterbox.onMovedToDisplay(displayId);
             }
         }
 
         /** Cleans up {@link Letterbox} if it exists.*/
-        void stop() {
+        @Override
+        public void stop() {
             if (isRunning()) {
                 mLetterbox.destroy();
                 mLetterbox = null;
@@ -319,7 +326,8 @@
                     .setLetterboxInnerBoundsSupplier(null);
         }
 
-        void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint,
+        @Override
+        public void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint,
                 @NonNull SurfaceControl.Transaction t,
                 @NonNull SurfaceControl.Transaction inputT) {
             if (shouldNotLayoutLetterbox(winHint)) {
@@ -331,15 +339,17 @@
             }
         }
 
-        void hide() {
+        @Override
+        public void hide() {
             if (isRunning()) {
                 mLetterbox.hide();
             }
         }
 
         /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */
+        @Override
         @NonNull
-        Rect getLetterboxInsets() {
+        public Rect getLetterboxInsets() {
             if (isRunning()) {
                 return mLetterbox.getInsets();
             } else {
@@ -348,7 +358,8 @@
         }
 
         /** Gets the inner bounds of letterbox. The bounds will be empty with no letterbox. */
-        void getLetterboxInnerBounds(@NonNull Rect outBounds) {
+        @Override
+        public void getLetterboxInnerBounds(@NonNull Rect outBounds) {
             if (isRunning()) {
                 outBounds.set(mLetterbox.getInnerFrame());
                 final WindowState w = mActivityRecord.findMainWindow();
@@ -361,7 +372,8 @@
         }
 
         /** Gets the outer bounds of letterbox. The bounds will be empty with no letterbox. */
-        private void getLetterboxOuterBounds(@NonNull Rect outBounds) {
+        @Override
+        public void getLetterboxOuterBounds(@NonNull Rect outBounds) {
             if (isRunning()) {
                 outBounds.set(mLetterbox.getOuterFrame());
             } else {
@@ -373,33 +385,12 @@
          * @return {@code true} if bar shown within a given rectangle is allowed to be fully
          *          transparent when the current activity is displayed.
          */
-        boolean isFullyTransparentBarAllowed(@NonNull Rect rect) {
+        @Override
+        public boolean isFullyTransparentBarAllowed(@NonNull Rect rect) {
             return !isRunning() || mLetterbox.notIntersectsOrFullyContains(rect);
         }
 
         @Nullable
-        LetterboxDetails getLetterboxDetails() {
-            final WindowState w = mActivityRecord.findMainWindow();
-            if (!isRunning() || w == null || w.isLetterboxedForDisplayCutout()) {
-                return null;
-            }
-            final Rect letterboxInnerBounds = new Rect();
-            final Rect letterboxOuterBounds = new Rect();
-            getLetterboxInnerBounds(letterboxInnerBounds);
-            getLetterboxOuterBounds(letterboxOuterBounds);
-
-            if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) {
-                return null;
-            }
-
-            return new LetterboxDetails(
-                    letterboxInnerBounds,
-                    letterboxOuterBounds,
-                    w.mAttrs.insetsFlags.appearance
-            );
-        }
-
-        @Nullable
         private SurfaceControl getLetterboxParentSurface() {
             if (mActivityRecord.isInLetterboxAnimation()) {
                 return mActivityRecord.getTask().getSurfaceControl();
@@ -408,4 +399,116 @@
         }
 
     }
+
+    /**
+     * {@link AppCompatLetterboxPolicyState} implementation for the letterbox presentation on shell.
+     */
+    private class ShellLetterboxPolicyState implements AppCompatLetterboxPolicyState {
+
+        private final Rect mInnerBounds = new Rect();
+        private final Rect mOuterBounds = new Rect();
+        private final Point mLetterboxPosition = new Point();
+        private boolean mRunning;
+
+        @Override
+        public void layoutLetterboxIfNeeded(@NonNull WindowState w) {
+            mRunning = true;
+            calculateLetterboxPosition(mActivityRecord, mLetterboxPosition);
+            calculateLetterboxOuterBounds(mActivityRecord, mOuterBounds);
+            calculateLetterboxInnerBounds(mActivityRecord, w, mInnerBounds);
+            mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+                    .setLetterboxInnerBoundsSupplier(() -> mInnerBounds);
+        }
+
+        @Override
+        public boolean isRunning() {
+            return mRunning;
+        }
+
+        @Override
+        public void onMovedToDisplay(int displayId) {
+            // TODO(b/374918469): Handle Display Change for Letterbox in Shell
+        }
+
+        @Override
+        public void stop() {
+            if (!isRunning()) {
+                return;
+            }
+            mRunning = false;
+            mLetterboxPosition.set(0, 0);
+            mInnerBounds.setEmpty();
+            mOuterBounds.setEmpty();
+            mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+                    .setLetterboxInnerBoundsSupplier(null);
+        }
+
+        @Override
+        public void hide() {
+            if (!isRunning()) {
+                return;
+            }
+            mLetterboxPosition.set(0, 0);
+            mInnerBounds.setEmpty();
+            mOuterBounds.setEmpty();
+        }
+
+        @NonNull
+        @Override
+        public Rect getLetterboxInsets() {
+            if (isRunning()) {
+                return new Rect(
+                        Math.max(0, mInnerBounds.left - mOuterBounds.left),
+                        Math.max(0, mOuterBounds.top - mInnerBounds.top),
+                        Math.max(0, mOuterBounds.right - mInnerBounds.right),
+                        Math.max(0, mInnerBounds.bottom - mOuterBounds.bottom)
+                );
+            }
+            return new Rect();
+        }
+
+        @Override
+        public void getLetterboxInnerBounds(@NonNull Rect outBounds) {
+            if (isRunning()) {
+                outBounds.set(mInnerBounds);
+                final WindowState w = mActivityRecord.findMainWindow();
+                if (w != null) {
+                    AppCompatUtils.adjustBoundsForTaskbar(w, outBounds);
+                }
+            } else {
+                outBounds.setEmpty();
+            }
+        }
+
+        @Override
+        public void getLetterboxOuterBounds(@NonNull Rect outBounds) {
+            if (isRunning()) {
+                outBounds.set(mOuterBounds);
+            } else {
+                outBounds.setEmpty();
+            }
+        }
+
+        @Override
+        public void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint,
+                @NonNull SurfaceControl.Transaction t,
+                @NonNull SurfaceControl.Transaction inputT) {
+
+            if (shouldNotLayoutLetterbox(winHint)) {
+                return;
+            }
+            start(winHint);
+        }
+
+        @Override
+        public boolean isFullyTransparentBarAllowed(@NonNull Rect rect) {
+            // TODO(b/374921442) Handle Transparent Activities Letterboxing in Shell.
+            // At the moment Shell handles letterbox with a single surface. This would make
+            // notIntersectsOrFullyContains() to return false in the existing Letterbox
+            // implementation.
+            // Note: Previous implementation is
+            //       !isRunning() || mLetterbox.notIntersectsOrFullyContains(rect);
+            return !isRunning();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicyState.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicyState.java
new file mode 100644
index 0000000..31ad536
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicyState.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+/**
+ * Abstraction for different Letterbox state implementations.
+ */
+interface AppCompatLetterboxPolicyState {
+
+    /**
+     * Checks if a relayout is necessary for the letterbox implementations.
+     * @param w The {@link WindowState} to use for defining Letterbox sizes.
+     */
+    void layoutLetterboxIfNeeded(@NonNull WindowState w);
+
+    /**
+     * @return  {@code true} if the policy is running and so if the current activity is
+     *          letterboxed.
+     */
+    boolean isRunning();
+
+    /**
+     * Called when the activity is moved to a new display.
+     * @param displayId Id for the new display
+     */
+    void onMovedToDisplay(int displayId);
+
+    /** Cleans up {@link Letterbox} if it exists.*/
+    void stop();
+
+    /** Hides the letterbox surfaces implementation. */
+    void hide();
+
+    /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */
+    @NonNull
+    Rect getLetterboxInsets();
+
+    /** Gets the inner bounds of letterbox. The bounds will be empty with no letterbox. */
+    void getLetterboxInnerBounds(@NonNull Rect outBounds);
+
+    /** Gets the outer bounds of letterbox. The bounds will be empty with no letterbox. */
+    void getLetterboxOuterBounds(@NonNull Rect outBounds);
+
+    /**
+     * Updates the letterbox surfaces in case this is needed.
+     *
+     * @param winHint   The WindowState for the letterboxed Activity.
+     * @param t         The current Transaction.
+     * @param inputT    The pending transaction used for the input surface.
+     */
+    void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint,
+            @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction inputT);
+
+    /**
+     * @return {@code true} if bar shown within a given rectangle is allowed to be fully
+     *          transparent when the current activity is displayed.
+     */
+    boolean isFullyTransparentBarAllowed(@NonNull Rect rect);
+
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxUtils.java b/services/core/java/com/android/server/wm/AppCompatLetterboxUtils.java
new file mode 100644
index 0000000..79b3a55
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxUtils.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * Some utility methods used by different Letterbox implementations.
+ */
+class AppCompatLetterboxUtils {
+    /**
+     * Provides the position of the top left letterbox area in the display coordinate system.
+     *
+     * @param activity             The Letterboxed activity.
+     * @param outLetterboxPosition InOut parameter that will contain the desired letterbox position.
+     */
+    static void calculateLetterboxPosition(@NonNull ActivityRecord activity,
+            @NonNull Point outLetterboxPosition) {
+        if (!activity.mAppCompatController.getAppCompatLetterboxPolicy().isRunning()) {
+            outLetterboxPosition.set(0, 0);
+            return;
+        }
+        if (activity.isInLetterboxAnimation()) {
+            // In this case we attach the letterbox to the task instead of the activity.
+            activity.getTask().getPosition(outLetterboxPosition);
+        } else {
+            activity.getPosition(outLetterboxPosition);
+        }
+    }
+
+    /**
+     * Provides all the available space, in display coordinate, to fill with the letterboxed
+     * activity and the letterbox areas.
+     *
+     * @param activity       The Letterboxed activity.
+     * @param outOuterBounds InOut parameter that will contain the outer bounds for the letterboxed
+     *                       activity.
+     */
+    static void calculateLetterboxOuterBounds(@NonNull ActivityRecord activity,
+            @NonNull Rect outOuterBounds) {
+        if (!activity.mAppCompatController.getAppCompatLetterboxPolicy().isRunning()) {
+            outOuterBounds.setEmpty();
+            return;
+        }
+        // Get the bounds of the "space-to-fill". The transformed bounds have the highest
+        // priority because the activity is launched in a rotated environment. In multi-window
+        // mode, the taskFragment-level represents this for both split-screen
+        // and activity-embedding. In fullscreen-mode, the task container does
+        // (since the orientation letterbox is also applied to the task).
+        final Rect transformedBounds =
+                activity.getFixedRotationTransformDisplayBounds();
+        outOuterBounds.set(transformedBounds != null
+                ? transformedBounds
+                : activity.inMultiWindowMode()
+                        ? activity.getTaskFragment().getBounds()
+                        : activity.getRootTask().getParent().getBounds());
+    }
+
+    /**
+     * Provides the inner bounds for the letterboxed activity in display coordinates. This is the
+     * space the letterboxed activity will use.
+     *
+     * @param activity       The Letterboxed activity.
+     * @param outInnerBounds InOut parameter that will contain the inner bounds for the letterboxed
+     *                       activity.
+     */
+    static void calculateLetterboxInnerBounds(@NonNull ActivityRecord activity,
+            @NonNull WindowState window, @NonNull Rect outInnerBounds) {
+        if (!activity.mAppCompatController.getAppCompatLetterboxPolicy().isRunning()) {
+            outInnerBounds.setEmpty();
+            return;
+        }
+        // In case of translucent activities an option is to use the WindowState#getFrame() of
+        // the first opaque activity beneath. In some cases (e.g. an opaque activity is using
+        // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct
+        // information and in particular it might provide a value for a smaller area making
+        // the letterbox overlap with the translucent activity's frame.
+        // If we use WindowState#getFrame() for the translucent activity's letterbox inner
+        // frame, the letterbox will then be overlapped with the translucent activity's frame.
+        // Because the surface layer of letterbox is lower than an activity window, this
+        // won't crop the content, but it may affect other features that rely on values stored
+        // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher
+        // For this reason we use ActivityRecord#getBounds() that the translucent activity
+        // inherits from the first opaque activity beneath and also takes care of the scaling
+        // in case of activities in size compat mode.
+        final TransparentPolicy transparentPolicy =
+                activity.mAppCompatController.getTransparentPolicy();
+        outInnerBounds.set(
+                transparentPolicy.isRunning() ? activity.getBounds() : window.getFrame());
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 08963f1..3742249 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -243,6 +243,10 @@
         doReturn(mTaskStack.top()).when(mActivityStack.top()).getOrganizedTask();
     }
 
+    void setIsInLetterboxAnimation(boolean inAnimation) {
+        doReturn(inAnimation).when(mActivityStack.top()).isInLetterboxAnimation();
+    }
+
     void setTopTaskInMultiWindowMode(boolean inMultiWindowMode) {
         doReturn(inMultiWindowMode).when(mTaskStack.top()).inMultiWindowMode();
     }
@@ -284,6 +288,10 @@
         }
     }
 
+    void setFixedRotationTransformDisplayBounds(@Nullable Rect bounds) {
+        doReturn(bounds).when(mActivityStack.top()).getFixedRotationTransformDisplayBounds();
+    }
+
     void destroyTopActivity() {
         mActivityStack.top().removeImmediately();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxUtilsTest.java
new file mode 100644
index 0000000..673d041
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxUtilsTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.AppCompatLetterboxUtils.calculateLetterboxInnerBounds;
+import static com.android.server.wm.AppCompatLetterboxUtils.calculateLetterboxOuterBounds;
+import static com.android.server.wm.AppCompatLetterboxUtils.calculateLetterboxPosition;
+
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for the {@link AppCompatLetterboxUtils} class.
+ *
+ * Build/Install/Run:
+ * atest WmTests:AppCompatLetterboxUtilsTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatLetterboxUtilsTest extends WindowTestsBase {
+
+    @Test
+    public void allEmptyWhenIsAppNotLetterboxed() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(false);
+            robot.getLetterboxPosition();
+            robot.assertPosition(/* x */ 0, /* y */0);
+            robot.getInnerBounds();
+            robot.assertInnerBounds(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0);
+            robot.getOuterBounds();
+            robot.assertOuterBounds(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0);
+        });
+    }
+
+    @Test
+    public void positionIsFromTaskWhenLetterboxAnimationIsRunning() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(true);
+            robot.activity().setIsInLetterboxAnimation(true);
+            robot.activity().configureTaskBounds(
+                    new Rect(/* left */ 100, /* top */ 200, /* right */ 300, /* bottom */ 400));
+            robot.getLetterboxPosition();
+
+            robot.assertPosition(/* x */ 100, /* y */ 200);
+        });
+    }
+
+    @Test
+    public void positionIsFromActivityWhenLetterboxAnimationIsNotRunning() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(true);
+            robot.activity().setIsInLetterboxAnimation(false);
+            robot.activity().configureTopActivityBounds(
+                    new Rect(/* left */ 200, /* top */ 400, /* right */ 300, /* bottom */ 400));
+            robot.getLetterboxPosition();
+
+            robot.assertPosition(/* x */ 200, /* y */ 400);
+        });
+    }
+
+    @Test
+    public void outerBoundsWhenFixedRotationTransformDisplayBoundsIsAvailable() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(true);
+            robot.activity().setFixedRotationTransformDisplayBounds(
+                    new Rect(/* left */ 1, /* top */ 2, /* right */ 3, /* bottom */ 4));
+            robot.getOuterBounds();
+
+            robot.assertOuterBounds(/* left */ 1, /* top */ 2, /* right */ 3, /* bottom */ 4);
+        });
+    }
+
+    @Test
+    public void outerBoundsNoFixedRotationTransformDisplayBoundsInMultiWindow() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(true);
+            robot.activity().setFixedRotationTransformDisplayBounds(null);
+            robot.activity().setTopActivityInMultiWindowMode(true);
+            robot.getOuterBounds();
+
+            robot.checkOuterBoundsAreTaskFragmentBounds();
+        });
+    }
+
+    @Test
+    public void outerBoundsNoFixedRotationTransformDisplayBoundsNotInMultiWindow() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(true);
+            robot.activity().setFixedRotationTransformDisplayBounds(null);
+            robot.activity().setTopActivityInMultiWindowMode(false);
+            robot.getOuterBounds();
+
+            robot.checkOuterBoundsAreRootTaskParentBounds();
+        });
+    }
+
+    @Test
+    public void innerBoundsTransparencyPolicyIsRunning() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(true);
+            robot.setTopActivityTransparentPolicyRunning(true);
+
+            robot.getInnerBounds();
+
+            robot.checkInnerBoundsAreActivityBounds();
+        });
+    }
+
+    @Test
+    public void innerBoundsTransparencyPolicyIsNotRunning() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.setTopActivityLetterboxPolicyRunning(true);
+            robot.setTopActivityTransparentPolicyRunning(false);
+            robot.setWindowFrame(
+                    new Rect(/* left */ 100, /* top */ 200, /* right */ 300, /* bottom */ 400));
+
+            robot.getInnerBounds();
+
+            robot.assertInnerBounds(/* left */ 100, /* top */ 200, /* right */ 300, /* bottom */
+                    400);
+        });
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    void runTestScenario(@NonNull Consumer<LetterboxUtilsRobotTest> consumer) {
+        final LetterboxUtilsRobotTest robot = new LetterboxUtilsRobotTest(mWm, mAtm, mSupervisor);
+        consumer.accept(robot);
+    }
+
+    private static class LetterboxUtilsRobotTest extends AppCompatRobotBase {
+
+        private final Point mPosition = new Point();
+        private final Rect mInnerBound = new Rect();
+        private final Rect mOuterBound = new Rect();
+
+        @NonNull
+        private final WindowState mWindowState;
+
+        LetterboxUtilsRobotTest(@NonNull WindowManagerService wm,
+                @NonNull ActivityTaskManagerService atm,
+                @NonNull ActivityTaskSupervisor supervisor) {
+            super(wm, atm, supervisor);
+            mWindowState = mock(WindowState.class);
+        }
+
+        @Override
+        void onPostActivityCreation(@NonNull ActivityRecord activity) {
+            super.onPostActivityCreation(activity);
+            spyOn(activity.mAppCompatController.getAppCompatLetterboxPolicy());
+            spyOn(activity.mAppCompatController.getTransparentPolicy());
+        }
+
+        void setTopActivityLetterboxPolicyRunning(boolean running) {
+            doReturn(running).when(activity().top().mAppCompatController
+                    .getAppCompatLetterboxPolicy()).isRunning();
+        }
+
+        void setTopActivityTransparentPolicyRunning(boolean running) {
+            doReturn(running).when(activity().top().mAppCompatController
+                    .getTransparentPolicy()).isRunning();
+        }
+
+        void setWindowFrame(@NonNull Rect frame) {
+            doReturn(frame).when(mWindowState).getFrame();
+        }
+
+        void getLetterboxPosition() {
+            calculateLetterboxPosition(activity().top(), mPosition);
+        }
+
+        void getInnerBounds() {
+            calculateLetterboxInnerBounds(activity().top(), mWindowState, mInnerBound);
+        }
+
+        void getOuterBounds() {
+            calculateLetterboxOuterBounds(activity().top(), mOuterBound);
+        }
+
+        void assertPosition(int expectedX, int expectedY) {
+            Assert.assertEquals(expectedX, mPosition.x);
+            Assert.assertEquals(expectedY, mPosition.y);
+        }
+
+        void assertInnerBounds(int expectedLeft, int expectedTop, int expectedRight,
+                int expectedBottom) {
+            Assert.assertEquals(expectedLeft, mInnerBound.left);
+            Assert.assertEquals(expectedTop, mInnerBound.top);
+            Assert.assertEquals(expectedRight, mInnerBound.right);
+            Assert.assertEquals(expectedBottom, mInnerBound.bottom);
+        }
+
+        void assertOuterBounds(int expectedLeft, int expectedTop, int expectedRight,
+                int expectedBottom) {
+            Assert.assertEquals(expectedLeft, mOuterBound.left);
+            Assert.assertEquals(expectedTop, mOuterBound.top);
+            Assert.assertEquals(expectedRight, mOuterBound.right);
+            Assert.assertEquals(expectedBottom, mOuterBound.bottom);
+        }
+
+        void checkOuterBoundsAreRootTaskParentBounds() {
+            Assert.assertEquals(mOuterBound,
+                    activity().top().getRootTask().getParent().getBounds());
+        }
+
+        void checkOuterBoundsAreTaskFragmentBounds() {
+            Assert.assertEquals(mOuterBound,
+                    activity().top().getTaskFragment().getBounds());
+        }
+
+        void checkInnerBoundsAreActivityBounds() {
+            Assert.assertEquals(mInnerBound, activity().top().getBounds());
+        }
+
+    }
+}