• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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