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