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