Merge "Add a way to disable auto rotation for immersive apps" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
53cf7e47f0
@@ -5213,6 +5213,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
|
||||
|
||||
@@ -4441,6 +4441,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" />
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -5948,6 +5948,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ActivityRecord topRunningActivity() {
|
||||
return topRunningActivity(false /* considerKeyguardState */);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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 static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
|
||||
import static android.view.InsetsState.ITYPE_STATUS_BAR;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.res.Configuration.Orientation;
|
||||
import android.view.InsetsVisibilities;
|
||||
import android.view.Surface;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
InsetsVisibilities insetsVisibilities = mainWindow.getRequestedVisibilities();
|
||||
return !insetsVisibilities.getVisibility(ITYPE_STATUS_BAR)
|
||||
&& !insetsVisibilities.getVisibility(ITYPE_NAVIGATION_BAR);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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 android.view.InsetsState.ITYPE_NAVIGATION_BAR;
|
||||
import static android.view.InsetsState.ITYPE_STATUS_BAR;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
|
||||
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.InsetsVisibilities;
|
||||
import android.view.Surface;
|
||||
|
||||
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 InsetsVisibilities mMockInsetsVisibilities;
|
||||
|
||||
@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);
|
||||
WindowState mockWindowState = mock(WindowState.class);
|
||||
mMockInsetsVisibilities = mock(InsetsVisibilities.class);
|
||||
when(mMockInsetsVisibilities.getVisibility(eq(ITYPE_STATUS_BAR))).thenReturn(false);
|
||||
when(mMockInsetsVisibilities.getVisibility(eq(ITYPE_NAVIGATION_BAR))).thenReturn(false);
|
||||
when(mockWindowState.getRequestedVisibilities()).thenReturn(mMockInsetsVisibilities);
|
||||
when(mMockActivityRecord.findMainWindow()).thenReturn(mockWindowState);
|
||||
|
||||
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(mMockInsetsVisibilities.getVisibility(eq(ITYPE_STATUS_BAR))).thenReturn(true);
|
||||
|
||||
assertIsRotationLockEnforcedReturnsFalseForAllRotations();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsRotationLockEnforced_navBarVisible_lockNotEnforced() {
|
||||
// Some system bars are visible
|
||||
when(mMockInsetsVisibilities.getVisibility(eq(ITYPE_NAVIGATION_BAR))).thenReturn(true);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user