blob: 5518de7b64fdf42c045ae9cdd89d5a1a76af0803 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
import static android.content.pm.ActivityInfo.isFixedOrientation;
import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
import static android.content.pm.ActivityInfo.screenOrientationToString;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
import static com.android.server.wm.ActivityRecord.computeAspectRatio;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager.TaskDescription;
import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.Slog;
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.RoundedCorner;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets;
import android.view.WindowManager;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.LetterboxDetails;
import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Predicate;
/** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */
// TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in
// SizeCompatTests and LetterboxTests but not all.
// TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the
// hierarchy in addition to ActivityRecord (Task, DisplayArea, ...).
// TODO(b/263021211): Consider renaming to more generic CompatUIController.
final class LetterboxUiController {
private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE =
activityRecord -> activityRecord.fillsParent() && !activityRecord.isFinishing();
private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
private static final float UNDEFINED_ASPECT_RATIO = 0f;
// Minimum value of mSetOrientationRequestCounter before qualifying as orientation request loop
@VisibleForTesting
static final int MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP = 2;
// Used to determine reset of mSetOrientationRequestCounter if next app requested
// orientation is after timeout value
@VisibleForTesting
static final int SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS = 1000;
private final Point mTmpPoint = new Point();
private final LetterboxConfiguration mLetterboxConfiguration;
private final ActivityRecord mActivityRecord;
// TODO(b/265576778): Cache other overrides as well.
// Corresponds to OVERRIDE_ANY_ORIENTATION
private final boolean mIsOverrideAnyOrientationEnabled;
// Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
private final boolean mIsOverrideToPortraitOrientationEnabled;
// Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR
private final boolean mIsOverrideToNosensorOrientationEnabled;
// Corresponds to OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE
private final boolean mIsOverrideToReverseLandscapeOrientationEnabled;
// Corresponds to OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA
private final boolean mIsOverrideOrientationOnlyForCameraEnabled;
// Corresponds to OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION
private final boolean mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled;
// Corresponds to OVERRIDE_RESPECT_REQUESTED_ORIENTATION
private final boolean mIsOverrideRespectRequestedOrientationEnabled;
// Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION
private final boolean mIsOverrideCameraCompatDisableForceRotationEnabled;
// Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH
private final boolean mIsOverrideCameraCompatDisableRefreshEnabled;
// Corresponds to OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE
private final boolean mIsOverrideCameraCompatEnableRefreshViaPauseEnabled;
// Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION
private final boolean mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled;
// Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED
private final boolean mIsOverrideEnableCompatIgnoreOrientationRequestWhenLoopDetectedEnabled;
// Corresponds to OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS
private final boolean mIsOverrideEnableCompatFakeFocusEnabled;
// The list of observers for the destroy event of candidate opaque activities
// when dealing with translucent activities.
private final List<LetterboxUiController> mDestroyListeners = new ArrayList<>();
// Corresponds to OVERRIDE_MIN_ASPECT_RATIO
private final boolean mIsOverrideMinAspectRatio;
// Corresponds to FORCE_RESIZE_APP
private final boolean mIsOverrideForceResizeApp;
// Corresponds to FORCE_NON_RESIZE_APP
private final boolean mIsOverrideForceNonResizeApp;
@Nullable
private final Boolean mBooleanPropertyAllowOrientationOverride;
@Nullable
private final Boolean mBooleanPropertyAllowDisplayOrientationOverride;
@Nullable
private final Boolean mBooleanPropertyAllowMinAspectRatioOverride;
@Nullable
private final Boolean mBooleanPropertyAllowForceResizeOverride;
@Nullable
private final Boolean mBooleanPropertyAllowUserAspectRatioOverride;
@Nullable
private final Boolean mBooleanPropertyAllowUserAspectRatioFullscreenOverride;
/*
* WindowContainerListener responsible to make translucent activities inherit
* constraints from the first opaque activity beneath them. It's null for not
* translucent activities.
*/
@Nullable
private WindowContainerListener mLetterboxConfigListener;
@Nullable
@VisibleForTesting
ActivityRecord mFirstOpaqueActivityBeneath;
private boolean mShowWallpaperForLetterboxBackground;
// In case of transparent activities we might need to access the aspectRatio of the
// first opaque activity beneath.
private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
// Updated when ActivityRecord#setRequestedOrientation is called
private long mTimeMsLastSetOrientationRequest = 0;
@Configuration.Orientation
private int mInheritedOrientation = ORIENTATION_UNDEFINED;
// The app compat state for the opaque activity if any
private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
// Counter for ActivityRecord#setRequestedOrientation
private int mSetOrientationRequestCounter = 0;
// The min aspect ratio override set by user. Stores the last selected aspect ratio after
// {@link #shouldApplyUserFullscreenOverride} or {@link #shouldApplyUserMinAspectRatioOverride}
// have been invoked.
@PackageManager.UserMinAspectRatio
private int mUserAspectRatio = USER_MIN_ASPECT_RATIO_UNSET;
// The CompatDisplayInsets of the opaque activity beneath the translucent one.
private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
@Nullable
private Letterbox mLetterbox;
@Nullable
private final Boolean mBooleanPropertyCameraCompatAllowForceRotation;
@Nullable
private final Boolean mBooleanPropertyCameraCompatAllowRefresh;
@Nullable
private final Boolean mBooleanPropertyCameraCompatEnableRefreshViaPause;
// Whether activity "refresh" was requested but not finished in
// ActivityRecord#activityResumedLocked following the camera compat force rotation in
// DisplayRotationCompatPolicy.
private boolean mIsRefreshAfterRotationRequested;
@Nullable
private final Boolean mBooleanPropertyIgnoreRequestedOrientation;
@Nullable
private final Boolean mBooleanPropertyAllowIgnoringOrientationRequestWhenLoopDetected;
@Nullable
private final Boolean mBooleanPropertyFakeFocus;
private boolean mIsRelaunchingAfterRequestedOrientationChanged;
private boolean mLastShouldShowLetterboxUi;
private boolean mDoubleTapEvent;
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
mLetterboxConfiguration = wmService.mLetterboxConfiguration;
// Given activityRecord may not be fully constructed since LetterboxUiController
// is created in its constructor. It shouldn't be used in this constructor but it's safe
// to use it after since controller is only used in ActivityRecord.
mActivityRecord = activityRecord;
PackageManager packageManager = wmService.mContext.getPackageManager();
mBooleanPropertyIgnoreRequestedOrientation =
readComponentProperty(packageManager, mActivityRecord.packageName,
mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
mBooleanPropertyAllowIgnoringOrientationRequestWhenLoopDetected =
readComponentProperty(packageManager, mActivityRecord.packageName,
mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED);
mBooleanPropertyFakeFocus =
readComponentProperty(packageManager, mActivityRecord.packageName,
mLetterboxConfiguration::isCompatFakeFocusEnabled,
PROPERTY_COMPAT_ENABLE_FAKE_FOCUS);
mBooleanPropertyCameraCompatAllowForceRotation =
readComponentProperty(packageManager, mActivityRecord.packageName,
() -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(),
PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION);
mBooleanPropertyCameraCompatAllowRefresh =
readComponentProperty(packageManager, mActivityRecord.packageName,
() -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(),
PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH);
mBooleanPropertyCameraCompatEnableRefreshViaPause =
readComponentProperty(packageManager, mActivityRecord.packageName,
() -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(),
PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
mBooleanPropertyAllowOrientationOverride =
readComponentProperty(packageManager, mActivityRecord.packageName,
/* gatingCondition */ null,
PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
mBooleanPropertyAllowDisplayOrientationOverride =
readComponentProperty(packageManager, mActivityRecord.packageName,
/* gatingCondition */ null,
PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE);
mBooleanPropertyAllowMinAspectRatioOverride =
readComponentProperty(packageManager, mActivityRecord.packageName,
/* gatingCondition */ null,
PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
mBooleanPropertyAllowForceResizeOverride =
readComponentProperty(packageManager, mActivityRecord.packageName,
/* gatingCondition */ null,
PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES);
mBooleanPropertyAllowUserAspectRatioOverride =
readComponentProperty(packageManager, mActivityRecord.packageName,
() -> mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled(),
PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
mBooleanPropertyAllowUserAspectRatioFullscreenOverride =
readComponentProperty(packageManager, mActivityRecord.packageName,
() -> mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled(),
PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION);
mIsOverrideToPortraitOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT);
mIsOverrideToReverseLandscapeOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE);
mIsOverrideToNosensorOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
mIsOverrideOrientationOnlyForCameraEnabled =
isCompatChangeEnabled(OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA);
mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION);
mIsOverrideRespectRequestedOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_RESPECT_REQUESTED_ORIENTATION);
mIsOverrideCameraCompatDisableForceRotationEnabled =
isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION);
mIsOverrideCameraCompatDisableRefreshEnabled =
isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH);
mIsOverrideCameraCompatEnableRefreshViaPauseEnabled =
isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION);
mIsOverrideEnableCompatIgnoreOrientationRequestWhenLoopDetectedEnabled =
isCompatChangeEnabled(
OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED);
mIsOverrideEnableCompatFakeFocusEnabled =
isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS);
mIsOverrideMinAspectRatio = isCompatChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO);
mIsOverrideForceResizeApp = isCompatChangeEnabled(FORCE_RESIZE_APP);
mIsOverrideForceNonResizeApp = isCompatChangeEnabled(FORCE_NON_RESIZE_APP);
}
/**
* Reads a {@link Boolean} component property fot a given {@code packageName} and a {@code
* propertyName}. Returns {@code null} if {@code gatingCondition} is {@code false} or if the
* property isn't specified for the package.
*
* <p>Return value is {@link Boolean} rather than {@code boolean} so we can know when the
* property is unset. Particularly, when this returns {@code null}, {@link
* #shouldEnableWithOverrideAndProperty} will check the value of override for the final
* decision.
*/
@Nullable
private static Boolean readComponentProperty(PackageManager packageManager, String packageName,
@Nullable BooleanSupplier gatingCondition, String propertyName) {
if (gatingCondition != null && !gatingCondition.getAsBoolean()) {
return null;
}
try {
return packageManager.getProperty(propertyName, packageName).getBoolean();
} catch (PackageManager.NameNotFoundException e) {
// No such property name.
}
return null;
}
/** Cleans up {@link Letterbox} if it exists.*/
void destroy() {
if (mLetterbox != null) {
mLetterbox.destroy();
mLetterbox = null;
}
for (int i = mDestroyListeners.size() - 1; i >= 0; i--) {
mDestroyListeners.get(i).updateInheritedLetterbox();
}
mDestroyListeners.clear();
if (mLetterboxConfigListener != null) {
mLetterboxConfigListener.onRemoved();
mLetterboxConfigListener = null;
}
}
void onMovedToDisplay(int displayId) {
if (mLetterbox != null) {
mLetterbox.onMovedToDisplay(displayId);
}
}
/**
* Whether should ignore app requested orientation in response to an app
* calling {@link android.app.Activity#setRequestedOrientation}.
*
* <p>This is needed to avoid getting into {@link android.app.Activity#setRequestedOrientation}
* loop when {@link DisplayContent#getIgnoreOrientationRequest} is enabled or device has
* landscape natural orientation which app developers don't expect. For example, the loop can
* look like this:
* <ol>
* <li>App sets default orientation to "unspecified" at runtime
* <li>App requests to "portrait" after checking some condition (e.g. display rotation).
* <li>(2) leads to fullscreen -> letterboxed bounds change and activity relaunch because
* app can't handle the corresponding config changes.
* <li>Loop goes back to (1)
* </ol>
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Flag gating the treatment is enabled
* <li>Opt-out component property isn't enabled
* <li>Opt-in component property or per-app override are enabled
* <li>Activity is relaunched after {@link android.app.Activity#setRequestedOrientation}
* call from an app or camera compat force rotation treatment is active for the activity.
* <li>Orientation request loop detected and is not letterboxed for fixed orientation
* </ul>
*/
boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
if (shouldEnableWithOverrideAndProperty(
/* gatingCondition */ mLetterboxConfiguration
::isPolicyForIgnoringRequestedOrientationEnabled,
mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled,
mBooleanPropertyIgnoreRequestedOrientation)) {
if (mIsRelaunchingAfterRequestedOrientationChanged) {
Slog.w(TAG, "Ignoring orientation update to "
+ screenOrientationToString(requestedOrientation)
+ " due to relaunching after setRequestedOrientation for "
+ mActivityRecord);
return true;
}
if (isCameraCompatTreatmentActive()) {
Slog.w(TAG, "Ignoring orientation update to "
+ screenOrientationToString(requestedOrientation)
+ " due to camera compat treatment for " + mActivityRecord);
return true;
}
}
if (shouldIgnoreOrientationRequestLoop()) {
Slog.w(TAG, "Ignoring orientation update to "
+ screenOrientationToString(requestedOrientation)
+ " as orientation request loop was detected for "
+ mActivityRecord);
return true;
}
return false;
}
/**
* Whether an app is calling {@link android.app.Activity#setRequestedOrientation}
* in a loop and orientation request should be ignored.
*
* <p>This should only be called once in response to
* {@link android.app.Activity#setRequestedOrientation}. See
* {@link #shouldIgnoreRequestedOrientation} for more details.
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Flag gating the treatment is enabled
* <li>Opt-out component property isn't enabled
* <li>Per-app override is enabled
* <li>App has requested orientation more than 2 times within 1-second
* timer and activity is not letterboxed for fixed orientation
* </ul>
*/
@VisibleForTesting
boolean shouldIgnoreOrientationRequestLoop() {
if (!shouldEnableWithOptInOverrideAndOptOutProperty(
/* gatingCondition */ mLetterboxConfiguration
::isPolicyForIgnoringRequestedOrientationEnabled,
mIsOverrideEnableCompatIgnoreOrientationRequestWhenLoopDetectedEnabled,
mBooleanPropertyAllowIgnoringOrientationRequestWhenLoopDetected)) {
return false;
}
final long currTimeMs = System.currentTimeMillis();
if (currTimeMs - mTimeMsLastSetOrientationRequest
< SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS) {
mSetOrientationRequestCounter += 1;
} else {
// Resets app setOrientationRequest counter if timed out
mSetOrientationRequestCounter = 0;
}
// Update time last called
mTimeMsLastSetOrientationRequest = currTimeMs;
return mSetOrientationRequestCounter >= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP
&& !mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio();
}
@VisibleForTesting
int getSetOrientationRequestCounter() {
return mSetOrientationRequestCounter;
}
/**
* Whether sending compat fake focus for split screen resumed activities is enabled. Needed
* because some game engines wait to get focus before drawing the content of the app which isn't
* guaranteed by default in multi-window modes.
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Flag gating the treatment is enabled
* <li>Component property is NOT set to false
* <li>Component property is set to true or per-app override is enabled
* </ul>
*/
boolean shouldSendFakeFocus() {
return shouldEnableWithOverrideAndProperty(
/* gatingCondition */ mLetterboxConfiguration::isCompatFakeFocusEnabled,
mIsOverrideEnableCompatFakeFocusEnabled,
mBooleanPropertyFakeFocus);
}
/**
* Whether we should apply the min aspect ratio per-app override. When this override is applied
* the min aspect ratio given in the app's manifest will be overridden to the largest enabled
* aspect ratio treatment unless the app's manifest value is higher. The treatment will also
* apply if no value is provided in the manifest.
*
* <p>This method returns {@code true} when the following conditions are met:
* <ul>
* <li>Opt-out component property isn't enabled
* <li>Per-app override is enabled
* </ul>
*/
boolean shouldOverrideMinAspectRatio() {
return shouldEnableWithOptInOverrideAndOptOutProperty(
/* gatingCondition */ () -> true,
mIsOverrideMinAspectRatio,
mBooleanPropertyAllowMinAspectRatioOverride);
}
/**
* Whether we should apply the force resize per-app override. When this override is applied it
* forces the packages it is applied to to be resizable. It won't change whether the app can be
* put into multi-windowing mode, but allow the app to resize without going into size-compat
* mode when the window container resizes, such as display size change or screen rotation.
*
* <p>This method returns {@code true} when the following conditions are met:
* <ul>
* <li>Opt-out component property isn't enabled
* <li>Per-app override is enabled
* </ul>
*/
boolean shouldOverrideForceResizeApp() {
return shouldEnableWithOptInOverrideAndOptOutProperty(
/* gatingCondition */ () -> true,
mIsOverrideForceResizeApp,
mBooleanPropertyAllowForceResizeOverride);
}
/**
* Whether we should apply the force non resize per-app override. When this override is applied
* it forces the packages it is applied to to be non-resizable.
*
* <p>This method returns {@code true} when the following conditions are met:
* <ul>
* <li>Opt-out component property isn't enabled
* <li>Per-app override is enabled
* </ul>
*/
boolean shouldOverrideForceNonResizeApp() {
return shouldEnableWithOptInOverrideAndOptOutProperty(
/* gatingCondition */ () -> true,
mIsOverrideForceNonResizeApp,
mBooleanPropertyAllowForceResizeOverride);
}
/**
* Sets whether an activity is relaunching after the app has called {@link
* android.app.Activity#setRequestedOrientation}.
*/
void setRelaunchingAfterRequestedOrientationChanged(boolean isRelaunching) {
mIsRelaunchingAfterRequestedOrientationChanged = isRelaunching;
}
/**
* Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}
* following the camera compat force rotation in {@link DisplayRotationCompatPolicy}.
*/
boolean isRefreshAfterRotationRequested() {
return mIsRefreshAfterRotationRequested;
}
void setIsRefreshAfterRotationRequested(boolean isRequested) {
mIsRefreshAfterRotationRequested = isRequested;
}
boolean isOverrideRespectRequestedOrientationEnabled() {
return mIsOverrideRespectRequestedOrientationEnabled;
}
/**
* Whether should fix display orientation to landscape natural orientation when a task is
* fullscreen and the display is ignoring orientation requests.
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Opt-out component property isn't enabled
* <li>Opt-in per-app override is enabled
* <li>Task is in fullscreen.
* <li>{@link DisplayContent#getIgnoreOrientationRequest} is enabled
* <li>Natural orientation of the display is landscape.
* </ul>
*/
boolean shouldUseDisplayLandscapeNaturalOrientation() {
return shouldEnableWithOptInOverrideAndOptOutProperty(
/* gatingCondition */ () -> mActivityRecord.mDisplayContent != null
&& mActivityRecord.getTask() != null
&& mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()
&& !mActivityRecord.getTask().inMultiWindowMode()
&& mActivityRecord.mDisplayContent.getNaturalOrientation()
== ORIENTATION_LANDSCAPE,
mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled,
mBooleanPropertyAllowDisplayOrientationOverride);
}
@ScreenOrientation
int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
if (shouldApplyUserFullscreenOverride()
&& mActivityRecord.mDisplayContent != null
&& mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()) {
Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ mActivityRecord + " is overridden to "
+ screenOrientationToString(SCREEN_ORIENTATION_USER)
+ " by user aspect ratio settings.");
return SCREEN_ORIENTATION_USER;
}
// In some cases (e.g. Kids app) we need to map the candidate orientation to some other
// orientation.
candidate = mActivityRecord.mWmService.mapOrientationRequest(candidate);
if (shouldApplyUserMinAspectRatioOverride() && (!isFixedOrientation(candidate)
|| candidate == SCREEN_ORIENTATION_LOCKED)) {
Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ mActivityRecord + " is overridden to "
+ screenOrientationToString(SCREEN_ORIENTATION_PORTRAIT)
+ " by user aspect ratio settings.");
return SCREEN_ORIENTATION_PORTRAIT;
}
if (FALSE.equals(mBooleanPropertyAllowOrientationOverride)) {
return candidate;
}
DisplayContent displayContent = mActivityRecord.mDisplayContent;
if (mIsOverrideOrientationOnlyForCameraEnabled && displayContent != null
&& (displayContent.mDisplayRotationCompatPolicy == null
|| !displayContent.mDisplayRotationCompatPolicy
.isActivityEligibleForOrientationOverride(mActivityRecord))) {
return candidate;
}
if (mIsOverrideToReverseLandscapeOrientationEnabled
&& (isFixedOrientationLandscape(candidate) || mIsOverrideAnyOrientationEnabled)) {
Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ mActivityRecord + " is overridden to "
+ screenOrientationToString(SCREEN_ORIENTATION_REVERSE_LANDSCAPE));
return SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
}
if (!mIsOverrideAnyOrientationEnabled && isFixedOrientation(candidate)) {
return candidate;
}
if (mIsOverrideToPortraitOrientationEnabled) {
Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ mActivityRecord + " is overridden to "
+ screenOrientationToString(SCREEN_ORIENTATION_PORTRAIT));
return SCREEN_ORIENTATION_PORTRAIT;
}
if (mIsOverrideToNosensorOrientationEnabled) {
Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ mActivityRecord + " is overridden to "
+ screenOrientationToString(SCREEN_ORIENTATION_NOSENSOR));
return SCREEN_ORIENTATION_NOSENSOR;
}
return candidate;
}
boolean isOverrideOrientationOnlyForCameraEnabled() {
return mIsOverrideOrientationOnlyForCameraEnabled;
}
/**
* Whether activity is eligible for activity "refresh" after camera compat force rotation
* treatment. See {@link DisplayRotationCompatPolicy} for context.
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Flag gating the camera compat treatment is enabled.
* <li>Activity isn't opted out by the device manufacturer with override or by the app
* developers with the component property.
* </ul>
*/
boolean shouldRefreshActivityForCameraCompat() {
return shouldEnableWithOptOutOverrideAndProperty(
/* gatingCondition */ () -> mLetterboxConfiguration
.isCameraCompatTreatmentEnabled(),
mIsOverrideCameraCompatDisableRefreshEnabled,
mBooleanPropertyCameraCompatAllowRefresh);
}
/**
* Whether activity should be "refreshed" after the camera compat force rotation treatment
* using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped
* -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context.
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Flag gating the camera compat treatment is enabled.
* <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the
* component property by the app developers.
* <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device
* manufacturer with override / by the app developers with the component property.
* </ul>
*/
boolean shouldRefreshActivityViaPauseForCameraCompat() {
return shouldEnableWithOverrideAndProperty(
/* gatingCondition */ () -> mLetterboxConfiguration
.isCameraCompatTreatmentEnabled(),
mIsOverrideCameraCompatEnableRefreshViaPauseEnabled,
mBooleanPropertyCameraCompatEnableRefreshViaPause);
}
/**
* Whether activity is eligible for camera compat force rotation treatment. See {@link
* DisplayRotationCompatPolicy} for context.
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Flag gating the camera compat treatment is enabled.
* <li>Activity isn't opted out by the device manufacturer with override or by the app
* developers with the component property.
* </ul>
*/
boolean shouldForceRotateForCameraCompat() {
return shouldEnableWithOptOutOverrideAndProperty(
/* gatingCondition */ () -> mLetterboxConfiguration
.isCameraCompatTreatmentEnabled(),
mIsOverrideCameraCompatDisableForceRotationEnabled,
mBooleanPropertyCameraCompatAllowForceRotation);
}
private boolean isCameraCompatTreatmentActive() {
DisplayContent displayContent = mActivityRecord.mDisplayContent;
if (displayContent == null) {
return false;
}
return displayContent.mDisplayRotationCompatPolicy != null
&& displayContent.mDisplayRotationCompatPolicy
.isTreatmentEnabledForActivity(mActivityRecord);
}
private boolean isCompatChangeEnabled(long overrideChangeId) {
return mActivityRecord.info.isChangeEnabled(overrideChangeId);
}
/**
* Returns {@code true} when the following conditions are met:
* <ul>
* <li>{@code gatingCondition} isn't {@code false}
* <li>OEM didn't opt out with a per-app override
* <li>App developers didn't opt out with a component {@code property}
* </ul>
*
* <p>This is used for the treatments that are enabled based with the heuristic but can be
* disabled on per-app basis by OEMs or app developers.
*/
private boolean shouldEnableWithOptOutOverrideAndProperty(BooleanSupplier gatingCondition,
boolean isOverrideChangeEnabled, Boolean property) {
if (!gatingCondition.getAsBoolean()) {
return false;
}
return !FALSE.equals(property) && !isOverrideChangeEnabled;
}
/**
* Returns {@code true} when the following conditions are met:
* <ul>
* <li>{@code gatingCondition} isn't {@code false}
* <li>OEM did opt in with a per-app override
* <li>App developers didn't opt out with a component {@code property}
* </ul>
*
* <p>This is used for the treatments that are enabled based with the heuristic but can be
* disabled on per-app basis by OEMs or app developers.
*/
private boolean shouldEnableWithOptInOverrideAndOptOutProperty(BooleanSupplier gatingCondition,
boolean isOverrideChangeEnabled, Boolean property) {
if (!gatingCondition.getAsBoolean()) {
return false;
}
return !FALSE.equals(property) && isOverrideChangeEnabled;
}
/**
* Returns {@code true} when the following conditions are met:
* <ul>
* <li>{@code gatingCondition} isn't {@code false}
* <li>App developers didn't opt out with a component {@code property}
* <li>App developers opted in with a component {@code property} or an OEM opted in with a
* per-app override
* </ul>
*
* <p>This is used for the treatments that are enabled only on per-app basis.
*/
private boolean shouldEnableWithOverrideAndProperty(BooleanSupplier gatingCondition,
boolean isOverrideChangeEnabled, Boolean property) {
if (!gatingCondition.getAsBoolean()) {
return false;
}
if (FALSE.equals(property)) {
return false;
}
return TRUE.equals(property) || isOverrideChangeEnabled;
}
boolean hasWallpaperBackgroundForLetterbox() {
return mShowWallpaperForLetterboxBackground;
}
/** Gets the letterbox insets. The insets will be empty if there is no letterbox. */
Rect getLetterboxInsets() {
if (mLetterbox != null) {
return mLetterbox.getInsets();
} else {
return new Rect();
}
}
/** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */
void getLetterboxInnerBounds(Rect outBounds) {
if (mLetterbox != null) {
outBounds.set(mLetterbox.getInnerFrame());
final WindowState w = mActivityRecord.findMainWindow();
if (w != null) {
adjustBoundsForTaskbar(w, outBounds);
}
} else {
outBounds.setEmpty();
}
}
/** Gets the outer bounds of letterbox. The bounds will be empty if there is no letterbox. */
private void getLetterboxOuterBounds(Rect outBounds) {
if (mLetterbox != null) {
outBounds.set(mLetterbox.getOuterFrame());
} else {
outBounds.setEmpty();
}
}
/**
* @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent
* when the current activity is displayed.
*/
boolean isFullyTransparentBarAllowed(Rect rect) {
return mLetterbox == null || mLetterbox.notIntersectsOrFullyContains(rect);
}
void updateLetterboxSurface(WindowState winHint) {
updateLetterboxSurface(winHint, mActivityRecord.getSyncTransaction());
}
void updateLetterboxSurface(WindowState winHint, Transaction t) {
final WindowState w = mActivityRecord.findMainWindow();
if (w != winHint && winHint != null && w != null) {
return;
}
layoutLetterbox(winHint);
if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
mLetterbox.applySurfaceChanges(t);
}
}
void layoutLetterbox(WindowState winHint) {
final WindowState w = mActivityRecord.findMainWindow();
if (w == null || winHint != null && w != winHint) {
return;
}
updateRoundedCornersIfNeeded(w);
// If there is another main window that is not an application-starting window, we should
// update rounded corners for it as well, to avoid flickering rounded corners.
final WindowState nonStartingAppW = mActivityRecord.findMainWindow(
/* includeStartingApp= */ false);
if (nonStartingAppW != null && nonStartingAppW != w) {
updateRoundedCornersIfNeeded(nonStartingAppW);
}
updateWallpaperForLetterbox(w);
if (shouldShowLetterboxUi(w)) {
if (mLetterbox == null) {
mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null),
mActivityRecord.mWmService.mTransactionFactory,
this::shouldLetterboxHaveRoundedCorners,
this::getLetterboxBackgroundColor,
this::hasWallpaperBackgroundForLetterbox,
this::getLetterboxWallpaperBlurRadiusPx,
this::getLetterboxWallpaperDarkScrimAlpha,
this::handleHorizontalDoubleTap,
this::handleVerticalDoubleTap,
this::getLetterboxParentSurface);
mLetterbox.attachInput(w);
}
if (mActivityRecord.isInLetterboxAnimation()) {
// In this case we attach the letterbox to the task instead of the activity.
mActivityRecord.getTask().getPosition(mTmpPoint);
} else {
mActivityRecord.getPosition(mTmpPoint);
}
// Get the bounds of the "space-to-fill". The transformed bounds have the highest
// priority because the activity is launched in a rotated environment. In multi-window
// mode, the taskFragment-level represents this for both split-screen
// and activity-embedding. In fullscreen-mode, the task container does
// (since the orientation letterbox is also applied to the task).
final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds();
final Rect spaceToFill = transformedBounds != null
? transformedBounds
: mActivityRecord.inMultiWindowMode()
? mActivityRecord.getTaskFragment().getBounds()
: mActivityRecord.getRootTask().getParent().getBounds();
// In case of translucent activities an option is to use the WindowState#getFrame() of
// the first opaque activity beneath. In some cases (e.g. an opaque activity is using
// non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct
// information and in particular it might provide a value for a smaller area making
// the letterbox overlap with the translucent activity's frame.
// If we use WindowState#getFrame() for the translucent activity's letterbox inner
// frame, the letterbox will then be overlapped with the translucent activity's frame.
// Because the surface layer of letterbox is lower than an activity window, this
// won't crop the content, but it may affect other features that rely on values stored
// in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher
// For this reason we use ActivityRecord#getBounds() that the translucent activity
// inherits from the first opaque activity beneath and also takes care of the scaling
// in case of activities in size compat mode.
final Rect innerFrame = hasInheritedLetterboxBehavior()
? mActivityRecord.getBounds() : w.getFrame();
mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
if (mDoubleTapEvent) {
// We need to notify Shell that letterbox position has changed.
mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
}
} else if (mLetterbox != null) {
mLetterbox.hide();
}
}
boolean isFromDoubleTap() {
final boolean isFromDoubleTap = mDoubleTapEvent;
mDoubleTapEvent = false;
return isFromDoubleTap;
}
SurfaceControl getLetterboxParentSurface() {
if (mActivityRecord.isInLetterboxAnimation()) {
return mActivityRecord.getTask().getSurfaceControl();
}
return mActivityRecord.getSurfaceControl();
}
private boolean shouldLetterboxHaveRoundedCorners() {
// TODO(b/214030873): remove once background is drawn for transparent activities
// Letterbox shouldn't have rounded corners if the activity is transparent
return mLetterboxConfiguration.isLetterboxActivityCornersRounded()
&& mActivityRecord.fillsParent();
}
// Check if we are in the given pose and in fullscreen mode.
// Note that we check the task rather than the parent as with ActivityEmbedding the parent might
// be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
// actually fullscreen.
private boolean isDisplayFullScreenAndInPosture(DeviceStateController.DeviceState state,
boolean isTabletop) {
Task task = mActivityRecord.getTask();
return mActivityRecord.mDisplayContent != null
&& mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(state,
isTabletop)
&& task != null
&& task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
}
// Note that we check the task rather than the parent as with ActivityEmbedding the parent might
// be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
// actually fullscreen.
private boolean isDisplayFullScreenAndSeparatingHinge() {
Task task = mActivityRecord.getTask();
return mActivityRecord.mDisplayContent != null
&& mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
&& task != null
&& task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
}
float getHorizontalPositionMultiplier(Configuration parentConfiguration) {
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
boolean bookModeEnabled = isFullScreenAndBookModeEnabled();
return isHorizontalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
? mLetterboxConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled)
: mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled);
}
private boolean isFullScreenAndBookModeEnabled() {
return isDisplayFullScreenAndInPosture(
DeviceStateController.DeviceState.HALF_FOLDED, false /* isTabletop */)
&& mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
}
float getVerticalPositionMultiplier(Configuration parentConfiguration) {
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
boolean tabletopMode = isDisplayFullScreenAndInPosture(
DeviceStateController.DeviceState.HALF_FOLDED, true /* isTabletop */);
return isVerticalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
? mLetterboxConfiguration.getVerticalMultiplierForReachability(tabletopMode)
: mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
}
float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) {
return shouldUseSplitScreenAspectRatio(parentConfiguration)
? getSplitScreenAspectRatio()
: mActivityRecord.shouldCreateCompatDisplayInsets()
? getDefaultMinAspectRatioForUnresizableApps()
: getDefaultMinAspectRatio();
}
void recomputeConfigurationForCameraCompatIfNeeded() {
if (isOverrideOrientationOnlyForCameraEnabled()
|| isCameraCompatSplitScreenAspectRatioAllowed()) {
mActivityRecord.recomputeConfiguration();
}
}
/**
* Whether we use split screen aspect ratio for the activity when camera compat treatment
* is active because the corresponding config is enabled and activity supports resizing.
*/
boolean isCameraCompatSplitScreenAspectRatioAllowed() {
return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()
&& !mActivityRecord.shouldCreateCompatDisplayInsets();
}
private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) {
final boolean isBookMode = isDisplayFullScreenAndInPosture(
DeviceStateController.DeviceState.HALF_FOLDED,
/* isTabletop */ false);
final boolean isNotCenteredHorizontally = getHorizontalPositionMultiplier(
parentConfiguration) != LETTERBOX_POSITION_MULTIPLIER_CENTER;
final boolean isTabletopMode = isDisplayFullScreenAndInPosture(
DeviceStateController.DeviceState.HALF_FOLDED,
/* isTabletop */ true);
// Don't resize to split screen size when in book mode if letterbox position is centered
return ((isBookMode && isNotCenteredHorizontally) || isTabletopMode)
|| isCameraCompatSplitScreenAspectRatioAllowed()
&& isCameraCompatTreatmentActive();
}
private float getDefaultMinAspectRatioForUnresizableApps() {
if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()
|| mActivityRecord.getDisplayArea() == null) {
return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
> MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
: getDefaultMinAspectRatio();
}
return getSplitScreenAspectRatio();
}
float getSplitScreenAspectRatio() {
// Getting the same aspect ratio that apps get in split screen.
final DisplayArea displayArea = mActivityRecord.getDisplayArea();
if (displayArea == null) {
return getDefaultMinAspectRatioForUnresizableApps();
}
int dividerWindowWidth =
getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_thickness);
int dividerInsets =
getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_insets);
int dividerSize = dividerWindowWidth - dividerInsets * 2;
final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
if (bounds.width() >= bounds.height()) {
bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
bounds.right = bounds.centerX();
} else {
bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
bounds.bottom = bounds.centerY();
}
return computeAspectRatio(bounds);
}
/**
* Whether we should enable users to resize the current app.
*/
boolean shouldEnableUserAspectRatioSettings() {
// We use mBooleanPropertyAllowUserAspectRatioOverride to allow apps to opt-out which has
// effect only if explicitly false. If mBooleanPropertyAllowUserAspectRatioOverride is null,
// the current app doesn't opt-out so the first part of the predicate is true.
return !FALSE.equals(mBooleanPropertyAllowUserAspectRatioOverride)
&& mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled()
&& mActivityRecord.mDisplayContent != null
&& mActivityRecord.mDisplayContent.getIgnoreOrientationRequest();
}
/**
* Whether we should apply the user aspect ratio override to the min aspect ratio for the
* current app.
*/
boolean shouldApplyUserMinAspectRatioOverride() {
if (!shouldEnableUserAspectRatioSettings()) {
return false;
}
mUserAspectRatio = getUserMinAspectRatioOverrideCode();
return mUserAspectRatio != USER_MIN_ASPECT_RATIO_UNSET
&& mUserAspectRatio != USER_MIN_ASPECT_RATIO_FULLSCREEN;
}
boolean shouldApplyUserFullscreenOverride() {
if (FALSE.equals(mBooleanPropertyAllowUserAspectRatioOverride)
|| FALSE.equals(mBooleanPropertyAllowUserAspectRatioFullscreenOverride)
|| !mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled()) {
return false;
}
mUserAspectRatio = getUserMinAspectRatioOverrideCode();
return mUserAspectRatio == USER_MIN_ASPECT_RATIO_FULLSCREEN;
}
float getUserMinAspectRatio() {
switch (mUserAspectRatio) {
case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
return getDisplaySizeMinAspectRatio();
case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
return getSplitScreenAspectRatio();
case USER_MIN_ASPECT_RATIO_16_9:
return 16 / 9f;
case USER_MIN_ASPECT_RATIO_4_3:
return 4 / 3f;
case USER_MIN_ASPECT_RATIO_3_2:
return 3 / 2f;
default:
throw new AssertionError("Unexpected user min aspect ratio override: "
+ mUserAspectRatio);
}
}
@VisibleForTesting
int getUserMinAspectRatioOverrideCode() {
try {
return mActivityRecord.mAtmService.getPackageManager()
.getUserMinAspectRatio(mActivityRecord.packageName, mActivityRecord.mUserId);
} catch (RemoteException e) {
Slog.w(TAG, "Exception thrown retrieving aspect ratio user override " + this, e);
}
return mUserAspectRatio;
}
private float getDisplaySizeMinAspectRatio() {
final DisplayArea displayArea = mActivityRecord.getDisplayArea();
if (displayArea == null) {
return mActivityRecord.info.getMinAspectRatio();
}
final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
return computeAspectRatio(bounds);
}
private float getDefaultMinAspectRatio() {
if (mActivityRecord.getDisplayArea() == null
|| !mLetterboxConfiguration
.getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
}
return getDisplaySizeMinAspectRatio();
}
Resources getResources() {
return mActivityRecord.mWmService.mContext.getResources();
}
@LetterboxConfiguration.LetterboxVerticalReachabilityPosition
int getLetterboxPositionForVerticalReachability() {
final boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
return mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(
isInFullScreenTabletopMode);
}
@LetterboxConfiguration.LetterboxHorizontalReachabilityPosition
int getLetterboxPositionForHorizontalReachability() {
final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled();
return mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(
isInFullScreenBookMode);
}
@VisibleForTesting
void handleHorizontalDoubleTap(int x) {
if (!isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) {
return;
}
if (mLetterbox.getInnerFrame().left <= x && mLetterbox.getInnerFrame().right >= x) {
// Only react to clicks at the sides of the letterboxed app window.
return;
}
boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge()
&& mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
.getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
if (mLetterbox.getInnerFrame().left > x) {
// Moving to the next stop on the left side of the app window: right > center > left.
mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
isInFullScreenBookMode);
int changeToLog =
letterboxPositionForHorizontalReachability
== LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
} else if (mLetterbox.getInnerFrame().right < x) {
// Moving to the next stop on the right side of the app window: left > center > right.
mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
isInFullScreenBookMode);
int changeToLog =
letterboxPositionForHorizontalReachability
== LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
}
// TODO(197549949): Add animation for transition.
mActivityRecord.recomputeConfiguration();
}
@VisibleForTesting
void handleVerticalDoubleTap(int y) {
if (!isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) {
return;
}
if (mLetterbox.getInnerFrame().top <= y && mLetterbox.getInnerFrame().bottom >= y) {
// Only react to clicks at the top and bottom of the letterboxed app window.
return;
}
boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
int letterboxPositionForVerticalReachability = mLetterboxConfiguration
.getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
if (mLetterbox.getInnerFrame().top > y) {
// Moving to the next stop on the top side of the app window: bottom > center > top.
mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(
isInFullScreenTabletopMode);
int changeToLog =
letterboxPositionForVerticalReachability
== LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
} else if (mLetterbox.getInnerFrame().bottom < y) {
// Moving to the next stop on the bottom side of the app window: top > center > bottom.
mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
isInFullScreenTabletopMode);
int changeToLog =
letterboxPositionForVerticalReachability
== LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
}
// TODO(197549949): Add animation for transition.
mActivityRecord.recomputeConfiguration();
}
/**
* Whether horizontal reachability is enabled for an activity in the current configuration.
*
* <p>Conditions that needs to be met:
* <ul>
* <li>Activity is portrait-only.
* <li>Fullscreen window in landscape device orientation.
* <li>Horizontal Reachability is enabled.
* <li>Activity fills parent vertically.
* </ul>
*/
private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) {
// Use screen resolved bounds which uses resolved bounds or size compat bounds
// as activity bounds can sometimes be empty
return mLetterboxConfiguration.getIsHorizontalReachabilityEnabled()
&& parentConfiguration.windowConfiguration.getWindowingMode()
== WINDOWING_MODE_FULLSCREEN
&& (parentConfiguration.orientation == ORIENTATION_LANDSCAPE
&& mActivityRecord.getOrientationForReachability() == ORIENTATION_PORTRAIT)
// Check whether the activity fills the parent vertically.
&& parentConfiguration.windowConfiguration.getAppBounds().height()
<= mActivityRecord.getScreenResolvedBounds().height();
}
@VisibleForTesting
boolean isHorizontalReachabilityEnabled() {
return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
}
boolean isLetterboxDoubleTapEducationEnabled() {
return isHorizontalReachabilityEnabled() || isVerticalReachabilityEnabled();
}
/**
* Whether vertical reachability is enabled for an activity in the current configuration.
*
* <p>Conditions that needs to be met:
* <ul>
* <li>Activity is landscape-only.
* <li>Fullscreen window in portrait device orientation.
* <li>Vertical Reachability is enabled.
* <li>Activity fills parent horizontally.
* </ul>
*/
private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) {
// Use screen resolved bounds which uses resolved bounds or size compat bounds
// as activity bounds can sometimes be empty
return mLetterboxConfiguration.getIsVerticalReachabilityEnabled()
&& parentConfiguration.windowConfiguration.getWindowingMode()
== WINDOWING_MODE_FULLSCREEN
&& (parentConfiguration.orientation == ORIENTATION_PORTRAIT
&& mActivityRecord.getOrientationForReachability() == ORIENTATION_LANDSCAPE)
// Check whether the activity fills the parent horizontally.
&& parentConfiguration.windowConfiguration.getBounds().width()
== mActivityRecord.getScreenResolvedBounds().width();
}
@VisibleForTesting
boolean isVerticalReachabilityEnabled() {
return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
}
@VisibleForTesting
boolean shouldShowLetterboxUi(WindowState mainWindow) {
if (mIsRelaunchingAfterRequestedOrientationChanged || !isSurfaceReadyToShow(mainWindow)) {
return mLastShouldShowLetterboxUi;
}
final boolean shouldShowLetterboxUi =
(mActivityRecord.isInLetterboxAnimation() || isSurfaceVisible(mainWindow))
&& mainWindow.areAppWindowBoundsLetterboxed()
// Check for FLAG_SHOW_WALLPAPER explicitly instead of using
// WindowContainer#showWallpaper because the later will return true when this
// activity is using blurred wallpaper for letterbox background.
&& (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0;
mLastShouldShowLetterboxUi = shouldShowLetterboxUi;
return shouldShowLetterboxUi;
}
@VisibleForTesting
boolean isSurfaceReadyToShow(WindowState mainWindow) {
return mainWindow.isDrawn() // Regular case
// Waiting for relayoutWindow to call preserveSurface
|| mainWindow.isDragResizeChanged();
}
@VisibleForTesting
boolean isSurfaceVisible(WindowState mainWindow) {
return mainWindow.isOnScreen() && (mActivityRecord.isVisible()
|| mActivityRecord.isVisibleRequested());
}
private Color getLetterboxBackgroundColor() {
final WindowState w = mActivityRecord.findMainWindow();
if (w == null || w.isLetterboxedForDisplayCutout()) {
return Color.valueOf(Color.BLACK);
}
@LetterboxBackgroundType int letterboxBackgroundType =
mLetterboxConfiguration.getLetterboxBackgroundType();
TaskDescription taskDescription = mActivityRecord.taskDescription;
switch (letterboxBackgroundType) {
case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING:
if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) {
return Color.valueOf(taskDescription.getBackgroundColorFloating());
}
break;
case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND:
if (taskDescription != null && taskDescription.getBackgroundColor() != 0) {
return Color.valueOf(taskDescription.getBackgroundColor());
}
break;
case LETTERBOX_BACKGROUND_WALLPAPER:
if (hasWallpaperBackgroundForLetterbox()) {
// Color is used for translucent scrim that dims wallpaper.
return mLetterboxConfiguration.getLetterboxBackgroundColor();
}
Slog.w(TAG, "Wallpaper option is selected for letterbox background but "
+ "blur is not supported by a device or not supported in the current "
+ "window configuration or both alpha scrim and blur radius aren't "
+ "provided so using solid color background");
break;
case LETTERBOX_BACKGROUND_SOLID_COLOR:
return mLetterboxConfiguration.getLetterboxBackgroundColor();
default:
throw new AssertionError(
"Unexpected letterbox background type: " + letterboxBackgroundType);
}
// If picked option configured incorrectly or not supported then default to a solid color
// background.
return mLetterboxConfiguration.getLetterboxBackgroundColor();
}
private void updateRoundedCornersIfNeeded(final WindowState mainWindow) {
final SurfaceControl windowSurface = mainWindow.getSurfaceControl();
if (windowSurface == null || !windowSurface.isValid()) {
return;
}
// cropBounds must be non-null for the cornerRadius to be ever applied.
mActivityRecord.getSyncTransaction()
.setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow))
.setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
}
@VisibleForTesting
@Nullable
Rect getCropBoundsIfNeeded(final WindowState mainWindow) {
if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
// We don't want corner radius on the window.
// In the case the ActivityRecord requires a letterboxed animation we never want
// rounded corners on the window because rounded corners are applied at the
// animation-bounds surface level and rounded corners on the window would interfere
// with that leading to unexpected rounded corner positioning during the animation.
return null;
}
final Rect cropBounds = new Rect(mActivityRecord.getBounds());
// In case of translucent activities we check if the requested size is different from
// the size provided using inherited bounds. In that case we decide to not apply rounded
// corners because we assume the specific layout would. This is the case when the layout
// of the translucent activity uses only a part of all the bounds because of the use of
// LayoutParams.WRAP_CONTENT.
if (hasInheritedLetterboxBehavior() && (cropBounds.width() != mainWindow.mRequestedWidth
|| cropBounds.height() != mainWindow.mRequestedHeight)) {
return null;
}
// It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo}
// because taskbar bounds used in {@link #adjustBoundsIfNeeded}
// are in screen coordinates
adjustBoundsForTaskbar(mainWindow, cropBounds);
final float scale = mainWindow.mInvGlobalScale;
if (scale != 1f && scale > 0f) {
cropBounds.scale(scale);
}
// ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface
// control is in the top left corner of an app window so offsetting bounds
// accordingly.
cropBounds.offsetTo(0, 0);
return cropBounds;
}
private boolean requiresRoundedCorners(final WindowState mainWindow) {
return isLetterboxedNotForDisplayCutout(mainWindow)
&& mLetterboxConfiguration.isLetterboxActivityCornersRounded();
}
// Returns rounded corners radius the letterboxed activity should have based on override in
// R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii.
// Device corners can be different on the right and left sides, but we use the same radius
// for all corners for consistency and pick a minimal bottom one for consistency with a
// taskbar rounded corners.
int getRoundedCornersRadius(final WindowState mainWindow) {
if (!requiresRoundedCorners(mainWindow)) {
return 0;
}
final int radius;
if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
radius = mLetterboxConfiguration.getLetterboxActivityCornersRadius();
} else {
final InsetsState insetsState = mainWindow.getInsetsState();
radius = Math.min(
getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
}
final float scale = mainWindow.mInvGlobalScale;
return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius;
}
/**
* Returns the taskbar in case it is visible and expanded in height, otherwise returns null.
*/
@VisibleForTesting
@Nullable
InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) {
final InsetsState state = mainWindow.getInsetsState();
for (int i = state.sourceSize() - 1; i >= 0; i--) {
final InsetsSource source = state.sourceAt(i);
if (source.getType() == WindowInsets.Type.navigationBars()
&& source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)
&& source.isVisible()) {
return source;
}
}
return null;
}
boolean getIsRelaunchingAfterRequestedOrientationChanged() {
return mIsRelaunchingAfterRequestedOrientationChanged;
}
private void adjustBoundsForTaskbar(final WindowState mainWindow, final Rect bounds) {
// Rounded corners should be displayed above the taskbar. When taskbar is hidden,
// an insets frame is equal to a navigation bar which shouldn't affect position of
// rounded corners since apps are expected to handle navigation bar inset.
// This condition checks whether the taskbar is visible.
// Do not crop the taskbar inset if the window is in immersive mode - the user can
// swipe to show/hide the taskbar as an overlay.
// Adjust the bounds only in case there is an expanded taskbar,
// otherwise the rounded corners will be shown behind the navbar.
final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow);
if (expandedTaskbarOrNull != null) {
// Rounded corners should be displayed above the expanded taskbar.
bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top);
}
}
private int getInsetsStateCornerRadius(
InsetsState insetsState, @RoundedCorner.Position int position) {
RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
return corner == null ? 0 : corner.getRadius();
}
private boolean isLetterboxedNotForDisplayCutout(WindowState mainWindow) {
return shouldShowLetterboxUi(mainWindow)
&& !mainWindow.isLetterboxedForDisplayCutout();
}
private void updateWallpaperForLetterbox(WindowState mainWindow) {
@LetterboxBackgroundType int letterboxBackgroundType =
mLetterboxConfiguration.getLetterboxBackgroundType();
boolean wallpaperShouldBeShown =
letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER
// Don't use wallpaper as a background if letterboxed for display cutout.
&& isLetterboxedNotForDisplayCutout(mainWindow)
// Check that dark scrim alpha or blur radius are provided
&& (getLetterboxWallpaperBlurRadiusPx() > 0
|| getLetterboxWallpaperDarkScrimAlpha() > 0)
// Check that blur is supported by a device if blur radius is provided.
&& (getLetterboxWallpaperBlurRadiusPx() <= 0
|| isLetterboxWallpaperBlurSupported());
if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) {
mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown;
mActivityRecord.requestUpdateWallpaperIfNeeded();
}
}
private int getLetterboxWallpaperBlurRadiusPx() {
int blurRadius = mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx();
return Math.max(blurRadius, 0);
}
private float getLetterboxWallpaperDarkScrimAlpha() {
float alpha = mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha();
// No scrim by default.
return (alpha < 0 || alpha >= 1) ? 0.0f : alpha;
}
private boolean isLetterboxWallpaperBlurSupported() {
return mLetterboxConfiguration.mContext.getSystemService(WindowManager.class)
.isCrossWindowBlurEnabled();
}
void dump(PrintWriter pw, String prefix) {
final WindowState mainWin = mActivityRecord.findMainWindow();
if (mainWin == null) {
return;
}
boolean areBoundsLetterboxed = mainWin.areAppWindowBoundsLetterboxed();
pw.println(prefix + "areBoundsLetterboxed=" + areBoundsLetterboxed);
if (!areBoundsLetterboxed) {
return;
}
pw.println(prefix + " letterboxReason=" + getLetterboxReasonString(mainWin));
pw.println(prefix + " activityAspectRatio="
+ mActivityRecord.computeAspectRatio(mActivityRecord.getBounds()));
boolean shouldShowLetterboxUi = shouldShowLetterboxUi(mainWin);
pw.println(prefix + "shouldShowLetterboxUi=" + shouldShowLetterboxUi);
if (!shouldShowLetterboxUi) {
return;
}
pw.println(prefix + " letterboxBackgroundColor=" + Integer.toHexString(
getLetterboxBackgroundColor().toArgb()));
pw.println(prefix + " letterboxBackgroundType="
+ letterboxBackgroundTypeToString(
mLetterboxConfiguration.getLetterboxBackgroundType()));
pw.println(prefix + " letterboxCornerRadius="
+ getRoundedCornersRadius(mainWin));
if (mLetterboxConfiguration.getLetterboxBackgroundType()
== LETTERBOX_BACKGROUND_WALLPAPER) {
pw.println(prefix + " isLetterboxWallpaperBlurSupported="
+ isLetterboxWallpaperBlurSupported());
pw.println(prefix + " letterboxBackgroundWallpaperDarkScrimAlpha="
+ getLetterboxWallpaperDarkScrimAlpha());
pw.println(prefix + " letterboxBackgroundWallpaperBlurRadius="
+ getLetterboxWallpaperBlurRadiusPx());
}
pw.println(prefix + " isHorizontalReachabilityEnabled="
+ isHorizontalReachabilityEnabled());
pw.println(prefix + " isVerticalReachabilityEnabled=" + isVerticalReachabilityEnabled());
pw.println(prefix + " letterboxHorizontalPositionMultiplier="
+ getHorizontalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
pw.println(prefix + " letterboxVerticalPositionMultiplier="
+ getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
pw.println(prefix + " letterboxPositionForHorizontalReachability="
+ LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
pw.println(prefix + " letterboxPositionForVerticalReachability="
+ LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
pw.println(prefix + " fixedOrientationLetterboxAspectRatio="
+ mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
pw.println(prefix + " defaultMinAspectRatioForUnresizableApps="
+ mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps());
pw.println(prefix + " isSplitScreenAspectRatioForUnresizableAppsEnabled="
+ mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
pw.println(prefix + " isDisplayAspectRatioEnabledForFixedOrientationLetterbox="
+ mLetterboxConfiguration
.getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
}
/**
* Returns a string representing the reason for letterboxing. This method assumes the activity
* is letterboxed.
*/
private String getLetterboxReasonString(WindowState mainWin) {
if (mActivityRecord.inSizeCompatMode()) {
return "SIZE_COMPAT_MODE";
}
if (mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio()) {
return "FIXED_ORIENTATION";
}
if (mainWin.isLetterboxedForDisplayCutout()) {
return "DISPLAY_CUTOUT";
}
if (mActivityRecord.isAspectRatioApplied()) {
return "ASPECT_RATIO";
}
return "UNKNOWN_REASON";
}
private int letterboxHorizontalReachabilityPositionToLetterboxPosition(
@LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) {
switch (position) {
case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT:
return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
default:
throw new AssertionError(
"Unexpected letterbox horizontal reachability position type: "
+ position);
}
}
private int letterboxVerticalReachabilityPositionToLetterboxPosition(
@LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) {
switch (position) {
case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM:
return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
default:
throw new AssertionError(
"Unexpected letterbox vertical reachability position type: "
+ position);
}
}
int getLetterboxPositionForLogging() {
int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
if (isHorizontalReachabilityEnabled()) {
int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
.getLetterboxPositionForHorizontalReachability(
isDisplayFullScreenAndInPosture(
DeviceStateController.DeviceState.HALF_FOLDED,
false /* isTabletop */));
positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
letterboxPositionForHorizontalReachability);
} else if (isVerticalReachabilityEnabled()) {
int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
.getLetterboxPositionForVerticalReachability(
isDisplayFullScreenAndInPosture(
DeviceStateController.DeviceState.HALF_FOLDED,
true /* isTabletop */));
positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
letterboxPositionForVerticalReachability);
}
return positionToLog;
}
private LetterboxConfiguration getLetterboxConfiguration() {
return mLetterboxConfiguration;
}
/**
* Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}.
*/
private void logLetterboxPositionChange(int letterboxPositionChange) {
mActivityRecord.mTaskSupervisor.getActivityMetricsLogger()
.logLetterboxPositionChange(mActivityRecord, letterboxPositionChange);
}
@Nullable
LetterboxDetails getLetterboxDetails() {
final WindowState w = mActivityRecord.findMainWindow();
if (mLetterbox == null || w == null || w.isLetterboxedForDisplayCutout()) {
return null;
}
Rect letterboxInnerBounds = new Rect();
Rect letterboxOuterBounds = new Rect();
getLetterboxInnerBounds(letterboxInnerBounds);
getLetterboxOuterBounds(letterboxOuterBounds);
if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) {
return null;
}
return new LetterboxDetails(
letterboxInnerBounds,
letterboxOuterBounds,
w.mAttrs.insetsFlags.appearance
);
}
/**
* Handles translucent activities letterboxing inheriting constraints from the
* first opaque activity beneath.
* @param parent The parent container.
*/
void updateInheritedLetterbox() {
final WindowContainer<?> parent = mActivityRecord.getParent();
if (parent == null) {
return;
}
if (!mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
return;
}
if (mLetterboxConfigListener != null) {
mLetterboxConfigListener.onRemoved();
clearInheritedConfig();
}
// In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the
// opaque activity constraints because we're expecting the activity is already letterboxed.
mFirstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity(
FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */,
mActivityRecord /* boundary */, false /* includeBoundary */,
true /* traverseTopToBottom */);
if (mFirstOpaqueActivityBeneath == null || mFirstOpaqueActivityBeneath.isEmbedded()) {
// We skip letterboxing if the translucent activity doesn't have any opaque
// activities beneath or the activity below is embedded which never has letterbox.
mActivityRecord.recomputeConfiguration();
return;
}
if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent()
|| mActivityRecord.hasCompatDisplayInsetsWithoutInheritance()) {
return;
}
mFirstOpaqueActivityBeneath.mLetterboxUiController.mDestroyListeners.add(this);
inheritConfiguration(mFirstOpaqueActivityBeneath);
mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation(
mActivityRecord, mFirstOpaqueActivityBeneath,
(opaqueConfig, transparentOverrideConfig) -> {
resetTranslucentOverrideConfig(transparentOverrideConfig);
final Rect parentBounds = parent.getWindowConfiguration().getBounds();
final Rect bounds = transparentOverrideConfig.windowConfiguration.getBounds();
final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds();
// We cannot use letterboxBounds directly here because the position relies on
// letterboxing. Using letterboxBounds directly, would produce a double offset.
bounds.set(parentBounds.left, parentBounds.top,
parentBounds.left + letterboxBounds.width(),
parentBounds.top + letterboxBounds.height());
// We need to initialize appBounds to avoid NPE. The actual value will
// be set ahead when resolving the Configuration for the activity.
transparentOverrideConfig.windowConfiguration.setAppBounds(new Rect());
inheritConfiguration(mFirstOpaqueActivityBeneath);
return transparentOverrideConfig;
});
}
/**
* @return {@code true} if the current activity is translucent with an opaque activity
* beneath. In this case it will inherit bounds, orientation and aspect ratios from
* the first opaque activity beneath.
*/
boolean hasInheritedLetterboxBehavior() {
return mLetterboxConfigListener != null;
}
/**
* @return {@code true} if the current activity is translucent with an opaque activity
* beneath and needs to inherit its orientation.
*/
boolean hasInheritedOrientation() {
// To force a different orientation, the transparent one needs to have an explicit one
// otherwise the existing one is fine and the actual orientation will depend on the
// bounds.
// To avoid wrong behaviour, we're not forcing orientation for activities with not
// fixed orientation (e.g. permission dialogs).
return hasInheritedLetterboxBehavior()
&& mActivityRecord.getOverrideOrientation()
!= SCREEN_ORIENTATION_UNSPECIFIED;
}
float getInheritedMinAspectRatio() {
return mInheritedMinAspectRatio;
}
float getInheritedMaxAspectRatio() {
return mInheritedMaxAspectRatio;
}
int getInheritedAppCompatState() {
return mInheritedAppCompatState;
}
@Configuration.Orientation
int getInheritedOrientation() {
return mInheritedOrientation;
}
ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() {
return mInheritedCompatDisplayInsets;
}
void clearInheritedCompatDisplayInsets() {
mInheritedCompatDisplayInsets = null;
}
/**
* In case of translucent activities, it consumes the {@link ActivityRecord} of the first opaque
* activity beneath using the given consumer and returns {@code true}.
*/
boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) {
return findOpaqueNotFinishingActivityBelow()
.map(activityRecord -> {
consumer.accept(activityRecord);
return true;
}).orElse(false);
}
/**
* @return The first not finishing opaque activity beneath the current translucent activity
* if it exists and the strategy is enabled.
*/
Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() {
if (!hasInheritedLetterboxBehavior() || mActivityRecord.getTask() == null) {
return Optional.empty();
}
return Optional.ofNullable(mFirstOpaqueActivityBeneath);
}
/** Resets the screen size related fields so they can be resolved by requested bounds later. */
private static void resetTranslucentOverrideConfig(Configuration config) {
// The values for the following properties will be defined during the configuration
// resolution in {@link ActivityRecord#resolveOverrideConfiguration} using the
// properties inherited from the first not finishing opaque activity beneath.
config.orientation = ORIENTATION_UNDEFINED;
config.screenWidthDp = config.compatScreenWidthDp = SCREEN_WIDTH_DP_UNDEFINED;
config.screenHeightDp = config.compatScreenHeightDp = SCREEN_HEIGHT_DP_UNDEFINED;
config.smallestScreenWidthDp = config.compatSmallestScreenWidthDp =
SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
}
private void inheritConfiguration(ActivityRecord firstOpaque) {
// To avoid wrong behaviour, we're not forcing a specific aspect ratio to activities
// which are not already providing one (e.g. permission dialogs) and presumably also
// not resizable.
if (mActivityRecord.getMinAspectRatio() != UNDEFINED_ASPECT_RATIO) {
mInheritedMinAspectRatio = firstOpaque.getMinAspectRatio();
}
if (mActivityRecord.getMaxAspectRatio() != UNDEFINED_ASPECT_RATIO) {
mInheritedMaxAspectRatio = firstOpaque.getMaxAspectRatio();
}
mInheritedOrientation = firstOpaque.getRequestedConfigurationOrientation();
mInheritedAppCompatState = firstOpaque.getAppCompatState();
mInheritedCompatDisplayInsets = firstOpaque.getCompatDisplayInsets();
}
private void clearInheritedConfig() {
if (mFirstOpaqueActivityBeneath != null) {
mFirstOpaqueActivityBeneath.mLetterboxUiController.mDestroyListeners.remove(this);
}
mFirstOpaqueActivityBeneath = null;
mLetterboxConfigListener = null;
mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
mInheritedOrientation = ORIENTATION_UNDEFINED;
mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
mInheritedCompatDisplayInsets = null;
}
}