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