1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wm; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; 21 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 22 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 23 import static android.content.res.Configuration.ORIENTATION_UNDEFINED; 24 25 import static com.android.server.policy.WindowManagerPolicy.USER_ROTATION_FREE; 26 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.content.res.Configuration.Orientation; 30 import android.view.Surface; 31 import android.view.WindowInsets.Type; 32 33 /** 34 * Policy to decide whether to enforce screen rotation lock for optimisation of the screen rotation 35 * user experience for immersive applications for compatibility when ignoring orientation request. 36 * 37 * <p>This is needed because immersive apps, such as games, are often not optimized for all 38 * orientations and can have a poor UX when rotated (e.g., state loss or entering size-compat mode). 39 * Additionally, some games rely on sensors for the gameplay so users can trigger such rotations 40 * accidentally when auto rotation is on. 41 */ 42 final class DisplayRotationImmersiveAppCompatPolicy { 43 44 @Nullable createIfNeeded( @onNull final AppCompatConfiguration appCompatConfiguration, @NonNull final DisplayRotation displayRotation, @NonNull final DisplayContent displayContent)45 static DisplayRotationImmersiveAppCompatPolicy createIfNeeded( 46 @NonNull final AppCompatConfiguration appCompatConfiguration, 47 @NonNull final DisplayRotation displayRotation, 48 @NonNull final DisplayContent displayContent) { 49 if (!appCompatConfiguration 50 .isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime()) { 51 return null; 52 } 53 54 return new DisplayRotationImmersiveAppCompatPolicy( 55 appCompatConfiguration, displayRotation, displayContent); 56 } 57 58 private final DisplayRotation mDisplayRotation; 59 private final AppCompatConfiguration mAppCompatConfiguration; 60 private final DisplayContent mDisplayContent; 61 DisplayRotationImmersiveAppCompatPolicy( @onNull final AppCompatConfiguration appCompatConfiguration, @NonNull final DisplayRotation displayRotation, @NonNull final DisplayContent displayContent)62 private DisplayRotationImmersiveAppCompatPolicy( 63 @NonNull final AppCompatConfiguration appCompatConfiguration, 64 @NonNull final DisplayRotation displayRotation, 65 @NonNull final DisplayContent displayContent) { 66 mDisplayRotation = displayRotation; 67 mAppCompatConfiguration = appCompatConfiguration; 68 mDisplayContent = displayContent; 69 } 70 71 /** 72 * Returns {@code true} if the orientation update should be skipped and it will update when 73 * transition is done. This is to keep the orientation which was preserved by 74 * {@link #isRotationLockEnforced} from being changed by a transient launch (i.e. recents). 75 */ deferOrientationUpdate()76 boolean deferOrientationUpdate() { 77 if (mDisplayRotation.getUserRotation() != USER_ROTATION_FREE 78 || mDisplayRotation.getLastOrientation() != SCREEN_ORIENTATION_UNSPECIFIED) { 79 return false; 80 } 81 final WindowOrientationListener orientationListener = 82 mDisplayRotation.getOrientationListener(); 83 if (orientationListener == null 84 || orientationListener.getProposedRotation() == mDisplayRotation.getRotation()) { 85 return false; 86 } 87 // The above conditions mean that isRotationLockEnforced might have taken effect: 88 // Auto-rotation is enabled and the proposed rotation is not applied. 89 // Then the update should defer until the transition idle to avoid disturbing animation. 90 if (!mDisplayContent.mTransitionController.hasTransientLaunch(mDisplayContent)) { 91 return false; 92 } 93 mDisplayContent.mTransitionController.mStateValidators.add(() -> { 94 if (!isRotationLockEnforcedLocked(orientationListener.getProposedRotation())) { 95 mDisplayContent.mWmService.updateRotation(false /* alwaysSendConfiguration */, 96 false /* forceRelayout */); 97 } 98 }); 99 return true; 100 } 101 102 /** 103 * Decides whether it is necessary to lock screen rotation, preventing auto rotation, based on 104 * the top activity configuration and proposed screen rotation. 105 * 106 * <p>This is needed because immersive apps, such as games, are often not optimized for all 107 * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors 108 * for the gameplay so users can trigger such rotations accidentally when auto rotation is on. 109 * 110 * <p>Screen rotation is locked when the following conditions are met: 111 * <ul> 112 * <li>Top activity requests to hide status and navigation bars 113 * <li>Top activity is fullscreen and in optimal orientation (without letterboxing) 114 * <li>Rotation will lead to letterboxing due to fixed orientation. 115 * <li>{@link DisplayContent#getIgnoreOrientationRequest} is {@code true} 116 * <li>This policy is enabled on the device, for details see 117 * {@link AppCompatConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled} 118 * </ul> 119 * 120 * @param proposedRotation new proposed {@link Surface.Rotation} for the screen. 121 * @return {@code true}, if there is a need to lock screen rotation, {@code false} otherwise. 122 */ isRotationLockEnforced(@urface.Rotation final int proposedRotation)123 boolean isRotationLockEnforced(@Surface.Rotation final int proposedRotation) { 124 if (!mAppCompatConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) { 125 return false; 126 } 127 synchronized (mDisplayContent.mWmService.mGlobalLock) { 128 return isRotationLockEnforcedLocked(proposedRotation); 129 } 130 } 131 isRotationLockEnforcedLocked(@urface.Rotation final int proposedRotation)132 private boolean isRotationLockEnforcedLocked(@Surface.Rotation final int proposedRotation) { 133 if (!mDisplayContent.getIgnoreOrientationRequest()) { 134 return false; 135 } 136 137 final ActivityRecord activityRecord = mDisplayContent.topRunningActivity(); 138 if (activityRecord == null) { 139 return false; 140 } 141 142 // Don't lock screen rotation if an activity hasn't requested to hide system bars. 143 if (!hasRequestedToHideStatusAndNavBars(activityRecord)) { 144 return false; 145 } 146 147 // Don't lock screen rotation if activity is not in fullscreen. Checking windowing mode 148 // for a task rather than an activity to exclude activity embedding scenario. 149 if (activityRecord.getTask() == null 150 || activityRecord.getTask().getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { 151 return false; 152 } 153 154 // Don't lock screen rotation if activity is letterboxed. 155 if (activityRecord.areBoundsLetterboxed()) { 156 return false; 157 } 158 159 if (activityRecord.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) { 160 return false; 161 } 162 163 // Lock screen rotation only if, after rotation the activity's orientation won't match 164 // the screen orientation, forcing the activity to enter letterbox mode after rotation. 165 return activityRecord.getRequestedConfigurationOrientation() 166 != surfaceRotationToConfigurationOrientation(proposedRotation); 167 } 168 169 /** 170 * Checks whether activity has requested to hide status and navigation bars. 171 */ hasRequestedToHideStatusAndNavBars(@onNull ActivityRecord activity)172 private boolean hasRequestedToHideStatusAndNavBars(@NonNull ActivityRecord activity) { 173 WindowState mainWindow = activity.findMainWindow(); 174 if (mainWindow == null) { 175 return false; 176 } 177 return (mainWindow.getRequestedVisibleTypes() 178 & (Type.statusBars() | Type.navigationBars())) == 0; 179 } 180 181 @Orientation surfaceRotationToConfigurationOrientation(@urface.Rotation final int rotation)182 private int surfaceRotationToConfigurationOrientation(@Surface.Rotation final int rotation) { 183 if (mDisplayRotation.isAnyPortrait(rotation)) { 184 return ORIENTATION_PORTRAIT; 185 } else if (mDisplayRotation.isLandscapeOrSeascape(rotation)) { 186 return ORIENTATION_LANDSCAPE; 187 } else { 188 return ORIENTATION_UNDEFINED; 189 } 190 } 191 } 192