• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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