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