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 22 import android.content.Context; 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, Context context)258 TapEventReceiver(InputChannel inputChannel, Context context) { 259 super(inputChannel, UiThread.getHandler().getLooper()); 260 mDoubleTapListener = new DoubleTapListener(); 261 mDoubleTapDetector = new GestureDetector( 262 context, 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 @Override onDoubleTapEvent(MotionEvent e)274 public boolean onDoubleTapEvent(MotionEvent e) { 275 if (e.getAction() == MotionEvent.ACTION_UP) { 276 mDoubleTapCallbackX.accept((int) e.getRawX()); 277 mDoubleTapCallbackY.accept((int) e.getRawY()); 278 return true; 279 } 280 return false; 281 } 282 } 283 284 private final class InputInterceptor { 285 286 private final InputChannel mClientChannel; 287 private final InputWindowHandle mWindowHandle; 288 private final InputEventReceiver mInputEventReceiver; 289 private final WindowManagerService mWmService; 290 private final IBinder mToken; 291 InputInterceptor(String namePrefix, WindowState win)292 InputInterceptor(String namePrefix, WindowState win) { 293 mWmService = win.mWmService; 294 final String name = namePrefix + (win.mActivityRecord != null ? win.mActivityRecord : win); 295 mClientChannel = mWmService.mInputManager.createInputChannel(name); 296 mInputEventReceiver = new TapEventReceiver(mClientChannel, mWmService.mContext); 297 298 mToken = mClientChannel.getToken(); 299 300 mWindowHandle = new InputWindowHandle(null /* inputApplicationHandle */, 301 win.getDisplayId()); 302 mWindowHandle.name = name; 303 mWindowHandle.token = mToken; 304 mWindowHandle.layoutParamsType = WindowManager.LayoutParams.TYPE_INPUT_CONSUMER; 305 mWindowHandle.dispatchingTimeoutMillis = DEFAULT_DISPATCHING_TIMEOUT_MILLIS; 306 mWindowHandle.ownerPid = WindowManagerService.MY_PID; 307 mWindowHandle.ownerUid = WindowManagerService.MY_UID; 308 mWindowHandle.scaleFactor = 1.0f; 309 mWindowHandle.inputConfig = InputConfig.NOT_FOCUSABLE | InputConfig.SLIPPERY; 310 } 311 updateTouchableRegion(Rect frame)312 void updateTouchableRegion(Rect frame) { 313 if (frame.isEmpty()) { 314 // Use null token to indicate the surface doesn't need to receive input event (see 315 // the usage of Layer.hasInput in SurfaceFlinger), so InputDispatcher won't keep the 316 // unnecessary records. 317 mWindowHandle.token = null; 318 return; 319 } 320 mWindowHandle.token = mToken; 321 mWindowHandle.touchableRegion.set(frame); 322 mWindowHandle.touchableRegion.translate(-frame.left, -frame.top); 323 } 324 dispose()325 void dispose() { 326 mWmService.mInputManager.removeInputChannel(mToken); 327 mInputEventReceiver.dispose(); 328 mClientChannel.dispose(); 329 } 330 } 331 332 private class LetterboxSurface { 333 334 private final String mType; 335 private SurfaceControl mSurface; 336 private Color mColor; 337 private boolean mHasWallpaperBackground; 338 private SurfaceControl mParentSurface; 339 340 private final Rect mSurfaceFrameRelative = new Rect(); 341 private final Rect mLayoutFrameGlobal = new Rect(); 342 private final Rect mLayoutFrameRelative = new Rect(); 343 344 private InputInterceptor mInputInterceptor; 345 LetterboxSurface(String type)346 public LetterboxSurface(String type) { 347 mType = type; 348 } 349 layout(int left, int top, int right, int bottom, Point surfaceOrigin)350 public void layout(int left, int top, int right, int bottom, Point surfaceOrigin) { 351 mLayoutFrameGlobal.set(left, top, right, bottom); 352 mLayoutFrameRelative.set(mLayoutFrameGlobal); 353 mLayoutFrameRelative.offset(-surfaceOrigin.x, -surfaceOrigin.y); 354 } 355 createSurface(SurfaceControl.Transaction t)356 private void createSurface(SurfaceControl.Transaction t) { 357 mSurface = mSurfaceControlFactory.get() 358 .setName("Letterbox - " + mType) 359 .setFlags(HIDDEN) 360 .setColorLayer() 361 .setCallsite("LetterboxSurface.createSurface") 362 .build(); 363 364 t.setLayer(mSurface, -1).setColorSpaceAgnostic(mSurface, true); 365 } 366 attachInput(WindowState win)367 void attachInput(WindowState win) { 368 if (mInputInterceptor != null) { 369 mInputInterceptor.dispose(); 370 } 371 mInputInterceptor = new InputInterceptor("Letterbox_" + mType + "_", win); 372 } 373 isRemoved()374 boolean isRemoved() { 375 return mSurface != null || mInputInterceptor != null; 376 } 377 remove()378 public void remove() { 379 if (mSurface != null) { 380 mTransactionFactory.get().remove(mSurface).apply(); 381 mSurface = null; 382 } 383 if (mInputInterceptor != null) { 384 mInputInterceptor.dispose(); 385 mInputInterceptor = null; 386 } 387 } 388 getWidth()389 public int getWidth() { 390 return Math.max(0, mLayoutFrameGlobal.width()); 391 } 392 getHeight()393 public int getHeight() { 394 return Math.max(0, mLayoutFrameGlobal.height()); 395 } 396 applySurfaceChanges(SurfaceControl.Transaction t)397 public void applySurfaceChanges(SurfaceControl.Transaction t) { 398 if (!needsApplySurfaceChanges()) { 399 // Nothing changed. 400 return; 401 } 402 mSurfaceFrameRelative.set(mLayoutFrameRelative); 403 if (!mSurfaceFrameRelative.isEmpty()) { 404 if (mSurface == null) { 405 createSurface(t); 406 } 407 408 mColor = mColorSupplier.get(); 409 mParentSurface = mParentSurfaceSupplier.get(); 410 t.setColor(mSurface, getRgbColorArray()); 411 t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top); 412 t.setWindowCrop(mSurface, mSurfaceFrameRelative.width(), 413 mSurfaceFrameRelative.height()); 414 t.reparent(mSurface, mParentSurface); 415 416 mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.get(); 417 updateAlphaAndBlur(t); 418 419 t.show(mSurface); 420 } else if (mSurface != null) { 421 t.hide(mSurface); 422 } 423 if (mSurface != null && mInputInterceptor != null) { 424 mInputInterceptor.updateTouchableRegion(mSurfaceFrameRelative); 425 t.setInputWindowInfo(mSurface, mInputInterceptor.mWindowHandle); 426 } 427 } 428 updateAlphaAndBlur(SurfaceControl.Transaction t)429 private void updateAlphaAndBlur(SurfaceControl.Transaction t) { 430 if (!mHasWallpaperBackground) { 431 // Opaque 432 t.setAlpha(mSurface, 1.0f); 433 // Removing pre-exesting blur 434 t.setBackgroundBlurRadius(mSurface, 0); 435 return; 436 } 437 final float alpha = mDarkScrimAlphaSupplier.get(); 438 t.setAlpha(mSurface, alpha); 439 440 // Translucent dark scrim can be shown without blur. 441 if (mBlurRadiusSupplier.get() <= 0) { 442 // Removing pre-exesting blur 443 t.setBackgroundBlurRadius(mSurface, 0); 444 return; 445 } 446 447 t.setBackgroundBlurRadius(mSurface, mBlurRadiusSupplier.get()); 448 } 449 getRgbColorArray()450 private float[] getRgbColorArray() { 451 final float[] rgbTmpFloat = new float[3]; 452 rgbTmpFloat[0] = mColor.red(); 453 rgbTmpFloat[1] = mColor.green(); 454 rgbTmpFloat[2] = mColor.blue(); 455 return rgbTmpFloat; 456 } 457 needsApplySurfaceChanges()458 public boolean needsApplySurfaceChanges() { 459 return !mSurfaceFrameRelative.equals(mLayoutFrameRelative) 460 // If mSurfaceFrameRelative is empty then mHasWallpaperBackground, mColor, 461 // and mParentSurface may never be updated in applySurfaceChanges but this 462 // doesn't mean that update is needed. 463 || !mSurfaceFrameRelative.isEmpty() 464 && (mHasWallpaperBackgroundSupplier.get() != mHasWallpaperBackground 465 || !mColorSupplier.get().equals(mColor) 466 || mParentSurfaceSupplier.get() != mParentSurface); 467 } 468 } 469 } 470