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