diff options
5 files changed, 620 insertions, 69 deletions
| diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java index afc6506f9091..4e390df14131 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 android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;  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 android.view.SurfaceControl;  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 @@ class AppCompatLetterboxPolicy {      @NonNull      private final ActivityRecord mActivityRecord;      @NonNull -    private final LetterboxPolicyState mLetterboxPolicyState; +    private final AppCompatLetterboxPolicyState mLetterboxPolicyState;      @NonNull      private final AppCompatRoundedCorners mAppCompatRoundedCorners;      @NonNull @@ -54,7 +58,8 @@ class AppCompatLetterboxPolicy {      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 @@ class AppCompatLetterboxPolicy {      @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 @@ class AppCompatLetterboxPolicy {          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 @@ class AppCompatLetterboxPolicy {                  || 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 @@ class AppCompatLetterboxPolicy {                          .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 @@ class AppCompatLetterboxPolicy {           * @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 @@ class AppCompatLetterboxPolicy {                      .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 @@ class AppCompatLetterboxPolicy {              }          } -        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 @@ class AppCompatLetterboxPolicy {          }          /** 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 @@ class AppCompatLetterboxPolicy {          }          /** 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,39 +385,130 @@ class AppCompatLetterboxPolicy {           * @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; +        private SurfaceControl getLetterboxParentSurface() { +            if (mActivityRecord.isInLetterboxAnimation()) { +                return mActivityRecord.getTask().getSurfaceControl(); +            } +            return mActivityRecord.getSurfaceControl(); +        } + +    } + +    /** +     * {@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;              } -            final Rect letterboxInnerBounds = new Rect(); -            final Rect letterboxOuterBounds = new Rect(); -            getLetterboxInnerBounds(letterboxInnerBounds); -            getLetterboxOuterBounds(letterboxOuterBounds); +            mLetterboxPosition.set(0, 0); +            mInnerBounds.setEmpty(); +            mOuterBounds.setEmpty(); +        } -            if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) { -                return null; +        @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(); +        } -            return new LetterboxDetails( -                    letterboxInnerBounds, -                    letterboxOuterBounds, -                    w.mAttrs.insetsFlags.appearance -            ); +        @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(); +            }          } -        @Nullable -        private SurfaceControl getLetterboxParentSurface() { -            if (mActivityRecord.isInLetterboxAnimation()) { -                return mActivityRecord.getTask().getSurfaceControl(); +        @Override +        public void getLetterboxOuterBounds(@NonNull Rect outBounds) { +            if (isRunning()) { +                outBounds.set(mOuterBounds); +            } else { +                outBounds.setEmpty();              } -            return mActivityRecord.getSurfaceControl();          } +        @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 000000000000..31ad536c2eb2 --- /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 000000000000..79b3a55d0463 --- /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 08963f1c7647..3742249bdffe 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 @@ class AppCompatActivityRobot {          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 @@ class AppCompatActivityRobot {          }      } +    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 000000000000..673d04166a7a --- /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()); +        } + +    } +} |