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.res.Configuration.ORIENTATION_LANDSCAPE; 21 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 22 import static android.content.res.Configuration.ORIENTATION_UNDEFINED; 23 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; 24 import static android.view.InsetsState.ITYPE_STATUS_BAR; 25 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.content.res.Configuration.Orientation; 29 import android.view.InsetsVisibilities; 30 import android.view.Surface; 31 32 /** 33 * Policy to decide whether to enforce screen rotation lock for optimisation of the screen rotation 34 * user experience for immersive applications for compatibility when ignoring orientation request. 35 * 36 * <p>This is needed because immersive apps, such as games, are often not optimized for all 37 * orientations and can have a poor UX when rotated (e.g., state loss or entering size-compat mode). 38 * Additionally, some games rely on sensors for the gameplay so users can trigger such rotations 39 * accidentally when auto rotation is on. 40 */ 41 final class DisplayRotationImmersiveAppCompatPolicy { 42 43 @Nullable createIfNeeded( @onNull final LetterboxConfiguration letterboxConfiguration, @NonNull final DisplayRotation displayRotation, @NonNull final DisplayContent displayContent)44 static DisplayRotationImmersiveAppCompatPolicy createIfNeeded( 45 @NonNull final LetterboxConfiguration letterboxConfiguration, 46 @NonNull final DisplayRotation displayRotation, 47 @NonNull final DisplayContent displayContent) { 48 if (!letterboxConfiguration 49 .isDisplayRotationImmersiveAppCompatPolicyEnabled(/* checkDeviceConfig */ false)) { 50 return null; 51 } 52 53 return new DisplayRotationImmersiveAppCompatPolicy( 54 letterboxConfiguration, displayRotation, displayContent); 55 } 56 57 private final DisplayRotation mDisplayRotation; 58 private final LetterboxConfiguration mLetterboxConfiguration; 59 private final DisplayContent mDisplayContent; 60 DisplayRotationImmersiveAppCompatPolicy( @onNull final LetterboxConfiguration letterboxConfiguration, @NonNull final DisplayRotation displayRotation, @NonNull final DisplayContent displayContent)61 private DisplayRotationImmersiveAppCompatPolicy( 62 @NonNull final LetterboxConfiguration letterboxConfiguration, 63 @NonNull final DisplayRotation displayRotation, 64 @NonNull final DisplayContent displayContent) { 65 mDisplayRotation = displayRotation; 66 mLetterboxConfiguration = letterboxConfiguration; 67 mDisplayContent = displayContent; 68 } 69 70 /** 71 * Decides whether it is necessary to lock screen rotation, preventing auto rotation, based on 72 * the top activity configuration and proposed screen rotation. 73 * 74 * <p>This is needed because immersive apps, such as games, are often not optimized for all 75 * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors 76 * for the gameplay so users can trigger such rotations accidentally when auto rotation is on. 77 * 78 * <p>Screen rotation is locked when the following conditions are met: 79 * <ul> 80 * <li>Top activity requests to hide status and navigation bars 81 * <li>Top activity is fullscreen and in optimal orientation (without letterboxing) 82 * <li>Rotation will lead to letterboxing due to fixed orientation. 83 * <li>{@link DisplayContent#getIgnoreOrientationRequest} is {@code true} 84 * <li>This policy is enabled on the device, for details see 85 * {@link LetterboxConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled} 86 * </ul> 87 * 88 * @param proposedRotation new proposed {@link Surface.Rotation} for the screen. 89 * @return {@code true}, if there is a need to lock screen rotation, {@code false} otherwise. 90 */ isRotationLockEnforced(@urface.Rotation final int proposedRotation)91 boolean isRotationLockEnforced(@Surface.Rotation final int proposedRotation) { 92 if (!mLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled( 93 /* checkDeviceConfig */ true)) { 94 return false; 95 } 96 synchronized (mDisplayContent.mWmService.mGlobalLock) { 97 return isRotationLockEnforcedLocked(proposedRotation); 98 } 99 } 100 isRotationLockEnforcedLocked(@urface.Rotation final int proposedRotation)101 private boolean isRotationLockEnforcedLocked(@Surface.Rotation final int proposedRotation) { 102 if (!mDisplayContent.getIgnoreOrientationRequest()) { 103 return false; 104 } 105 106 final ActivityRecord activityRecord = mDisplayContent.topRunningActivity(); 107 if (activityRecord == null) { 108 return false; 109 } 110 111 // Don't lock screen rotation if an activity hasn't requested to hide system bars. 112 if (!hasRequestedToHideStatusAndNavBars(activityRecord)) { 113 return false; 114 } 115 116 // Don't lock screen rotation if activity is not in fullscreen. Checking windowing mode 117 // for a task rather than an activity to exclude activity embedding scenario. 118 if (activityRecord.getTask() == null 119 || activityRecord.getTask().getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { 120 return false; 121 } 122 123 // Don't lock screen rotation if activity is letterboxed. 124 if (activityRecord.areBoundsLetterboxed()) { 125 return false; 126 } 127 128 if (activityRecord.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) { 129 return false; 130 } 131 132 // Lock screen rotation only if, after rotation the activity's orientation won't match 133 // the screen orientation, forcing the activity to enter letterbox mode after rotation. 134 return activityRecord.getRequestedConfigurationOrientation() 135 != surfaceRotationToConfigurationOrientation(proposedRotation); 136 } 137 138 /** 139 * Checks whether activity has requested to hide status and navigation bars. 140 */ hasRequestedToHideStatusAndNavBars(@onNull ActivityRecord activity)141 private boolean hasRequestedToHideStatusAndNavBars(@NonNull ActivityRecord activity) { 142 WindowState mainWindow = activity.findMainWindow(); 143 if (mainWindow == null) { 144 return false; 145 } 146 InsetsVisibilities insetsVisibilities = mainWindow.getRequestedVisibilities(); 147 return !insetsVisibilities.getVisibility(ITYPE_STATUS_BAR) 148 && !insetsVisibilities.getVisibility(ITYPE_NAVIGATION_BAR); 149 } 150 151 @Orientation surfaceRotationToConfigurationOrientation(@urface.Rotation final int rotation)152 private int surfaceRotationToConfigurationOrientation(@Surface.Rotation final int rotation) { 153 if (mDisplayRotation.isAnyPortrait(rotation)) { 154 return ORIENTATION_PORTRAIT; 155 } else if (mDisplayRotation.isLandscapeOrSeascape(rotation)) { 156 return ORIENTATION_LANDSCAPE; 157 } else { 158 return ORIENTATION_UNDEFINED; 159 } 160 } 161 } 162