diff options
12 files changed, 706 insertions, 24 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index aad88c45f956..24de6ad48ca3 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5245,6 +5245,14 @@ having a separating hinge. --> <bool name="config_isDisplayHingeAlwaysSeparating">false</bool> + <!-- Whether enabling rotation compat policy for immersive apps that prevents auto rotation + into non-optimal screen orientation while in fullscreen. This is needed because immersive + apps, such as games, are often not optimized for all orientations and can have a poor UX + when rotated. Additionally, some games rely on sensors for the gameplay so users can + trigger such rotations accidentally when auto rotation is on. + Applicable only if ignoreOrientationRequest is enabled. --> + <bool name="config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled">false</bool> + <!-- Aspect ratio of letterboxing for fixed orientation. Values <= 1.0 will be ignored. Note: Activity min/max aspect ratio restrictions will still be respected. Therefore this override can control the maximum screen area that can be occupied by diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 85bafb9b9f4e..cd39e590310b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4413,6 +4413,7 @@ <java-symbol type="dimen" name="controls_thumbnail_image_max_height" /> <java-symbol type="dimen" name="controls_thumbnail_image_max_width" /> + <java-symbol type="bool" name="config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled" /> <java-symbol type="dimen" name="config_fixedOrientationLetterboxAspectRatio" /> <java-symbol type="dimen" name="config_letterboxBackgroundWallpaperBlurRadius" /> <java-symbol type="integer" name="config_letterboxActivityCornersRadius" /> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index 8ee893c14727..359da13a9799 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -249,7 +249,8 @@ public class RotationButtonController { } public void setRotationLockedAtAngle(int rotationSuggestion) { - RotationPolicy.setRotationLockAtAngle(mContext, true, rotationSuggestion); + RotationPolicy.setRotationLockAtAngle(mContext, /* enabled= */ isRotationLocked(), + /* rotation= */ rotationSuggestion); } public boolean isRotationLocked() { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index f10824fc76ed..3cb4fbd44f9a 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -6024,6 +6024,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } + @Nullable ActivityRecord topRunningActivity() { return topRunningActivity(false /* considerKeyguardState */); } diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index cf3a6880e712..e6d8b3db4564 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -100,6 +100,8 @@ public class DisplayRotation { private final DisplayWindowSettings mDisplayWindowSettings; private final Context mContext; private final Object mLock; + @Nullable + private final DisplayRotationImmersiveAppCompatPolicy mCompatPolicyForImmersiveApps; public final boolean isDefaultDisplay; private final boolean mSupportAutoRotation; @@ -205,7 +207,7 @@ public class DisplayRotation { /** * A flag to indicate if the display rotation should be fixed to user specified rotation - * regardless of all other states (including app requrested orientation). {@code true} the + * regardless of all other states (including app requested orientation). {@code true} the * display rotation should be fixed to user specified rotation, {@code false} otherwise. */ private int mFixedToUserRotation = IWindowManager.FIXED_TO_USER_ROTATION_DEFAULT; @@ -232,6 +234,7 @@ public class DisplayRotation { mContext = context; mLock = lock; isDefaultDisplay = displayContent.isDefaultDisplay; + mCompatPolicyForImmersiveApps = initImmersiveAppCompatPolicy(service, displayContent); mSupportAutoRotation = mContext.getResources().getBoolean(R.bool.config_supportAutoRotation); @@ -255,6 +258,14 @@ public class DisplayRotation { } } + @VisibleForTesting + @Nullable + DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy( + WindowManagerService service, DisplayContent displayContent) { + return DisplayRotationImmersiveAppCompatPolicy.createIfNeeded( + service.mLetterboxConfiguration, this, displayContent); + } + // Change the default value to the value specified in the sysprop // ro.bootanim.set_orientation_<display_id>. Four values are supported: ORIENTATION_0, // ORIENTATION_90, ORIENTATION_180 and ORIENTATION_270. @@ -1305,11 +1316,11 @@ public class DisplayRotation { return mAllowAllRotations; } - private boolean isLandscapeOrSeascape(int rotation) { + boolean isLandscapeOrSeascape(@Surface.Rotation final int rotation) { return rotation == mLandscapeRotation || rotation == mSeascapeRotation; } - private boolean isAnyPortrait(int rotation) { + boolean isAnyPortrait(@Surface.Rotation final int rotation) { return rotation == mPortraitRotation || rotation == mUpsideDownRotation; } @@ -1348,9 +1359,16 @@ public class DisplayRotation { return mFoldController != null && mFoldController.overrideFrozenRotation(); } - private boolean isRotationChoicePossible(int orientation) { - // Rotation choice is only shown when the user is in locked mode. - if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false; + private boolean isRotationChoiceAllowed(@Surface.Rotation final int proposedRotation) { + final boolean isRotationLockEnforced = mCompatPolicyForImmersiveApps != null + && mCompatPolicyForImmersiveApps.isRotationLockEnforced(proposedRotation); + + // Don't show rotation choice button if + if (!isRotationLockEnforced // not enforcing locked rotation + // and the screen rotation is not locked by the user. + && mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) { + return false; + } // Don't show rotation choice if we are in tabletop or book modes. if (isTabletopAutoRotateOverrideEnabled()) return false; @@ -1402,7 +1420,7 @@ public class DisplayRotation { } // Ensure that some rotation choice is possible for the given orientation. - switch (orientation) { + switch (mCurrentAppOrientation) { case ActivityInfo.SCREEN_ORIENTATION_FULL_USER: case ActivityInfo.SCREEN_ORIENTATION_USER: case ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED: @@ -1719,11 +1737,11 @@ public class DisplayRotation { @Override - public void onProposedRotationChanged(int rotation) { + public void onProposedRotationChanged(@Surface.Rotation int rotation) { ProtoLog.v(WM_DEBUG_ORIENTATION, "onProposedRotationChanged, rotation=%d", rotation); // Send interaction power boost to improve redraw performance. mService.mPowerManagerInternal.setPowerBoost(Boost.INTERACTION, 0); - if (isRotationChoicePossible(mCurrentAppOrientation)) { + if (isRotationChoiceAllowed(rotation)) { final boolean isValid = isValidRotationChoice(rotation); sendProposedRotationChangeToStatusBarInternal(rotation, isValid); } else { diff --git a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java new file mode 100644 index 000000000000..74494ddd9f59 --- /dev/null +++ b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2022 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 android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Configuration.Orientation; +import android.view.Surface; +import android.view.WindowInsets.Type; + +/** + * Policy to decide whether to enforce screen rotation lock for optimisation of the screen rotation + * user experience for immersive applications for compatibility when ignoring orientation request. + * + * <p>This is needed because immersive apps, such as games, are often not optimized for all + * orientations and can have a poor UX when rotated (e.g., state loss or entering size-compat mode). + * Additionally, some games rely on sensors for the gameplay so users can trigger such rotations + * accidentally when auto rotation is on. + */ +final class DisplayRotationImmersiveAppCompatPolicy { + + @Nullable + static DisplayRotationImmersiveAppCompatPolicy createIfNeeded( + @NonNull final LetterboxConfiguration letterboxConfiguration, + @NonNull final DisplayRotation displayRotation, + @NonNull final DisplayContent displayContent) { + if (!letterboxConfiguration + .isDisplayRotationImmersiveAppCompatPolicyEnabled(/* checkDeviceConfig */ false)) { + return null; + } + + return new DisplayRotationImmersiveAppCompatPolicy( + letterboxConfiguration, displayRotation, displayContent); + } + + private final DisplayRotation mDisplayRotation; + private final LetterboxConfiguration mLetterboxConfiguration; + private final DisplayContent mDisplayContent; + + private DisplayRotationImmersiveAppCompatPolicy( + @NonNull final LetterboxConfiguration letterboxConfiguration, + @NonNull final DisplayRotation displayRotation, + @NonNull final DisplayContent displayContent) { + mDisplayRotation = displayRotation; + mLetterboxConfiguration = letterboxConfiguration; + mDisplayContent = displayContent; + } + + /** + * Decides whether it is necessary to lock screen rotation, preventing auto rotation, based on + * the top activity configuration and proposed screen rotation. + * + * <p>This is needed because immersive apps, such as games, are often not optimized for all + * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors + * for the gameplay so users can trigger such rotations accidentally when auto rotation is on. + * + * <p>Screen rotation is locked when the following conditions are met: + * <ul> + * <li>Top activity requests to hide status and navigation bars + * <li>Top activity is fullscreen and in optimal orientation (without letterboxing) + * <li>Rotation will lead to letterboxing due to fixed orientation. + * <li>{@link DisplayContent#getIgnoreOrientationRequest} is {@code true} + * <li>This policy is enabled on the device, for details see + * {@link LetterboxConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled} + * </ul> + * + * @param proposedRotation new proposed {@link Surface.Rotation} for the screen. + * @return {@code true}, if there is a need to lock screen rotation, {@code false} otherwise. + */ + boolean isRotationLockEnforced(@Surface.Rotation final int proposedRotation) { + if (!mLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled( + /* checkDeviceConfig */ true)) { + return false; + } + synchronized (mDisplayContent.mWmService.mGlobalLock) { + return isRotationLockEnforcedLocked(proposedRotation); + } + } + + private boolean isRotationLockEnforcedLocked(@Surface.Rotation final int proposedRotation) { + if (!mDisplayContent.getIgnoreOrientationRequest()) { + return false; + } + + final ActivityRecord activityRecord = mDisplayContent.topRunningActivity(); + if (activityRecord == null) { + return false; + } + + // Don't lock screen rotation if an activity hasn't requested to hide system bars. + if (!hasRequestedToHideStatusAndNavBars(activityRecord)) { + return false; + } + + // Don't lock screen rotation if activity is not in fullscreen. Checking windowing mode + // for a task rather than an activity to exclude activity embedding scenario. + if (activityRecord.getTask() == null + || activityRecord.getTask().getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { + return false; + } + + // Don't lock screen rotation if activity is letterboxed. + if (activityRecord.areBoundsLetterboxed()) { + return false; + } + + if (activityRecord.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) { + return false; + } + + // Lock screen rotation only if, after rotation the activity's orientation won't match + // the screen orientation, forcing the activity to enter letterbox mode after rotation. + return activityRecord.getRequestedConfigurationOrientation() + != surfaceRotationToConfigurationOrientation(proposedRotation); + } + + /** + * Checks whether activity has requested to hide status and navigation bars. + */ + private boolean hasRequestedToHideStatusAndNavBars(@NonNull ActivityRecord activity) { + WindowState mainWindow = activity.findMainWindow(); + if (mainWindow == null) { + return false; + } + return (mainWindow.getRequestedVisibleTypes() + & (Type.statusBars() | Type.navigationBars())) == 0; + } + + @Orientation + private int surfaceRotationToConfigurationOrientation(@Surface.Rotation final int rotation) { + if (mDisplayRotation.isAnyPortrait(rotation)) { + return ORIENTATION_PORTRAIT; + } else if (mDisplayRotation.isLandscapeOrSeascape(rotation)) { + return ORIENTATION_LANDSCAPE; + } else { + return ORIENTATION_UNDEFINED; + } + } +} diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 642732652ce9..68a1f88842a2 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY; import android.annotation.IntDef; import android.annotation.NonNull; @@ -227,23 +228,35 @@ final class LetterboxConfiguration { // LetterboxUiController#shouldIgnoreRequestedOrientation for details. private final boolean mIsPolicyForIgnoringRequestedOrientationEnabled; - LetterboxConfiguration(Context systemUiContext) { - this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext, - () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext, - /* forBookMode */ false), - () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext, - /* forTabletopMode */ false), - () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext, - /* forBookMode */ true), - () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext, - /* forTabletopMode */ true) - )); + // Whether enabling rotation compat policy for immersive apps that prevents auto rotation + // into non-optimal screen orientation while in fullscreen. This is needed because immersive + // apps, such as games, are often not optimized for all orientations and can have a poor UX + // when rotated. Additionally, some games rely on sensors for the gameplay so users can trigger + // such rotations accidentally when auto rotation is on. + private final boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled; + + // Flags dynamically updated with {@link android.provider.DeviceConfig}. + @NonNull private final LetterboxConfigurationDeviceConfig mDeviceConfig; + + LetterboxConfiguration(@NonNull final Context systemUiContext) { + this(systemUiContext, + new LetterboxConfigurationPersister(systemUiContext, + () -> readLetterboxHorizontalReachabilityPositionFromConfig( + systemUiContext, /* forBookMode */ false), + () -> readLetterboxVerticalReachabilityPositionFromConfig( + systemUiContext, /* forTabletopMode */ false), + () -> readLetterboxHorizontalReachabilityPositionFromConfig( + systemUiContext, /* forBookMode */ true), + () -> readLetterboxVerticalReachabilityPositionFromConfig( + systemUiContext, /* forTabletopMode */ true))); } @VisibleForTesting - LetterboxConfiguration(Context systemUiContext, - LetterboxConfigurationPersister letterboxConfigurationPersister) { + LetterboxConfiguration(@NonNull final Context systemUiContext, + @NonNull final LetterboxConfigurationPersister letterboxConfigurationPersister) { mContext = systemUiContext; + mDeviceConfig = new LetterboxConfigurationDeviceConfig(systemUiContext.getMainExecutor()); + mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat( R.dimen.config_fixedOrientationLetterboxAspectRatio); mLetterboxActivityCornersRadius = mContext.getResources().getInteger( @@ -284,6 +297,12 @@ final class LetterboxConfiguration { mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled); + mIsDisplayRotationImmersiveAppCompatPolicyEnabled = mContext.getResources().getBoolean( + R.bool.config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled); + mDeviceConfig.updateFlagActiveStatus( + /* isActive */ mIsDisplayRotationImmersiveAppCompatPolicyEnabled, + /* key */ KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY); + mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); } @@ -1105,4 +1124,20 @@ final class LetterboxConfiguration { mIsCameraCompatRefreshCycleThroughStopEnabled = true; } + /** + * Checks whether rotation compat policy for immersive apps that prevents auto rotation + * into non-optimal screen orientation while in fullscreen is enabled. + * + * <p>This is needed because immersive apps, such as games, are often not optimized for all + * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors + * for the gameplay so users can trigger such rotations accidentally when auto rotation is on. + * + * @param checkDeviceConfig whether should check both static config and a dynamic property + * from {@link DeviceConfig} or only static value. + */ + boolean isDisplayRotationImmersiveAppCompatPolicyEnabled(final boolean checkDeviceConfig) { + return mIsDisplayRotationImmersiveAppCompatPolicyEnabled && (!checkDeviceConfig + || mDeviceConfig.getFlag(KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY)); + } + } diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java new file mode 100644 index 000000000000..cf123a1f9ace --- /dev/null +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 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.provider.DeviceConfig; +import android.util.ArraySet; + + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Utility class that caches {@link DeviceConfig} flags for app compat features and listens + * to updates by implementing {@link DeviceConfig.OnPropertiesChangedListener}. + */ +final class LetterboxConfigurationDeviceConfig + implements DeviceConfig.OnPropertiesChangedListener { + + static final String KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY = + "enable_display_rotation_immersive_app_compat_policy"; + private static final boolean DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY = + true; + + @VisibleForTesting + static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of( + KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY, + DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY + ); + + // Whether enabling rotation compat policy for immersive apps that prevents auto rotation + // into non-optimal screen orientation while in fullscreen. This is needed because immersive + // apps, such as games, are often not optimized for all orientations and can have a poor UX + // when rotated. Additionally, some games rely on sensors for the gameplay so users can trigger + // such rotations accidentally when auto rotation is on. + private boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled = + DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY; + + // Set of active device configs that need to be updated in + // DeviceConfig.OnPropertiesChangedListener#onPropertiesChanged. + private final ArraySet<String> mActiveDeviceConfigsSet = new ArraySet<>(); + + LetterboxConfigurationDeviceConfig(@NonNull final Executor executor) { + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + executor, /* onPropertiesChangedListener */ this); + } + + @Override + public void onPropertiesChanged(@NonNull final DeviceConfig.Properties properties) { + for (int i = mActiveDeviceConfigsSet.size() - 1; i >= 0; i--) { + String key = mActiveDeviceConfigsSet.valueAt(i); + // Reads the new configuration, if the device config properties contain the key. + if (properties.getKeyset().contains(key)) { + readAndSaveValueFromDeviceConfig(key); + } + } + } + + /** + * Adds {@code key} to a set of flags that can be updated from the server if + * {@code isActive} is {@code true} and read it's current value from {@link DeviceConfig}. + */ + void updateFlagActiveStatus(boolean isActive, String key) { + if (!isActive) { + return; + } + mActiveDeviceConfigsSet.add(key); + readAndSaveValueFromDeviceConfig(key); + } + + /** + * Returns values of the {@code key} flag. + * + * @throws AssertionError {@code key} isn't recognised. + */ + boolean getFlag(String key) { + switch (key) { + case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY: + return mIsDisplayRotationImmersiveAppCompatPolicyEnabled; + default: + throw new AssertionError("Unexpected flag name: " + key); + } + } + + private void readAndSaveValueFromDeviceConfig(String key) { + Boolean defaultValue = sKeyToDefaultValueMap.get(key); + if (defaultValue == null) { + throw new AssertionError("Haven't found default value for flag: " + key); + } + switch (key) { + case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY: + mIsDisplayRotationImmersiveAppCompatPolicyEnabled = + getDeviceConfig(key, defaultValue); + break; + default: + throw new AssertionError("Unexpected flag name: " + key); + } + } + + private boolean getDeviceConfig(String key, boolean defaultValue) { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + key, defaultValue); + } +} diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp index 079d765868fd..2ce7cea08a3d 100644 --- a/services/tests/wmtests/Android.bp +++ b/services/tests/wmtests/Android.bp @@ -68,6 +68,10 @@ android_test { "android.test.runner", ], + defaults: [ + "modules-utils-testable-device-config-defaults", + ], + // These are not normally accessible from apps so they must be explicitly included. jni_libs: [ "libdexmakerjvmtiagent", diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java new file mode 100644 index 000000000000..d29b18f89f77 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2022 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 android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; + +import android.platform.test.annotations.Presubmit; +import android.view.Surface; +import android.view.WindowInsets.Type; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test class for {@link DisplayRotationImmersiveAppCompatPolicy}. + * + * Build/Install/Run: + * atest WmTests:DisplayRotationImmersiveAppCompatPolicyTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DisplayRotationImmersiveAppCompatPolicyTests extends WindowTestsBase { + + private DisplayRotationImmersiveAppCompatPolicy mPolicy; + + private LetterboxConfiguration mMockLetterboxConfiguration; + private ActivityRecord mMockActivityRecord; + private Task mMockTask; + private WindowState mMockWindowState; + + @Before + public void setUp() throws Exception { + mMockActivityRecord = mock(ActivityRecord.class); + mMockTask = mock(Task.class); + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_FULLSCREEN); + when(mMockActivityRecord.getTask()).thenReturn(mMockTask); + when(mMockActivityRecord.areBoundsLetterboxed()).thenReturn(false); + when(mMockActivityRecord.getRequestedConfigurationOrientation()).thenReturn( + ORIENTATION_LANDSCAPE); + mMockWindowState = mock(WindowState.class); + when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(0); + when(mMockActivityRecord.findMainWindow()).thenReturn(mMockWindowState); + + spy(mDisplayContent); + doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity(); + when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true); + + mMockLetterboxConfiguration = mock(LetterboxConfiguration.class); + when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled( + /* checkDeviceConfig */ anyBoolean())).thenReturn(true); + + mPolicy = DisplayRotationImmersiveAppCompatPolicy.createIfNeeded( + mMockLetterboxConfiguration, createDisplayRotationMock(), + mDisplayContent); + } + + private DisplayRotation createDisplayRotationMock() { + DisplayRotation mockDisplayRotation = mock(DisplayRotation.class); + + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_0)).thenReturn(true); + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_90)).thenReturn(false); + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_180)).thenReturn(true); + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_270)).thenReturn(false); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_0)).thenReturn(false); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_90)).thenReturn(true); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_180)).thenReturn(false); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_270)).thenReturn(true); + + return mockDisplayRotation; + } + + @Test + public void testIsRotationLockEnforced_landscapeActivity_lockedWhenRotatingToPortrait() { + // Base case: App is optimal in Landscape. + + // ROTATION_* is the target display orientation counted from the natural display + // orientation. Outside of test environment, ROTATION_0 means that proposed display + // rotation is the natural device orientation. + // DisplayRotationImmersiveAppCompatPolicy assesses whether the proposed target + // orientation ROTATION_* is optimal for the top fullscreen activity or not. + // For instance, ROTATION_0 means portrait screen orientation (see + // createDisplayRotationMock) which isn't optimal for a landscape-only activity so + // we should show a rotation suggestion button instead of rotating directly. + + // Rotation to portrait + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_0)); + // Rotation to landscape + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_90)); + // Rotation to portrait + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_180)); + // Rotation to landscape + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_270)); + } + + @Test + public void testIsRotationLockEnforced_portraitActivity_lockedWhenRotatingToLandscape() { + when(mMockActivityRecord.getRequestedConfigurationOrientation()).thenReturn( + ORIENTATION_PORTRAIT); + + // Rotation to portrait + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_0)); + // Rotation to landscape + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_90)); + // Rotation to portrait + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_180)); + // Rotation to landscape + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_270)); + } + + @Test + public void testIsRotationLockEnforced_responsiveActivity_lockNotEnforced() { + // Do not fix screen orientation + when(mMockActivityRecord.getRequestedConfigurationOrientation()).thenReturn( + ORIENTATION_UNDEFINED); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_statusBarVisible_lockNotEnforced() { + // Some system bars are visible + when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(Type.statusBars()); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_navBarVisible_lockNotEnforced() { + // Some system bars are visible + when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(Type.navigationBars()); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_activityIsLetterboxed_lockNotEnforced() { + // Activity is letterboxed + when(mMockActivityRecord.areBoundsLetterboxed()).thenReturn(true); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_notFullscreen_lockNotEnforced() { + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_MULTI_WINDOW); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_PINNED); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_FREEFORM); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_ignoreOrientationRequestDisabled_lockNotEnforced() { + when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(false); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testRotationChoiceEnforcedOnly_nullTopRunningActivity_lockNotEnforced() { + when(mDisplayContent.topRunningActivity()).thenReturn(null); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testRotationChoiceEnforcedOnly_featureFlagDisabled_lockNotEnforced() { + when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled( + /* checkDeviceConfig */ true)).thenReturn(false); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + private void assertIsRotationLockEnforcedReturnsFalseForAllRotations() { + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_0)); + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_90)); + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_180)); + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_270)); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java index 491f876dceed..4ce43e1fc469 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -1096,8 +1096,16 @@ public class DisplayRotationTests { mMockDisplayAddress = mock(DisplayAddress.class); mMockDisplayWindowSettings = mock(DisplayWindowSettings.class); + mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress, - mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object()); + mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object()) { + @Override + DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy( + WindowManagerService service, DisplayContent displayContent) { + return null; + } + }; + reset(sMockWm); captureObservers(); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java new file mode 100644 index 000000000000..2b7a06bd35f3 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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.server.wm.LetterboxConfigurationDeviceConfig.sKeyToDefaultValueMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; + +import androidx.test.filters.SmallTest; + +import com.android.modules.utils.testing.TestableDeviceConfig; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Map; + +/** + * Test class for {@link LetterboxConfigurationDeviceConfig}. + * + * atest WmTests:LetterboxConfigurationDeviceConfigTests + */ +@SmallTest +@Presubmit +public class LetterboxConfigurationDeviceConfigTests { + + private LetterboxConfigurationDeviceConfig mDeviceConfig; + + @Rule + public final TestableDeviceConfig.TestableDeviceConfigRule + mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule(); + + @Before + public void setUp() { + mDeviceConfig = new LetterboxConfigurationDeviceConfig(/* executor */ Runnable::run); + } + + @Test + public void testGetFlag_flagIsActive_flagChanges() throws Throwable { + for (Map.Entry<String, Boolean> entry : sKeyToDefaultValueMap.entrySet()) { + testGetFlagForKey_flagIsActive_flagChanges(entry.getKey(), entry.getValue()); + } + } + + private void testGetFlagForKey_flagIsActive_flagChanges(final String key, boolean defaultValue) + throws InterruptedException { + mDeviceConfig.updateFlagActiveStatus(/* isActive */ true, key); + + assertEquals("Unexpected default value for " + key, + mDeviceConfig.getFlag(key), defaultValue); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.TRUE.toString(), /* makeDefault */ false); + + assertTrue("Flag " + key + "is not true after change", mDeviceConfig.getFlag(key)); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.FALSE.toString(), /* makeDefault */ false); + + assertFalse("Flag " + key + "is not false after change", mDeviceConfig.getFlag(key)); + } + + @Test + public void testGetFlag_flagIsNotActive_alwaysReturnDefaultValue() throws Throwable { + for (Map.Entry<String, Boolean> entry : sKeyToDefaultValueMap.entrySet()) { + testGetFlagForKey_flagIsNotActive_alwaysReturnDefaultValue( + entry.getKey(), entry.getValue()); + } + } + + private void testGetFlagForKey_flagIsNotActive_alwaysReturnDefaultValue(final String key, + boolean defaultValue) throws InterruptedException { + assertEquals("Unexpected default value for " + key, + mDeviceConfig.getFlag(key), defaultValue); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.TRUE.toString(), /* makeDefault */ false); + + assertEquals("Flag " + key + "is not set to default after change", + mDeviceConfig.getFlag(key), defaultValue); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.FALSE.toString(), /* makeDefault */ false); + + assertEquals("Flag " + key + "is not set to default after change", + mDeviceConfig.getFlag(key), defaultValue); + } + +} |