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