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