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