1 /*
2  * Copyright (C) 2021 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.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21 import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
22 import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
23 import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
24 import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
25 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
26 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
27 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
28 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
29 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
30 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
31 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
32 import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
33 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
34 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
35 import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
36 import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION;
37 import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
38 import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
39 import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
40 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
41 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
42 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
43 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
44 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
45 import static android.content.pm.ActivityInfo.isFixedOrientation;
46 import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
47 import static android.content.pm.ActivityInfo.screenOrientationToString;
48 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
49 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
50 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
51 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT;
52 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
53 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
54 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
55 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
56 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
57 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
58 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
59 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
60 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
61 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
62 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
63 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
64 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
65 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
66 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
67 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
68 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
69 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
70 import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
71 import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
72 
73 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
74 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
75 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
76 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
77 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
78 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
79 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
80 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
81 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
82 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT;
83 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP;
84 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
85 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
86 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
87 import static com.android.server.wm.ActivityRecord.computeAspectRatio;
88 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
89 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
90 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
91 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
92 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
93 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
94 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
95 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
96 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
97 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
98 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
99 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
100 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
101 import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
102 import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString;
103 
104 import android.annotation.NonNull;
105 import android.annotation.Nullable;
106 import android.app.ActivityManager.TaskDescription;
107 import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
108 import android.content.pm.ActivityInfo.ScreenOrientation;
109 import android.content.pm.PackageManager;
110 import android.content.res.Configuration;
111 import android.content.res.Resources;
112 import android.graphics.Color;
113 import android.graphics.Point;
114 import android.graphics.Rect;
115 import android.os.RemoteException;
116 import android.util.Slog;
117 import android.view.InsetsSource;
118 import android.view.InsetsState;
119 import android.view.RoundedCorner;
120 import android.view.SurfaceControl;
121 import android.view.SurfaceControl.Transaction;
122 import android.view.WindowInsets;
123 import android.view.WindowManager;
124 
125 import com.android.internal.R;
126 import com.android.internal.annotations.VisibleForTesting;
127 import com.android.internal.statusbar.LetterboxDetails;
128 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
129 import com.android.server.wm.utils.OptPropFactory;
130 import com.android.server.wm.utils.OptPropFactory.OptProp;
131 import com.android.window.flags.Flags;
132 
133 import java.io.PrintWriter;
134 import java.util.function.BooleanSupplier;
135 
136 /** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */
137 // TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in
138 // SizeCompatTests and LetterboxTests but not all.
139 // TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the
140 // hierarchy in addition to ActivityRecord (Task, DisplayArea, ...).
141 // TODO(b/263021211): Consider renaming to more generic CompatUIController.
142 final class LetterboxUiController {
143 
144     private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
145 
146     // Minimum value of mSetOrientationRequestCounter before qualifying as orientation request loop
147     @VisibleForTesting
148     static final int MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP = 2;
149     // Used to determine reset of mSetOrientationRequestCounter if next app requested
150     // orientation is after timeout value
151     @VisibleForTesting
152     static final int SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS = 1000;
153 
154     private final Point mTmpPoint = new Point();
155 
156     private final LetterboxConfiguration mLetterboxConfiguration;
157 
158     private final ActivityRecord mActivityRecord;
159 
160     // TODO(b/265576778): Cache other overrides as well.
161 
162     // Corresponds to OVERRIDE_ANY_ORIENTATION
163     private final boolean mIsOverrideAnyOrientationEnabled;
164     // Corresponds to OVERRIDE_ANY_ORIENTATION_TO_USER
165     private final boolean mIsSystemOverrideToFullscreenEnabled;
166     // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
167     private final boolean mIsOverrideToPortraitOrientationEnabled;
168     // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR
169     private final boolean mIsOverrideToNosensorOrientationEnabled;
170     // Corresponds to OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE
171     private final boolean mIsOverrideToReverseLandscapeOrientationEnabled;
172     // Corresponds to OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA
173     private final boolean mIsOverrideOrientationOnlyForCameraEnabled;
174     // Corresponds to OVERRIDE_RESPECT_REQUESTED_ORIENTATION
175     private final boolean mIsOverrideRespectRequestedOrientationEnabled;
176 
177     @NonNull
178     private final OptProp mAllowOrientationOverrideOptProp;
179     @NonNull
180     private final OptProp mAllowDisplayOrientationOverrideOptProp;
181     @NonNull
182     private final OptProp mAllowMinAspectRatioOverrideOptProp;
183     @NonNull
184     private final OptProp mAllowForceResizeOverrideOptProp;
185 
186     @NonNull
187     private final OptProp mAllowUserAspectRatioOverrideOptProp;
188     @NonNull
189     private final OptProp mAllowUserAspectRatioFullscreenOverrideOptProp;
190 
191     private boolean mShowWallpaperForLetterboxBackground;
192 
193     // Updated when ActivityRecord#setRequestedOrientation is called
194     private long mTimeMsLastSetOrientationRequest = 0;
195 
196     // Counter for ActivityRecord#setRequestedOrientation
197     private int mSetOrientationRequestCounter = 0;
198 
199     // TODO(b/315140179): Make mUserAspectRatio final
200     // The min aspect ratio override set by user
201     @PackageManager.UserMinAspectRatio
202     private int mUserAspectRatio = USER_MIN_ASPECT_RATIO_UNSET;
203 
204     @Nullable
205     private Letterbox mLetterbox;
206 
207     @NonNull
208     private final OptProp mCameraCompatAllowForceRotationOptProp;
209 
210     @NonNull
211     private final OptProp mCameraCompatAllowRefreshOptProp;
212 
213     @NonNull
214     private final OptProp mCameraCompatEnableRefreshViaPauseOptProp;
215 
216     // Whether activity "refresh" was requested but not finished in
217     // ActivityRecord#activityResumedLocked following the camera compat force rotation in
218     // DisplayRotationCompatPolicy.
219     private boolean mIsRefreshRequested;
220 
221     @NonNull
222     private final OptProp mIgnoreRequestedOrientationOptProp;
223 
224     @NonNull
225     private final OptProp mAllowIgnoringOrientationRequestWhenLoopDetectedOptProp;
226 
227     @NonNull
228     private final OptProp mFakeFocusOptProp;
229 
230     private boolean mIsRelaunchingAfterRequestedOrientationChanged;
231 
232     private boolean mLastShouldShowLetterboxUi;
233 
234     private boolean mDoubleTapEvent;
235 
236     @FreeformCameraCompatMode
237     private int mFreeformCameraCompatMode = CAMERA_COMPAT_FREEFORM_NONE;
238 
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord)239     LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
240         mLetterboxConfiguration = wmService.mLetterboxConfiguration;
241         // Given activityRecord may not be fully constructed since LetterboxUiController
242         // is created in its constructor. It shouldn't be used in this constructor but it's safe
243         // to use it after since controller is only used in ActivityRecord.
244         mActivityRecord = activityRecord;
245 
246         PackageManager packageManager = wmService.mContext.getPackageManager();
247 
248         final OptPropFactory optPropBuilder = new OptPropFactory(packageManager,
249                 activityRecord.packageName);
250 
251         final BooleanSupplier isPolicyForIgnoringRequestedOrientationEnabled = asLazy(
252                 mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled);
253         mIgnoreRequestedOrientationOptProp = optPropBuilder.create(
254                 PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION,
255                 isPolicyForIgnoringRequestedOrientationEnabled);
256         mAllowIgnoringOrientationRequestWhenLoopDetectedOptProp = optPropBuilder.create(
257                 PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED,
258                 isPolicyForIgnoringRequestedOrientationEnabled);
259 
260         mFakeFocusOptProp = optPropBuilder.create(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS,
261                 mLetterboxConfiguration::isCompatFakeFocusEnabled);
262 
263         final BooleanSupplier isCameraCompatTreatmentEnabled = asLazy(
264                 mLetterboxConfiguration::isCameraCompatTreatmentEnabled);
265         mCameraCompatAllowForceRotationOptProp = optPropBuilder.create(
266                 PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION,
267                 isCameraCompatTreatmentEnabled);
268         mCameraCompatAllowRefreshOptProp = optPropBuilder.create(
269                 PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH,
270                 isCameraCompatTreatmentEnabled);
271         mCameraCompatEnableRefreshViaPauseOptProp = optPropBuilder.create(
272                 PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE,
273                 isCameraCompatTreatmentEnabled);
274 
275         mAllowOrientationOverrideOptProp = optPropBuilder.create(
276                 PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
277 
278         mAllowDisplayOrientationOverrideOptProp = optPropBuilder.create(
279                 PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE,
280                 () -> mActivityRecord.mDisplayContent != null
281                         && mActivityRecord.getTask() != null
282                         && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()
283                         && !mActivityRecord.getTask().inMultiWindowMode()
284                         && mActivityRecord.mDisplayContent.getNaturalOrientation()
285                             == ORIENTATION_LANDSCAPE
286         );
287 
288         mAllowMinAspectRatioOverrideOptProp = optPropBuilder.create(
289                 PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
290 
291         mAllowForceResizeOverrideOptProp = optPropBuilder.create(
292                 PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES);
293 
294         mAllowUserAspectRatioOverrideOptProp = optPropBuilder.create(
295                 PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE,
296                 mLetterboxConfiguration::isUserAppAspectRatioSettingsEnabled);
297 
298         mAllowUserAspectRatioFullscreenOverrideOptProp = optPropBuilder.create(
299                 PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE,
300                 mLetterboxConfiguration::isUserAppAspectRatioFullscreenEnabled);
301 
302         mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION);
303         mIsSystemOverrideToFullscreenEnabled =
304                 isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION_TO_USER);
305         mIsOverrideToPortraitOrientationEnabled =
306                 isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT);
307         mIsOverrideToReverseLandscapeOrientationEnabled =
308                 isCompatChangeEnabled(OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE);
309         mIsOverrideToNosensorOrientationEnabled =
310                 isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
311         mIsOverrideOrientationOnlyForCameraEnabled =
312                 isCompatChangeEnabled(OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA);
313         mIsOverrideRespectRequestedOrientationEnabled =
314                 isCompatChangeEnabled(OVERRIDE_RESPECT_REQUESTED_ORIENTATION);
315     }
316 
317     /** Cleans up {@link Letterbox} if it exists.*/
destroy()318     void destroy() {
319         if (mLetterbox != null) {
320             mLetterbox.destroy();
321             mLetterbox = null;
322         }
323         mActivityRecord.mTransparentPolicy.stop();
324     }
325 
onMovedToDisplay(int displayId)326     void onMovedToDisplay(int displayId) {
327         if (mLetterbox != null) {
328             mLetterbox.onMovedToDisplay(displayId);
329         }
330     }
331 
332     /**
333      * Whether should ignore app requested orientation in response to an app
334      * calling {@link android.app.Activity#setRequestedOrientation}.
335      *
336      * <p>This is needed to avoid getting into {@link android.app.Activity#setRequestedOrientation}
337      * loop when {@link DisplayContent#getIgnoreOrientationRequest} is enabled or device has
338      * landscape natural orientation which app developers don't expect. For example, the loop can
339      * look like this:
340      * <ol>
341      *     <li>App sets default orientation to "unspecified" at runtime
342      *     <li>App requests to "portrait" after checking some condition (e.g. display rotation).
343      *     <li>(2) leads to fullscreen -> letterboxed bounds change and activity relaunch because
344      *     app can't handle the corresponding config changes.
345      *     <li>Loop goes back to (1)
346      * </ol>
347      *
348      * <p>This treatment is enabled when the following conditions are met:
349      * <ul>
350      *     <li>Flag gating the treatment is enabled
351      *     <li>Opt-out component property isn't enabled
352      *     <li>Opt-in component property or per-app override are enabled
353      *     <li>Activity is relaunched after {@link android.app.Activity#setRequestedOrientation}
354      *     call from an app or camera compat force rotation treatment is active for the activity.
355      *     <li>Orientation request loop detected and is not letterboxed for fixed orientation
356      * </ul>
357      */
shouldIgnoreRequestedOrientation(@creenOrientation int requestedOrientation)358     boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
359         if (mIgnoreRequestedOrientationOptProp.shouldEnableWithOverrideAndProperty(
360                 isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION))) {
361             if (mIsRelaunchingAfterRequestedOrientationChanged) {
362                 Slog.w(TAG, "Ignoring orientation update to "
363                         + screenOrientationToString(requestedOrientation)
364                         + " due to relaunching after setRequestedOrientation for "
365                         + mActivityRecord);
366                 return true;
367             }
368             if (isCameraCompatTreatmentActive()) {
369                 Slog.w(TAG, "Ignoring orientation update to "
370                         + screenOrientationToString(requestedOrientation)
371                         + " due to camera compat treatment for " + mActivityRecord);
372                 return true;
373             }
374         }
375 
376         if (shouldIgnoreOrientationRequestLoop()) {
377             Slog.w(TAG, "Ignoring orientation update to "
378                     + screenOrientationToString(requestedOrientation)
379                     + " as orientation request loop was detected for "
380                     + mActivityRecord);
381             return true;
382         }
383         return false;
384     }
385 
386     /**
387      * Whether an app is calling {@link android.app.Activity#setRequestedOrientation}
388      * in a loop and orientation request should be ignored.
389      *
390      * <p>This should only be called once in response to
391      * {@link android.app.Activity#setRequestedOrientation}. See
392      * {@link #shouldIgnoreRequestedOrientation} for more details.
393      *
394      * <p>This treatment is enabled when the following conditions are met:
395      * <ul>
396      *     <li>Flag gating the treatment is enabled
397      *     <li>Opt-out component property isn't enabled
398      *     <li>Per-app override is enabled
399      *     <li>App has requested orientation more than 2 times within 1-second
400      *     timer and activity is not letterboxed for fixed orientation
401      * </ul>
402      */
shouldIgnoreOrientationRequestLoop()403     boolean shouldIgnoreOrientationRequestLoop() {
404         final boolean loopDetectionEnabled = isCompatChangeEnabled(
405                 OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED);
406         if (!mAllowIgnoringOrientationRequestWhenLoopDetectedOptProp
407                 .shouldEnableWithOptInOverrideAndOptOutProperty(loopDetectionEnabled)) {
408             return false;
409         }
410 
411         final long currTimeMs = System.currentTimeMillis();
412         if (currTimeMs - mTimeMsLastSetOrientationRequest
413                 < SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS) {
414             mSetOrientationRequestCounter += 1;
415         } else {
416             // Resets app setOrientationRequest counter if timed out
417             mSetOrientationRequestCounter = 0;
418         }
419         // Update time last called
420         mTimeMsLastSetOrientationRequest = currTimeMs;
421 
422         return mSetOrientationRequestCounter >= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP
423                 && !mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio();
424     }
425 
426     @VisibleForTesting
getSetOrientationRequestCounter()427     int getSetOrientationRequestCounter() {
428         return mSetOrientationRequestCounter;
429     }
430 
431     /**
432      * Whether sending compat fake focus for split screen resumed activities is enabled. Needed
433      * because some game engines wait to get focus before drawing the content of the app which isn't
434      * guaranteed by default in multi-window modes.
435      *
436      * <p>This treatment is enabled when the following conditions are met:
437      * <ul>
438      *     <li>Flag gating the treatment is enabled
439      *     <li>Component property is NOT set to false
440      *     <li>Component property is set to true or per-app override is enabled
441      * </ul>
442      */
shouldSendFakeFocus()443     boolean shouldSendFakeFocus() {
444         return mFakeFocusOptProp.shouldEnableWithOverrideAndProperty(
445                 isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS));
446     }
447 
448     /**
449      * Whether we should apply the min aspect ratio per-app override. When this override is applied
450      * the min aspect ratio given in the app's manifest will be overridden to the largest enabled
451      * aspect ratio treatment unless the app's manifest value is higher. The treatment will also
452      * apply if no value is provided in the manifest.
453      *
454      * <p>This method returns {@code true} when the following conditions are met:
455      * <ul>
456      *     <li>Opt-out component property isn't enabled
457      *     <li>Per-app override is enabled
458      * </ul>
459      */
shouldOverrideMinAspectRatio()460     boolean shouldOverrideMinAspectRatio() {
461         return mAllowMinAspectRatioOverrideOptProp.shouldEnableWithOptInOverrideAndOptOutProperty(
462                 isCompatChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO));
463     }
464 
465     /**
466      * Whether we should apply the min aspect ratio per-app override only when an app is connected
467      * to the camera.
468      * When this override is applied the min aspect ratio given in the app's manifest will be
469      * overridden to the largest enabled aspect ratio treatment unless the app's manifest value
470      * is higher. The treatment will also apply if no value is provided in the manifest.
471      *
472      * <p>This method returns {@code true} when the following conditions are met:
473      * <ul>
474      *     <li>Opt-out component property isn't enabled
475      *     <li>Per-app override is enabled
476      * </ul>
477      */
shouldOverrideMinAspectRatioForCamera()478     boolean shouldOverrideMinAspectRatioForCamera() {
479         return mActivityRecord.isCameraActive()
480                 && mAllowMinAspectRatioOverrideOptProp
481                 .shouldEnableWithOptInOverrideAndOptOutProperty(
482                         isCompatChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA));
483     }
484 
485     /**
486      * Whether we should apply the force resize per-app override. When this override is applied it
487      * forces the packages it is applied to to be resizable. It won't change whether the app can be
488      * put into multi-windowing mode, but allow the app to resize without going into size-compat
489      * mode when the window container resizes, such as display size change or screen rotation.
490      *
491      * <p>This method returns {@code true} when the following conditions are met:
492      * <ul>
493      *     <li>Opt-out component property isn't enabled
494      *     <li>Per-app override is enabled
495      * </ul>
496      */
shouldOverrideForceResizeApp()497     boolean shouldOverrideForceResizeApp() {
498         return mAllowForceResizeOverrideOptProp.shouldEnableWithOptInOverrideAndOptOutProperty(
499                 isCompatChangeEnabled(FORCE_RESIZE_APP));
500     }
501 
502     /**
503      * Whether we should apply the force non resize per-app override. When this override is applied
504      * it forces the packages it is applied to to be non-resizable.
505      *
506      * <p>This method returns {@code true} when the following conditions are met:
507      * <ul>
508      *     <li>Opt-out component property isn't enabled
509      *     <li>Per-app override is enabled
510      * </ul>
511      */
shouldOverrideForceNonResizeApp()512     boolean shouldOverrideForceNonResizeApp() {
513         return mAllowForceResizeOverrideOptProp.shouldEnableWithOptInOverrideAndOptOutProperty(
514                 isCompatChangeEnabled(FORCE_NON_RESIZE_APP));
515     }
516 
517     /**
518      * Sets whether an activity is relaunching after the app has called {@link
519      * android.app.Activity#setRequestedOrientation}.
520      */
setRelaunchingAfterRequestedOrientationChanged(boolean isRelaunching)521     void setRelaunchingAfterRequestedOrientationChanged(boolean isRelaunching) {
522         mIsRelaunchingAfterRequestedOrientationChanged = isRelaunching;
523     }
524 
525     /**
526      * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}.
527      */
isRefreshRequested()528     boolean isRefreshRequested() {
529         return mIsRefreshRequested;
530     }
531 
setIsRefreshRequested(boolean isRequested)532     void setIsRefreshRequested(boolean isRequested) {
533         mIsRefreshRequested = isRequested;
534     }
535 
isOverrideRespectRequestedOrientationEnabled()536     boolean isOverrideRespectRequestedOrientationEnabled() {
537         return mIsOverrideRespectRequestedOrientationEnabled;
538     }
539 
540     /**
541      * Whether should fix display orientation to landscape natural orientation when a task is
542      * fullscreen and the display is ignoring orientation requests.
543      *
544      * <p>This treatment is enabled when the following conditions are met:
545      * <ul>
546      *     <li>Opt-out component property isn't enabled
547      *     <li>Opt-in per-app override is enabled
548      *     <li>Task is in fullscreen.
549      *     <li>{@link DisplayContent#getIgnoreOrientationRequest} is enabled
550      *     <li>Natural orientation of the display is landscape.
551      * </ul>
552      */
shouldUseDisplayLandscapeNaturalOrientation()553     boolean shouldUseDisplayLandscapeNaturalOrientation() {
554         return mAllowDisplayOrientationOverrideOptProp
555                 .shouldEnableWithOptInOverrideAndOptOutProperty(
556                         isCompatChangeEnabled(OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION));
557     }
558 
559     @ScreenOrientation
overrideOrientationIfNeeded(@creenOrientation int candidate)560     int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
561         final DisplayContent displayContent = mActivityRecord.mDisplayContent;
562         final boolean isIgnoreOrientationRequestEnabled = displayContent != null
563                 && displayContent.getIgnoreOrientationRequest();
564         if (shouldApplyUserFullscreenOverride() && isIgnoreOrientationRequestEnabled
565                 // Do not override orientation to fullscreen for camera activities.
566                 // Fixed-orientation activities are rarely tested in other orientations, and it
567                 // often results in sideways or stretched previews. As the camera compat treatment
568                 // targets fixed-orientation activities, overriding the orientation disables the
569                 // treatment.
570                 && !mActivityRecord.isCameraActive()) {
571             Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
572                     + mActivityRecord + " is overridden to "
573                     + screenOrientationToString(SCREEN_ORIENTATION_USER)
574                     + " by user aspect ratio settings.");
575             return SCREEN_ORIENTATION_USER;
576         }
577 
578         // In some cases (e.g. Kids app) we need to map the candidate orientation to some other
579         // orientation.
580         candidate = mActivityRecord.mWmService.mapOrientationRequest(candidate);
581 
582         if (shouldApplyUserMinAspectRatioOverride() && (!isFixedOrientation(candidate)
583                 || candidate == SCREEN_ORIENTATION_LOCKED)) {
584             Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
585                     + mActivityRecord + " is overridden to "
586                     + screenOrientationToString(SCREEN_ORIENTATION_PORTRAIT)
587                     + " by user aspect ratio settings.");
588             return SCREEN_ORIENTATION_PORTRAIT;
589         }
590 
591         if (mAllowOrientationOverrideOptProp.isFalse()) {
592             return candidate;
593         }
594 
595         if (mIsOverrideOrientationOnlyForCameraEnabled && displayContent != null
596                 && (displayContent.mDisplayRotationCompatPolicy == null
597                         || !displayContent.mDisplayRotationCompatPolicy
598                                 .isActivityEligibleForOrientationOverride(mActivityRecord))) {
599             return candidate;
600         }
601 
602         // mUserAspectRatio is always initialized first in shouldApplyUserFullscreenOverride(),
603         // which will always come first before this check as user override > device
604         // manufacturer override.
605         if (isSystemOverrideToFullscreenEnabled() && isIgnoreOrientationRequestEnabled
606                 // Do not override orientation to fullscreen for camera activities.
607                 // Fixed-orientation activities are rarely tested in other orientations, and it
608                 // often results in sideways or stretched previews. As the camera compat treatment
609                 // targets fixed-orientation activities, overriding the orientation disables the
610                 // treatment.
611                 && !mActivityRecord.isCameraActive()) {
612             Slog.v(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
613                     + mActivityRecord + " is overridden to "
614                     + screenOrientationToString(SCREEN_ORIENTATION_USER));
615             return SCREEN_ORIENTATION_USER;
616         }
617 
618         if (mIsOverrideToReverseLandscapeOrientationEnabled
619                 && (isFixedOrientationLandscape(candidate) || mIsOverrideAnyOrientationEnabled)) {
620             Slog.w(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
621                     + mActivityRecord + " is overridden to "
622                     + screenOrientationToString(SCREEN_ORIENTATION_REVERSE_LANDSCAPE));
623             return SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
624         }
625 
626         if (!mIsOverrideAnyOrientationEnabled && isFixedOrientation(candidate)) {
627             return candidate;
628         }
629 
630         if (mIsOverrideToPortraitOrientationEnabled) {
631             Slog.w(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
632                     + mActivityRecord + " is overridden to "
633                     + screenOrientationToString(SCREEN_ORIENTATION_PORTRAIT));
634             return SCREEN_ORIENTATION_PORTRAIT;
635         }
636 
637         if (mIsOverrideToNosensorOrientationEnabled) {
638             Slog.w(TAG, "Requested orientation  " + screenOrientationToString(candidate) + " for "
639                     + mActivityRecord + " is overridden to "
640                     + screenOrientationToString(SCREEN_ORIENTATION_NOSENSOR));
641             return SCREEN_ORIENTATION_NOSENSOR;
642         }
643 
644         return candidate;
645     }
646 
isOverrideOrientationOnlyForCameraEnabled()647     boolean isOverrideOrientationOnlyForCameraEnabled() {
648         return mIsOverrideOrientationOnlyForCameraEnabled;
649     }
650 
651     /**
652      * Whether activity is eligible for activity "refresh" after camera compat force rotation
653      * treatment. See {@link DisplayRotationCompatPolicy} for context.
654      *
655      * <p>This treatment is enabled when the following conditions are met:
656      * <ul>
657      *     <li>Flag gating the camera compat treatment is enabled.
658      *     <li>Activity isn't opted out by the device manufacturer with override or by the app
659      *     developers with the component property.
660      * </ul>
661      */
shouldRefreshActivityForCameraCompat()662     boolean shouldRefreshActivityForCameraCompat() {
663         return mCameraCompatAllowRefreshOptProp.shouldEnableWithOptOutOverrideAndProperty(
664                 isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH));
665     }
666 
667     /**
668      * Whether activity should be "refreshed" after the camera compat force rotation treatment
669      * using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped
670      * -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context.
671      *
672      * <p>This treatment is enabled when the following conditions are met:
673      * <ul>
674      *     <li>Flag gating the camera compat treatment is enabled.
675      *     <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the
676      *     component property by the app developers.
677      *     <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device
678      *     manufacturer with override / by the app developers with the component property.
679      * </ul>
680      */
shouldRefreshActivityViaPauseForCameraCompat()681     boolean shouldRefreshActivityViaPauseForCameraCompat() {
682         return mCameraCompatEnableRefreshViaPauseOptProp.shouldEnableWithOverrideAndProperty(
683                 isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE));
684     }
685 
686     /**
687      * Whether activity is eligible for camera compat force rotation treatment. See {@link
688      * DisplayRotationCompatPolicy} for context.
689      *
690      * <p>This treatment is enabled when the following conditions are met:
691      * <ul>
692      *     <li>Flag gating the camera compat treatment is enabled.
693      *     <li>Activity isn't opted out by the device manufacturer with override or by the app
694      *     developers with the component property.
695      * </ul>
696      */
shouldForceRotateForCameraCompat()697     boolean shouldForceRotateForCameraCompat() {
698         return mCameraCompatAllowForceRotationOptProp.shouldEnableWithOptOutOverrideAndProperty(
699                 isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION));
700     }
701 
702     /**
703      * Whether activity is eligible for camera compatibility free-form treatment.
704      *
705      * <p>The treatment is applied to a fixed-orientation camera activity in free-form windowing
706      * mode. The treatment letterboxes or pillarboxes the activity to the expected orientation and
707      * provides changes to the camera and display orientation signals to match those expected on a
708      * portrait device in that orientation (for example, on a standard phone).
709      *
710      * <p>The treatment is enabled when the following conditions are met:
711      * <ul>
712      * <li>Property gating the camera compatibility free-form treatment is enabled.
713      * <li>Activity isn't opted out by the device manufacturer with override.
714      * </ul>
715      */
shouldApplyFreeformTreatmentForCameraCompat()716     boolean shouldApplyFreeformTreatmentForCameraCompat() {
717         return Flags.cameraCompatForFreeform() && !isCompatChangeEnabled(
718                         OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
719     }
720 
isCameraCompatTreatmentActive()721     private boolean isCameraCompatTreatmentActive() {
722         DisplayContent displayContent = mActivityRecord.mDisplayContent;
723         if (displayContent == null) {
724             return false;
725         }
726         return displayContent.mDisplayRotationCompatPolicy != null
727                 && displayContent.mDisplayRotationCompatPolicy
728                         .isTreatmentEnabledForActivity(mActivityRecord);
729     }
730 
731     @FreeformCameraCompatMode
getFreeformCameraCompatMode()732     int getFreeformCameraCompatMode() {
733         return mFreeformCameraCompatMode;
734     }
735 
setFreeformCameraCompatMode(@reeformCameraCompatMode int freeformCameraCompatMode)736     void setFreeformCameraCompatMode(@FreeformCameraCompatMode int freeformCameraCompatMode) {
737         mFreeformCameraCompatMode = freeformCameraCompatMode;
738     }
739 
isCompatChangeEnabled(long overrideChangeId)740     private boolean isCompatChangeEnabled(long overrideChangeId) {
741         return mActivityRecord.info.isChangeEnabled(overrideChangeId);
742     }
743 
hasWallpaperBackgroundForLetterbox()744     boolean hasWallpaperBackgroundForLetterbox() {
745         return mShowWallpaperForLetterboxBackground;
746     }
747 
748     /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */
getLetterboxInsets()749     Rect getLetterboxInsets() {
750         if (mLetterbox != null) {
751             return mLetterbox.getInsets();
752         } else {
753             return new Rect();
754         }
755     }
756 
757     /** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */
getLetterboxInnerBounds(Rect outBounds)758     void getLetterboxInnerBounds(Rect outBounds) {
759         if (mLetterbox != null) {
760             outBounds.set(mLetterbox.getInnerFrame());
761             final WindowState w = mActivityRecord.findMainWindow();
762             if (w != null) {
763                 adjustBoundsForTaskbar(w, outBounds);
764             }
765         } else {
766             outBounds.setEmpty();
767         }
768     }
769 
770     /** Gets the outer bounds of letterbox. The bounds will be empty if there is no letterbox. */
getLetterboxOuterBounds(Rect outBounds)771     private void getLetterboxOuterBounds(Rect outBounds) {
772         if (mLetterbox != null) {
773             outBounds.set(mLetterbox.getOuterFrame());
774         } else {
775             outBounds.setEmpty();
776         }
777     }
778 
779     /**
780      * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent
781      *     when the current activity is displayed.
782      */
isFullyTransparentBarAllowed(Rect rect)783     boolean isFullyTransparentBarAllowed(Rect rect) {
784         return mLetterbox == null || mLetterbox.notIntersectsOrFullyContains(rect);
785     }
786 
updateLetterboxSurfaceIfNeeded(WindowState winHint)787     void updateLetterboxSurfaceIfNeeded(WindowState winHint) {
788         updateLetterboxSurfaceIfNeeded(winHint, mActivityRecord.getSyncTransaction(),
789                 mActivityRecord.getPendingTransaction());
790     }
791 
updateLetterboxSurfaceIfNeeded(WindowState winHint, @NonNull Transaction t, @NonNull Transaction inputT)792     void updateLetterboxSurfaceIfNeeded(WindowState winHint, @NonNull Transaction t,
793             @NonNull Transaction inputT) {
794         if (shouldNotLayoutLetterbox(winHint)) {
795             return;
796         }
797         layoutLetterboxIfNeeded(winHint);
798         if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
799             mLetterbox.applySurfaceChanges(t, inputT);
800         }
801     }
802 
layoutLetterboxIfNeeded(WindowState w)803     void layoutLetterboxIfNeeded(WindowState w) {
804         if (shouldNotLayoutLetterbox(w)) {
805             return;
806         }
807         updateRoundedCornersIfNeeded(w);
808         updateWallpaperForLetterbox(w);
809         if (shouldShowLetterboxUi(w)) {
810             if (mLetterbox == null) {
811                 mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null),
812                         mActivityRecord.mWmService.mTransactionFactory,
813                         this::shouldLetterboxHaveRoundedCorners,
814                         this::getLetterboxBackgroundColor,
815                         this::hasWallpaperBackgroundForLetterbox,
816                         this::getLetterboxWallpaperBlurRadiusPx,
817                         this::getLetterboxWallpaperDarkScrimAlpha,
818                         this::handleHorizontalDoubleTap,
819                         this::handleVerticalDoubleTap,
820                         this::getLetterboxParentSurface);
821                 mLetterbox.attachInput(w);
822             }
823 
824             if (mActivityRecord.isInLetterboxAnimation()) {
825                 // In this case we attach the letterbox to the task instead of the activity.
826                 mActivityRecord.getTask().getPosition(mTmpPoint);
827             } else {
828                 mActivityRecord.getPosition(mTmpPoint);
829             }
830 
831             // Get the bounds of the "space-to-fill". The transformed bounds have the highest
832             // priority because the activity is launched in a rotated environment. In multi-window
833             // mode, the taskFragment-level represents this for both split-screen
834             // and activity-embedding. In fullscreen-mode, the task container does
835             // (since the orientation letterbox is also applied to the task).
836             final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds();
837             final Rect spaceToFill = transformedBounds != null
838                     ? transformedBounds
839                     : mActivityRecord.inMultiWindowMode()
840                             ? mActivityRecord.getTaskFragment().getBounds()
841                             : mActivityRecord.getRootTask().getParent().getBounds();
842             // In case of translucent activities an option is to use the WindowState#getFrame() of
843             // the first opaque activity beneath. In some cases (e.g. an opaque activity is using
844             // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct
845             // information and in particular it might provide a value for a smaller area making
846             // the letterbox overlap with the translucent activity's frame.
847             // If we use WindowState#getFrame() for the translucent activity's letterbox inner
848             // frame, the letterbox will then be overlapped with the translucent activity's frame.
849             // Because the surface layer of letterbox is lower than an activity window, this
850             // won't crop the content, but it may affect other features that rely on values stored
851             // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher
852             // For this reason we use ActivityRecord#getBounds() that the translucent activity
853             // inherits from the first opaque activity beneath and also takes care of the scaling
854             // in case of activities in size compat mode.
855             final Rect innerFrame = mActivityRecord.mTransparentPolicy.isRunning()
856                     ? mActivityRecord.getBounds() : w.getFrame();
857             mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
858             if (mDoubleTapEvent) {
859                 // We need to notify Shell that letterbox position has changed.
860                 mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
861             }
862         } else if (mLetterbox != null) {
863             mLetterbox.hide();
864         }
865     }
866 
isFromDoubleTap()867     boolean isFromDoubleTap() {
868         final boolean isFromDoubleTap = mDoubleTapEvent;
869         mDoubleTapEvent = false;
870         return isFromDoubleTap;
871     }
872 
getLetterboxParentSurface()873     SurfaceControl getLetterboxParentSurface() {
874         if (mActivityRecord.isInLetterboxAnimation()) {
875             return mActivityRecord.getTask().getSurfaceControl();
876         }
877         return mActivityRecord.getSurfaceControl();
878     }
879 
shouldNotLayoutLetterbox(WindowState w)880     private static boolean shouldNotLayoutLetterbox(WindowState w) {
881         if (w == null) {
882             return true;
883         }
884         final int type = w.mAttrs.type;
885         // Allow letterbox to be displayed early for base application or application starting
886         // windows even if it is not on the top z order to prevent flickering when the
887         // letterboxed window is brought to the top
888         return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING)
889                 || w.mAnimatingExit;
890     }
891 
shouldLetterboxHaveRoundedCorners()892     private boolean shouldLetterboxHaveRoundedCorners() {
893         // TODO(b/214030873): remove once background is drawn for transparent activities
894         // Letterbox shouldn't have rounded corners if the activity is transparent
895         return mLetterboxConfiguration.isLetterboxActivityCornersRounded()
896                 && mActivityRecord.fillsParent();
897     }
898 
899     // Check if we are in the given pose and in fullscreen mode.
900     // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
901     // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
902     // actually fullscreen. If display is still in transition e.g. unfolding, don't return true
903     // for HALF_FOLDED state or app will flicker.
isDisplayFullScreenAndInPosture(boolean isTabletop)904     private boolean isDisplayFullScreenAndInPosture(boolean isTabletop) {
905         Task task = mActivityRecord.getTask();
906         return mActivityRecord.mDisplayContent != null && task != null
907                 && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(
908                         DeviceStateController.DeviceState.HALF_FOLDED, isTabletop)
909                 && !mActivityRecord.mDisplayContent.inTransition()
910                 && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
911     }
912 
913     // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
914     // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
915     // actually fullscreen.
isDisplayFullScreenAndSeparatingHinge()916     private boolean isDisplayFullScreenAndSeparatingHinge() {
917         Task task = mActivityRecord.getTask();
918         return mActivityRecord.mDisplayContent != null
919                 && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
920                 && task != null
921                 && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
922     }
923 
924 
getHorizontalPositionMultiplier(Configuration parentConfiguration)925     float getHorizontalPositionMultiplier(Configuration parentConfiguration) {
926         // Don't check resolved configuration because it may not be updated yet during
927         // configuration change.
928         boolean bookModeEnabled = isFullScreenAndBookModeEnabled();
929         return isHorizontalReachabilityEnabled(parentConfiguration)
930                 // Using the last global dynamic position to avoid "jumps" when moving
931                 // between apps or activities.
932                 ? mLetterboxConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled)
933                 : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled);
934     }
935 
isFullScreenAndBookModeEnabled()936     private boolean isFullScreenAndBookModeEnabled() {
937         return isDisplayFullScreenAndInPosture(/* isTabletop */ false)
938                 && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
939     }
940 
getVerticalPositionMultiplier(Configuration parentConfiguration)941     float getVerticalPositionMultiplier(Configuration parentConfiguration) {
942         // Don't check resolved configuration because it may not be updated yet during
943         // configuration change.
944         boolean tabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
945         return isVerticalReachabilityEnabled(parentConfiguration)
946                 // Using the last global dynamic position to avoid "jumps" when moving
947                 // between apps or activities.
948                 ? mLetterboxConfiguration.getVerticalMultiplierForReachability(tabletopMode)
949                 : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
950     }
951 
getFixedOrientationLetterboxAspectRatio(@onNull Configuration parentConfiguration)952     float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) {
953         return shouldUseSplitScreenAspectRatio(parentConfiguration)
954                 ? getSplitScreenAspectRatio()
955                 : mActivityRecord.shouldCreateCompatDisplayInsets()
956                         ? getDefaultMinAspectRatioForUnresizableApps()
957                         : getDefaultMinAspectRatio();
958     }
959 
recomputeConfigurationForCameraCompatIfNeeded()960     void recomputeConfigurationForCameraCompatIfNeeded() {
961         if (isOverrideOrientationOnlyForCameraEnabled()
962                 || isCameraCompatSplitScreenAspectRatioAllowed()
963                 || shouldOverrideMinAspectRatioForCamera()) {
964             mActivityRecord.recomputeConfiguration();
965         }
966     }
967 
isLetterboxEducationEnabled()968     boolean isLetterboxEducationEnabled() {
969         return mLetterboxConfiguration.getIsEducationEnabled();
970     }
971 
972     /**
973      * Whether we use split screen aspect ratio for the activity when camera compat treatment
974      * is active because the corresponding config is enabled and activity supports resizing.
975      */
isCameraCompatSplitScreenAspectRatioAllowed()976     boolean isCameraCompatSplitScreenAspectRatioAllowed() {
977         return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()
978                 && !mActivityRecord.shouldCreateCompatDisplayInsets();
979     }
980 
shouldUseSplitScreenAspectRatio(@onNull Configuration parentConfiguration)981     private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) {
982         final boolean isBookMode = isDisplayFullScreenAndInPosture(/* isTabletop */ false);
983         final boolean isNotCenteredHorizontally = getHorizontalPositionMultiplier(
984                 parentConfiguration) != LETTERBOX_POSITION_MULTIPLIER_CENTER;
985         final boolean isTabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
986         final boolean isLandscape = isFixedOrientationLandscape(
987                 mActivityRecord.getOverrideOrientation());
988 
989         // Don't resize to split screen size when in book mode if letterbox position is centered
990         return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
991                     || isCameraCompatSplitScreenAspectRatioAllowed()
992                         && isCameraCompatTreatmentActive();
993     }
994 
getDefaultMinAspectRatioForUnresizableApps()995     private float getDefaultMinAspectRatioForUnresizableApps() {
996         if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()
997                 || mActivityRecord.getDisplayArea() == null) {
998             return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
999                     > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
1000                             ? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
1001                             : getDefaultMinAspectRatio();
1002         }
1003 
1004         return getSplitScreenAspectRatio();
1005     }
1006 
1007     /**
1008      * @return {@value true} if the resulting app is letterboxed in a way defined as thin.
1009      */
isVerticalThinLetterboxed()1010     boolean isVerticalThinLetterboxed() {
1011         final int thinHeight = mLetterboxConfiguration.getThinLetterboxHeightPx();
1012         if (thinHeight < 0) {
1013             return false;
1014         }
1015         final Task task = mActivityRecord.getTask();
1016         if (task == null) {
1017             return false;
1018         }
1019         final int padding = Math.abs(
1020                 task.getBounds().height() - mActivityRecord.getBounds().height()) / 2;
1021         return padding <= thinHeight;
1022     }
1023 
1024     /**
1025      * @return {@value true} if the resulting app is pillarboxed in a way defined as thin.
1026      */
isHorizontalThinLetterboxed()1027     boolean isHorizontalThinLetterboxed() {
1028         final int thinWidth = mLetterboxConfiguration.getThinLetterboxWidthPx();
1029         if (thinWidth < 0) {
1030             return false;
1031         }
1032         final Task task = mActivityRecord.getTask();
1033         if (task == null) {
1034             return false;
1035         }
1036         final int padding = Math.abs(
1037                 task.getBounds().width() - mActivityRecord.getBounds().width()) / 2;
1038         return padding <= thinWidth;
1039     }
1040 
1041 
1042     /**
1043      * @return {@value true} if the vertical reachability should be allowed in case of
1044      * thin letteboxing
1045      */
allowVerticalReachabilityForThinLetterbox()1046     boolean allowVerticalReachabilityForThinLetterbox() {
1047         if (!Flags.disableThinLetterboxingPolicy()) {
1048             return true;
1049         }
1050         // When the flag is enabled we allow vertical reachability only if the
1051         // app is not thin letterboxed vertically.
1052         return !isVerticalThinLetterboxed();
1053     }
1054 
1055     /**
1056      * @return {@value true} if the vertical reachability should be enabled in case of
1057      * thin letteboxing
1058      */
allowHorizontalReachabilityForThinLetterbox()1059     boolean allowHorizontalReachabilityForThinLetterbox() {
1060         if (!Flags.disableThinLetterboxingPolicy()) {
1061             return true;
1062         }
1063         // When the flag is enabled we allow horizontal reachability only if the
1064         // app is not thin pillarboxed.
1065         return !isHorizontalThinLetterboxed();
1066     }
1067 
getSplitScreenAspectRatio()1068     float getSplitScreenAspectRatio() {
1069         // Getting the same aspect ratio that apps get in split screen.
1070         final DisplayArea displayArea = mActivityRecord.getDisplayArea();
1071         if (displayArea == null) {
1072             return getDefaultMinAspectRatioForUnresizableApps();
1073         }
1074         int dividerWindowWidth =
1075                 getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_thickness);
1076         int dividerInsets =
1077                 getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_insets);
1078         int dividerSize = dividerWindowWidth - dividerInsets * 2;
1079         final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
1080         if (bounds.width() >= bounds.height()) {
1081             bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
1082             bounds.right = bounds.centerX();
1083         } else {
1084             bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
1085             bounds.bottom = bounds.centerY();
1086         }
1087         return computeAspectRatio(bounds);
1088     }
1089 
1090     /**
1091      * Whether we should enable users to resize the current app.
1092      */
shouldEnableUserAspectRatioSettings()1093     boolean shouldEnableUserAspectRatioSettings() {
1094         // We use mBooleanPropertyAllowUserAspectRatioOverride to allow apps to opt-out which has
1095         // effect only if explicitly false. If mBooleanPropertyAllowUserAspectRatioOverride is null,
1096         // the current app doesn't opt-out so the first part of the predicate is true.
1097         return !mAllowUserAspectRatioOverrideOptProp.isFalse()
1098                 && mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled()
1099                 && mActivityRecord.mDisplayContent != null
1100                 && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest();
1101     }
1102 
1103     /**
1104      * Whether we should apply the user aspect ratio override to the min aspect ratio for the
1105      * current app.
1106      */
shouldApplyUserMinAspectRatioOverride()1107     boolean shouldApplyUserMinAspectRatioOverride() {
1108         if (!shouldEnableUserAspectRatioSettings()) {
1109             return false;
1110         }
1111 
1112         mUserAspectRatio = getUserMinAspectRatioOverrideCode();
1113 
1114         return mUserAspectRatio != USER_MIN_ASPECT_RATIO_UNSET
1115                 && mUserAspectRatio != USER_MIN_ASPECT_RATIO_APP_DEFAULT
1116                 && mUserAspectRatio != USER_MIN_ASPECT_RATIO_FULLSCREEN;
1117     }
1118 
isUserFullscreenOverrideEnabled()1119     boolean isUserFullscreenOverrideEnabled() {
1120         if (mAllowUserAspectRatioOverrideOptProp.isFalse()
1121                 || mAllowUserAspectRatioFullscreenOverrideOptProp.isFalse()
1122                 || !mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled()) {
1123             return false;
1124         }
1125         return true;
1126     }
1127 
shouldApplyUserFullscreenOverride()1128     boolean shouldApplyUserFullscreenOverride() {
1129         if (isUserFullscreenOverrideEnabled()) {
1130             mUserAspectRatio = getUserMinAspectRatioOverrideCode();
1131 
1132             return mUserAspectRatio == USER_MIN_ASPECT_RATIO_FULLSCREEN;
1133         }
1134 
1135         return false;
1136     }
1137 
isSystemOverrideToFullscreenEnabled()1138     boolean isSystemOverrideToFullscreenEnabled() {
1139         return mIsSystemOverrideToFullscreenEnabled
1140                 && !mAllowOrientationOverrideOptProp.isFalse()
1141                 && (mUserAspectRatio == USER_MIN_ASPECT_RATIO_UNSET
1142                     || mUserAspectRatio == USER_MIN_ASPECT_RATIO_FULLSCREEN);
1143     }
1144 
hasFullscreenOverride()1145     boolean hasFullscreenOverride() {
1146         // `mUserAspectRatio` is always initialized first in `shouldApplyUserFullscreenOverride()`.
1147         return shouldApplyUserFullscreenOverride() || isSystemOverrideToFullscreenEnabled();
1148     }
1149 
getUserMinAspectRatio()1150     float getUserMinAspectRatio() {
1151         switch (mUserAspectRatio) {
1152             case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
1153                 return getDisplaySizeMinAspectRatio();
1154             case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
1155                 return getSplitScreenAspectRatio();
1156             case USER_MIN_ASPECT_RATIO_16_9:
1157                 return 16 / 9f;
1158             case USER_MIN_ASPECT_RATIO_4_3:
1159                 return 4 / 3f;
1160             case USER_MIN_ASPECT_RATIO_3_2:
1161                 return 3 / 2f;
1162             default:
1163                 throw new AssertionError("Unexpected user min aspect ratio override: "
1164                         + mUserAspectRatio);
1165         }
1166     }
1167 
1168     @VisibleForTesting
getUserMinAspectRatioOverrideCode()1169     int getUserMinAspectRatioOverrideCode() {
1170         try {
1171             return mActivityRecord.mAtmService.getPackageManager()
1172                     .getUserMinAspectRatio(mActivityRecord.packageName, mActivityRecord.mUserId);
1173         } catch (RemoteException e) {
1174             Slog.w(TAG, "Exception thrown retrieving aspect ratio user override " + this, e);
1175         }
1176         return mUserAspectRatio;
1177     }
1178 
getDisplaySizeMinAspectRatio()1179     private float getDisplaySizeMinAspectRatio() {
1180         final DisplayArea displayArea = mActivityRecord.getDisplayArea();
1181         if (displayArea == null) {
1182             return mActivityRecord.info.getMinAspectRatio();
1183         }
1184         final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
1185         return computeAspectRatio(bounds);
1186     }
1187 
getDefaultMinAspectRatio()1188     private float getDefaultMinAspectRatio() {
1189         if (mActivityRecord.getDisplayArea() == null
1190                 || !mLetterboxConfiguration
1191                     .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
1192             return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
1193         }
1194         return getDisplaySizeMinAspectRatio();
1195     }
1196 
getResources()1197     Resources getResources() {
1198         return mActivityRecord.mWmService.mContext.getResources();
1199     }
1200 
1201     @LetterboxConfiguration.LetterboxVerticalReachabilityPosition
getLetterboxPositionForVerticalReachability()1202     int getLetterboxPositionForVerticalReachability() {
1203         final boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
1204         return mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(
1205                 isInFullScreenTabletopMode);
1206     }
1207 
1208     @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition
getLetterboxPositionForHorizontalReachability()1209     int getLetterboxPositionForHorizontalReachability() {
1210         final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled();
1211         return mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(
1212                 isInFullScreenBookMode);
1213     }
1214 
1215     @VisibleForTesting
handleHorizontalDoubleTap(int x)1216     void handleHorizontalDoubleTap(int x) {
1217         if (!isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) {
1218             return;
1219         }
1220 
1221         if (mLetterbox.getInnerFrame().left <= x && mLetterbox.getInnerFrame().right >= x) {
1222             // Only react to clicks at the sides of the letterboxed app window.
1223             return;
1224         }
1225 
1226         boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge()
1227                 && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
1228         int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
1229                 .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
1230         if (mLetterbox.getInnerFrame().left > x) {
1231             // Moving to the next stop on the left side of the app window: right > center > left.
1232             mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
1233                     isInFullScreenBookMode);
1234             int changeToLog =
1235                     letterboxPositionForHorizontalReachability
1236                             == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
1237                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
1238                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
1239             logLetterboxPositionChange(changeToLog);
1240             mDoubleTapEvent = true;
1241         } else if (mLetterbox.getInnerFrame().right < x) {
1242             // Moving to the next stop on the right side of the app window: left > center > right.
1243             mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
1244                     isInFullScreenBookMode);
1245             int changeToLog =
1246                     letterboxPositionForHorizontalReachability
1247                             == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
1248                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
1249                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
1250             logLetterboxPositionChange(changeToLog);
1251             mDoubleTapEvent = true;
1252         }
1253         // TODO(197549949): Add animation for transition.
1254         mActivityRecord.recomputeConfiguration();
1255     }
1256 
1257     @VisibleForTesting
handleVerticalDoubleTap(int y)1258     void handleVerticalDoubleTap(int y) {
1259         if (!isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) {
1260             return;
1261         }
1262 
1263         if (mLetterbox.getInnerFrame().top <= y && mLetterbox.getInnerFrame().bottom >= y) {
1264             // Only react to clicks at the top and bottom of the letterboxed app window.
1265             return;
1266         }
1267         boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
1268         int letterboxPositionForVerticalReachability = mLetterboxConfiguration
1269                 .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
1270         if (mLetterbox.getInnerFrame().top > y) {
1271             // Moving to the next stop on the top side of the app window: bottom > center > top.
1272             mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(
1273                     isInFullScreenTabletopMode);
1274             int changeToLog =
1275                     letterboxPositionForVerticalReachability
1276                             == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
1277                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
1278                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
1279             logLetterboxPositionChange(changeToLog);
1280             mDoubleTapEvent = true;
1281         } else if (mLetterbox.getInnerFrame().bottom < y) {
1282             // Moving to the next stop on the bottom side of the app window: top > center > bottom.
1283             mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
1284                     isInFullScreenTabletopMode);
1285             int changeToLog =
1286                     letterboxPositionForVerticalReachability
1287                             == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
1288                                 ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
1289                                 : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
1290             logLetterboxPositionChange(changeToLog);
1291             mDoubleTapEvent = true;
1292         }
1293         // TODO(197549949): Add animation for transition.
1294         mActivityRecord.recomputeConfiguration();
1295     }
1296 
1297     /**
1298      * Whether horizontal reachability is enabled for an activity in the current configuration.
1299      *
1300      * <p>Conditions that needs to be met:
1301      * <ul>
1302      *   <li>Windowing mode is fullscreen.
1303      *   <li>Horizontal Reachability is enabled.
1304      *   <li>First top opaque activity fills parent vertically, but not horizontally.
1305      * </ul>
1306      */
isHorizontalReachabilityEnabled(Configuration parentConfiguration)1307     private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) {
1308         if (!allowHorizontalReachabilityForThinLetterbox()) {
1309             return false;
1310         }
1311         final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride();
1312         final Rect parentAppBounds = parentAppBoundsOverride != null
1313                 ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds();
1314         // Use screen resolved bounds which uses resolved bounds or size compat bounds
1315         // as activity bounds can sometimes be empty
1316         final Rect opaqueActivityBounds = mActivityRecord.mTransparentPolicy
1317                 .getFirstOpaqueActivity().map(ActivityRecord::getScreenResolvedBounds)
1318                 .orElse(mActivityRecord.getScreenResolvedBounds());
1319         return mLetterboxConfiguration.getIsHorizontalReachabilityEnabled()
1320                 && parentConfiguration.windowConfiguration.getWindowingMode()
1321                         == WINDOWING_MODE_FULLSCREEN
1322                 // Check whether the activity fills the parent vertically.
1323                 && parentAppBounds.height() <= opaqueActivityBounds.height()
1324                 && parentAppBounds.width() > opaqueActivityBounds.width();
1325     }
1326 
1327     @VisibleForTesting
isHorizontalReachabilityEnabled()1328     boolean isHorizontalReachabilityEnabled() {
1329         return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
1330     }
1331 
isLetterboxDoubleTapEducationEnabled()1332     boolean isLetterboxDoubleTapEducationEnabled() {
1333         return isHorizontalReachabilityEnabled() || isVerticalReachabilityEnabled();
1334     }
1335 
1336     /**
1337      * Whether vertical reachability is enabled for an activity in the current configuration.
1338      *
1339      * <p>Conditions that needs to be met:
1340      * <ul>
1341      *   <li>Windowing mode is fullscreen.
1342      *   <li>Vertical Reachability is enabled.
1343      *   <li>First top opaque activity fills parent horizontally but not vertically.
1344      * </ul>
1345      */
isVerticalReachabilityEnabled(Configuration parentConfiguration)1346     private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) {
1347         if (!allowVerticalReachabilityForThinLetterbox()) {
1348             return false;
1349         }
1350         final Rect parentAppBoundsOverride = mActivityRecord.getParentAppBoundsOverride();
1351         final Rect parentAppBounds = parentAppBoundsOverride != null
1352                 ? parentAppBoundsOverride : parentConfiguration.windowConfiguration.getAppBounds();
1353         // Use screen resolved bounds which uses resolved bounds or size compat bounds
1354         // as activity bounds can sometimes be empty.
1355         final Rect opaqueActivityBounds = mActivityRecord.mTransparentPolicy
1356                 .getFirstOpaqueActivity().map(ActivityRecord::getScreenResolvedBounds)
1357                 .orElse(mActivityRecord.getScreenResolvedBounds());
1358         return mLetterboxConfiguration.getIsVerticalReachabilityEnabled()
1359                 && parentConfiguration.windowConfiguration.getWindowingMode()
1360                         == WINDOWING_MODE_FULLSCREEN
1361                 // Check whether the activity fills the parent horizontally.
1362                 && parentAppBounds.width() <= opaqueActivityBounds.width()
1363                 && parentAppBounds.height() > opaqueActivityBounds.height();
1364     }
1365 
1366     @VisibleForTesting
isVerticalReachabilityEnabled()1367     boolean isVerticalReachabilityEnabled() {
1368         return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
1369     }
1370 
1371     @VisibleForTesting
shouldShowLetterboxUi(WindowState mainWindow)1372     boolean shouldShowLetterboxUi(WindowState mainWindow) {
1373         if (mIsRelaunchingAfterRequestedOrientationChanged) {
1374             return mLastShouldShowLetterboxUi;
1375         }
1376 
1377         final boolean shouldShowLetterboxUi =
1378                 (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible()
1379                         || mActivityRecord.isVisibleRequested())
1380                 && mainWindow.areAppWindowBoundsLetterboxed()
1381                 // Check for FLAG_SHOW_WALLPAPER explicitly instead of using
1382                 // WindowContainer#showWallpaper because the later will return true when this
1383                 // activity is using blurred wallpaper for letterbox background.
1384                 && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0;
1385 
1386         mLastShouldShowLetterboxUi = shouldShowLetterboxUi;
1387 
1388         return shouldShowLetterboxUi;
1389     }
1390 
getLetterboxBackgroundColor()1391     Color getLetterboxBackgroundColor() {
1392         final WindowState w = mActivityRecord.findMainWindow();
1393         if (w == null || w.isLetterboxedForDisplayCutout()) {
1394             return Color.valueOf(Color.BLACK);
1395         }
1396         @LetterboxBackgroundType int letterboxBackgroundType =
1397                 mLetterboxConfiguration.getLetterboxBackgroundType();
1398         TaskDescription taskDescription = mActivityRecord.taskDescription;
1399         switch (letterboxBackgroundType) {
1400             case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING:
1401                 if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) {
1402                     return Color.valueOf(taskDescription.getBackgroundColorFloating());
1403                 }
1404                 break;
1405             case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND:
1406                 if (taskDescription != null && taskDescription.getBackgroundColor() != 0) {
1407                     return Color.valueOf(taskDescription.getBackgroundColor());
1408                 }
1409                 break;
1410             case LETTERBOX_BACKGROUND_WALLPAPER:
1411                 if (hasWallpaperBackgroundForLetterbox()) {
1412                     // Color is used for translucent scrim that dims wallpaper.
1413                     return mLetterboxConfiguration.getLetterboxBackgroundColor();
1414                 }
1415                 Slog.w(TAG, "Wallpaper option is selected for letterbox background but "
1416                         + "blur is not supported by a device or not supported in the current "
1417                         + "window configuration or both alpha scrim and blur radius aren't "
1418                         + "provided so using solid color background");
1419                 break;
1420             case LETTERBOX_BACKGROUND_SOLID_COLOR:
1421                 return mLetterboxConfiguration.getLetterboxBackgroundColor();
1422             default:
1423                 throw new AssertionError(
1424                     "Unexpected letterbox background type: " + letterboxBackgroundType);
1425         }
1426         // If picked option configured incorrectly or not supported then default to a solid color
1427         // background.
1428         return mLetterboxConfiguration.getLetterboxBackgroundColor();
1429     }
1430 
updateRoundedCornersIfNeeded(final WindowState mainWindow)1431     private void updateRoundedCornersIfNeeded(final WindowState mainWindow) {
1432         final SurfaceControl windowSurface = mainWindow.getSurfaceControl();
1433         if (windowSurface == null || !windowSurface.isValid()) {
1434             return;
1435         }
1436 
1437         // cropBounds must be non-null for the cornerRadius to be ever applied.
1438         mActivityRecord.getSyncTransaction()
1439                 .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow))
1440                 .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
1441     }
1442 
1443     @VisibleForTesting
1444     @Nullable
getCropBoundsIfNeeded(final WindowState mainWindow)1445     Rect getCropBoundsIfNeeded(final WindowState mainWindow) {
1446         if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
1447             // We don't want corner radius on the window.
1448             // In the case the ActivityRecord requires a letterboxed animation we never want
1449             // rounded corners on the window because rounded corners are applied at the
1450             // animation-bounds surface level and rounded corners on the window would interfere
1451             // with that leading to unexpected rounded corner positioning during the animation.
1452             return null;
1453         }
1454 
1455         final Rect cropBounds = new Rect(mActivityRecord.getBounds());
1456 
1457         // In case of translucent activities we check if the requested size is different from
1458         // the size provided using inherited bounds. In that case we decide to not apply rounded
1459         // corners because we assume the specific layout would. This is the case when the layout
1460         // of the translucent activity uses only a part of all the bounds because of the use of
1461         // LayoutParams.WRAP_CONTENT.
1462         if (mActivityRecord.mTransparentPolicy.isRunning()
1463                 && (cropBounds.width() != mainWindow.mRequestedWidth
1464                 || cropBounds.height() != mainWindow.mRequestedHeight)) {
1465             return null;
1466         }
1467 
1468         // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo}
1469         // because taskbar bounds used in {@link #adjustBoundsIfNeeded}
1470         // are in screen coordinates
1471         adjustBoundsForTaskbar(mainWindow, cropBounds);
1472 
1473         final float scale = mainWindow.mInvGlobalScale;
1474         if (scale != 1f && scale > 0f) {
1475             cropBounds.scale(scale);
1476         }
1477 
1478         // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface
1479         // control is in the top left corner of an app window so offsetting bounds
1480         // accordingly.
1481         cropBounds.offsetTo(0, 0);
1482         return cropBounds;
1483     }
1484 
requiresRoundedCorners(final WindowState mainWindow)1485     private boolean requiresRoundedCorners(final WindowState mainWindow) {
1486         return isLetterboxedNotForDisplayCutout(mainWindow)
1487                 && mLetterboxConfiguration.isLetterboxActivityCornersRounded();
1488     }
1489 
1490     // Returns rounded corners radius the letterboxed activity should have based on override in
1491     // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii.
1492     // Device corners can be different on the right and left sides, but we use the same radius
1493     // for all corners for consistency and pick a minimal bottom one for consistency with a
1494     // taskbar rounded corners.
getRoundedCornersRadius(final WindowState mainWindow)1495     int getRoundedCornersRadius(final WindowState mainWindow) {
1496         if (!requiresRoundedCorners(mainWindow)) {
1497             return 0;
1498         }
1499 
1500         final int radius;
1501         if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
1502             radius = mLetterboxConfiguration.getLetterboxActivityCornersRadius();
1503         } else {
1504             final InsetsState insetsState = mainWindow.getInsetsState();
1505             radius = Math.min(
1506                     getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
1507                     getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
1508         }
1509 
1510         final float scale = mainWindow.mInvGlobalScale;
1511         return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius;
1512     }
1513 
1514     /**
1515      * Returns the taskbar in case it is visible and expanded in height, otherwise returns null.
1516      */
1517     @VisibleForTesting
1518     @Nullable
getExpandedTaskbarOrNull(final WindowState mainWindow)1519     InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) {
1520         final InsetsState state = mainWindow.getInsetsState();
1521         for (int i = state.sourceSize() - 1; i >= 0; i--) {
1522             final InsetsSource source = state.sourceAt(i);
1523             if (source.getType() == WindowInsets.Type.navigationBars()
1524                     && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)
1525                     && source.isVisible()) {
1526                 return source;
1527             }
1528         }
1529         return null;
1530     }
1531 
getIsRelaunchingAfterRequestedOrientationChanged()1532     boolean getIsRelaunchingAfterRequestedOrientationChanged() {
1533         return mIsRelaunchingAfterRequestedOrientationChanged;
1534     }
1535 
adjustBoundsForTaskbar(final WindowState mainWindow, final Rect bounds)1536     private void adjustBoundsForTaskbar(final WindowState mainWindow, final Rect bounds) {
1537         // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
1538         // an insets frame is equal to a navigation bar which shouldn't affect position of
1539         // rounded corners since apps are expected to handle navigation bar inset.
1540         // This condition checks whether the taskbar is visible.
1541         // Do not crop the taskbar inset if the window is in immersive mode - the user can
1542         // swipe to show/hide the taskbar as an overlay.
1543         // Adjust the bounds only in case there is an expanded taskbar,
1544         // otherwise the rounded corners will be shown behind the navbar.
1545         final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow);
1546         if (expandedTaskbarOrNull != null) {
1547             // Rounded corners should be displayed above the expanded taskbar.
1548             bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top);
1549         }
1550     }
1551 
getInsetsStateCornerRadius( InsetsState insetsState, @RoundedCorner.Position int position)1552     private int getInsetsStateCornerRadius(
1553                 InsetsState insetsState, @RoundedCorner.Position int position) {
1554         RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
1555         return corner == null ? 0 : corner.getRadius();
1556     }
1557 
isLetterboxedNotForDisplayCutout(WindowState mainWindow)1558     private boolean isLetterboxedNotForDisplayCutout(WindowState mainWindow) {
1559         return shouldShowLetterboxUi(mainWindow)
1560                 && !mainWindow.isLetterboxedForDisplayCutout();
1561     }
1562 
updateWallpaperForLetterbox(WindowState mainWindow)1563     private void updateWallpaperForLetterbox(WindowState mainWindow) {
1564         @LetterboxBackgroundType int letterboxBackgroundType =
1565                 mLetterboxConfiguration.getLetterboxBackgroundType();
1566         boolean wallpaperShouldBeShown =
1567                 letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER
1568                         // Don't use wallpaper as a background if letterboxed for display cutout.
1569                         && isLetterboxedNotForDisplayCutout(mainWindow)
1570                         // Check that dark scrim alpha or blur radius are provided
1571                         && (getLetterboxWallpaperBlurRadiusPx() > 0
1572                                 || getLetterboxWallpaperDarkScrimAlpha() > 0)
1573                         // Check that blur is supported by a device if blur radius is provided.
1574                         && (getLetterboxWallpaperBlurRadiusPx() <= 0
1575                                 || isLetterboxWallpaperBlurSupported());
1576         if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) {
1577             mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown;
1578             mActivityRecord.requestUpdateWallpaperIfNeeded();
1579         }
1580     }
1581 
getLetterboxWallpaperBlurRadiusPx()1582     private int getLetterboxWallpaperBlurRadiusPx() {
1583         int blurRadius = mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx();
1584         return Math.max(blurRadius, 0);
1585     }
1586 
getLetterboxWallpaperDarkScrimAlpha()1587     private float getLetterboxWallpaperDarkScrimAlpha() {
1588         float alpha = mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha();
1589         // No scrim by default.
1590         return (alpha < 0 || alpha >= 1) ? 0.0f : alpha;
1591     }
1592 
isLetterboxWallpaperBlurSupported()1593     private boolean isLetterboxWallpaperBlurSupported() {
1594         return mLetterboxConfiguration.mContext.getSystemService(WindowManager.class)
1595                 .isCrossWindowBlurEnabled();
1596     }
1597 
dump(PrintWriter pw, String prefix)1598     void dump(PrintWriter pw, String prefix) {
1599         final WindowState mainWin = mActivityRecord.findMainWindow();
1600         if (mainWin == null) {
1601             return;
1602         }
1603 
1604         boolean areBoundsLetterboxed = mainWin.areAppWindowBoundsLetterboxed();
1605         pw.println(prefix + "areBoundsLetterboxed=" + areBoundsLetterboxed);
1606         if (!areBoundsLetterboxed) {
1607             return;
1608         }
1609 
1610         pw.println(prefix + "  letterboxReason=" + getLetterboxReasonString(mainWin));
1611         pw.println(prefix + "  activityAspectRatio="
1612                 + mActivityRecord.computeAspectRatio(mActivityRecord.getBounds()));
1613 
1614         boolean shouldShowLetterboxUi = shouldShowLetterboxUi(mainWin);
1615         pw.println(prefix + "shouldShowLetterboxUi=" + shouldShowLetterboxUi);
1616 
1617         if (!shouldShowLetterboxUi) {
1618             return;
1619         }
1620         pw.println(prefix + "  isVerticalThinLetterboxed=" + isVerticalThinLetterboxed());
1621         pw.println(prefix + "  isHorizontalThinLetterboxed=" + isHorizontalThinLetterboxed());
1622         pw.println(prefix + "  letterboxBackgroundColor=" + Integer.toHexString(
1623                 getLetterboxBackgroundColor().toArgb()));
1624         pw.println(prefix + "  letterboxBackgroundType="
1625                 + letterboxBackgroundTypeToString(
1626                         mLetterboxConfiguration.getLetterboxBackgroundType()));
1627         pw.println(prefix + "  letterboxCornerRadius="
1628                 + getRoundedCornersRadius(mainWin));
1629         if (mLetterboxConfiguration.getLetterboxBackgroundType()
1630                 == LETTERBOX_BACKGROUND_WALLPAPER) {
1631             pw.println(prefix + "  isLetterboxWallpaperBlurSupported="
1632                     + isLetterboxWallpaperBlurSupported());
1633             pw.println(prefix + "  letterboxBackgroundWallpaperDarkScrimAlpha="
1634                     + getLetterboxWallpaperDarkScrimAlpha());
1635             pw.println(prefix + "  letterboxBackgroundWallpaperBlurRadius="
1636                     + getLetterboxWallpaperBlurRadiusPx());
1637         }
1638 
1639         pw.println(prefix + "  isHorizontalReachabilityEnabled="
1640                 + isHorizontalReachabilityEnabled());
1641         pw.println(prefix + "  isVerticalReachabilityEnabled=" + isVerticalReachabilityEnabled());
1642         pw.println(prefix + "  letterboxHorizontalPositionMultiplier="
1643                 + getHorizontalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
1644         pw.println(prefix + "  letterboxVerticalPositionMultiplier="
1645                 + getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
1646         pw.println(prefix + "  letterboxPositionForHorizontalReachability="
1647                 + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
1648                 mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
1649         pw.println(prefix + "  letterboxPositionForVerticalReachability="
1650                 + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
1651                 mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
1652         pw.println(prefix + "  fixedOrientationLetterboxAspectRatio="
1653                 + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
1654         pw.println(prefix + "  defaultMinAspectRatioForUnresizableApps="
1655                 + mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps());
1656         pw.println(prefix + "  isSplitScreenAspectRatioForUnresizableAppsEnabled="
1657                 + mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
1658         pw.println(prefix + "  isDisplayAspectRatioEnabledForFixedOrientationLetterbox="
1659                 + mLetterboxConfiguration
1660                 .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
1661     }
1662 
1663     /**
1664      * Returns a string representing the reason for letterboxing. This method assumes the activity
1665      * is letterboxed.
1666      */
getLetterboxReasonString(WindowState mainWin)1667     private String getLetterboxReasonString(WindowState mainWin) {
1668         if (mActivityRecord.inSizeCompatMode()) {
1669             return "SIZE_COMPAT_MODE";
1670         }
1671         if (mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio()) {
1672             return "FIXED_ORIENTATION";
1673         }
1674         if (mainWin.isLetterboxedForDisplayCutout()) {
1675             return "DISPLAY_CUTOUT";
1676         }
1677         if (mActivityRecord.isLetterboxedForAspectRatioOnly()) {
1678             return "ASPECT_RATIO";
1679         }
1680         return "UNKNOWN_REASON";
1681     }
1682 
letterboxHorizontalReachabilityPositionToLetterboxPosition( @etterboxConfiguration.LetterboxHorizontalReachabilityPosition int position)1683     private int letterboxHorizontalReachabilityPositionToLetterboxPosition(
1684             @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) {
1685         switch (position) {
1686             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
1687                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
1688             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
1689                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
1690             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT:
1691                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
1692             default:
1693                 throw new AssertionError(
1694                         "Unexpected letterbox horizontal reachability position type: "
1695                                 + position);
1696         }
1697     }
1698 
letterboxVerticalReachabilityPositionToLetterboxPosition( @etterboxConfiguration.LetterboxVerticalReachabilityPosition int position)1699     private int letterboxVerticalReachabilityPositionToLetterboxPosition(
1700             @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) {
1701         switch (position) {
1702             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
1703                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
1704             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
1705                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
1706             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM:
1707                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
1708             default:
1709                 throw new AssertionError(
1710                         "Unexpected letterbox vertical reachability position type: "
1711                                 + position);
1712         }
1713     }
1714 
getLetterboxPositionForLogging()1715     int getLetterboxPositionForLogging() {
1716         int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
1717         if (isHorizontalReachabilityEnabled()) {
1718             int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
1719                     .getLetterboxPositionForHorizontalReachability(
1720                             isDisplayFullScreenAndInPosture(/* isTabletop */ false));
1721             positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
1722                     letterboxPositionForHorizontalReachability);
1723         } else if (isVerticalReachabilityEnabled()) {
1724             int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
1725                     .getLetterboxPositionForVerticalReachability(
1726                             isDisplayFullScreenAndInPosture(/* isTabletop */ true));
1727             positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
1728                     letterboxPositionForVerticalReachability);
1729         }
1730         return positionToLog;
1731     }
1732 
getLetterboxConfiguration()1733     private LetterboxConfiguration getLetterboxConfiguration() {
1734         return mLetterboxConfiguration;
1735     }
1736 
1737     /**
1738      * Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}.
1739      */
logLetterboxPositionChange(int letterboxPositionChange)1740     private void logLetterboxPositionChange(int letterboxPositionChange) {
1741         mActivityRecord.mTaskSupervisor.getActivityMetricsLogger()
1742                 .logLetterboxPositionChange(mActivityRecord, letterboxPositionChange);
1743     }
1744 
1745     @Nullable
getLetterboxDetails()1746     LetterboxDetails getLetterboxDetails() {
1747         final WindowState w = mActivityRecord.findMainWindow();
1748         if (mLetterbox == null || w == null || w.isLetterboxedForDisplayCutout()) {
1749             return null;
1750         }
1751         Rect letterboxInnerBounds = new Rect();
1752         Rect letterboxOuterBounds = new Rect();
1753         getLetterboxInnerBounds(letterboxInnerBounds);
1754         getLetterboxOuterBounds(letterboxOuterBounds);
1755 
1756         if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) {
1757             return null;
1758         }
1759 
1760         return new LetterboxDetails(
1761                 letterboxInnerBounds,
1762                 letterboxOuterBounds,
1763                 w.mAttrs.insetsFlags.appearance
1764         );
1765     }
1766 
1767     @NonNull
asLazy(@onNull BooleanSupplier supplier)1768     private static BooleanSupplier asLazy(@NonNull BooleanSupplier supplier) {
1769         return new BooleanSupplier() {
1770             private boolean mRead;
1771             private boolean mValue;
1772 
1773             @Override
1774             public boolean getAsBoolean() {
1775                 if (!mRead) {
1776                     mRead = true;
1777                     mValue = supplier.getAsBoolean();
1778                 }
1779                 return mValue;
1780             }
1781         };
1782     }
1783 }
1784