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