• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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_FREEFORM;
20 import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
21 import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET;
22 
23 import static com.android.server.wm.ActivityRecord.State.RESUMED;
24 import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.AppCompatTaskInfo;
29 import android.app.CameraCompatTaskInfo;
30 import android.app.TaskInfo;
31 import android.app.WindowConfiguration.WindowingMode;
32 import android.content.Context;
33 import android.content.res.Configuration;
34 import android.graphics.Rect;
35 import android.view.InsetsSource;
36 import android.view.InsetsState;
37 import android.view.WindowInsets;
38 
39 import com.android.window.flags.Flags;
40 
41 import java.util.function.BooleanSupplier;
42 
43 /**
44  * Utilities for App Compat policies and overrides.
45  */
46 final class AppCompatUtils {
47 
48     /**
49      * Lazy version of a {@link BooleanSupplier} which access an existing BooleanSupplier and
50      * caches the value.
51      *
52      * @param supplier The BooleanSupplier to decorate.
53      * @return A lazy implementation of a BooleanSupplier
54      */
55     @NonNull
asLazy(@onNull BooleanSupplier supplier)56     static BooleanSupplier asLazy(@NonNull BooleanSupplier supplier) {
57         return new BooleanSupplier() {
58             private boolean mRead;
59             private boolean mValue;
60 
61             @Override
62             public boolean getAsBoolean() {
63                 if (!mRead) {
64                     mRead = true;
65                     mValue = supplier.getAsBoolean();
66                 }
67                 return mValue;
68             }
69         };
70     }
71 
72     /**
73      * Returns the aspect ratio of the given {@code rect}.
74      */
computeAspectRatio(@onNull Rect rect)75     static float computeAspectRatio(@NonNull Rect rect) {
76         final int width = rect.width();
77         final int height = rect.height();
78         if (width == 0 || height == 0) {
79             return 0;
80         }
81         return Math.max(width, height) / (float) Math.min(width, height);
82     }
83 
84     /**
85      * @param config The current {@link Configuration}
86      * @return {@code true} if using a VR headset.
87      */
isInVrUiMode(Configuration config)88     static boolean isInVrUiMode(Configuration config) {
89         return (config.uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET;
90     }
91 
92     /**
93      * @param activityRecord The {@link ActivityRecord} for the app package.
94      * @param overrideChangeId The per-app override identifier.
95      * @return {@code true} if the per-app override is enable for the given activity and the
96      * display does not ignore fixed orientation, aspect ratio and resizability of activity.
97      */
isChangeEnabled(@onNull ActivityRecord activityRecord, long overrideChangeId)98     static boolean isChangeEnabled(@NonNull ActivityRecord activityRecord, long overrideChangeId) {
99         return activityRecord.info.isChangeEnabled(overrideChangeId)
100                 && !isDisplayIgnoreActivitySizeRestrictions(activityRecord);
101     }
102 
103     /**
104      * Whether the display ignores fixed orientation, aspect ratio and resizability of activities.
105      */
isDisplayIgnoreActivitySizeRestrictions( @onNull ActivityRecord activityRecord)106     static boolean isDisplayIgnoreActivitySizeRestrictions(
107             @NonNull ActivityRecord activityRecord) {
108         final DisplayContent dc = activityRecord.mDisplayContent;
109         return Flags.vdmForceAppUniversalResizableApi() && dc != null
110                 && dc.isDisplayIgnoreActivitySizeRestrictions();
111     }
112 
113     /**
114      * Attempts to return the app bounds (bounds without insets) of the top most opaque activity. If
115      * these are not available, it defaults to the bounds of the activity which include insets. In
116      * the event the activity is in Size Compat Mode, the Size Compat bounds are returned instead.
117      */
118     @NonNull
getAppBounds(@onNull ActivityRecord activityRecord)119     static Rect getAppBounds(@NonNull ActivityRecord activityRecord) {
120         // TODO(b/268458693): Refactor configuration inheritance in case of translucent activities
121         final Rect appBounds = activityRecord.getConfiguration().windowConfiguration.getAppBounds();
122         if (appBounds == null) {
123             return activityRecord.getBounds();
124         }
125         return activityRecord.mAppCompatController.getTransparentPolicy()
126                 .findOpaqueNotFinishingActivityBelow()
127                 .map(AppCompatUtils::getAppBounds)
128                 .orElseGet(() -> {
129                     if (activityRecord.hasSizeCompatBounds()) {
130                         return activityRecord.getScreenResolvedBounds();
131                     }
132                     return appBounds;
133                 });
134     }
135 
136     static void fillAppCompatTaskInfo(@NonNull Task task, @NonNull TaskInfo info,
137             @Nullable ActivityRecord top) {
138         final AppCompatTaskInfo appCompatTaskInfo = info.appCompatTaskInfo;
139         clearAppCompatTaskInfo(appCompatTaskInfo);
140 
141         if (top == null) {
142             return;
143         }
144         final AppCompatReachabilityOverrides reachabilityOverrides = top.mAppCompatController
145                 .getReachabilityOverrides();
146         final boolean isTopActivityResumed = top.getOrganizedTask() == task && top.isState(RESUMED);
147         final boolean isTopActivityVisible = top.getOrganizedTask() == task && top.isVisible();
148         // Whether the direct top activity is in size compat mode.
149         appCompatTaskInfo.setTopActivityInSizeCompat(
150                 isTopActivityVisible && top.inSizeCompatMode());
151         if (appCompatTaskInfo.isTopActivityInSizeCompat()
152                 && top.mWmService.mAppCompatConfiguration.isTranslucentLetterboxingEnabled()) {
153             // We hide the restart button in case of transparent activities.
154             appCompatTaskInfo.setTopActivityInSizeCompat(top.fillsParent());
155         }
156         // Whether the direct top activity is eligible for letterbox education.
157         appCompatTaskInfo.setEligibleForLetterboxEducation(isTopActivityResumed
158                 && top.mAppCompatController.getLetterboxPolicy()
159                     .isEligibleForLetterboxEducation());
160         appCompatTaskInfo.setLetterboxEducationEnabled(
161                 top.mAppCompatController.getLetterboxOverrides()
162                         .isLetterboxEducationEnabled());
163 
164         appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(top.mAppCompatController
165                 .getDisplayCompatModePolicy().isRestartMenuEnabledForDisplayMove());
166 
167         final AppCompatAspectRatioOverrides aspectRatioOverrides =
168                 top.mAppCompatController.getAspectRatioOverrides();
169         appCompatTaskInfo.setUserFullscreenOverrideEnabled(
170                 aspectRatioOverrides.shouldApplyUserFullscreenOverride());
171         appCompatTaskInfo.setSystemFullscreenOverrideEnabled(
172                 aspectRatioOverrides.isSystemOverrideToFullscreenEnabled());
173 
174         appCompatTaskInfo.setIsFromLetterboxDoubleTap(reachabilityOverrides.isFromDoubleTap());
175 
176         appCompatTaskInfo.topActivityAppBounds.set(getAppBounds(top));
177         final boolean isTopActivityLetterboxed = top.areBoundsLetterboxed();
178         appCompatTaskInfo.setTopActivityLetterboxed(isTopActivityLetterboxed);
179         if (isTopActivityLetterboxed) {
180             final Rect bounds = top.getBounds();
181             appCompatTaskInfo.topActivityLetterboxWidth = bounds.width();
182             appCompatTaskInfo.topActivityLetterboxHeight = bounds.height();
183             // TODO(b/379824541) Remove duplicate information.
184             appCompatTaskInfo.topActivityLetterboxBounds = bounds;
185             // We need to consider if letterboxed or pillarboxed.
186             // TODO(b/336807329) Encapsulate reachability logic
187             appCompatTaskInfo.setLetterboxDoubleTapEnabled(reachabilityOverrides
188                     .isLetterboxDoubleTapEducationEnabled());
189             if (appCompatTaskInfo.isLetterboxDoubleTapEnabled()) {
190                 if (appCompatTaskInfo.isTopActivityPillarboxShaped()) {
191                     if (reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()) {
192                         // Pillarboxed.
193                         appCompatTaskInfo.topActivityLetterboxHorizontalPosition =
194                                 reachabilityOverrides
195                                         .getLetterboxPositionForHorizontalReachability();
196                     } else {
197                         appCompatTaskInfo.setLetterboxDoubleTapEnabled(false);
198                     }
199                 } else {
200                     if (reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()) {
201                         // Letterboxed.
202                         appCompatTaskInfo.topActivityLetterboxVerticalPosition =
203                                 reachabilityOverrides.getLetterboxPositionForVerticalReachability();
204                     } else {
205                         appCompatTaskInfo.setLetterboxDoubleTapEnabled(false);
206                     }
207                 }
208             }
209         }
210 
211         final boolean eligibleForAspectRatioButton =
212                 !info.isTopActivityTransparent && !appCompatTaskInfo.isTopActivityInSizeCompat()
213                         && aspectRatioOverrides.shouldEnableUserAspectRatioSettings();
214         appCompatTaskInfo.setEligibleForUserAspectRatioButton(eligibleForAspectRatioButton);
215         appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode =
216                 AppCompatCameraPolicy.getCameraCompatFreeformMode(top);
217         appCompatTaskInfo.setHasMinAspectRatioOverride(top.mAppCompatController
218                 .getDesktopAspectRatioPolicy().hasMinAspectRatioOverride(task));
219         appCompatTaskInfo.setOptOutEdgeToEdge(top.mOptOutEdgeToEdge);
220     }
221 
222     /**
223      * Returns a string representing the reason for letterboxing. This method assumes the activity
224      * is letterboxed.
225      * @param activityRecord The {@link ActivityRecord} for the letterboxed activity.
226      * @param mainWin   The {@link WindowState} used to letterboxing.
227      */
228     @NonNull
229     static String getLetterboxReasonString(@NonNull ActivityRecord activityRecord,
230             @NonNull WindowState mainWin) {
231         if (activityRecord.inSizeCompatMode()) {
232             return "SIZE_COMPAT_MODE";
233         }
234         final AppCompatAspectRatioPolicy aspectRatioPolicy = activityRecord.mAppCompatController
235                 .getAspectRatioPolicy();
236         if (aspectRatioPolicy.isLetterboxedForFixedOrientationAndAspectRatio()) {
237             return "FIXED_ORIENTATION";
238         }
239         if (mainWin.isLetterboxedForDisplayCutout()) {
240             return "DISPLAY_CUTOUT";
241         }
242         if (aspectRatioPolicy.isLetterboxedForAspectRatioOnly()) {
243             return "ASPECT_RATIO";
244         }
245         if (activityRecord.mAppCompatController.getSafeRegionPolicy()
246                 .isLetterboxedForSafeRegionOnlyAllowed()) {
247             return "SAFE_REGION";
248         }
249         return "UNKNOWN_REASON";
250     }
251 
252     /**
253      * Returns the taskbar in case it is visible and expanded in height, otherwise returns null.
254      */
255     @Nullable
256     static InsetsSource getExpandedTaskbarOrNull(@NonNull final WindowState mainWindow) {
257         final InsetsState state = mainWindow.getInsetsState();
258         for (int i = state.sourceSize() - 1; i >= 0; i--) {
259             final InsetsSource source = state.sourceAt(i);
260             if (source.getType() == WindowInsets.Type.navigationBars()
261                     && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)
262                     && source.isVisible()) {
263                 return source;
264             }
265         }
266         return null;
267     }
268 
269     static void adjustBoundsForTaskbar(@NonNull final WindowState mainWindow,
270             @NonNull final Rect bounds) {
271         // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
272         // an insets frame is equal to a navigation bar which shouldn't affect position of
273         // rounded corners since apps are expected to handle navigation bar inset.
274         // This condition checks whether the taskbar is visible.
275         // Do not crop the taskbar inset if the window is in immersive mode - the user can
276         // swipe to show/hide the taskbar as an overlay.
277         // Adjust the bounds only in case there is an expanded taskbar,
278         // otherwise the rounded corners will be shown behind the navbar.
279         final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow);
280         if (expandedTaskbarOrNull != null) {
281             // Rounded corners should be displayed above the expanded taskbar.
282             bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top);
283         }
284     }
285 
286     static void offsetBounds(@NonNull Configuration inOutConfig, int offsetX, int offsetY) {
287         inOutConfig.windowConfiguration.getBounds().offset(offsetX, offsetY);
288         inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY);
289     }
290 
291     /**
292      * Return {@code true} if window is currently in desktop mode.
293      */
294     static boolean isInDesktopMode(@NonNull Context context,
295             @WindowingMode int parentWindowingMode) {
296         return parentWindowingMode == WINDOWING_MODE_FREEFORM && canEnterDesktopMode(context);
297     }
298 
299     private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) {
300         info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
301         info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
302         info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET;
303         info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET;
304         info.topActivityAppBounds.setEmpty();
305         info.topActivityLetterboxBounds = null;
306         info.cameraCompatTaskInfo.freeformCameraCompatMode =
307                 CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED;
308         info.clearTopActivityFlags();
309     }
310 }
311