1 /* 2 * Copyright (C) 2018 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.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; 20 import static android.view.SurfaceControl.HIDDEN; 21 import static android.window.TaskConstants.TASK_CHILD_LAYER_LETTERBOX_BACKGROUND; 22 import static android.window.TaskConstants.TASK_CHILD_LAYER_TASK_OVERLAY; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.graphics.Color; 27 import android.graphics.Point; 28 import android.graphics.Rect; 29 import android.os.Handler; 30 import android.os.IBinder; 31 import android.os.InputConfig; 32 import android.view.GestureDetector; 33 import android.view.InputChannel; 34 import android.view.InputEvent; 35 import android.view.InputEventReceiver; 36 import android.view.InputWindowHandle; 37 import android.view.MotionEvent; 38 import android.view.SurfaceControl; 39 import android.view.WindowManager; 40 41 import com.android.server.UiThread; 42 import com.android.window.flags.Flags; 43 44 import java.util.function.Supplier; 45 46 /** 47 * Manages a set of {@link SurfaceControl}s to draw a black letterbox between an 48 * outer rect and an inner rect. 49 */ 50 public class Letterbox { 51 52 static final Rect EMPTY_RECT = new Rect(); 53 private static final Point ZERO_POINT = new Point(0, 0); 54 55 private final Supplier<SurfaceControl.Builder> mSurfaceControlFactory; 56 private final Supplier<SurfaceControl.Transaction> mTransactionFactory; 57 58 private final Rect mOuter = new Rect(); 59 private final Rect mInner = new Rect(); 60 private final LetterboxSurface mTop = new LetterboxSurface("top"); 61 private final LetterboxSurface mLeft = new LetterboxSurface("left"); 62 private final LetterboxSurface mBottom = new LetterboxSurface("bottom"); 63 private final LetterboxSurface mRight = new LetterboxSurface("right"); 64 // One surface that fills the whole window is used over multiple surfaces to: 65 // - Prevents wallpaper from peeking through near rounded corners. 66 // - For "blurred wallpaper" background, to avoid having visible border between surfaces. 67 // One surface approach isn't always preferred over multiple surfaces due to rendering cost 68 // for overlaping an app window and letterbox surfaces. 69 private final LetterboxSurface mFullWindowSurface = new LetterboxSurface("fullWindow"); 70 private final LetterboxSurface[] mSurfaces = { mLeft, mTop, mRight, mBottom }; 71 72 @NonNull 73 private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; 74 @NonNull 75 private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; 76 77 /** 78 * Constructs a Letterbox. 79 * 80 * @param surfaceControlFactory a factory for creating the managed {@link SurfaceControl}s 81 */ Letterbox(Supplier<SurfaceControl.Builder> surfaceControlFactory, Supplier<SurfaceControl.Transaction> transactionFactory, @NonNull AppCompatReachabilityPolicy appCompatReachabilityPolicy, @NonNull AppCompatLetterboxOverrides appCompatLetterboxOverrides)82 public Letterbox(Supplier<SurfaceControl.Builder> surfaceControlFactory, 83 Supplier<SurfaceControl.Transaction> transactionFactory, 84 @NonNull AppCompatReachabilityPolicy appCompatReachabilityPolicy, 85 @NonNull AppCompatLetterboxOverrides appCompatLetterboxOverrides) { 86 mSurfaceControlFactory = surfaceControlFactory; 87 mTransactionFactory = transactionFactory; 88 mAppCompatReachabilityPolicy = appCompatReachabilityPolicy; 89 mAppCompatLetterboxOverrides = appCompatLetterboxOverrides; 90 } 91 92 /** 93 * Lays out the letterbox, such that the area between the outer and inner 94 * frames will be covered by black color surfaces. 95 * 96 * The caller must use {@link #applySurfaceChanges} to apply the new layout to the surface. 97 * @param outer the outer frame of the letterbox (this frame will be black, except the area 98 * that intersects with the {code inner} frame), in global coordinates 99 * @param inner the inner frame of the letterbox (this frame will be clear), in global 100 * coordinates 101 * @param surfaceOrigin the origin of the surface factory in global coordinates 102 */ layout(Rect outer, Rect inner, Point surfaceOrigin)103 public void layout(Rect outer, Rect inner, Point surfaceOrigin) { 104 mOuter.set(outer); 105 mInner.set(inner); 106 107 mTop.layout(outer.left, outer.top, outer.right, inner.top, surfaceOrigin); 108 mLeft.layout(outer.left, outer.top, inner.left, outer.bottom, surfaceOrigin); 109 mBottom.layout(outer.left, inner.bottom, outer.right, outer.bottom, surfaceOrigin); 110 mRight.layout(inner.right, outer.top, outer.right, outer.bottom, surfaceOrigin); 111 mFullWindowSurface.layout(outer.left, outer.top, outer.right, outer.bottom, surfaceOrigin); 112 } 113 114 /** 115 * Gets the insets between the outer and inner rects. 116 */ getInsets()117 public Rect getInsets() { 118 return new Rect( 119 mLeft.getWidth(), 120 mTop.getHeight(), 121 mRight.getWidth(), 122 mBottom.getHeight()); 123 } 124 125 /** @return The frame that used to place the content. */ getInnerFrame()126 Rect getInnerFrame() { 127 return mInner; 128 } 129 130 /** @return The frame that contains the inner frame and the insets. */ getOuterFrame()131 Rect getOuterFrame() { 132 return mOuter; 133 } 134 135 /** 136 * Returns {@code true} if the letterbox does not overlap with the bar, or the letterbox can 137 * fully cover the window frame. 138 * 139 * @param rect The area of the window frame. 140 */ notIntersectsOrFullyContains(Rect rect)141 boolean notIntersectsOrFullyContains(Rect rect) { 142 int emptyCount = 0; 143 int noOverlappingCount = 0; 144 for (LetterboxSurface surface : mSurfaces) { 145 final Rect surfaceRect = surface.mLayoutFrameGlobal; 146 if (surfaceRect.isEmpty()) { 147 // empty letterbox 148 emptyCount++; 149 } else if (!Rect.intersects(surfaceRect, rect)) { 150 // no overlapping 151 noOverlappingCount++; 152 } else if (surfaceRect.contains(rect)) { 153 // overlapping and covered 154 return true; 155 } 156 } 157 return (emptyCount + noOverlappingCount) == mSurfaces.length; 158 } 159 160 /** 161 * Hides the letterbox. 162 * 163 * The caller must use {@link #applySurfaceChanges} to apply the new layout to the surface. 164 */ hide()165 public void hide() { 166 layout(EMPTY_RECT, EMPTY_RECT, ZERO_POINT); 167 } 168 169 /** 170 * Destroys the managed {@link SurfaceControl}s. 171 */ destroy()172 public void destroy() { 173 mOuter.setEmpty(); 174 mInner.setEmpty(); 175 final SurfaceControl.Transaction tx = mTransactionFactory.get(); 176 for (LetterboxSurface surface : mSurfaces) { 177 surface.remove(tx); 178 } 179 mFullWindowSurface.remove(tx); 180 tx.apply(); 181 } 182 183 /** Returns whether a call to {@link #applySurfaceChanges} would change the surface. */ needsApplySurfaceChanges()184 public boolean needsApplySurfaceChanges() { 185 if (useFullWindowSurface()) { 186 return mFullWindowSurface.needsApplySurfaceChanges(); 187 } 188 for (LetterboxSurface surface : mSurfaces) { 189 if (surface.needsApplySurfaceChanges()) { 190 return true; 191 } 192 } 193 return false; 194 } 195 196 /** Applies surface changes such as colour, window crop, position and input info. */ applySurfaceChanges(@onNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction inputT, @NonNull WindowState windowState)197 public void applySurfaceChanges(@NonNull SurfaceControl.Transaction t, 198 @NonNull SurfaceControl.Transaction inputT, @NonNull WindowState windowState) { 199 if (useFullWindowSurface()) { 200 for (LetterboxSurface surface : mSurfaces) { 201 surface.remove(t); 202 } 203 mFullWindowSurface.attachInput(windowState); 204 mFullWindowSurface.applySurfaceChanges(t, inputT); 205 } else { 206 mFullWindowSurface.remove(t); 207 for (LetterboxSurface surface : mSurfaces) { 208 surface.attachInput(windowState); 209 surface.applySurfaceChanges(t, inputT); 210 } 211 } 212 } 213 onMovedToDisplay(int displayId)214 void onMovedToDisplay(int displayId) { 215 for (LetterboxSurface surface : mSurfaces) { 216 setSurfaceDisplayID(surface, displayId); 217 } 218 setSurfaceDisplayID(mFullWindowSurface, displayId); 219 } 220 setSurfaceDisplayID(LetterboxSurface surface, int displayId)221 private void setSurfaceDisplayID(LetterboxSurface surface, int displayId) { 222 if (surface.mInputInterceptor != null) { 223 surface.mInputInterceptor.mWindowHandle.displayId = displayId; 224 } 225 } 226 227 /** 228 * Returns {@code true} when using {@link #mFullWindowSurface} instead of {@link mSurfaces}. 229 */ useFullWindowSurface()230 private boolean useFullWindowSurface() { 231 return mAppCompatLetterboxOverrides.shouldLetterboxHaveRoundedCorners() 232 || mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox(); 233 } 234 235 private final class TapEventReceiver extends InputEventReceiver { 236 237 private final GestureDetector mDoubleTapDetector; 238 TapEventReceiver(InputChannel inputChannel, WindowManagerService wmService, Handler uiHandler)239 TapEventReceiver(InputChannel inputChannel, WindowManagerService wmService, 240 Handler uiHandler) { 241 super(inputChannel, uiHandler.getLooper()); 242 final DoubleTapListener doubleTapListener = new DoubleTapListener(wmService); 243 mDoubleTapDetector = 244 new GestureDetector(wmService.mContext, doubleTapListener, uiHandler); 245 } 246 247 @Override onInputEvent(InputEvent event)248 public void onInputEvent(InputEvent event) { 249 final MotionEvent motionEvent = (MotionEvent) event; 250 finishInputEvent(event, mDoubleTapDetector.onTouchEvent(motionEvent)); 251 } 252 } 253 254 private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { 255 private final WindowManagerService mWmService; 256 DoubleTapListener(WindowManagerService wmService)257 private DoubleTapListener(WindowManagerService wmService) { 258 mWmService = wmService; 259 } 260 261 @Override onDoubleTapEvent(MotionEvent e)262 public boolean onDoubleTapEvent(MotionEvent e) { 263 synchronized (mWmService.mGlobalLock) { 264 // This check prevents late events to be handled in case the Letterbox has been 265 // already destroyed and so mOuter.isEmpty() is true. 266 if (!mOuter.isEmpty() && e.getAction() == MotionEvent.ACTION_UP) { 267 mAppCompatReachabilityPolicy.handleDoubleTap((int) e.getRawX(), 268 (int) e.getRawY()); 269 return true; 270 } 271 return false; 272 } 273 } 274 } 275 276 private final class InputInterceptor implements Runnable { 277 278 private final InputChannel mClientChannel; 279 private final InputWindowHandle mWindowHandle; 280 private final InputEventReceiver mInputEventReceiver; 281 private final WindowManagerService mWmService; 282 private final IBinder mToken; 283 private final Handler mHandler; 284 InputInterceptor(String namePrefix, WindowState win)285 InputInterceptor(String namePrefix, WindowState win) { 286 mWmService = win.mWmService; 287 mHandler = UiThread.getHandler(); 288 final String name = namePrefix 289 + (win.mActivityRecord != null ? win.mActivityRecord : win); 290 mClientChannel = mWmService.mInputManager.createInputChannel(name); 291 mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService, mHandler); 292 293 mToken = mClientChannel.getToken(); 294 295 mWindowHandle = new InputWindowHandle(null /* inputApplicationHandle */, 296 win.getDisplayId()); 297 mWindowHandle.name = name; 298 mWindowHandle.token = mToken; 299 mWindowHandle.layoutParamsType = Flags.scrollingFromLetterbox() 300 ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 301 : WindowManager.LayoutParams.TYPE_INPUT_CONSUMER; 302 mWindowHandle.dispatchingTimeoutMillis = DEFAULT_DISPATCHING_TIMEOUT_MILLIS; 303 mWindowHandle.ownerPid = WindowManagerService.MY_PID; 304 mWindowHandle.ownerUid = WindowManagerService.MY_UID; 305 mWindowHandle.scaleFactor = 1.0f; 306 mWindowHandle.inputConfig = InputConfig.NOT_FOCUSABLE 307 | (Flags.scrollingFromLetterbox() ? InputConfig.SPY : InputConfig.SLIPPERY); 308 } 309 updateTouchableRegion(Rect frame)310 void updateTouchableRegion(Rect frame) { 311 if (frame.isEmpty()) { 312 // Use null token to indicate the surface doesn't need to receive input event (see 313 // the usage of Layer.hasInput in SurfaceFlinger), so InputDispatcher won't keep the 314 // unnecessary records. 315 mWindowHandle.token = null; 316 return; 317 } 318 mWindowHandle.token = mToken; 319 mWindowHandle.touchableRegion.set(frame); 320 mWindowHandle.touchableRegion.translate(-frame.left, -frame.top); 321 } 322 323 @Override run()324 public void run() { 325 mInputEventReceiver.dispose(); 326 mClientChannel.dispose(); 327 } 328 dispose()329 void dispose() { 330 mWmService.mInputManager.removeInputChannel(mToken); 331 // Perform dispose on the same thread that dispatches input event 332 mHandler.post(this); 333 } 334 } 335 336 private class LetterboxSurface { 337 338 private final String mType; 339 private SurfaceControl mSurface; 340 private SurfaceControl mInputSurface; 341 private Color mColor; 342 private boolean mHasWallpaperBackground; 343 344 private final Rect mSurfaceFrameRelative = new Rect(); 345 private final Rect mLayoutFrameGlobal = new Rect(); 346 private final Rect mLayoutFrameRelative = new Rect(); 347 348 @Nullable 349 private InputInterceptor mInputInterceptor; 350 LetterboxSurface(@onNull String type)351 LetterboxSurface(@NonNull String type) { 352 mType = type; 353 } 354 layout(int left, int top, int right, int bottom, Point surfaceOrigin)355 public void layout(int left, int top, int right, int bottom, Point surfaceOrigin) { 356 mLayoutFrameGlobal.set(left, top, right, bottom); 357 mLayoutFrameRelative.set(mLayoutFrameGlobal); 358 mLayoutFrameRelative.offset(-surfaceOrigin.x, -surfaceOrigin.y); 359 } 360 createSurface(SurfaceControl.Transaction t)361 private void createSurface(SurfaceControl.Transaction t) { 362 mSurface = mSurfaceControlFactory.get() 363 .setName("Letterbox - " + mType) 364 .setFlags(HIDDEN) 365 .setColorLayer() 366 .setCallsite("LetterboxSurface.createSurface") 367 .build(); 368 369 t.setLayer(mSurface, TASK_CHILD_LAYER_LETTERBOX_BACKGROUND) 370 .setColorSpaceAgnostic(mSurface, true); 371 } 372 createInputSurface(SurfaceControl.Transaction t)373 private void createInputSurface(SurfaceControl.Transaction t) { 374 mInputSurface = mSurfaceControlFactory.get() 375 .setName("LetterboxInput - " + mType) 376 .setFlags(HIDDEN) 377 .setContainerLayer() 378 .setOpaque(true) 379 .setCallsite("LetterboxSurface.createInputSurface") 380 .build(); 381 382 t.setLayer(mInputSurface, TASK_CHILD_LAYER_TASK_OVERLAY); 383 } 384 attachInput(@onNull WindowState windowState)385 void attachInput(@NonNull WindowState windowState) { 386 if (mInputInterceptor != null || windowState.mDisplayContent == null) { 387 return; 388 } 389 // TODO(b/371179559): only detect double tap on LB surfaces not used for cutout area. 390 // Potentially, the input interceptor may still be needed for slippery feature. 391 mInputInterceptor = new InputInterceptor("Letterbox_" + mType + "_", windowState); 392 } 393 remove(@onNull SurfaceControl.Transaction t)394 void remove(@NonNull SurfaceControl.Transaction t) { 395 if (mInputInterceptor != null) { 396 mInputInterceptor.dispose(); 397 mInputInterceptor = null; 398 } 399 if (mSurface != null) { 400 t.remove(mSurface); 401 } 402 if (mInputSurface != null) { 403 t.remove(mInputSurface); 404 } 405 mInputSurface = null; 406 mSurface = null; 407 } 408 getWidth()409 public int getWidth() { 410 return Math.max(0, mLayoutFrameGlobal.width()); 411 } 412 getHeight()413 public int getHeight() { 414 return Math.max(0, mLayoutFrameGlobal.height()); 415 } 416 applySurfaceChanges(@onNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction inputT)417 public void applySurfaceChanges(@NonNull SurfaceControl.Transaction t, 418 @NonNull SurfaceControl.Transaction inputT) { 419 if (!needsApplySurfaceChanges()) { 420 // Nothing changed. 421 return; 422 } 423 mSurfaceFrameRelative.set(mLayoutFrameRelative); 424 if (!mSurfaceFrameRelative.isEmpty()) { 425 if (mSurface == null) { 426 createSurface(t); 427 } 428 429 if (Flags.scrollingFromLetterbox() 430 && mInputInterceptor != null 431 && mInputSurface == null) { 432 createInputSurface(inputT); 433 } 434 435 mColor = mAppCompatLetterboxOverrides.getLetterboxBackgroundColor(); 436 t.setColor(mSurface, getRgbColorArray()); 437 setPositionAndCrop(t, mSurface); 438 439 mHasWallpaperBackground = mAppCompatLetterboxOverrides 440 .hasWallpaperBackgroundForLetterbox(); 441 updateAlphaAndBlur(t); 442 443 t.show(mSurface); 444 445 if (mInputSurface != null) { 446 setPositionAndCrop(inputT, mInputSurface); 447 inputT.setTrustedOverlay(mInputSurface, true); 448 inputT.show(mInputSurface); 449 } 450 451 } else { 452 if (mSurface != null) { 453 t.hide(mSurface); 454 } 455 if (mInputSurface != null) { 456 inputT.hide(mInputSurface); 457 } 458 } 459 460 SurfaceControl surfaceWithInput = 461 Flags.scrollingFromLetterbox() ? mInputSurface : mSurface; 462 if (surfaceWithInput != null && mInputInterceptor != null) { 463 mInputInterceptor.updateTouchableRegion(mSurfaceFrameRelative); 464 inputT.setInputWindowInfo(surfaceWithInput, mInputInterceptor.mWindowHandle); 465 } 466 } 467 setPositionAndCrop(@onNull SurfaceControl.Transaction t, @NonNull SurfaceControl surface)468 private void setPositionAndCrop(@NonNull SurfaceControl.Transaction t, 469 @NonNull SurfaceControl surface) { 470 t.setPosition(surface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top); 471 t.setWindowCrop(surface, mSurfaceFrameRelative.width(), 472 mSurfaceFrameRelative.height()); 473 } 474 updateAlphaAndBlur(SurfaceControl.Transaction t)475 private void updateAlphaAndBlur(SurfaceControl.Transaction t) { 476 if (!mHasWallpaperBackground) { 477 // Opaque 478 t.setAlpha(mSurface, 1.0f); 479 // Removing pre-exesting blur 480 t.setBackgroundBlurRadius(mSurface, 0); 481 return; 482 } 483 final float alpha = mAppCompatLetterboxOverrides.getLetterboxWallpaperDarkScrimAlpha(); 484 t.setAlpha(mSurface, alpha); 485 486 // Translucent dark scrim can be shown without blur. 487 final int blurRadiusPx = mAppCompatLetterboxOverrides 488 .getLetterboxWallpaperBlurRadiusPx(); 489 if (blurRadiusPx <= 0) { 490 // Removing pre-exesting blur 491 t.setBackgroundBlurRadius(mSurface, 0); 492 return; 493 } 494 495 t.setBackgroundBlurRadius(mSurface, blurRadiusPx); 496 } 497 getRgbColorArray()498 private float[] getRgbColorArray() { 499 final float[] rgbTmpFloat = new float[3]; 500 rgbTmpFloat[0] = mColor.red(); 501 rgbTmpFloat[1] = mColor.green(); 502 rgbTmpFloat[2] = mColor.blue(); 503 return rgbTmpFloat; 504 } 505 needsApplySurfaceChanges()506 public boolean needsApplySurfaceChanges() { 507 return !mSurfaceFrameRelative.equals(mLayoutFrameRelative) 508 // If mSurfaceFrameRelative is empty, then mHasWallpaperBackground and mColor 509 // may never be updated in applySurfaceChanges but this doesn't mean that 510 // update is needed. 511 || !mSurfaceFrameRelative.isEmpty() 512 && (mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox() 513 != mHasWallpaperBackground 514 || !mAppCompatLetterboxOverrides.getLetterboxBackgroundColor().equals(mColor)); 515 } 516 } 517 } 518