• 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.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