[1/n] Create AppCompatLetterboxPolicyState abstraction
The AppCompatLetterboxPolicyState abstraction allows to
decouple the letterbox state from the actual letterbox
presentation implementation.
LegacyLetterboxPolicyState is the existing implementation
which creates and updates surfaces depending on letterbox bounds.
ShellLetterboxPolicyState is the implementation which delegates
the letterbox presentation to Shell. It basically only handles
inner and outer bounds along with insets. They are useful for
creating/updating Surfaces which will exist in shell.
The AppCompatLetterboxUtils class contains methods used by different
AppCompatLetterboxPolicyState implementations.
Flag: com.android.window.flags.app_compat_refactoring
Bug: 309593314
Test: atest WmTests:LetterboxTest
Test: atest WmTests:AppCompatLetterboxPolicyTest
Test: atest WmTests:AppCompatLetterboxUtilsTest
Change-Id: I2c0bd7264c73936093909eee05477d0937270762
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());
+ }
+
+ }
+}