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.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 21 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_EXCLUDE_PORTRAIT_FULLSCREEN; 22 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE; 23 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; 24 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY; 25 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL; 26 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN_WITH_SPLIT_SCREEN; 27 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 28 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 29 30 import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; 31 import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; 32 33 import android.annotation.NonNull; 34 import android.annotation.Nullable; 35 import android.app.WindowConfiguration; 36 import android.content.pm.ActivityInfo; 37 import android.content.res.Configuration; 38 import android.graphics.Rect; 39 import android.window.DesktopModeFlags; 40 41 /** 42 * Encapsulate app compat policy logic related to aspect ratio. 43 */ 44 class AppCompatAspectRatioPolicy { 45 46 // Rounding tolerance to be used in aspect ratio computations 47 private static final float ASPECT_RATIO_ROUNDING_TOLERANCE = 0.005f; 48 49 @NonNull 50 private final ActivityRecord mActivityRecord; 51 @NonNull 52 private final TransparentPolicy mTransparentPolicy; 53 @NonNull 54 private final AppCompatOverrides mAppCompatOverrides; 55 @NonNull 56 private final AppCompatAspectRatioState mAppCompatAspectRatioState; 57 58 private final Rect mTmpBounds = new Rect(); 59 AppCompatAspectRatioPolicy(@onNull ActivityRecord activityRecord, @NonNull TransparentPolicy transparentPolicy, @NonNull AppCompatOverrides appCompatOverrides)60 AppCompatAspectRatioPolicy(@NonNull ActivityRecord activityRecord, 61 @NonNull TransparentPolicy transparentPolicy, 62 @NonNull AppCompatOverrides appCompatOverrides) { 63 mActivityRecord = activityRecord; 64 mTransparentPolicy = transparentPolicy; 65 mAppCompatOverrides = appCompatOverrides; 66 mAppCompatAspectRatioState = new AppCompatAspectRatioState(); 67 } 68 69 /** 70 * Starts the evaluation of app compat aspect ratio when a new configuration needs to be 71 * resolved. 72 */ reset()73 void reset() { 74 mAppCompatAspectRatioState.reset(); 75 } 76 getDesiredAspectRatio(@onNull Configuration newParentConfig, @NonNull Rect parentBounds)77 private float getDesiredAspectRatio(@NonNull Configuration newParentConfig, 78 @NonNull Rect parentBounds) { 79 final float letterboxAspectRatioOverride = 80 mAppCompatOverrides.getAspectRatioOverrides() 81 .getFixedOrientationLetterboxAspectRatio(newParentConfig); 82 // Aspect ratio as suggested by the system. Apps requested mix/max aspect ratio will 83 // be respected in #applyAspectRatio. 84 if (isDefaultMultiWindowLetterboxAspectRatioDesired(newParentConfig)) { 85 return DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; 86 } else if (letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) { 87 return letterboxAspectRatioOverride; 88 } 89 return AppCompatUtils.computeAspectRatio(parentBounds); 90 } 91 applyDesiredAspectRatio(@onNull Configuration newParentConfig, @NonNull Rect parentBounds, @NonNull Rect resolvedBounds, @NonNull Rect containingBoundsWithInsets, @NonNull Rect containingBounds)92 void applyDesiredAspectRatio(@NonNull Configuration newParentConfig, @NonNull Rect parentBounds, 93 @NonNull Rect resolvedBounds, @NonNull Rect containingBoundsWithInsets, 94 @NonNull Rect containingBounds) { 95 final float desiredAspectRatio = getDesiredAspectRatio(newParentConfig, parentBounds); 96 mAppCompatAspectRatioState.mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, 97 containingBoundsWithInsets, containingBounds, desiredAspectRatio); 98 } 99 applyAspectRatioForLetterbox(Rect outBounds, Rect containingAppBounds, Rect containingBounds)100 void applyAspectRatioForLetterbox(Rect outBounds, Rect containingAppBounds, 101 Rect containingBounds) { 102 mAppCompatAspectRatioState.mIsAspectRatioApplied = applyAspectRatio(outBounds, 103 containingAppBounds, containingBounds, 0 /* desiredAspectRatio */); 104 } 105 106 /** 107 * @return {@code true} when an app compat aspect ratio has been applied. 108 */ isAspectRatioApplied()109 boolean isAspectRatioApplied() { 110 return mAppCompatAspectRatioState.mIsAspectRatioApplied; 111 } 112 113 /** 114 * Returns the min aspect ratio of this activity. 115 */ getMinAspectRatio()116 float getMinAspectRatio() { 117 if (mTransparentPolicy.isRunning()) { 118 return mTransparentPolicy.getInheritedMinAspectRatio(); 119 } 120 121 final ActivityInfo info = mActivityRecord.info; 122 123 // If in camera compat mode, aspect ratio from the camera compat policy has priority over 124 // the default aspect ratio. 125 if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio(mActivityRecord)) { 126 return Math.max(AppCompatCameraPolicy.getCameraCompatMinAspectRatio(mActivityRecord), 127 info.getMinAspectRatio()); 128 } 129 130 final AppCompatAspectRatioOverrides aspectRatioOverrides = 131 mAppCompatOverrides.getAspectRatioOverrides(); 132 if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) { 133 return aspectRatioOverrides.getUserMinAspectRatio(); 134 } 135 if (!aspectRatioOverrides.shouldOverrideMinAspectRatio() 136 && !AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord)) { 137 final float minAspectRatio = info.getMinAspectRatio(); 138 if (minAspectRatio == 0 || mActivityRecord.isUniversalResizeable()) { 139 return 0; 140 } 141 return minAspectRatio; 142 } 143 144 if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY) 145 && !ActivityInfo.isFixedOrientationPortrait( 146 mActivityRecord.getOverrideOrientation())) { 147 return info.getMinAspectRatio(); 148 } 149 150 if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_EXCLUDE_PORTRAIT_FULLSCREEN) 151 && isParentFullscreenPortrait()) { 152 // We are using the parent configuration here as this is the most recent one that gets 153 // passed to onConfigurationChanged when a relevant change takes place 154 return info.getMinAspectRatio(); 155 } 156 157 if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN_WITH_SPLIT_SCREEN)) { 158 return Math.max(aspectRatioOverrides.getSplitScreenAspectRatio(), 159 info.getMinAspectRatio()); 160 } 161 162 if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_LARGE)) { 163 return Math.max(ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, 164 info.getMinAspectRatio()); 165 } 166 167 if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_MEDIUM)) { 168 return Math.max(ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE, 169 info.getMinAspectRatio()); 170 } 171 172 if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_SMALL)) { 173 return Math.max(ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE, 174 info.getMinAspectRatio()); 175 } 176 return info.getMinAspectRatio(); 177 } 178 getMaxAspectRatio()179 float getMaxAspectRatio() { 180 if (mTransparentPolicy.isRunning()) { 181 return mTransparentPolicy.getInheritedMaxAspectRatio(); 182 } 183 final float maxAspectRatio = mActivityRecord.info.getMaxAspectRatio(); 184 if (maxAspectRatio == 0 || mActivityRecord.isUniversalResizeable()) { 185 return 0; 186 } 187 return maxAspectRatio; 188 } 189 190 @Nullable getLetterboxedContainerBounds()191 Rect getLetterboxedContainerBounds() { 192 return mAppCompatAspectRatioState.getLetterboxedContainerBounds(); 193 } 194 195 /** 196 * Whether this activity is letterboxed for fixed orientation. If letterboxed due to fixed 197 * orientation then aspect ratio restrictions are also already respected. 198 * 199 * <p>This happens when an activity has fixed orientation which doesn't match orientation of the 200 * parent because a display setting 'ignoreOrientationRequest' is set to true. See {@link 201 * WindowManagerService#getIgnoreOrientationRequest} for more context. 202 */ isLetterboxedForFixedOrientationAndAspectRatio()203 boolean isLetterboxedForFixedOrientationAndAspectRatio() { 204 return mAppCompatAspectRatioState.isLetterboxedForFixedOrientationAndAspectRatio(); 205 } 206 isLetterboxedForAspectRatioOnly()207 boolean isLetterboxedForAspectRatioOnly() { 208 return mAppCompatAspectRatioState.isLetterboxedForAspectRatioOnly(); 209 } 210 setLetterboxBoundsForFixedOrientationAndAspectRatio(@onNull Rect bounds)211 void setLetterboxBoundsForFixedOrientationAndAspectRatio(@NonNull Rect bounds) { 212 mAppCompatAspectRatioState.mLetterboxBoundsForFixedOrientationAndAspectRatio = bounds; 213 } 214 setLetterboxBoundsForAspectRatio(@onNull Rect bounds)215 void setLetterboxBoundsForAspectRatio(@NonNull Rect bounds) { 216 mAppCompatAspectRatioState.mLetterboxBoundsForAspectRatio = bounds; 217 } 218 219 /** 220 * Returns true if the activity has maximum or minimum aspect ratio. 221 */ hasFixedAspectRatio()222 boolean hasFixedAspectRatio() { 223 return getMaxAspectRatio() != 0 || getMinAspectRatio() != 0; 224 } 225 226 /** 227 * Resolves aspect ratio restrictions for an activity. If the bounds are restricted by 228 * aspect ratio, the position will be adjusted later in {@link #updateResolvedBoundsPosition} 229 * within parent's app bounds to balance the visual appearance. The policy of aspect ratio has 230 * higher priority than the requested override bounds. 231 */ resolveAspectRatioRestrictionIfNeeded(@onNull Configuration newParentConfiguration)232 void resolveAspectRatioRestrictionIfNeeded(@NonNull Configuration newParentConfiguration) { 233 // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds 234 // are already calculated in resolveFixedOrientationConfiguration. 235 // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. 236 if (isLetterboxedForFixedOrientationAndAspectRatio() 237 || getOverrides().hasFullscreenOverride()) { 238 return; 239 } 240 final Configuration resolvedConfig = mActivityRecord.getResolvedOverrideConfiguration(); 241 final Rect parentAppBounds = 242 mActivityRecord.mResolveConfigHint.mParentAppBoundsOverride; 243 final Rect parentBounds = mActivityRecord.mResolveConfigHint.mParentBoundsOverride; 244 final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); 245 // Use tmp bounds to calculate aspect ratio so we can know whether the activity should 246 // use restricted size (resolved bounds may be the requested override bounds). 247 mTmpBounds.setEmpty(); 248 applyAspectRatioForLetterbox(mTmpBounds, parentAppBounds, parentBounds); 249 // If the out bounds is not empty, it means the activity cannot fill parent's app 250 // bounds, then they should be aligned later in #updateResolvedBoundsPosition(). 251 if (!mTmpBounds.isEmpty()) { 252 resolvedBounds.set(mTmpBounds); 253 } 254 if (!resolvedBounds.isEmpty() && !resolvedBounds.equals(parentBounds)) { 255 // Compute the configuration based on the resolved bounds. If aspect ratio doesn't 256 // restrict, the bounds should be the requested override bounds. 257 // TODO(b/384473893): Improve ActivityRecord usage here. 258 mActivityRecord.mResolveConfigHint.mTmpOverrideDisplayInfo = 259 mActivityRecord.getFixedRotationTransformDisplayInfo(); 260 mActivityRecord.computeConfigByResolveHint(resolvedConfig, newParentConfiguration); 261 setLetterboxBoundsForAspectRatio(new Rect(resolvedBounds)); 262 } 263 } 264 isParentFullscreenPortrait()265 private boolean isParentFullscreenPortrait() { 266 final WindowContainer<?> parent = mActivityRecord.getParent(); 267 return parent != null 268 && parent.getConfiguration().orientation == ORIENTATION_PORTRAIT 269 && parent.getWindowConfiguration().getWindowingMode() == WINDOWING_MODE_FULLSCREEN; 270 } 271 272 /** 273 * Applies aspect ratio restrictions to outBounds. If no restrictions, then no change is 274 * made to outBounds. 275 * 276 * @return {@code true} if aspect ratio restrictions were applied. 277 */ applyAspectRatio(Rect outBounds, Rect containingAppBounds, Rect containingBounds, float desiredAspectRatio)278 private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, 279 Rect containingBounds, float desiredAspectRatio) { 280 final float maxAspectRatio = getMaxAspectRatio(); 281 final Task rootTask = mActivityRecord.getRootTask(); 282 final Task task = mActivityRecord.getTask(); 283 final float minAspectRatio = getMinAspectRatio(); 284 final TaskFragment organizedTf = mActivityRecord.getOrganizedTaskFragment(); 285 float aspectRatioToApply = desiredAspectRatio; 286 if (task == null || rootTask == null 287 || (maxAspectRatio < 1 && minAspectRatio < 1 && aspectRatioToApply < 1) 288 // Don't set aspect ratio if we are in VR mode. 289 || AppCompatUtils.isInVrUiMode(mActivityRecord.getConfiguration()) 290 // TODO(b/232898850): Always respect aspect ratio requests. 291 // Don't set aspect ratio for activity in ActivityEmbedding split. 292 || (organizedTf != null && !organizedTf.fillsParent()) 293 // Don't set aspect ratio for resizeable activities in freeform. 294 // {@link ActivityRecord#shouldCreateAppCompatDisplayInsets()} will be false for 295 // both activities that are naturally resizeable and activities that have been 296 // forced resizeable. 297 // Camera compat mode is an exception to this, where the activity is letterboxed 298 // to an aspect ratio commonly found on phones, e.g. 16:9, to avoid issues like 299 // stretching of the camera preview. 300 || (DesktopModeFlags 301 .IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES.isTrue() 302 && task.getWindowingMode() == WINDOWING_MODE_FREEFORM 303 && !mActivityRecord.shouldCreateAppCompatDisplayInsets() 304 && !AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio( 305 mActivityRecord))) { 306 return false; 307 } 308 309 final int containingAppWidth = containingAppBounds.width(); 310 final int containingAppHeight = containingAppBounds.height(); 311 final float containingRatio = AppCompatUtils.computeAspectRatio(containingAppBounds); 312 313 if (aspectRatioToApply < 1) { 314 aspectRatioToApply = containingRatio; 315 } 316 317 if (maxAspectRatio >= 1 && aspectRatioToApply > maxAspectRatio) { 318 aspectRatioToApply = maxAspectRatio; 319 } else if (minAspectRatio >= 1 && aspectRatioToApply < minAspectRatio) { 320 aspectRatioToApply = minAspectRatio; 321 } 322 323 int activityWidth = containingAppWidth; 324 int activityHeight = containingAppHeight; 325 326 if (containingRatio - aspectRatioToApply > ASPECT_RATIO_ROUNDING_TOLERANCE) { 327 if (containingAppWidth < containingAppHeight) { 328 // Width is the shorter side, so we use that to figure-out what the max. height 329 // should be given the aspect ratio. 330 activityHeight = (int) ((activityWidth * aspectRatioToApply) + 0.5f); 331 } else { 332 // Height is the shorter side, so we use that to figure-out what the max. width 333 // should be given the aspect ratio. 334 activityWidth = (int) ((activityHeight * aspectRatioToApply) + 0.5f); 335 } 336 } else if (aspectRatioToApply - containingRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) { 337 boolean adjustWidth; 338 switch (mActivityRecord.getRequestedConfigurationOrientation()) { 339 case ORIENTATION_LANDSCAPE: 340 // Width should be the longer side for this landscape app, so we use the width 341 // to figure-out what the max. height should be given the aspect ratio. 342 adjustWidth = false; 343 break; 344 case ORIENTATION_PORTRAIT: 345 // Height should be the longer side for this portrait app, so we use the height 346 // to figure-out what the max. width should be given the aspect ratio. 347 adjustWidth = true; 348 break; 349 default: 350 // This app doesn't have a preferred orientation, so we keep the length of the 351 // longer side, and use it to figure-out the length of the shorter side. 352 if (containingAppWidth < containingAppHeight) { 353 // Width is the shorter side, so we use the height to figure-out what the 354 // max. width should be given the aspect ratio. 355 adjustWidth = true; 356 } else { 357 // Height is the shorter side, so we use the width to figure-out what the 358 // max. height should be given the aspect ratio. 359 adjustWidth = false; 360 } 361 break; 362 } 363 if (adjustWidth) { 364 activityWidth = (int) ((activityHeight / aspectRatioToApply) + 0.5f); 365 } else { 366 activityHeight = (int) ((activityWidth / aspectRatioToApply) + 0.5f); 367 } 368 } 369 370 if (containingAppWidth <= activityWidth && containingAppHeight <= activityHeight) { 371 // The display matches or is less than the activity aspect ratio, so nothing else to do. 372 return false; 373 } 374 375 // Compute configuration based on max or min supported width and height. 376 // Also account for the insets (e.g. display cutouts, navigation bar), which will be 377 // clipped away later in {@link Task#computeConfigResourceOverrides()}, i.e., the out 378 // bounds are the app bounds restricted by aspect ratio + clippable insets. Otherwise, 379 // the app bounds would end up too small. To achieve this we will also add clippable insets 380 // when the corresponding dimension fully fills the parent 381 382 int right = activityWidth + containingAppBounds.left; 383 int left = containingAppBounds.left; 384 if (right >= containingAppBounds.right) { 385 right = containingBounds.right; 386 left = containingBounds.left; 387 } 388 int bottom = activityHeight + containingAppBounds.top; 389 int top = containingAppBounds.top; 390 if (bottom >= containingAppBounds.bottom) { 391 bottom = containingBounds.bottom; 392 top = containingBounds.top; 393 } 394 outBounds.set(left, top, right, bottom); 395 return true; 396 } 397 398 /** 399 * Returns {@code true} if the default aspect ratio for a letterboxed app in multi-window mode 400 * should be used. 401 */ isDefaultMultiWindowLetterboxAspectRatioDesired( @onNull Configuration parentConfig)402 private boolean isDefaultMultiWindowLetterboxAspectRatioDesired( 403 @NonNull Configuration parentConfig) { 404 final DisplayContent dc = mActivityRecord.mDisplayContent; 405 if (dc == null) { 406 return false; 407 } 408 final int windowingMode = parentConfig.windowConfiguration.getWindowingMode(); 409 return WindowConfiguration.inMultiWindowMode(windowingMode) 410 && !dc.getIgnoreOrientationRequest(); 411 } 412 413 @NonNull getOverrides()414 private AppCompatAspectRatioOverrides getOverrides() { 415 return mActivityRecord.mAppCompatController.getAspectRatioOverrides(); 416 } 417 418 private static class AppCompatAspectRatioState { 419 // Whether the aspect ratio restrictions applied to the activity bounds 420 // in applyAspectRatio(). 421 private boolean mIsAspectRatioApplied = false; 422 423 // Bounds populated in resolveAspectRatioRestriction when this activity is letterboxed for 424 // aspect ratio. If not null, they are used as parent container in 425 // resolveSizeCompatModeConfiguration and in a constructor of CompatDisplayInsets. 426 @Nullable 427 private Rect mLetterboxBoundsForAspectRatio; 428 // Bounds populated in resolveFixedOrientationConfiguration when this activity is 429 // letterboxed for fixed orientation. If not null, they are used as parent container in 430 // resolveSizeCompatModeConfiguration and in a constructor of CompatDisplayInsets. If 431 // letterboxed due to fixed orientation then aspect ratio restrictions are also respected. 432 // This happens when an activity has fixed orientation which doesn't match orientation of 433 // the parent because a display is ignoring orientation request or fixed to user rotation. 434 // See WindowManagerService#getIgnoreOrientationRequest and 435 // WindowManagerService#getFixedToUserRotation for more context. 436 @Nullable 437 private Rect mLetterboxBoundsForFixedOrientationAndAspectRatio; 438 439 @Nullable getLetterboxedContainerBounds()440 Rect getLetterboxedContainerBounds() { 441 return mLetterboxBoundsForFixedOrientationAndAspectRatio != null 442 ? mLetterboxBoundsForFixedOrientationAndAspectRatio 443 : mLetterboxBoundsForAspectRatio; 444 } 445 reset()446 void reset() { 447 mIsAspectRatioApplied = false; 448 mLetterboxBoundsForFixedOrientationAndAspectRatio = null; 449 mLetterboxBoundsForAspectRatio = null; 450 } 451 isLetterboxedForFixedOrientationAndAspectRatio()452 boolean isLetterboxedForFixedOrientationAndAspectRatio() { 453 return mLetterboxBoundsForFixedOrientationAndAspectRatio != null; 454 } 455 isLetterboxedForAspectRatioOnly()456 boolean isLetterboxedForAspectRatioOnly() { 457 return mLetterboxBoundsForAspectRatio != null; 458 } 459 } 460 } 461