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