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