| /* |
| * 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.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; |
| |
| 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.letterboxBackgroundTypeToString; |
| |
| import android.annotation.Nullable; |
| import android.app.ActivityManager.TaskDescription; |
| import android.graphics.Color; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.util.Slog; |
| import android.view.SurfaceControl; |
| import android.view.SurfaceControl.Transaction; |
| import android.view.WindowManager; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType; |
| |
| import java.io.PrintWriter; |
| |
| /** 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, ...). |
| final class LetterboxUiController { |
| |
| private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; |
| |
| private final Point mTmpPoint = new Point(); |
| |
| private final LetterboxConfiguration mLetterboxConfiguration; |
| private final ActivityRecord mActivityRecord; |
| |
| private boolean mShowWallpaperForLetterboxBackground; |
| |
| @Nullable |
| private Letterbox mLetterbox; |
| |
| 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; |
| } |
| |
| /** Cleans up {@link Letterbox} if it exists.*/ |
| void destroy() { |
| if (mLetterbox != null) { |
| mLetterbox.destroy(); |
| mLetterbox = null; |
| } |
| } |
| |
| void onMovedToDisplay(int displayId) { |
| if (mLetterbox != null) { |
| mLetterbox.onMovedToDisplay(displayId); |
| } |
| } |
| |
| boolean hasWallpaperBackgroudForLetterbox() { |
| 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()); |
| } 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) { |
| final WindowState w = mActivityRecord.findMainWindow(); |
| if (w != winHint && winHint != null && w != null) { |
| return; |
| } |
| layoutLetterbox(winHint); |
| if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) { |
| mLetterbox.applySurfaceChanges(mActivityRecord.getSyncTransaction()); |
| } |
| } |
| |
| void layoutLetterbox(WindowState winHint) { |
| final WindowState w = mActivityRecord.findMainWindow(); |
| if (w == null || winHint != null && w != winHint) { |
| return; |
| } |
| updateRoundedCorners(w); |
| updateWallpaperForLetterbox(w); |
| if (shouldShowLetterboxUi(w)) { |
| if (mLetterbox == null) { |
| mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), |
| mActivityRecord.mWmService.mTransactionFactory, |
| mLetterboxConfiguration::isLetterboxActivityCornersRounded, |
| this::getLetterboxBackgroundColor, |
| this::hasWallpaperBackgroudForLetterbox, |
| this::getLetterboxWallpaperBlurRadius, |
| this::getLetterboxWallpaperDarkScrimAlpha); |
| mLetterbox.attachInput(w); |
| } |
| 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 task-level represents this. 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.getRootTask().getBounds() |
| : mActivityRecord.getRootTask().getParent().getBounds(); |
| mLetterbox.layout(spaceToFill, w.getFrame(), mTmpPoint); |
| } else if (mLetterbox != null) { |
| mLetterbox.hide(); |
| } |
| } |
| |
| @VisibleForTesting |
| boolean shouldShowLetterboxUi(WindowState mainWindow) { |
| return isSurfaceReadyAndVisible(mainWindow) && mainWindow.areAppWindowBoundsLetterboxed() |
| // Check that an activity isn't transparent. |
| && mActivityRecord.fillsParent() |
| // 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 backgroud. |
| && (mainWindow.mAttrs.flags & FLAG_SHOW_WALLPAPER) == 0; |
| } |
| |
| @VisibleForTesting |
| boolean isSurfaceReadyAndVisible(WindowState mainWindow) { |
| boolean surfaceReady = mainWindow.isDrawn() // Regular case |
| // Waiting for relayoutWindow to call preserveSurface |
| || mainWindow.isDragResizeChanged(); |
| return surfaceReady && (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 (hasWallpaperBackgroudForLetterbox()) { |
| // Color is used for translucent scrim that dims wallpaper. |
| return Color.valueOf(Color.BLACK); |
| } |
| 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 updateRoundedCorners(WindowState mainWindow) { |
| int cornersRadius = |
| // Don't round corners if letterboxed only for display cutout. |
| shouldShowLetterboxUi(mainWindow) |
| && !mainWindow.isLetterboxedForDisplayCutout() |
| ? Math.max(0, mLetterboxConfiguration.getLetterboxActivityCornersRadius()) |
| : 0; |
| setCornersRadius(mainWindow, cornersRadius); |
| } |
| |
| private void setCornersRadius(WindowState mainWindow, int cornersRadius) { |
| final SurfaceControl windowSurface = mainWindow.getClientViewRootSurface(); |
| if (windowSurface != null && windowSurface.isValid()) { |
| Transaction transaction = mActivityRecord.getSyncTransaction(); |
| transaction.setCornerRadius(windowSurface, cornersRadius); |
| } |
| } |
| |
| private void updateWallpaperForLetterbox(WindowState mainWindow) { |
| @LetterboxBackgroundType int letterboxBackgroundType = |
| mLetterboxConfiguration.getLetterboxBackgroundType(); |
| boolean wallpaperShouldBeShown = |
| letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER |
| && shouldShowLetterboxUi(mainWindow) |
| // Don't use wallpaper as a background if letterboxed for display cutout. |
| && !mainWindow.isLetterboxedForDisplayCutout() |
| // Check that dark scrim alpha or blur radius are provided |
| && (getLetterboxWallpaperBlurRadius() > 0 |
| || getLetterboxWallpaperDarkScrimAlpha() > 0) |
| // Check that blur is supported by a device if blur radius is provided. |
| && (getLetterboxWallpaperBlurRadius() <= 0 |
| || isLetterboxWallpaperBlurSupported()); |
| if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) { |
| mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown; |
| mActivityRecord.requestUpdateWallpaperIfNeeded(); |
| } |
| } |
| |
| private int getLetterboxWallpaperBlurRadius() { |
| int blurRadius = mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius(); |
| return blurRadius < 0 ? 0 : blurRadius; |
| } |
| |
| 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 + " letterboxAspectRatio=" |
| + 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())); |
| if (mLetterboxConfiguration.getLetterboxBackgroundType() |
| == LETTERBOX_BACKGROUND_WALLPAPER) { |
| pw.println(prefix + " isLetterboxWallpaperBlurSupported=" |
| + isLetterboxWallpaperBlurSupported()); |
| pw.println(prefix + " letterboxBackgroundWallpaperDarkScrimAlpha=" |
| + getLetterboxWallpaperDarkScrimAlpha()); |
| pw.println(prefix + " letterboxBackgroundWallpaperBlurRadius=" |
| + getLetterboxWallpaperBlurRadius()); |
| } |
| pw.println(prefix + " letterboxHorizontalPositionMultiplier=" |
| + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier()); |
| } |
| |
| /** |
| * 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"; |
| } |
| return "UNKNOWN_REASON"; |
| } |
| |
| } |