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