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