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