1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wm; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 21 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 22 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; 23 24 import static com.android.server.wm.ActivityRecord.computeAspectRatio; 25 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; 26 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; 27 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; 28 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; 29 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; 30 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; 31 import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString; 32 33 import android.annotation.Nullable; 34 import android.app.ActivityManager.TaskDescription; 35 import android.content.res.Configuration; 36 import android.content.res.Resources; 37 import android.graphics.Color; 38 import android.graphics.Point; 39 import android.graphics.Rect; 40 import android.util.Slog; 41 import android.view.InsetsSource; 42 import android.view.InsetsState; 43 import android.view.RoundedCorner; 44 import android.view.SurfaceControl; 45 import android.view.SurfaceControl.Transaction; 46 import android.view.WindowManager; 47 48 import com.android.internal.R; 49 import com.android.internal.annotations.VisibleForTesting; 50 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType; 51 52 import java.io.PrintWriter; 53 54 /** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */ 55 // TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in 56 // SizeCompatTests and LetterboxTests but not all. 57 // TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the 58 // hierarchy in addition to ActivityRecord (Task, DisplayArea, ...). 59 final class LetterboxUiController { 60 61 private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; 62 63 private final Point mTmpPoint = new Point(); 64 65 private final LetterboxConfiguration mLetterboxConfiguration; 66 private final ActivityRecord mActivityRecord; 67 68 // Taskbar expanded height. Used to determine whether to crop an app window to display rounded 69 // corners above the taskbar. 70 private float mExpandedTaskBarHeight; 71 72 private boolean mShowWallpaperForLetterboxBackground; 73 74 @Nullable 75 private Letterbox mLetterbox; 76 LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord)77 LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) { 78 mLetterboxConfiguration = wmService.mLetterboxConfiguration; 79 // Given activityRecord may not be fully constructed since LetterboxUiController 80 // is created in its constructor. It shouldn't be used in this constructor but it's safe 81 // to use it after since controller is only used in ActivityRecord. 82 mActivityRecord = activityRecord; 83 mExpandedTaskBarHeight = 84 getResources().getDimensionPixelSize(R.dimen.taskbar_frame_height); 85 } 86 87 /** Cleans up {@link Letterbox} if it exists.*/ destroy()88 void destroy() { 89 if (mLetterbox != null) { 90 mLetterbox.destroy(); 91 mLetterbox = null; 92 } 93 } 94 onMovedToDisplay(int displayId)95 void onMovedToDisplay(int displayId) { 96 if (mLetterbox != null) { 97 mLetterbox.onMovedToDisplay(displayId); 98 } 99 } 100 hasWallpaperBackgroudForLetterbox()101 boolean hasWallpaperBackgroudForLetterbox() { 102 return mShowWallpaperForLetterboxBackground; 103 } 104 105 /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */ getLetterboxInsets()106 Rect getLetterboxInsets() { 107 if (mLetterbox != null) { 108 return mLetterbox.getInsets(); 109 } else { 110 return new Rect(); 111 } 112 } 113 114 /** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */ getLetterboxInnerBounds(Rect outBounds)115 void getLetterboxInnerBounds(Rect outBounds) { 116 if (mLetterbox != null) { 117 outBounds.set(mLetterbox.getInnerFrame()); 118 } else { 119 outBounds.setEmpty(); 120 } 121 } 122 123 /** 124 * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent 125 * when the current activity is displayed. 126 */ isFullyTransparentBarAllowed(Rect rect)127 boolean isFullyTransparentBarAllowed(Rect rect) { 128 return mLetterbox == null || mLetterbox.notIntersectsOrFullyContains(rect); 129 } 130 updateLetterboxSurface(WindowState winHint)131 void updateLetterboxSurface(WindowState winHint) { 132 final WindowState w = mActivityRecord.findMainWindow(); 133 if (w != winHint && winHint != null && w != null) { 134 return; 135 } 136 layoutLetterbox(winHint); 137 if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) { 138 mLetterbox.applySurfaceChanges(mActivityRecord.getSyncTransaction()); 139 } 140 } 141 layoutLetterbox(WindowState winHint)142 void layoutLetterbox(WindowState winHint) { 143 final WindowState w = mActivityRecord.findMainWindow(); 144 if (w == null || winHint != null && w != winHint) { 145 return; 146 } 147 updateRoundedCorners(w); 148 updateWallpaperForLetterbox(w); 149 if (shouldShowLetterboxUi(w)) { 150 if (mLetterbox == null) { 151 mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), 152 mActivityRecord.mWmService.mTransactionFactory, 153 this::shouldLetterboxHaveRoundedCorners, 154 this::getLetterboxBackgroundColor, 155 this::hasWallpaperBackgroudForLetterbox, 156 this::getLetterboxWallpaperBlurRadius, 157 this::getLetterboxWallpaperDarkScrimAlpha, 158 this::handleDoubleTap); 159 mLetterbox.attachInput(w); 160 } 161 mActivityRecord.getPosition(mTmpPoint); 162 // Get the bounds of the "space-to-fill". The transformed bounds have the highest 163 // priority because the activity is launched in a rotated environment. In multi-window 164 // mode, the task-level represents this. In fullscreen-mode, the task container does 165 // (since the orientation letterbox is also applied to the task). 166 final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds(); 167 final Rect spaceToFill = transformedBounds != null 168 ? transformedBounds 169 : mActivityRecord.inMultiWindowMode() 170 ? mActivityRecord.getRootTask().getBounds() 171 : mActivityRecord.getRootTask().getParent().getBounds(); 172 mLetterbox.layout(spaceToFill, w.getFrame(), mTmpPoint); 173 } else if (mLetterbox != null) { 174 mLetterbox.hide(); 175 } 176 } 177 shouldLetterboxHaveRoundedCorners()178 private boolean shouldLetterboxHaveRoundedCorners() { 179 // TODO(b/214030873): remove once background is drawn for transparent activities 180 // Letterbox shouldn't have rounded corners if the activity is transparent 181 return mLetterboxConfiguration.isLetterboxActivityCornersRounded() 182 && mActivityRecord.fillsParent(); 183 } 184 getHorizontalPositionMultiplier(Configuration parentConfiguration)185 float getHorizontalPositionMultiplier(Configuration parentConfiguration) { 186 // Don't check resolved configuration because it may not be updated yet during 187 // configuration change. 188 return isReachabilityEnabled(parentConfiguration) 189 // Using the last global dynamic position to avoid "jumps" when moving 190 // between apps or activities. 191 ? mLetterboxConfiguration.getHorizontalMultiplierForReachability() 192 : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(); 193 } 194 getFixedOrientationLetterboxAspectRatio(Configuration parentConfiguration)195 float getFixedOrientationLetterboxAspectRatio(Configuration parentConfiguration) { 196 // Don't check resolved windowing mode because it may not be updated yet during 197 // configuration change. 198 if (!isReachabilityEnabled(parentConfiguration)) { 199 return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(); 200 } 201 202 int dividerWindowWidth = 203 getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_thickness); 204 int dividerInsets = 205 getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_insets); 206 int dividerSize = dividerWindowWidth - dividerInsets * 2; 207 208 // Getting the same aspect ratio that apps get in split screen. 209 Rect bounds = new Rect(parentConfiguration.windowConfiguration.getAppBounds()); 210 bounds.inset(dividerSize, /* dy */ 0); 211 bounds.right = bounds.centerX(); 212 213 return computeAspectRatio(bounds); 214 } 215 getResources()216 Resources getResources() { 217 return mActivityRecord.mWmService.mContext.getResources(); 218 } 219 handleDoubleTap(int x)220 private void handleDoubleTap(int x) { 221 if (!isReachabilityEnabled() || mActivityRecord.isInTransition()) { 222 return; 223 } 224 225 if (mLetterbox.getInnerFrame().left <= x && mLetterbox.getInnerFrame().right >= x) { 226 // Only react to clicks at the sides of the letterboxed app window. 227 return; 228 } 229 230 if (mLetterbox.getInnerFrame().left > x) { 231 // Moving to the next stop on the left side of the app window: right > center > left. 232 mLetterboxConfiguration.movePositionForReachabilityToNextLeftStop(); 233 } else if (mLetterbox.getInnerFrame().right < x) { 234 // Moving to the next stop on the right side of the app window: left > center > right. 235 mLetterboxConfiguration.movePositionForReachabilityToNextRightStop(); 236 } 237 238 // TODO(197549949): Add animation for transition. 239 mActivityRecord.recomputeConfiguration(); 240 } 241 242 /** 243 * Whether reachability is enabled for an activity in the curren configuration. 244 * 245 * <p>Conditions that needs to be met: 246 * <ul> 247 * <li>Activity is portrait-only. 248 * <li>Fullscreen window in landscape device orientation. 249 * <li>Reachability is enabled. 250 * </ul> 251 */ isReachabilityEnabled(Configuration parentConfiguration)252 private boolean isReachabilityEnabled(Configuration parentConfiguration) { 253 return mLetterboxConfiguration.getIsReachabilityEnabled() 254 && parentConfiguration.windowConfiguration.getWindowingMode() 255 == WINDOWING_MODE_FULLSCREEN 256 && parentConfiguration.orientation == ORIENTATION_LANDSCAPE 257 && mActivityRecord.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT; 258 } 259 isReachabilityEnabled()260 private boolean isReachabilityEnabled() { 261 return isReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); 262 } 263 264 @VisibleForTesting shouldShowLetterboxUi(WindowState mainWindow)265 boolean shouldShowLetterboxUi(WindowState mainWindow) { 266 return isSurfaceReadyAndVisible(mainWindow) && mainWindow.areAppWindowBoundsLetterboxed() 267 // Check for FLAG_SHOW_WALLPAPER explicitly instead of using 268 // WindowContainer#showWallpaper because the later will return true when this 269 // activity is using blurred wallpaper for letterbox backgroud. 270 && (mainWindow.mAttrs.flags & FLAG_SHOW_WALLPAPER) == 0; 271 } 272 273 @VisibleForTesting isSurfaceReadyAndVisible(WindowState mainWindow)274 boolean isSurfaceReadyAndVisible(WindowState mainWindow) { 275 boolean surfaceReady = mainWindow.isDrawn() // Regular case 276 // Waiting for relayoutWindow to call preserveSurface 277 || mainWindow.isDragResizeChanged(); 278 return surfaceReady && (mActivityRecord.isVisible() 279 || mActivityRecord.isVisibleRequested()); 280 } 281 getLetterboxBackgroundColor()282 private Color getLetterboxBackgroundColor() { 283 final WindowState w = mActivityRecord.findMainWindow(); 284 if (w == null || w.isLetterboxedForDisplayCutout()) { 285 return Color.valueOf(Color.BLACK); 286 } 287 @LetterboxBackgroundType int letterboxBackgroundType = 288 mLetterboxConfiguration.getLetterboxBackgroundType(); 289 TaskDescription taskDescription = mActivityRecord.taskDescription; 290 switch (letterboxBackgroundType) { 291 case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: 292 if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) { 293 return Color.valueOf(taskDescription.getBackgroundColorFloating()); 294 } 295 break; 296 case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND: 297 if (taskDescription != null && taskDescription.getBackgroundColor() != 0) { 298 return Color.valueOf(taskDescription.getBackgroundColor()); 299 } 300 break; 301 case LETTERBOX_BACKGROUND_WALLPAPER: 302 if (hasWallpaperBackgroudForLetterbox()) { 303 // Color is used for translucent scrim that dims wallpaper. 304 return Color.valueOf(Color.BLACK); 305 } 306 Slog.w(TAG, "Wallpaper option is selected for letterbox background but " 307 + "blur is not supported by a device or not supported in the current " 308 + "window configuration or both alpha scrim and blur radius aren't " 309 + "provided so using solid color background"); 310 break; 311 case LETTERBOX_BACKGROUND_SOLID_COLOR: 312 return mLetterboxConfiguration.getLetterboxBackgroundColor(); 313 default: 314 throw new AssertionError( 315 "Unexpected letterbox background type: " + letterboxBackgroundType); 316 } 317 // If picked option configured incorrectly or not supported then default to a solid color 318 // background. 319 return mLetterboxConfiguration.getLetterboxBackgroundColor(); 320 } 321 updateRoundedCorners(WindowState mainWindow)322 private void updateRoundedCorners(WindowState mainWindow) { 323 final SurfaceControl windowSurface = mainWindow.getClientViewRootSurface(); 324 if (windowSurface != null && windowSurface.isValid()) { 325 Transaction transaction = mActivityRecord.getSyncTransaction(); 326 327 if (!isLetterboxedNotForDisplayCutout(mainWindow) 328 || !mLetterboxConfiguration.isLetterboxActivityCornersRounded()) { 329 transaction 330 .setWindowCrop(windowSurface, null) 331 .setCornerRadius(windowSurface, 0); 332 return; 333 } 334 335 final InsetsState insetsState = mainWindow.getInsetsState(); 336 final InsetsSource taskbarInsetsSource = 337 insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); 338 339 Rect cropBounds = null; 340 341 // Rounded corners should be displayed above the taskbar. When taskbar is hidden, 342 // an insets frame is equal to a navigation bar which shouldn't affect position of 343 // rounded corners since apps are expected to handle navigation bar inset. 344 // This condition checks whether the taskbar is visible. 345 if (taskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { 346 cropBounds = new Rect(mActivityRecord.getBounds()); 347 // Activity bounds are in screen coordinates while (0,0) for activity's surface 348 // control is at the top left corner of an app window so offsetting bounds 349 // accordingly. 350 cropBounds.offsetTo(0, 0); 351 // Rounded cornerners should be displayed above the taskbar. 352 cropBounds.bottom = 353 Math.min(cropBounds.bottom, taskbarInsetsSource.getFrame().top); 354 if (mActivityRecord.inSizeCompatMode() 355 && mActivityRecord.getSizeCompatScale() < 1.0f) { 356 cropBounds.scale(1.0f / mActivityRecord.getSizeCompatScale()); 357 } 358 } 359 360 transaction 361 .setWindowCrop(windowSurface, cropBounds) 362 .setCornerRadius(windowSurface, getRoundedCorners(insetsState)); 363 } 364 } 365 366 // Returns rounded corners radius based on override in 367 // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii. 368 // Device corners can be different on the right and left sides but we use the same radius 369 // for all corners for consistency and pick a minimal bottom one for consistency with a 370 // taskbar rounded corners. getRoundedCorners(InsetsState insetsState)371 private int getRoundedCorners(InsetsState insetsState) { 372 if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) { 373 return mLetterboxConfiguration.getLetterboxActivityCornersRadius(); 374 } 375 return Math.min( 376 getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT), 377 getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT)); 378 } 379 getInsetsStateCornerRadius( InsetsState insetsState, @RoundedCorner.Position int position)380 private int getInsetsStateCornerRadius( 381 InsetsState insetsState, @RoundedCorner.Position int position) { 382 RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position); 383 return corner == null ? 0 : corner.getRadius(); 384 } 385 isLetterboxedNotForDisplayCutout(WindowState mainWindow)386 private boolean isLetterboxedNotForDisplayCutout(WindowState mainWindow) { 387 return shouldShowLetterboxUi(mainWindow) 388 && !mainWindow.isLetterboxedForDisplayCutout(); 389 } 390 updateWallpaperForLetterbox(WindowState mainWindow)391 private void updateWallpaperForLetterbox(WindowState mainWindow) { 392 @LetterboxBackgroundType int letterboxBackgroundType = 393 mLetterboxConfiguration.getLetterboxBackgroundType(); 394 boolean wallpaperShouldBeShown = 395 letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER 396 // Don't use wallpaper as a background if letterboxed for display cutout. 397 && isLetterboxedNotForDisplayCutout(mainWindow) 398 // Check that dark scrim alpha or blur radius are provided 399 && (getLetterboxWallpaperBlurRadius() > 0 400 || getLetterboxWallpaperDarkScrimAlpha() > 0) 401 // Check that blur is supported by a device if blur radius is provided. 402 && (getLetterboxWallpaperBlurRadius() <= 0 403 || isLetterboxWallpaperBlurSupported()); 404 if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) { 405 mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown; 406 mActivityRecord.requestUpdateWallpaperIfNeeded(); 407 } 408 } 409 getLetterboxWallpaperBlurRadius()410 private int getLetterboxWallpaperBlurRadius() { 411 int blurRadius = mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius(); 412 return blurRadius < 0 ? 0 : blurRadius; 413 } 414 getLetterboxWallpaperDarkScrimAlpha()415 private float getLetterboxWallpaperDarkScrimAlpha() { 416 float alpha = mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha(); 417 // No scrim by default. 418 return (alpha < 0 || alpha >= 1) ? 0.0f : alpha; 419 } 420 isLetterboxWallpaperBlurSupported()421 private boolean isLetterboxWallpaperBlurSupported() { 422 return mLetterboxConfiguration.mContext.getSystemService(WindowManager.class) 423 .isCrossWindowBlurEnabled(); 424 } 425 dump(PrintWriter pw, String prefix)426 void dump(PrintWriter pw, String prefix) { 427 final WindowState mainWin = mActivityRecord.findMainWindow(); 428 if (mainWin == null) { 429 return; 430 } 431 432 boolean areBoundsLetterboxed = mainWin.areAppWindowBoundsLetterboxed(); 433 pw.println(prefix + "areBoundsLetterboxed=" + areBoundsLetterboxed); 434 if (!areBoundsLetterboxed) { 435 return; 436 } 437 438 pw.println(prefix + " letterboxReason=" + getLetterboxReasonString(mainWin)); 439 pw.println(prefix + " activityAspectRatio=" 440 + mActivityRecord.computeAspectRatio(mActivityRecord.getBounds())); 441 442 boolean shouldShowLetterboxUi = shouldShowLetterboxUi(mainWin); 443 pw.println(prefix + "shouldShowLetterboxUi=" + shouldShowLetterboxUi); 444 445 if (!shouldShowLetterboxUi) { 446 return; 447 } 448 pw.println(prefix + " letterboxBackgroundColor=" + Integer.toHexString( 449 getLetterboxBackgroundColor().toArgb())); 450 pw.println(prefix + " letterboxBackgroundType=" 451 + letterboxBackgroundTypeToString( 452 mLetterboxConfiguration.getLetterboxBackgroundType())); 453 pw.println(prefix + " letterboxCornerRadius=" 454 + getRoundedCorners(mainWin.getInsetsState())); 455 if (mLetterboxConfiguration.getLetterboxBackgroundType() 456 == LETTERBOX_BACKGROUND_WALLPAPER) { 457 pw.println(prefix + " isLetterboxWallpaperBlurSupported=" 458 + isLetterboxWallpaperBlurSupported()); 459 pw.println(prefix + " letterboxBackgroundWallpaperDarkScrimAlpha=" 460 + getLetterboxWallpaperDarkScrimAlpha()); 461 pw.println(prefix + " letterboxBackgroundWallpaperBlurRadius=" 462 + getLetterboxWallpaperBlurRadius()); 463 } 464 465 pw.println(prefix + " isReachabilityEnabled=" + isReachabilityEnabled()); 466 pw.println(prefix + " letterboxHorizontalPositionMultiplier=" 467 + getHorizontalPositionMultiplier(mActivityRecord.getParent().getConfiguration())); 468 pw.println(prefix + " fixedOrientationLetterboxAspectRatio=" 469 + getFixedOrientationLetterboxAspectRatio( 470 mActivityRecord.getParent().getConfiguration())); 471 } 472 473 /** 474 * Returns a string representing the reason for letterboxing. This method assumes the activity 475 * is letterboxed. 476 */ getLetterboxReasonString(WindowState mainWin)477 private String getLetterboxReasonString(WindowState mainWin) { 478 if (mActivityRecord.inSizeCompatMode()) { 479 return "SIZE_COMPAT_MODE"; 480 } 481 if (mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio()) { 482 return "FIXED_ORIENTATION"; 483 } 484 if (mainWin.isLetterboxedForDisplayCutout()) { 485 return "DISPLAY_CUTOUT"; 486 } 487 return "UNKNOWN_REASON"; 488 } 489 490 } 491