• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui;
16 
17 import static android.view.Display.DEFAULT_DISPLAY;
18 import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
19 import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
20 import static android.view.DisplayCutout.BOUNDS_POSITION_LENGTH;
21 import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
22 import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
23 import static android.view.Surface.ROTATION_0;
24 import static android.view.Surface.ROTATION_180;
25 import static android.view.Surface.ROTATION_270;
26 import static android.view.Surface.ROTATION_90;
27 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
28 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
29 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
30 
31 import android.animation.Animator;
32 import android.animation.AnimatorListenerAdapter;
33 import android.animation.ValueAnimator;
34 import android.annotation.Dimension;
35 import android.annotation.NonNull;
36 import android.annotation.Nullable;
37 import android.app.ActivityManager;
38 import android.content.BroadcastReceiver;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.IntentFilter;
42 import android.content.res.ColorStateList;
43 import android.content.res.Configuration;
44 import android.content.res.Resources;
45 import android.graphics.Canvas;
46 import android.graphics.Color;
47 import android.graphics.Matrix;
48 import android.graphics.Paint;
49 import android.graphics.Path;
50 import android.graphics.PixelFormat;
51 import android.graphics.Point;
52 import android.graphics.Rect;
53 import android.graphics.RectF;
54 import android.graphics.Region;
55 import android.graphics.drawable.Drawable;
56 import android.hardware.display.DisplayManager;
57 import android.os.Handler;
58 import android.os.SystemProperties;
59 import android.os.UserHandle;
60 import android.provider.Settings.Secure;
61 import android.util.DisplayMetrics;
62 import android.util.Log;
63 import android.view.Display;
64 import android.view.DisplayCutout;
65 import android.view.DisplayCutout.BoundsPosition;
66 import android.view.DisplayInfo;
67 import android.view.Gravity;
68 import android.view.LayoutInflater;
69 import android.view.Surface;
70 import android.view.View;
71 import android.view.View.OnLayoutChangeListener;
72 import android.view.ViewGroup;
73 import android.view.ViewGroup.LayoutParams;
74 import android.view.ViewTreeObserver;
75 import android.view.WindowManager;
76 import android.widget.FrameLayout;
77 import android.widget.ImageView;
78 
79 import androidx.annotation.VisibleForTesting;
80 
81 import com.android.internal.util.Preconditions;
82 import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView;
83 import com.android.systemui.animation.Interpolators;
84 import com.android.systemui.broadcast.BroadcastDispatcher;
85 import com.android.systemui.dagger.SysUISingleton;
86 import com.android.systemui.dagger.qualifiers.Main;
87 import com.android.systemui.qs.SecureSetting;
88 import com.android.systemui.settings.UserTracker;
89 import com.android.systemui.statusbar.events.PrivacyDotViewController;
90 import com.android.systemui.tuner.TunerService;
91 import com.android.systemui.tuner.TunerService.Tunable;
92 import com.android.systemui.util.concurrency.DelayableExecutor;
93 import com.android.systemui.util.concurrency.ThreadFactory;
94 import com.android.systemui.util.settings.SecureSettings;
95 
96 import java.util.ArrayList;
97 import java.util.List;
98 import java.util.concurrent.Executor;
99 
100 import javax.inject.Inject;
101 
102 /**
103  * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
104  * for antialiasing and emulation purposes.
105  */
106 @SysUISingleton
107 public class ScreenDecorations extends SystemUI implements Tunable {
108     private static final boolean DEBUG = false;
109     private static final String TAG = "ScreenDecorations";
110 
111     public static final String SIZE = "sysui_rounded_size";
112     public static final String PADDING = "sysui_rounded_content_padding";
113     // Provide a way for factory to disable ScreenDecorations to run the Display tests.
114     private static final boolean DEBUG_DISABLE_SCREEN_DECORATIONS =
115             SystemProperties.getBoolean("debug.disable_screen_decorations", false);
116     private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
117             SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
118     private static final boolean VERBOSE = false;
119     private static final boolean DEBUG_COLOR = DEBUG_SCREENSHOT_ROUNDED_CORNERS;
120 
121     private DisplayManager mDisplayManager;
122     @VisibleForTesting
123     protected boolean mIsRegistered;
124     private final BroadcastDispatcher mBroadcastDispatcher;
125     private final Executor mMainExecutor;
126     private final TunerService mTunerService;
127     private final SecureSettings mSecureSettings;
128     private DisplayManager.DisplayListener mDisplayListener;
129     private CameraAvailabilityListener mCameraListener;
130     private final UserTracker mUserTracker;
131     private final PrivacyDotViewController mDotViewController;
132     private final ThreadFactory mThreadFactory;
133 
134     //TODO: These are piecemeal being updated to Points for now to support non-square rounded
135     // corners. for now it is only supposed when reading the intrinsic size from the drawables with
136     // mIsRoundedCornerMultipleRadius is set
137     @VisibleForTesting
138     protected Point mRoundedDefault = new Point(0, 0);
139     @VisibleForTesting
140     protected Point mRoundedDefaultTop = new Point(0, 0);
141     @VisibleForTesting
142     protected Point mRoundedDefaultBottom = new Point(0, 0);
143     @VisibleForTesting
144     protected View[] mOverlays;
145     @Nullable
146     private DisplayCutoutView[] mCutoutViews;
147     //TODO:
148     View mTopLeftDot;
149     View mTopRightDot;
150     View mBottomLeftDot;
151     View mBottomRightDot;
152     private float mDensity;
153     private WindowManager mWindowManager;
154     private int mRotation;
155     private SecureSetting mColorInversionSetting;
156     private DelayableExecutor mExecutor;
157     private Handler mHandler;
158     private boolean mPendingRotationChange;
159     private boolean mIsRoundedCornerMultipleRadius;
160     private int mStatusBarHeightPortrait;
161     private int mStatusBarHeightLandscape;
162 
163     private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback =
164             new CameraAvailabilityListener.CameraTransitionCallback() {
165         @Override
166         public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
167             if (mCutoutViews == null) {
168                 Log.w(TAG, "DisplayCutoutView do not initialized");
169                 return;
170             }
171             // Show the extra protection around the front facing camera if necessary
172             for (DisplayCutoutView dcv : mCutoutViews) {
173                 // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
174                 if (dcv != null) {
175                     dcv.setProtection(protectionPath, bounds);
176                     dcv.setShowProtection(true);
177                 }
178             }
179         }
180 
181         @Override
182         public void onHideCameraProtection() {
183             if (mCutoutViews == null) {
184                 Log.w(TAG, "DisplayCutoutView do not initialized");
185                 return;
186             }
187             // Go back to the regular anti-aliasing
188             for (DisplayCutoutView dcv : mCutoutViews) {
189                 // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
190                 if (dcv != null) {
191                     dcv.setShowProtection(false);
192                 }
193             }
194         }
195     };
196 
197     /**
198      * Converts a set of {@link Rect}s into a {@link Region}
199      *
200      * @hide
201      */
rectsToRegion(List<Rect> rects)202     public static Region rectsToRegion(List<Rect> rects) {
203         Region result = Region.obtain();
204         if (rects != null) {
205             for (Rect r : rects) {
206                 if (r != null && !r.isEmpty()) {
207                     result.op(r, Region.Op.UNION);
208                 }
209             }
210         }
211         return result;
212     }
213 
214     @Inject
ScreenDecorations(Context context, @Main Executor mainExecutor, SecureSettings secureSettings, BroadcastDispatcher broadcastDispatcher, TunerService tunerService, UserTracker userTracker, PrivacyDotViewController dotViewController, ThreadFactory threadFactory)215     public ScreenDecorations(Context context,
216             @Main Executor mainExecutor,
217             SecureSettings secureSettings,
218             BroadcastDispatcher broadcastDispatcher,
219             TunerService tunerService,
220             UserTracker userTracker,
221             PrivacyDotViewController dotViewController,
222             ThreadFactory threadFactory) {
223         super(context);
224         mMainExecutor = mainExecutor;
225         mSecureSettings = secureSettings;
226         mBroadcastDispatcher = broadcastDispatcher;
227         mTunerService = tunerService;
228         mUserTracker = userTracker;
229         mDotViewController = dotViewController;
230         mThreadFactory = threadFactory;
231     }
232 
233     @Override
start()234     public void start() {
235         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
236             Log.i(TAG, "ScreenDecorations is disabled");
237             return;
238         }
239         mHandler = mThreadFactory.buildHandlerOnNewThread("ScreenDecorations");
240         mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
241         mExecutor.execute(this::startOnScreenDecorationsThread);
242         mDotViewController.setUiExecutor(mExecutor);
243     }
244 
startOnScreenDecorationsThread()245     private void startOnScreenDecorationsThread() {
246         mRotation = mContext.getDisplay().getRotation();
247         mWindowManager = mContext.getSystemService(WindowManager.class);
248         mDisplayManager = mContext.getSystemService(DisplayManager.class);
249         mIsRoundedCornerMultipleRadius = mContext.getResources().getBoolean(
250                 R.bool.config_roundedCornerMultipleRadius);
251         updateRoundedCornerRadii();
252         setupDecorations();
253         setupCameraListener();
254 
255         mDisplayListener = new DisplayManager.DisplayListener() {
256             @Override
257             public void onDisplayAdded(int displayId) {
258                 // do nothing
259             }
260 
261             @Override
262             public void onDisplayRemoved(int displayId) {
263                 // do nothing
264             }
265 
266             @Override
267             public void onDisplayChanged(int displayId) {
268                 final int newRotation = mContext.getDisplay().getRotation();
269                 if (mOverlays != null && mRotation != newRotation) {
270                     // We cannot immediately update the orientation. Otherwise
271                     // WindowManager is still deferring layout until it has finished dispatching
272                     // the config changes, which may cause divergence between what we draw
273                     // (new orientation), and where we are placed on the screen (old orientation).
274                     // Instead we wait until either:
275                     // - we are trying to redraw. This because WM resized our window and told us to.
276                     // - the config change has been dispatched, so WM is no longer deferring layout.
277                     mPendingRotationChange = true;
278                     if (DEBUG) {
279                         Log.i(TAG, "Rotation changed, deferring " + newRotation + ", staying at "
280                                 + mRotation);
281                     }
282 
283                     for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
284                         if (mOverlays[i] != null) {
285                             mOverlays[i].getViewTreeObserver().addOnPreDrawListener(
286                                     new RestartingPreDrawListener(mOverlays[i], i, newRotation));
287                         }
288                     }
289                 }
290                 updateOrientation();
291             }
292         };
293 
294         mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
295         updateOrientation();
296     }
297 
setupDecorations()298     private void setupDecorations() {
299         if (hasRoundedCorners() || shouldDrawCutout()) {
300             updateStatusBarHeight();
301             final DisplayCutout cutout = getCutout();
302             final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll();
303             int rotatedPos;
304             for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
305                 rotatedPos = getBoundPositionFromRotation(i, mRotation);
306                 if ((bounds != null && !bounds[rotatedPos].isEmpty())
307                         || shouldShowRoundedCorner(i)) {
308                     createOverlay(i);
309                 } else {
310                     removeOverlay(i);
311                 }
312             }
313             // Overlays have been created, send the dots to the controller
314             //TODO: need a better way to do this
315             mDotViewController.initialize(
316                     mTopLeftDot, mTopRightDot, mBottomLeftDot, mBottomRightDot);
317         } else {
318             removeAllOverlays();
319         }
320 
321         if (hasOverlays()) {
322             if (mIsRegistered) {
323                 return;
324             }
325             DisplayMetrics metrics = new DisplayMetrics();
326             mDisplayManager.getDisplay(DEFAULT_DISPLAY).getMetrics(metrics);
327             mDensity = metrics.density;
328 
329             mExecutor.execute(() -> mTunerService.addTunable(this, SIZE));
330 
331             // Watch color inversion and invert the overlay as needed.
332             if (mColorInversionSetting == null) {
333                 mColorInversionSetting = new SecureSetting(mSecureSettings, mHandler,
334                         Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
335                         mUserTracker.getUserId()) {
336                     @Override
337                     protected void handleValueChanged(int value, boolean observedChange) {
338                         updateColorInversion(value);
339                     }
340                 };
341             }
342             mColorInversionSetting.setListening(true);
343             mColorInversionSetting.onChange(false);
344 
345             IntentFilter filter = new IntentFilter();
346             filter.addAction(Intent.ACTION_USER_SWITCHED);
347             mBroadcastDispatcher.registerReceiver(mUserSwitchIntentReceiver, filter,
348                     mExecutor, UserHandle.ALL);
349             mIsRegistered = true;
350         } else {
351             mMainExecutor.execute(() -> mTunerService.removeTunable(this));
352 
353             if (mColorInversionSetting != null) {
354                 mColorInversionSetting.setListening(false);
355             }
356 
357             mBroadcastDispatcher.unregisterReceiver(mUserSwitchIntentReceiver);
358             mIsRegistered = false;
359         }
360     }
361 
362     @VisibleForTesting
getCutout()363     DisplayCutout getCutout() {
364         return mContext.getDisplay().getCutout();
365     }
366 
367     @VisibleForTesting
hasOverlays()368     boolean hasOverlays() {
369         if (mOverlays == null) {
370             return false;
371         }
372 
373         for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
374             if (mOverlays[i] != null) {
375                 return true;
376             }
377         }
378         mOverlays = null;
379         return false;
380     }
381 
removeAllOverlays()382     private void removeAllOverlays() {
383         if (mOverlays == null) {
384             return;
385         }
386 
387         for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
388             if (mOverlays[i] != null) {
389                 removeOverlay(i);
390             }
391         }
392         mOverlays = null;
393     }
394 
removeOverlay(@oundsPosition int pos)395     private void removeOverlay(@BoundsPosition int pos) {
396         if (mOverlays == null || mOverlays[pos] == null) {
397             return;
398         }
399         mWindowManager.removeViewImmediate(mOverlays[pos]);
400         mOverlays[pos] = null;
401     }
402 
createOverlay(@oundsPosition int pos)403     private void createOverlay(@BoundsPosition int pos) {
404         if (mOverlays == null) {
405             mOverlays = new View[BOUNDS_POSITION_LENGTH];
406         }
407 
408         if (mCutoutViews == null) {
409             mCutoutViews = new DisplayCutoutView[BOUNDS_POSITION_LENGTH];
410         }
411 
412         if (mOverlays[pos] != null) {
413             return;
414         }
415         mOverlays[pos] = overlayForPosition(pos);
416 
417         mCutoutViews[pos] = new DisplayCutoutView(mContext, pos, this);
418         ((ViewGroup) mOverlays[pos]).addView(mCutoutViews[pos]);
419 
420         mOverlays[pos].setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
421         mOverlays[pos].setAlpha(0);
422         mOverlays[pos].setForceDarkAllowed(false);
423 
424         updateView(pos);
425 
426         mWindowManager.addView(mOverlays[pos], getWindowLayoutParams(pos));
427 
428         mOverlays[pos].addOnLayoutChangeListener(new OnLayoutChangeListener() {
429             @Override
430             public void onLayoutChange(View v, int left, int top, int right, int bottom,
431                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
432                 mOverlays[pos].removeOnLayoutChangeListener(this);
433                 mOverlays[pos].animate()
434                         .alpha(1)
435                         .setDuration(1000)
436                         .start();
437             }
438         });
439 
440         mOverlays[pos].getViewTreeObserver().addOnPreDrawListener(
441                 new ValidatingPreDrawListener(mOverlays[pos]));
442     }
443 
444     /**
445      * Allow overrides for top/bottom positions
446      */
overlayForPosition(@oundsPosition int pos)447     private View overlayForPosition(@BoundsPosition int pos) {
448         switch (pos) {
449             case BOUNDS_POSITION_TOP:
450             case BOUNDS_POSITION_LEFT:
451                 View top = LayoutInflater.from(mContext)
452                         .inflate(R.layout.rounded_corners_top, null);
453                 mTopLeftDot = top.findViewById(R.id.privacy_dot_left_container);
454                 mTopRightDot = top.findViewById(R.id.privacy_dot_right_container);
455                 return top;
456             case BOUNDS_POSITION_BOTTOM:
457             case BOUNDS_POSITION_RIGHT:
458                 View bottom =  LayoutInflater.from(mContext)
459                         .inflate(R.layout.rounded_corners_bottom, null);
460                 mBottomLeftDot = bottom.findViewById(R.id.privacy_dot_left_container);
461                 mBottomRightDot = bottom.findViewById(R.id.privacy_dot_right_container);
462                 return bottom;
463             default:
464                 throw new IllegalArgumentException("Unknown bounds position");
465         }
466     }
467 
updateView(@oundsPosition int pos)468     private void updateView(@BoundsPosition int pos) {
469         if (mOverlays == null || mOverlays[pos] == null) {
470             return;
471         }
472 
473         // update rounded corner view rotation
474         updateRoundedCornerView(pos, R.id.left);
475         updateRoundedCornerView(pos, R.id.right);
476         updateRoundedCornerSize(mRoundedDefault, mRoundedDefaultTop, mRoundedDefaultBottom);
477 
478         // update cutout view rotation
479         if (mCutoutViews != null && mCutoutViews[pos] != null) {
480             mCutoutViews[pos].setRotation(mRotation);
481         }
482     }
483 
484     @VisibleForTesting
getWindowLayoutParams(@oundsPosition int pos)485     WindowManager.LayoutParams getWindowLayoutParams(@BoundsPosition int pos) {
486         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
487                 getWidthLayoutParamByPos(pos),
488                 getHeightLayoutParamByPos(pos),
489                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
490                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
491                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
492                         | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
493                         | WindowManager.LayoutParams.FLAG_SLIPPERY
494                         | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
495                 PixelFormat.TRANSLUCENT);
496         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
497                 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
498 
499         if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS) {
500             lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
501         }
502 
503         lp.setTitle(getWindowTitleByPos(pos));
504         lp.gravity = getOverlayWindowGravity(pos);
505         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
506         lp.setFitInsetsTypes(0 /* types */);
507         lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
508         return lp;
509     }
510 
getWidthLayoutParamByPos(@oundsPosition int pos)511     private int getWidthLayoutParamByPos(@BoundsPosition int pos) {
512         final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
513         return rotatedPos == BOUNDS_POSITION_TOP || rotatedPos == BOUNDS_POSITION_BOTTOM
514                 ? MATCH_PARENT : WRAP_CONTENT;
515     }
516 
getHeightLayoutParamByPos(@oundsPosition int pos)517     private int getHeightLayoutParamByPos(@BoundsPosition int pos) {
518         final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
519         return rotatedPos == BOUNDS_POSITION_TOP || rotatedPos == BOUNDS_POSITION_BOTTOM
520                 ? WRAP_CONTENT : MATCH_PARENT;
521     }
522 
getWindowTitleByPos(@oundsPosition int pos)523     private static String getWindowTitleByPos(@BoundsPosition int pos) {
524         switch (pos) {
525             case BOUNDS_POSITION_LEFT:
526                 return "ScreenDecorOverlayLeft";
527             case BOUNDS_POSITION_TOP:
528                 return "ScreenDecorOverlay";
529             case BOUNDS_POSITION_RIGHT:
530                 return "ScreenDecorOverlayRight";
531             case BOUNDS_POSITION_BOTTOM:
532                 return "ScreenDecorOverlayBottom";
533             default:
534                 throw new IllegalArgumentException("unknown bound position: " + pos);
535         }
536     }
537 
getOverlayWindowGravity(@oundsPosition int pos)538     private int getOverlayWindowGravity(@BoundsPosition int pos) {
539         final int rotated = getBoundPositionFromRotation(pos, mRotation);
540         switch (rotated) {
541             case BOUNDS_POSITION_TOP:
542                 return Gravity.TOP;
543             case BOUNDS_POSITION_BOTTOM:
544                 return Gravity.BOTTOM;
545             case BOUNDS_POSITION_LEFT:
546                 return Gravity.LEFT;
547             case BOUNDS_POSITION_RIGHT:
548                 return Gravity.RIGHT;
549             default:
550                 throw new IllegalArgumentException("unknown bound position: " + pos);
551         }
552     }
553 
getBoundPositionFromRotation(@oundsPosition int pos, int rotation)554     private static int getBoundPositionFromRotation(@BoundsPosition int pos, int rotation) {
555         return (pos - rotation) < 0
556                 ? pos - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH
557                 : pos - rotation;
558     }
559 
setupCameraListener()560     private void setupCameraListener() {
561         Resources res = mContext.getResources();
562         boolean enabled = res.getBoolean(R.bool.config_enableDisplayCutoutProtection);
563         if (enabled) {
564             mCameraListener = CameraAvailabilityListener.Factory.build(mContext, mExecutor);
565             mCameraListener.addTransitionCallback(mCameraTransitionCallback);
566             mCameraListener.startListening();
567         }
568     }
569 
570     private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() {
571         @Override
572         public void onReceive(Context context, Intent intent) {
573             int newUserId = ActivityManager.getCurrentUser();
574             if (DEBUG) {
575                 Log.d(TAG, "UserSwitched newUserId=" + newUserId);
576             }
577             // update color inversion setting to the new user
578             mColorInversionSetting.setUserId(newUserId);
579             updateColorInversion(mColorInversionSetting.getValue());
580         }
581     };
582 
updateColorInversion(int colorsInvertedValue)583     private void updateColorInversion(int colorsInvertedValue) {
584         int tint = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK;
585         if (DEBUG_COLOR) {
586             tint = Color.RED;
587         }
588         ColorStateList tintList = ColorStateList.valueOf(tint);
589 
590         if (mOverlays == null) {
591             return;
592         }
593         for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
594             if (mOverlays[i] == null) {
595                 continue;
596             }
597             final int size = ((ViewGroup) mOverlays[i]).getChildCount();
598             View child;
599             for (int j = 0; j < size; j++) {
600                 child = ((ViewGroup) mOverlays[i]).getChildAt(j);
601                 if (child.getId() == R.id.privacy_dot_left_container
602                         || child.getId() == R.id.privacy_dot_right_container) {
603                     // Exclude privacy dot from color inversion (for now?)
604                     continue;
605                 }
606                 if (child instanceof ImageView) {
607                     ((ImageView) child).setImageTintList(tintList);
608                 } else if (child instanceof DisplayCutoutView) {
609                     ((DisplayCutoutView) child).setColor(tint);
610                 }
611             }
612         }
613     }
614 
615     @Override
onConfigurationChanged(Configuration newConfig)616     protected void onConfigurationChanged(Configuration newConfig) {
617         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
618             Log.i(TAG, "ScreenDecorations is disabled");
619             return;
620         }
621         mExecutor.execute(() -> {
622             int oldRotation = mRotation;
623             mPendingRotationChange = false;
624             updateOrientation();
625             updateRoundedCornerRadii();
626             if (DEBUG) Log.i(TAG, "onConfigChanged from rot " + oldRotation + " to " + mRotation);
627             setupDecorations();
628             if (mOverlays != null) {
629                 // Updating the layout params ensures that ViewRootImpl will call relayoutWindow(),
630                 // which ensures that the forced seamless rotation will end, even if we updated
631                 // the rotation before window manager was ready (and was still waiting for sending
632                 // the updated rotation).
633                 updateLayoutParams();
634             }
635         });
636     }
637 
updateOrientation()638     private void updateOrientation() {
639         Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(),
640                 "must call on " + mHandler.getLooper().getThread()
641                         + ", but was " + Thread.currentThread());
642 
643         int newRotation = mContext.getDisplay().getRotation();
644         if (mRotation != newRotation) {
645             mDotViewController.setNewRotation(newRotation);
646         }
647 
648         if (mPendingRotationChange) {
649             return;
650         }
651         if (newRotation != mRotation) {
652             mRotation = newRotation;
653 
654             if (mOverlays != null) {
655                 updateLayoutParams();
656                 for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
657                     if (mOverlays[i] == null) {
658                         continue;
659                     }
660                     updateView(i);
661                 }
662             }
663         }
664     }
665 
updateStatusBarHeight()666     private void updateStatusBarHeight() {
667         mStatusBarHeightLandscape = mContext.getResources().getDimensionPixelSize(
668                 com.android.internal.R.dimen.status_bar_height_landscape);
669         mStatusBarHeightPortrait = mContext.getResources().getDimensionPixelSize(
670                 com.android.internal.R.dimen.status_bar_height_portrait);
671         mDotViewController.setStatusBarHeights(mStatusBarHeightPortrait, mStatusBarHeightLandscape);
672     }
673 
updateRoundedCornerRadii()674     private void updateRoundedCornerRadii() {
675         // We should eventually move to just using the intrinsic size of the drawables since
676         // they should be sized to the exact pixels they want to cover. Therefore I'm purposely not
677         // upgrading all of the configs to contain (width, height) pairs. Instead assume that a
678         // device configured using the single integer config value is okay with drawing the corners
679         // as a square
680         final int newRoundedDefault = mContext.getResources().getDimensionPixelSize(
681                 com.android.internal.R.dimen.rounded_corner_radius);
682         final int newRoundedDefaultTop = mContext.getResources().getDimensionPixelSize(
683                 com.android.internal.R.dimen.rounded_corner_radius_top);
684         final int newRoundedDefaultBottom = mContext.getResources().getDimensionPixelSize(
685                 com.android.internal.R.dimen.rounded_corner_radius_bottom);
686 
687         final boolean changed = mRoundedDefault.x != newRoundedDefault
688                         || mRoundedDefaultTop.x != newRoundedDefaultTop
689                         || mRoundedDefaultBottom.x != newRoundedDefaultBottom;
690 
691         if (changed) {
692             // If config_roundedCornerMultipleRadius set as true, ScreenDecorations respect the
693             // (width, height) size of drawable/rounded.xml instead of rounded_corner_radius
694             if (mIsRoundedCornerMultipleRadius) {
695                 Drawable d =  mContext.getDrawable(R.drawable.rounded);
696                 mRoundedDefault.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
697                 d =  mContext.getDrawable(R.drawable.rounded_corner_top);
698                 mRoundedDefaultTop.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
699                 d =  mContext.getDrawable(R.drawable.rounded_corner_bottom);
700                 mRoundedDefaultBottom.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
701             } else {
702                 mRoundedDefault.set(newRoundedDefault, newRoundedDefault);
703                 mRoundedDefaultTop.set(newRoundedDefaultTop, newRoundedDefaultTop);
704                 mRoundedDefaultBottom.set(newRoundedDefaultBottom, newRoundedDefaultBottom);
705             }
706             onTuningChanged(SIZE, null);
707         }
708     }
709 
updateRoundedCornerView(@oundsPosition int pos, int id)710     private void updateRoundedCornerView(@BoundsPosition int pos, int id) {
711         final View rounded = mOverlays[pos].findViewById(id);
712         if (rounded == null) {
713             return;
714         }
715         rounded.setVisibility(View.GONE);
716         if (shouldShowRoundedCorner(pos)) {
717             final int gravity = getRoundedCornerGravity(pos, id == R.id.left);
718             ((FrameLayout.LayoutParams) rounded.getLayoutParams()).gravity = gravity;
719             setRoundedCornerOrientation(rounded, gravity);
720             rounded.setVisibility(View.VISIBLE);
721         }
722     }
723 
getRoundedCornerGravity(@oundsPosition int pos, boolean isStart)724     private int getRoundedCornerGravity(@BoundsPosition int pos, boolean isStart) {
725         final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
726         switch (rotatedPos) {
727             case BOUNDS_POSITION_LEFT:
728                 return isStart ? Gravity.TOP | Gravity.LEFT : Gravity.BOTTOM | Gravity.LEFT;
729             case BOUNDS_POSITION_TOP:
730                 return isStart ? Gravity.TOP | Gravity.LEFT : Gravity.TOP | Gravity.RIGHT;
731             case BOUNDS_POSITION_RIGHT:
732                 return isStart ? Gravity.TOP | Gravity.RIGHT : Gravity.BOTTOM | Gravity.RIGHT;
733             case BOUNDS_POSITION_BOTTOM:
734                 return isStart ? Gravity.BOTTOM | Gravity.LEFT : Gravity.BOTTOM | Gravity.RIGHT;
735             default:
736                 throw new IllegalArgumentException("Incorrect position: " + rotatedPos);
737         }
738     }
739 
740     /**
741      * Configures the rounded corner drawable's view matrix based on the gravity.
742      *
743      * The gravity describes which corner to configure for, and the drawable we are rotating is
744      * assumed to be oriented for the top-left corner of the device regardless of the target corner.
745      * Therefore we need to rotate 180 degrees to get a bottom-left corner, and mirror in the x- or
746      * y-axis for the top-right and bottom-left corners.
747      */
setRoundedCornerOrientation(View corner, int gravity)748     private void setRoundedCornerOrientation(View corner, int gravity) {
749         corner.setRotation(0);
750         corner.setScaleX(1);
751         corner.setScaleY(1);
752         switch (gravity) {
753             case Gravity.TOP | Gravity.LEFT:
754                 return;
755             case Gravity.TOP | Gravity.RIGHT:
756                 corner.setScaleX(-1); // flip X axis
757                 return;
758             case Gravity.BOTTOM | Gravity.LEFT:
759                 corner.setScaleY(-1); // flip Y axis
760                 return;
761             case Gravity.BOTTOM | Gravity.RIGHT:
762                 corner.setRotation(180);
763                 return;
764             default:
765                 throw new IllegalArgumentException("Unsupported gravity: " + gravity);
766         }
767     }
hasRoundedCorners()768     private boolean hasRoundedCorners() {
769         return mRoundedDefault.x > 0
770                 || mRoundedDefaultBottom.x > 0
771                 || mRoundedDefaultTop.x > 0
772                 || mIsRoundedCornerMultipleRadius;
773     }
774 
shouldShowRoundedCorner(@oundsPosition int pos)775     private boolean shouldShowRoundedCorner(@BoundsPosition int pos) {
776         if (!hasRoundedCorners()) {
777             return false;
778         }
779 
780         DisplayCutout cutout = getCutout();
781         // for cutout is null or cutout with only waterfall.
782         final boolean emptyBoundsOrWaterfall = cutout == null || cutout.isBoundsEmpty();
783         // Shows rounded corner on left and right overlays only when there is no top or bottom
784         // cutout.
785         final int rotatedTop = getBoundPositionFromRotation(BOUNDS_POSITION_TOP, mRotation);
786         final int rotatedBottom = getBoundPositionFromRotation(BOUNDS_POSITION_BOTTOM, mRotation);
787         if (emptyBoundsOrWaterfall || !cutout.getBoundingRectsAll()[rotatedTop].isEmpty()
788                 || !cutout.getBoundingRectsAll()[rotatedBottom].isEmpty()) {
789             return pos == BOUNDS_POSITION_TOP || pos == BOUNDS_POSITION_BOTTOM;
790         } else {
791             return pos == BOUNDS_POSITION_LEFT || pos == BOUNDS_POSITION_RIGHT;
792         }
793     }
794 
shouldDrawCutout()795     private boolean shouldDrawCutout() {
796         return shouldDrawCutout(mContext);
797     }
798 
shouldDrawCutout(Context context)799     static boolean shouldDrawCutout(Context context) {
800         return context.getResources().getBoolean(
801                 com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
802     }
803 
updateLayoutParams()804     private void updateLayoutParams() {
805         if (mOverlays == null) {
806             return;
807         }
808         for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
809             if (mOverlays[i] == null) {
810                 continue;
811             }
812             mWindowManager.updateViewLayout(mOverlays[i], getWindowLayoutParams(i));
813         }
814     }
815 
816     @Override
onTuningChanged(String key, String newValue)817     public void onTuningChanged(String key, String newValue) {
818         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
819             Log.i(TAG, "ScreenDecorations is disabled");
820             return;
821         }
822         mExecutor.execute(() -> {
823             if (mOverlays == null) return;
824             if (SIZE.equals(key)) {
825                 Point size = mRoundedDefault;
826                 Point sizeTop = mRoundedDefaultTop;
827                 Point sizeBottom = mRoundedDefaultBottom;
828                 if (newValue != null) {
829                     try {
830                         int s = (int) (Integer.parseInt(newValue) * mDensity);
831                         size = new Point(s, s);
832                     } catch (Exception e) {
833                     }
834                 }
835                 updateRoundedCornerSize(size, sizeTop, sizeBottom);
836             }
837         });
838     }
839 
updateRoundedCornerSize( Point sizeDefault, Point sizeTop, Point sizeBottom)840     private void updateRoundedCornerSize(
841             Point sizeDefault,
842             Point sizeTop,
843             Point sizeBottom) {
844         if (mOverlays == null) {
845             return;
846         }
847         if (sizeTop.x == 0) {
848             sizeTop = sizeDefault;
849         }
850         if (sizeBottom.x == 0) {
851             sizeBottom = sizeDefault;
852         }
853 
854         for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
855             if (mOverlays[i] == null) {
856                 continue;
857             }
858             if (i == BOUNDS_POSITION_LEFT || i == BOUNDS_POSITION_RIGHT) {
859                 if (mRotation == ROTATION_270) {
860                     setSize(mOverlays[i].findViewById(R.id.left), sizeBottom);
861                     setSize(mOverlays[i].findViewById(R.id.right), sizeTop);
862                 } else {
863                     setSize(mOverlays[i].findViewById(R.id.left), sizeTop);
864                     setSize(mOverlays[i].findViewById(R.id.right), sizeBottom);
865                 }
866             } else if (i == BOUNDS_POSITION_TOP) {
867                 setSize(mOverlays[i].findViewById(R.id.left), sizeTop);
868                 setSize(mOverlays[i].findViewById(R.id.right), sizeTop);
869             } else if (i == BOUNDS_POSITION_BOTTOM) {
870                 setSize(mOverlays[i].findViewById(R.id.left), sizeBottom);
871                 setSize(mOverlays[i].findViewById(R.id.right), sizeBottom);
872             }
873         }
874     }
875 
876     @VisibleForTesting
setSize(View view, Point pixelSize)877     protected void setSize(View view, Point pixelSize) {
878         LayoutParams params = view.getLayoutParams();
879         params.width = pixelSize.x;
880         params.height = pixelSize.y;
881         view.setLayoutParams(params);
882     }
883 
884     public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener,
885             RegionInterceptableView {
886 
887         private static final float HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f;
888 
889         private Display.Mode mDisplayMode = null;
890         private final DisplayInfo mInfo = new DisplayInfo();
891         private final Paint mPaint = new Paint();
892         private final List<Rect> mBounds = new ArrayList();
893         private final Rect mBoundingRect = new Rect();
894         private final Path mBoundingPath = new Path();
895         // Don't initialize these yet because they may never exist
896         private RectF mProtectionRect;
897         private RectF mProtectionRectOrig;
898         private Path mProtectionPath;
899         private Path mProtectionPathOrig;
900         private Rect mTotalBounds = new Rect();
901         // Whether or not to show the cutout protection path
902         private boolean mShowProtection = false;
903 
904         private final int[] mLocation = new int[2];
905         private final ScreenDecorations mDecorations;
906         private int mColor = Color.BLACK;
907         private int mRotation;
908         private int mInitialPosition;
909         private int mPosition;
910         private float mCameraProtectionProgress = HIDDEN_CAMERA_PROTECTION_SCALE;
911         private ValueAnimator mCameraProtectionAnimator;
912 
DisplayCutoutView(Context context, @BoundsPosition int pos, ScreenDecorations decorations)913         public DisplayCutoutView(Context context, @BoundsPosition int pos,
914                 ScreenDecorations decorations) {
915             super(context);
916             mInitialPosition = pos;
917             mDecorations = decorations;
918             setId(R.id.display_cutout);
919             if (DEBUG) {
920                 getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG,
921                         getWindowTitleByPos(pos) + " drawn in rot " + mRotation));
922             }
923         }
924 
setColor(int color)925         public void setColor(int color) {
926             mColor = color;
927             invalidate();
928         }
929 
930         @Override
onAttachedToWindow()931         protected void onAttachedToWindow() {
932             super.onAttachedToWindow();
933             mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
934                     getHandler());
935             update();
936         }
937 
938         @Override
onDetachedFromWindow()939         protected void onDetachedFromWindow() {
940             super.onDetachedFromWindow();
941             mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
942         }
943 
944         @Override
onDraw(Canvas canvas)945         protected void onDraw(Canvas canvas) {
946             super.onDraw(canvas);
947             getLocationOnScreen(mLocation);
948             canvas.translate(-mLocation[0], -mLocation[1]);
949 
950             if (!mBoundingPath.isEmpty()) {
951                 mPaint.setColor(mColor);
952                 mPaint.setStyle(Paint.Style.FILL);
953                 mPaint.setAntiAlias(true);
954                 canvas.drawPath(mBoundingPath, mPaint);
955             }
956             if (mCameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE
957                     && !mProtectionRect.isEmpty()) {
958                 canvas.scale(mCameraProtectionProgress, mCameraProtectionProgress,
959                         mProtectionRect.centerX(), mProtectionRect.centerY());
960                 canvas.drawPath(mProtectionPath, mPaint);
961             }
962         }
963 
964         @Override
onDisplayAdded(int displayId)965         public void onDisplayAdded(int displayId) {
966         }
967 
968         @Override
onDisplayRemoved(int displayId)969         public void onDisplayRemoved(int displayId) {
970         }
971 
972         @Override
onDisplayChanged(int displayId)973         public void onDisplayChanged(int displayId) {
974             Display.Mode oldMode = mDisplayMode;
975             mDisplayMode = getDisplay().getMode();
976 
977             // Display mode hasn't meaningfully changed, we can ignore it
978             if (!modeChanged(oldMode, mDisplayMode)) {
979                 return;
980             }
981 
982             if (displayId == getDisplay().getDisplayId()) {
983                 update();
984             }
985         }
986 
modeChanged(Display.Mode oldMode, Display.Mode newMode)987         private boolean modeChanged(Display.Mode oldMode, Display.Mode newMode) {
988             if (oldMode == null) {
989                 return true;
990             }
991 
992             boolean changed = false;
993             changed |= oldMode.getPhysicalHeight() != newMode.getPhysicalHeight();
994             changed |= oldMode.getPhysicalWidth() != newMode.getPhysicalWidth();
995             // We purposely ignore refresh rate and id changes here, because we don't need to
996             // invalidate for those, and they can trigger the refresh rate to increase
997 
998             return changed;
999         }
1000 
setRotation(int rotation)1001         public void setRotation(int rotation) {
1002             mRotation = rotation;
1003             update();
1004         }
1005 
setProtection(Path protectionPath, Rect pathBounds)1006         void setProtection(Path protectionPath, Rect pathBounds) {
1007             if (mProtectionPathOrig == null) {
1008                 mProtectionPathOrig = new Path();
1009                 mProtectionPath = new Path();
1010             }
1011             mProtectionPathOrig.set(protectionPath);
1012             if (mProtectionRectOrig == null) {
1013                 mProtectionRectOrig = new RectF();
1014                 mProtectionRect = new RectF();
1015             }
1016             mProtectionRectOrig.set(pathBounds);
1017         }
1018 
setShowProtection(boolean shouldShow)1019         void setShowProtection(boolean shouldShow) {
1020             if (mShowProtection == shouldShow) {
1021                 return;
1022             }
1023 
1024             mShowProtection = shouldShow;
1025             updateBoundingPath();
1026             // Delay the relayout until the end of the animation when hiding the cutout,
1027             // otherwise we'd clip it.
1028             if (mShowProtection) {
1029                 requestLayout();
1030             }
1031             if (mCameraProtectionAnimator != null) {
1032                 mCameraProtectionAnimator.cancel();
1033             }
1034             mCameraProtectionAnimator = ValueAnimator.ofFloat(mCameraProtectionProgress,
1035                     mShowProtection ? 1.0f : HIDDEN_CAMERA_PROTECTION_SCALE).setDuration(750);
1036             mCameraProtectionAnimator.setInterpolator(Interpolators.DECELERATE_QUINT);
1037             mCameraProtectionAnimator.addUpdateListener(animation -> {
1038                 mCameraProtectionProgress = (float) animation.getAnimatedValue();
1039                 invalidate();
1040             });
1041             mCameraProtectionAnimator.addListener(new AnimatorListenerAdapter() {
1042                 @Override
1043                 public void onAnimationEnd(Animator animation) {
1044                     mCameraProtectionAnimator = null;
1045                     if (!mShowProtection) {
1046                         requestLayout();
1047                     }
1048                 }
1049             });
1050             mCameraProtectionAnimator.start();
1051         }
1052 
update()1053         private void update() {
1054             if (!isAttachedToWindow() || mDecorations.mPendingRotationChange) {
1055                 return;
1056             }
1057             mPosition = getBoundPositionFromRotation(mInitialPosition, mRotation);
1058             requestLayout();
1059             getDisplay().getDisplayInfo(mInfo);
1060             mBounds.clear();
1061             mBoundingRect.setEmpty();
1062             mBoundingPath.reset();
1063             int newVisible;
1064             if (shouldDrawCutout(getContext()) && hasCutout()) {
1065                 mBounds.addAll(mInfo.displayCutout.getBoundingRects());
1066                 localBounds(mBoundingRect);
1067                 updateGravity();
1068                 updateBoundingPath();
1069                 invalidate();
1070                 newVisible = VISIBLE;
1071             } else {
1072                 newVisible = GONE;
1073             }
1074             if (newVisible != getVisibility()) {
1075                 setVisibility(newVisible);
1076             }
1077         }
1078 
updateBoundingPath()1079         private void updateBoundingPath() {
1080             int lw = mInfo.logicalWidth;
1081             int lh = mInfo.logicalHeight;
1082 
1083             boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270;
1084 
1085             int dw = flipped ? lh : lw;
1086             int dh = flipped ? lw : lh;
1087 
1088             Path path = DisplayCutout.pathFromResources(getResources(), dw, dh);
1089             if (path != null) {
1090                 mBoundingPath.set(path);
1091             } else {
1092                 mBoundingPath.reset();
1093             }
1094             Matrix m = new Matrix();
1095             transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
1096             mBoundingPath.transform(m);
1097             if (mProtectionPathOrig != null) {
1098                 // Reset the protection path so we don't aggregate rotations
1099                 mProtectionPath.set(mProtectionPathOrig);
1100                 mProtectionPath.transform(m);
1101                 m.mapRect(mProtectionRect, mProtectionRectOrig);
1102             }
1103         }
1104 
transformPhysicalToLogicalCoordinates(@urface.Rotation int rotation, @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out)1105         private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation,
1106                 @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
1107             switch (rotation) {
1108                 case ROTATION_0:
1109                     out.reset();
1110                     break;
1111                 case ROTATION_90:
1112                     out.setRotate(270);
1113                     out.postTranslate(0, physicalWidth);
1114                     break;
1115                 case ROTATION_180:
1116                     out.setRotate(180);
1117                     out.postTranslate(physicalWidth, physicalHeight);
1118                     break;
1119                 case ROTATION_270:
1120                     out.setRotate(90);
1121                     out.postTranslate(physicalHeight, 0);
1122                     break;
1123                 default:
1124                     throw new IllegalArgumentException("Unknown rotation: " + rotation);
1125             }
1126         }
1127 
updateGravity()1128         private void updateGravity() {
1129             LayoutParams lp = getLayoutParams();
1130             if (lp instanceof FrameLayout.LayoutParams) {
1131                 FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) lp;
1132                 int newGravity = getGravity(mInfo.displayCutout);
1133                 if (flp.gravity != newGravity) {
1134                     flp.gravity = newGravity;
1135                     setLayoutParams(flp);
1136                 }
1137             }
1138         }
1139 
hasCutout()1140         private boolean hasCutout() {
1141             final DisplayCutout displayCutout = mInfo.displayCutout;
1142             if (displayCutout == null) {
1143                 return false;
1144             }
1145 
1146             if (mPosition == BOUNDS_POSITION_LEFT) {
1147                 return !displayCutout.getBoundingRectLeft().isEmpty();
1148             } else if (mPosition == BOUNDS_POSITION_TOP) {
1149                 return !displayCutout.getBoundingRectTop().isEmpty();
1150             } else if (mPosition == BOUNDS_POSITION_BOTTOM) {
1151                 return !displayCutout.getBoundingRectBottom().isEmpty();
1152             } else if (mPosition == BOUNDS_POSITION_RIGHT) {
1153                 return !displayCutout.getBoundingRectRight().isEmpty();
1154             }
1155             return false;
1156         }
1157 
1158         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)1159         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1160             if (mBounds.isEmpty()) {
1161                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1162                 return;
1163             }
1164 
1165             if (mShowProtection) {
1166                 // Make sure that our measured height encompases the protection
1167                 mTotalBounds.union(mBoundingRect);
1168                 mTotalBounds.union((int) mProtectionRect.left, (int) mProtectionRect.top,
1169                         (int) mProtectionRect.right, (int) mProtectionRect.bottom);
1170                 setMeasuredDimension(
1171                         resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
1172                         resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0));
1173             } else {
1174                 setMeasuredDimension(
1175                         resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
1176                         resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
1177             }
1178         }
1179 
boundsFromDirection(DisplayCutout displayCutout, int gravity, Rect out)1180         public static void boundsFromDirection(DisplayCutout displayCutout, int gravity,
1181                 Rect out) {
1182             switch (gravity) {
1183                 case Gravity.TOP:
1184                     out.set(displayCutout.getBoundingRectTop());
1185                     break;
1186                 case Gravity.LEFT:
1187                     out.set(displayCutout.getBoundingRectLeft());
1188                     break;
1189                 case Gravity.BOTTOM:
1190                     out.set(displayCutout.getBoundingRectBottom());
1191                     break;
1192                 case Gravity.RIGHT:
1193                     out.set(displayCutout.getBoundingRectRight());
1194                     break;
1195                 default:
1196                     out.setEmpty();
1197             }
1198         }
1199 
localBounds(Rect out)1200         private void localBounds(Rect out) {
1201             DisplayCutout displayCutout = mInfo.displayCutout;
1202             boundsFromDirection(displayCutout, getGravity(displayCutout), out);
1203         }
1204 
getGravity(DisplayCutout displayCutout)1205         private int getGravity(DisplayCutout displayCutout) {
1206             if (mPosition == BOUNDS_POSITION_LEFT) {
1207                 if (!displayCutout.getBoundingRectLeft().isEmpty()) {
1208                     return Gravity.LEFT;
1209                 }
1210             } else if (mPosition == BOUNDS_POSITION_TOP) {
1211                 if (!displayCutout.getBoundingRectTop().isEmpty()) {
1212                     return Gravity.TOP;
1213                 }
1214             } else if (mPosition == BOUNDS_POSITION_BOTTOM) {
1215                 if (!displayCutout.getBoundingRectBottom().isEmpty()) {
1216                     return Gravity.BOTTOM;
1217                 }
1218             } else if (mPosition == BOUNDS_POSITION_RIGHT) {
1219                 if (!displayCutout.getBoundingRectRight().isEmpty()) {
1220                     return Gravity.RIGHT;
1221                 }
1222             }
1223             return Gravity.NO_GRAVITY;
1224         }
1225 
1226         @Override
shouldInterceptTouch()1227         public boolean shouldInterceptTouch() {
1228             return mInfo.displayCutout != null && getVisibility() == VISIBLE;
1229         }
1230 
1231         @Override
getInterceptRegion()1232         public Region getInterceptRegion() {
1233             if (mInfo.displayCutout == null) {
1234                 return null;
1235             }
1236 
1237             View rootView = getRootView();
1238             Region cutoutBounds = rectsToRegion(
1239                     mInfo.displayCutout.getBoundingRects());
1240 
1241             // Transform to window's coordinate space
1242             rootView.getLocationOnScreen(mLocation);
1243             cutoutBounds.translate(-mLocation[0], -mLocation[1]);
1244 
1245             // Intersect with window's frame
1246             cutoutBounds.op(rootView.getLeft(), rootView.getTop(), rootView.getRight(),
1247                     rootView.getBottom(), Region.Op.INTERSECT);
1248 
1249             return cutoutBounds;
1250         }
1251     }
1252 
1253     /**
1254      * A pre-draw listener, that cancels the draw and restarts the traversal with the updated
1255      * window attributes.
1256      */
1257     private class RestartingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
1258 
1259         private final View mView;
1260         private final int mTargetRotation;
1261         private final int mPosition;
1262 
RestartingPreDrawListener(View view, @BoundsPosition int position, int targetRotation)1263         private RestartingPreDrawListener(View view, @BoundsPosition int position,
1264                 int targetRotation) {
1265             mView = view;
1266             mTargetRotation = targetRotation;
1267             mPosition = position;
1268         }
1269 
1270         @Override
onPreDraw()1271         public boolean onPreDraw() {
1272             mView.getViewTreeObserver().removeOnPreDrawListener(this);
1273 
1274             if (mTargetRotation == mRotation) {
1275                 if (DEBUG) {
1276                     Log.i(TAG, getWindowTitleByPos(mPosition) + " already in target rot "
1277                             + mTargetRotation + ", allow draw without restarting it");
1278                 }
1279                 return true;
1280             }
1281 
1282             mPendingRotationChange = false;
1283             // This changes the window attributes - we need to restart the traversal for them to
1284             // take effect.
1285             updateOrientation();
1286             if (DEBUG) {
1287                 Log.i(TAG, getWindowTitleByPos(mPosition)
1288                         + " restarting listener fired, restarting draw for rot " + mRotation);
1289             }
1290             mView.invalidate();
1291             return false;
1292         }
1293     }
1294 
1295     /**
1296      * A pre-draw listener, that validates that the rotation we draw in matches the displays
1297      * rotation before continuing the draw.
1298      *
1299      * This is to prevent a race condition, where we have not received the display changed event
1300      * yet, and would thus draw in an old orientation.
1301      */
1302     private class ValidatingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
1303 
1304         private final View mView;
1305 
ValidatingPreDrawListener(View view)1306         public ValidatingPreDrawListener(View view) {
1307             mView = view;
1308         }
1309 
1310         @Override
onPreDraw()1311         public boolean onPreDraw() {
1312             final int displayRotation = mContext.getDisplay().getRotation();
1313             if (displayRotation != mRotation && !mPendingRotationChange) {
1314                 if (DEBUG) {
1315                     Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
1316                             + displayRotation + ". Restarting draw");
1317                 }
1318                 mView.invalidate();
1319                 return false;
1320             }
1321             return true;
1322         }
1323     }
1324 }
1325