blob: 16b54b4cea5bdcd22499fc81e334f5ad1997c3b7 [file] [log] [blame]
Adrian Roos5b518852018-01-23 17:23:38 +01001/*
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
15package com.android.systemui;
16
Adrian Roos51072a82018-04-10 15:17:08 -070017import static android.view.Surface.ROTATION_0;
18import static android.view.Surface.ROTATION_180;
19import static android.view.Surface.ROTATION_270;
20import static android.view.Surface.ROTATION_90;
Adrian Roos5b518852018-01-23 17:23:38 +010021import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
22import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
23import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
24
Adrian Roos5b518852018-01-23 17:23:38 +010025import static com.android.systemui.tuner.TunablePadding.FLAG_END;
Beverlye91f0d02018-05-15 14:40:47 -040026import static com.android.systemui.tuner.TunablePadding.FLAG_START;
Adrian Roos5b518852018-01-23 17:23:38 +010027
Adrian Roos51072a82018-04-10 15:17:08 -070028import android.annotation.Dimension;
Adrian Roos5b518852018-01-23 17:23:38 +010029import android.app.Fragment;
30import android.content.Context;
31import android.content.res.ColorStateList;
32import android.content.res.Configuration;
33import android.graphics.Canvas;
34import android.graphics.Color;
Adrian Roos51072a82018-04-10 15:17:08 -070035import android.graphics.Matrix;
Adrian Roos5b518852018-01-23 17:23:38 +010036import android.graphics.Paint;
37import android.graphics.Path;
38import android.graphics.PixelFormat;
39import android.graphics.Rect;
Adrian Roos6a4fa0e2018-03-05 19:50:16 +010040import android.graphics.Region;
Adrian Roos5b518852018-01-23 17:23:38 +010041import android.hardware.display.DisplayManager;
Adrian Roos73ab97c2018-08-02 15:56:15 +020042import android.os.Handler;
43import android.os.HandlerThread;
Adrian Roos56d1a2c2018-03-08 23:22:19 +010044import android.os.SystemProperties;
Adrian Roos5b518852018-01-23 17:23:38 +010045import android.provider.Settings.Secure;
Aurimas Liutikasa14377a2018-04-17 09:50:46 -070046import androidx.annotation.VisibleForTesting;
Adrian Roos5b518852018-01-23 17:23:38 +010047import android.util.DisplayMetrics;
Adrian Roos73ab97c2018-08-02 15:56:15 +020048import android.util.Log;
Adrian Roos5b518852018-01-23 17:23:38 +010049import android.view.DisplayCutout;
50import android.view.DisplayInfo;
51import android.view.Gravity;
52import android.view.LayoutInflater;
Adrian Roos51072a82018-04-10 15:17:08 -070053import android.view.Surface;
Adrian Roos5b518852018-01-23 17:23:38 +010054import android.view.View;
55import android.view.View.OnLayoutChangeListener;
56import android.view.ViewGroup;
57import android.view.ViewGroup.LayoutParams;
Adrian Roos28c25e22018-05-31 18:07:28 +020058import android.view.ViewTreeObserver;
Adrian Roos5b518852018-01-23 17:23:38 +010059import android.view.WindowManager;
60import android.widget.FrameLayout;
61import android.widget.ImageView;
62
Adrian Roos73ab97c2018-08-02 15:56:15 +020063import com.android.internal.util.Preconditions;
Evan Lairdb0506ca2018-03-15 10:34:31 -040064import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView;
Adrian Roos5b518852018-01-23 17:23:38 +010065import com.android.systemui.fragments.FragmentHostManager;
66import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
67import com.android.systemui.plugins.qs.QS;
68import com.android.systemui.qs.SecureSetting;
69import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
70import com.android.systemui.statusbar.phone.StatusBar;
71import com.android.systemui.tuner.TunablePadding;
72import com.android.systemui.tuner.TunerService;
73import com.android.systemui.tuner.TunerService.Tunable;
Beverlye91f0d02018-05-15 14:40:47 -040074import com.android.systemui.util.leak.RotationUtils;
Adrian Roos5b518852018-01-23 17:23:38 +010075
76/**
77 * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
78 * for antialiasing and emulation purposes.
79 */
80public class ScreenDecorations extends SystemUI implements Tunable {
Adrian Roos73ab97c2018-08-02 15:56:15 +020081 private static final boolean DEBUG = false;
82 private static final String TAG = "ScreenDecorations";
83
Adrian Roos5b518852018-01-23 17:23:38 +010084 public static final String SIZE = "sysui_rounded_size";
85 public static final String PADDING = "sysui_rounded_content_padding";
Adrian Roos56d1a2c2018-03-08 23:22:19 +010086 private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
87 SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
Adrian Roos5b518852018-01-23 17:23:38 +010088
Beverlye91f0d02018-05-15 14:40:47 -040089 private DisplayManager mDisplayManager;
90 private DisplayManager.DisplayListener mDisplayListener;
91
Adrian Roos64c9d902018-08-20 13:43:38 +020092 @VisibleForTesting protected int mRoundedDefault;
93 @VisibleForTesting protected int mRoundedDefaultTop;
94 @VisibleForTesting protected int mRoundedDefaultBottom;
Adrian Roos5b518852018-01-23 17:23:38 +010095 private View mOverlay;
96 private View mBottomOverlay;
97 private float mDensity;
98 private WindowManager mWindowManager;
Beverlye91f0d02018-05-15 14:40:47 -040099 private int mRotation;
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200100 private DisplayCutoutView mCutoutTop;
101 private DisplayCutoutView mCutoutBottom;
Adrian Roos28c25e22018-05-31 18:07:28 +0200102 private boolean mPendingRotationChange;
Adrian Roos73ab97c2018-08-02 15:56:15 +0200103 private Handler mHandler;
Adrian Roos5b518852018-01-23 17:23:38 +0100104
105 @Override
106 public void start() {
Adrian Roos73ab97c2018-08-02 15:56:15 +0200107 mHandler = startHandlerThread();
108 mHandler.post(this::startOnScreenDecorationsThread);
109 setupStatusBarPaddingIfNeeded();
110 }
111
112 @VisibleForTesting
113 Handler startHandlerThread() {
114 HandlerThread thread = new HandlerThread("ScreenDecorations");
115 thread.start();
116 return thread.getThreadHandler();
117 }
118
119 private void startOnScreenDecorationsThread() {
120 mRotation = RotationUtils.getExactRotation(mContext);
Adrian Roos5b518852018-01-23 17:23:38 +0100121 mWindowManager = mContext.getSystemService(WindowManager.class);
Adrian Roos64c9d902018-08-20 13:43:38 +0200122 updateRoundedCornerRadii();
Beverlya5f7a302018-04-25 09:19:05 -0400123 if (hasRoundedCorners() || shouldDrawCutout()) {
Adrian Roos5b518852018-01-23 17:23:38 +0100124 setupDecorations();
125 }
Beverlya5f7a302018-04-25 09:19:05 -0400126
Beverlye91f0d02018-05-15 14:40:47 -0400127 mDisplayListener = new DisplayManager.DisplayListener() {
128 @Override
129 public void onDisplayAdded(int displayId) {
130 // do nothing
131 }
132
133 @Override
134 public void onDisplayRemoved(int displayId) {
135 // do nothing
136 }
137
138 @Override
139 public void onDisplayChanged(int displayId) {
Adrian Roos73ab97c2018-08-02 15:56:15 +0200140 final int newRotation = RotationUtils.getExactRotation(mContext);
141 if (mOverlay != null && mBottomOverlay != null && mRotation != newRotation) {
Adrian Roos28c25e22018-05-31 18:07:28 +0200142 // We cannot immediately update the orientation. Otherwise
143 // WindowManager is still deferring layout until it has finished dispatching
144 // the config changes, which may cause divergence between what we draw
145 // (new orientation), and where we are placed on the screen (old orientation).
146 // Instead we wait until either:
147 // - we are trying to redraw. This because WM resized our window and told us to.
148 // - the config change has been dispatched, so WM is no longer deferring layout.
149 mPendingRotationChange = true;
Adrian Roos73ab97c2018-08-02 15:56:15 +0200150 if (DEBUG) {
151 Log.i(TAG, "Rotation changed, deferring " + newRotation + ", staying at "
152 + mRotation);
153 }
Adrian Roos28c25e22018-05-31 18:07:28 +0200154
Adrian Roos73ab97c2018-08-02 15:56:15 +0200155 mOverlay.getViewTreeObserver().addOnPreDrawListener(
156 new RestartingPreDrawListener(mOverlay, newRotation));
157 mBottomOverlay.getViewTreeObserver().addOnPreDrawListener(
158 new RestartingPreDrawListener(mBottomOverlay, newRotation));
Adrian Roos28c25e22018-05-31 18:07:28 +0200159 }
Beverlye91f0d02018-05-15 14:40:47 -0400160 updateOrientation();
161 }
162 };
163
Beverlye91f0d02018-05-15 14:40:47 -0400164 mDisplayManager = (DisplayManager) mContext.getSystemService(
165 Context.DISPLAY_SERVICE);
Adrian Roos73ab97c2018-08-02 15:56:15 +0200166 mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
167 updateOrientation();
Adrian Roos5b518852018-01-23 17:23:38 +0100168 }
169
170 private void setupDecorations() {
171 mOverlay = LayoutInflater.from(mContext)
172 .inflate(R.layout.rounded_corners, null);
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200173 mCutoutTop = new DisplayCutoutView(mContext, true,
Adrian Roos28c25e22018-05-31 18:07:28 +0200174 this::updateWindowVisibilities, this);
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200175 ((ViewGroup)mOverlay).addView(mCutoutTop);
Adrian Roos5b518852018-01-23 17:23:38 +0100176 mBottomOverlay = LayoutInflater.from(mContext)
177 .inflate(R.layout.rounded_corners, null);
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200178 mCutoutBottom = new DisplayCutoutView(mContext, false,
Adrian Roos28c25e22018-05-31 18:07:28 +0200179 this::updateWindowVisibilities, this);
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200180 ((ViewGroup)mBottomOverlay).addView(mCutoutBottom);
Adrian Roos5b518852018-01-23 17:23:38 +0100181
182 mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
183 mOverlay.setAlpha(0);
184
185 mBottomOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
186 mBottomOverlay.setAlpha(0);
187
188 updateViews();
189
190 mWindowManager.addView(mOverlay, getWindowLayoutParams());
191 mWindowManager.addView(mBottomOverlay, getBottomLayoutParams());
192
193 DisplayMetrics metrics = new DisplayMetrics();
194 mWindowManager.getDefaultDisplay().getMetrics(metrics);
195 mDensity = metrics.density;
196
Adrian Roos73ab97c2018-08-02 15:56:15 +0200197 Dependency.get(Dependency.MAIN_HANDLER).post(
198 () -> Dependency.get(TunerService.class).addTunable(this, SIZE));
Adrian Roos5b518852018-01-23 17:23:38 +0100199
200 // Watch color inversion and invert the overlay as needed.
Adrian Roos73ab97c2018-08-02 15:56:15 +0200201 SecureSetting setting = new SecureSetting(mContext, mHandler,
Adrian Roos5b518852018-01-23 17:23:38 +0100202 Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
203 @Override
204 protected void handleValueChanged(int value, boolean observedChange) {
205 int tint = value != 0 ? Color.WHITE : Color.BLACK;
206 ColorStateList tintList = ColorStateList.valueOf(tint);
207 ((ImageView) mOverlay.findViewById(R.id.left)).setImageTintList(tintList);
208 ((ImageView) mOverlay.findViewById(R.id.right)).setImageTintList(tintList);
209 ((ImageView) mBottomOverlay.findViewById(R.id.left)).setImageTintList(tintList);
210 ((ImageView) mBottomOverlay.findViewById(R.id.right)).setImageTintList(tintList);
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200211 mCutoutTop.setColor(tint);
212 mCutoutBottom.setColor(tint);
Adrian Roos5b518852018-01-23 17:23:38 +0100213 }
214 };
215 setting.setListening(true);
216 setting.onChange(false);
217
218 mOverlay.addOnLayoutChangeListener(new OnLayoutChangeListener() {
219 @Override
220 public void onLayoutChange(View v, int left, int top, int right, int bottom,
221 int oldLeft,
222 int oldTop, int oldRight, int oldBottom) {
223 mOverlay.removeOnLayoutChangeListener(this);
224 mOverlay.animate()
225 .alpha(1)
226 .setDuration(1000)
227 .start();
228 mBottomOverlay.animate()
229 .alpha(1)
230 .setDuration(1000)
231 .start();
232 }
233 });
Adrian Roos73ab97c2018-08-02 15:56:15 +0200234
235 mOverlay.getViewTreeObserver().addOnPreDrawListener(
236 new ValidatingPreDrawListener(mOverlay));
237 mBottomOverlay.getViewTreeObserver().addOnPreDrawListener(
238 new ValidatingPreDrawListener(mBottomOverlay));
Adrian Roos5b518852018-01-23 17:23:38 +0100239 }
240
241 @Override
242 protected void onConfigurationChanged(Configuration newConfig) {
Adrian Roos73ab97c2018-08-02 15:56:15 +0200243 mHandler.post(() -> {
244 int oldRotation = mRotation;
245 mPendingRotationChange = false;
246 updateOrientation();
Adrian Roos64c9d902018-08-20 13:43:38 +0200247 updateRoundedCornerRadii();
Adrian Roos73ab97c2018-08-02 15:56:15 +0200248 if (DEBUG) Log.i(TAG, "onConfigChanged from rot " + oldRotation + " to " + mRotation);
249 if (shouldDrawCutout() && mOverlay == null) {
250 setupDecorations();
251 }
252 if (mOverlay != null) {
253 // Updating the layout params ensures that ViewRootImpl will call relayoutWindow(),
254 // which ensures that the forced seamless rotation will end, even if we updated
255 // the rotation before window manager was ready (and was still waiting for sending
256 // the updated rotation).
257 updateLayoutParams();
258 }
259 });
Beverlye91f0d02018-05-15 14:40:47 -0400260 }
261
Adrian Roos73ab97c2018-08-02 15:56:15 +0200262 private void updateOrientation() {
263 Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(),
264 "must call on " + mHandler.getLooper().getThread()
265 + ", but was " + Thread.currentThread());
Adrian Roos28c25e22018-05-31 18:07:28 +0200266 if (mPendingRotationChange) {
267 return;
268 }
Beverlye91f0d02018-05-15 14:40:47 -0400269 int newRotation = RotationUtils.getExactRotation(mContext);
270 if (newRotation != mRotation) {
271 mRotation = newRotation;
Adrian Roos5b518852018-01-23 17:23:38 +0100272
273 if (mOverlay != null) {
274 updateLayoutParams();
275 updateViews();
276 }
Adrian Roos5b518852018-01-23 17:23:38 +0100277 }
278 }
279
Adrian Roos64c9d902018-08-20 13:43:38 +0200280 private void updateRoundedCornerRadii() {
281 final int newRoundedDefault = mContext.getResources().getDimensionPixelSize(
282 R.dimen.rounded_corner_radius);
283 final int newRoundedDefaultTop = mContext.getResources().getDimensionPixelSize(
284 R.dimen.rounded_corner_radius_top);
285 final int newRoundedDefaultBottom = mContext.getResources().getDimensionPixelSize(
286 R.dimen.rounded_corner_radius_bottom);
287
288 final boolean roundedCornersChanged = mRoundedDefault != newRoundedDefault
289 || mRoundedDefaultBottom != newRoundedDefaultBottom
290 || mRoundedDefaultTop != newRoundedDefaultTop;
291
292 if (roundedCornersChanged) {
293 mRoundedDefault = newRoundedDefault;
294 mRoundedDefaultTop = newRoundedDefaultTop;
295 mRoundedDefaultBottom = newRoundedDefaultBottom;
296 onTuningChanged(SIZE, null);
297 }
298 }
299
Adrian Roos5b518852018-01-23 17:23:38 +0100300 private void updateViews() {
301 View topLeft = mOverlay.findViewById(R.id.left);
302 View topRight = mOverlay.findViewById(R.id.right);
303 View bottomLeft = mBottomOverlay.findViewById(R.id.left);
304 View bottomRight = mBottomOverlay.findViewById(R.id.right);
Beverlye91f0d02018-05-15 14:40:47 -0400305
306 if (mRotation == RotationUtils.ROTATION_NONE) {
307 updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
308 updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90);
309 updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
310 updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
311 } else if (mRotation == RotationUtils.ROTATION_LANDSCAPE) {
312 updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
313 updateView(topRight, Gravity.BOTTOM | Gravity.LEFT, 270);
314 updateView(bottomLeft, Gravity.TOP | Gravity.RIGHT, 90);;
315 updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
316 } else if (mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) {
317 updateView(topLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
318 updateView(topRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
319 updateView(bottomLeft, Gravity.TOP | Gravity.LEFT, 0);
320 updateView(bottomRight, Gravity.TOP | Gravity.RIGHT, 90);
321 } else if (mRotation == RotationUtils.ROTATION_SEASCAPE) {
322 updateView(topLeft, Gravity.BOTTOM | Gravity.RIGHT, 180);
323 updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90);
324 updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
325 updateView(bottomRight, Gravity.TOP | Gravity.LEFT, 0);
Adrian Roos5b518852018-01-23 17:23:38 +0100326 }
Adrian Roos5b518852018-01-23 17:23:38 +0100327
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200328 mCutoutTop.setRotation(mRotation);
329 mCutoutBottom.setRotation(mRotation);
330
Adrian Roos5b518852018-01-23 17:23:38 +0100331 updateWindowVisibilities();
332 }
333
334 private void updateView(View v, int gravity, int rotation) {
335 ((FrameLayout.LayoutParams)v.getLayoutParams()).gravity = gravity;
336 v.setRotation(rotation);
337 }
338
339 private void updateWindowVisibilities() {
340 updateWindowVisibility(mOverlay);
341 updateWindowVisibility(mBottomOverlay);
342 }
343
344 private void updateWindowVisibility(View overlay) {
345 boolean visibleForCutout = shouldDrawCutout()
346 && overlay.findViewById(R.id.display_cutout).getVisibility() == View.VISIBLE;
Beverlya5f7a302018-04-25 09:19:05 -0400347 boolean visibleForRoundedCorners = hasRoundedCorners();
Adrian Roos5b518852018-01-23 17:23:38 +0100348 overlay.setVisibility(visibleForCutout || visibleForRoundedCorners
349 ? View.VISIBLE : View.GONE);
350 }
351
Beverlya5f7a302018-04-25 09:19:05 -0400352 private boolean hasRoundedCorners() {
353 return mRoundedDefault > 0 || mRoundedDefaultBottom > 0 || mRoundedDefaultTop > 0;
354 }
355
Adrian Roos5b518852018-01-23 17:23:38 +0100356 private boolean shouldDrawCutout() {
Adrian Roosc41b32b2018-04-12 10:13:48 +0200357 return shouldDrawCutout(mContext);
358 }
359
360 static boolean shouldDrawCutout(Context context) {
361 return context.getResources().getBoolean(
Adrian Roos5b518852018-01-23 17:23:38 +0100362 com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
363 }
364
Adrian Roos73ab97c2018-08-02 15:56:15 +0200365
366 private void setupStatusBarPaddingIfNeeded() {
367 // TODO: This should be moved to a more appropriate place, as it is not related to the
368 // screen decorations overlay.
369 int padding = mContext.getResources().getDimensionPixelSize(
370 R.dimen.rounded_corner_content_padding);
371 if (padding != 0) {
372 setupStatusBarPadding(padding);
373 }
374
375 }
376
377 private void setupStatusBarPadding(int padding) {
Adrian Roos5b518852018-01-23 17:23:38 +0100378 // Add some padding to all the content near the edge of the screen.
379 StatusBar sb = getComponent(StatusBar.class);
380 View statusBar = (sb != null ? sb.getStatusBarWindow() : null);
381 if (statusBar != null) {
382 TunablePadding.addTunablePadding(statusBar.findViewById(R.id.keyguard_header), PADDING,
383 padding, FLAG_END);
384
385 FragmentHostManager fragmentHostManager = FragmentHostManager.get(statusBar);
386 fragmentHostManager.addTagListener(CollapsedStatusBarFragment.TAG,
387 new TunablePaddingTagListener(padding, R.id.status_bar));
388 fragmentHostManager.addTagListener(QS.TAG,
389 new TunablePaddingTagListener(padding, R.id.header));
390 }
391 }
392
393 @VisibleForTesting
394 WindowManager.LayoutParams getWindowLayoutParams() {
395 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
396 ViewGroup.LayoutParams.MATCH_PARENT,
397 LayoutParams.WRAP_CONTENT,
398 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
Evan Lairdb0506ca2018-03-15 10:34:31 -0400399 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
Adrian Roos5b518852018-01-23 17:23:38 +0100400 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
401 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
402 | WindowManager.LayoutParams.FLAG_SLIPPERY
403 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
404 PixelFormat.TRANSLUCENT);
Robert Carr772e8bc2018-03-14 11:51:23 -0700405 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
406 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
407
Adrian Roos56d1a2c2018-03-08 23:22:19 +0100408 if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS) {
409 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
410 }
Robert Carr772e8bc2018-03-14 11:51:23 -0700411
Adrian Roos5b518852018-01-23 17:23:38 +0100412 lp.setTitle("ScreenDecorOverlay");
Beverlye91f0d02018-05-15 14:40:47 -0400413 if (mRotation == RotationUtils.ROTATION_SEASCAPE
414 || mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) {
415 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
416 } else {
417 lp.gravity = Gravity.TOP | Gravity.LEFT;
418 }
Adrian Roos5b518852018-01-23 17:23:38 +0100419 lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
Beverlye91f0d02018-05-15 14:40:47 -0400420 if (isLandscape(mRotation)) {
Adrian Roos5b518852018-01-23 17:23:38 +0100421 lp.width = WRAP_CONTENT;
422 lp.height = MATCH_PARENT;
423 }
424 return lp;
425 }
426
427 private WindowManager.LayoutParams getBottomLayoutParams() {
428 WindowManager.LayoutParams lp = getWindowLayoutParams();
429 lp.setTitle("ScreenDecorOverlayBottom");
Beverlye91f0d02018-05-15 14:40:47 -0400430 if (mRotation == RotationUtils.ROTATION_SEASCAPE
431 || mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) {
432 lp.gravity = Gravity.TOP | Gravity.LEFT;
433 } else {
434 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
435 }
Adrian Roos5b518852018-01-23 17:23:38 +0100436 return lp;
437 }
438
439 private void updateLayoutParams() {
440 mWindowManager.updateViewLayout(mOverlay, getWindowLayoutParams());
441 mWindowManager.updateViewLayout(mBottomOverlay, getBottomLayoutParams());
442 }
443
444 @Override
445 public void onTuningChanged(String key, String newValue) {
Adrian Roos73ab97c2018-08-02 15:56:15 +0200446 mHandler.post(() -> {
447 if (mOverlay == null) return;
448 if (SIZE.equals(key)) {
449 int size = mRoundedDefault;
450 int sizeTop = mRoundedDefaultTop;
451 int sizeBottom = mRoundedDefaultBottom;
452 if (newValue != null) {
453 try {
454 size = (int) (Integer.parseInt(newValue) * mDensity);
455 } catch (Exception e) {
456 }
Beverlya5f7a302018-04-25 09:19:05 -0400457 }
Beverlya5f7a302018-04-25 09:19:05 -0400458
Adrian Roos73ab97c2018-08-02 15:56:15 +0200459 if (sizeTop == 0) {
460 sizeTop = size;
461 }
462 if (sizeBottom == 0) {
463 sizeBottom = size;
464 }
Beverlya5f7a302018-04-25 09:19:05 -0400465
Adrian Roos73ab97c2018-08-02 15:56:15 +0200466 setSize(mOverlay.findViewById(R.id.left), sizeTop);
467 setSize(mOverlay.findViewById(R.id.right), sizeTop);
468 setSize(mBottomOverlay.findViewById(R.id.left), sizeBottom);
469 setSize(mBottomOverlay.findViewById(R.id.right), sizeBottom);
470 }
471 });
Adrian Roos5b518852018-01-23 17:23:38 +0100472 }
473
474 private void setSize(View view, int pixelSize) {
475 LayoutParams params = view.getLayoutParams();
476 params.width = pixelSize;
477 params.height = pixelSize;
478 view.setLayoutParams(params);
479 }
480
481 @VisibleForTesting
482 static class TunablePaddingTagListener implements FragmentListener {
483
484 private final int mPadding;
485 private final int mId;
486 private TunablePadding mTunablePadding;
487
488 public TunablePaddingTagListener(int padding, int id) {
489 mPadding = padding;
490 mId = id;
491 }
492
493 @Override
494 public void onFragmentViewCreated(String tag, Fragment fragment) {
495 if (mTunablePadding != null) {
496 mTunablePadding.destroy();
497 }
498 View view = fragment.getView();
499 if (mId != 0) {
500 view = view.findViewById(mId);
501 }
502 mTunablePadding = TunablePadding.addTunablePadding(view, PADDING, mPadding,
503 FLAG_START | FLAG_END);
504 }
505 }
506
Evan Lairdb0506ca2018-03-15 10:34:31 -0400507 public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener,
508 RegionInterceptableView {
Adrian Roos5b518852018-01-23 17:23:38 +0100509
510 private final DisplayInfo mInfo = new DisplayInfo();
511 private final Paint mPaint = new Paint();
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100512 private final Region mBounds = new Region();
Adrian Roos5b518852018-01-23 17:23:38 +0100513 private final Rect mBoundingRect = new Rect();
514 private final Path mBoundingPath = new Path();
515 private final int[] mLocation = new int[2];
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200516 private final boolean mInitialStart;
Adrian Roos5b518852018-01-23 17:23:38 +0100517 private final Runnable mVisibilityChangedListener;
Adrian Roos28c25e22018-05-31 18:07:28 +0200518 private final ScreenDecorations mDecorations;
Adrian Roos8b29a842018-05-31 14:14:13 +0200519 private int mColor = Color.BLACK;
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200520 private boolean mStart;
521 private int mRotation;
Adrian Roos5b518852018-01-23 17:23:38 +0100522
523 public DisplayCutoutView(Context context, boolean start,
Adrian Roos28c25e22018-05-31 18:07:28 +0200524 Runnable visibilityChangedListener, ScreenDecorations decorations) {
Adrian Roos5b518852018-01-23 17:23:38 +0100525 super(context);
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200526 mInitialStart = start;
Adrian Roos5b518852018-01-23 17:23:38 +0100527 mVisibilityChangedListener = visibilityChangedListener;
Adrian Roos28c25e22018-05-31 18:07:28 +0200528 mDecorations = decorations;
Adrian Roos5b518852018-01-23 17:23:38 +0100529 setId(R.id.display_cutout);
Adrian Roos73ab97c2018-08-02 15:56:15 +0200530 if (DEBUG) {
531 getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG,
532 (mInitialStart ? "OverlayTop" : "OverlayBottom")
533 + " drawn in rot " + mRotation));
534 }
Adrian Roos5b518852018-01-23 17:23:38 +0100535 }
536
Adrian Roos8b29a842018-05-31 14:14:13 +0200537 public void setColor(int color) {
538 mColor = color;
539 invalidate();
540 }
541
Adrian Roos5b518852018-01-23 17:23:38 +0100542 @Override
543 protected void onAttachedToWindow() {
544 super.onAttachedToWindow();
545 mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
546 getHandler());
547 update();
548 }
549
550 @Override
551 protected void onDetachedFromWindow() {
552 super.onDetachedFromWindow();
553 mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
554 }
555
556 @Override
557 protected void onDraw(Canvas canvas) {
558 super.onDraw(canvas);
559 getLocationOnScreen(mLocation);
560 canvas.translate(-mLocation[0], -mLocation[1]);
561 if (!mBoundingPath.isEmpty()) {
Adrian Roos8b29a842018-05-31 14:14:13 +0200562 mPaint.setColor(mColor);
Adrian Roos5b518852018-01-23 17:23:38 +0100563 mPaint.setStyle(Paint.Style.FILL);
Adrian Roos51072a82018-04-10 15:17:08 -0700564 mPaint.setAntiAlias(true);
Adrian Roos5b518852018-01-23 17:23:38 +0100565 canvas.drawPath(mBoundingPath, mPaint);
566 }
567 }
568
569 @Override
570 public void onDisplayAdded(int displayId) {
571 }
572
573 @Override
574 public void onDisplayRemoved(int displayId) {
575 }
576
577 @Override
578 public void onDisplayChanged(int displayId) {
579 if (displayId == getDisplay().getDisplayId()) {
580 update();
581 }
582 }
583
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200584 public void setRotation(int rotation) {
585 mRotation = rotation;
586 update();
587 }
588
589 private boolean isStart() {
590 final boolean flipped = (mRotation == RotationUtils.ROTATION_SEASCAPE
591 || mRotation == RotationUtils.ROTATION_UPSIDE_DOWN);
592 return flipped ? !mInitialStart : mInitialStart;
593 }
594
Adrian Roos5b518852018-01-23 17:23:38 +0100595 private void update() {
Adrian Roos28c25e22018-05-31 18:07:28 +0200596 if (!isAttachedToWindow() || mDecorations.mPendingRotationChange) {
Adrian Roosbeeb7f42018-05-31 19:54:53 +0200597 return;
598 }
Adrian Roos28c25e22018-05-31 18:07:28 +0200599 mStart = isStart();
Adrian Roos5b518852018-01-23 17:23:38 +0100600 requestLayout();
601 getDisplay().getDisplayInfo(mInfo);
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100602 mBounds.setEmpty();
Adrian Roos5b518852018-01-23 17:23:38 +0100603 mBoundingRect.setEmpty();
604 mBoundingPath.reset();
605 int newVisible;
Adrian Roosc41b32b2018-04-12 10:13:48 +0200606 if (shouldDrawCutout(getContext()) && hasCutout()) {
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100607 mBounds.set(mInfo.displayCutout.getBounds());
608 localBounds(mBoundingRect);
Adrian Roos51072a82018-04-10 15:17:08 -0700609 updateBoundingPath();
Adrian Roos1b028282018-03-14 14:43:03 +0100610 invalidate();
Adrian Roos5b518852018-01-23 17:23:38 +0100611 newVisible = VISIBLE;
612 } else {
613 newVisible = GONE;
614 }
615 if (newVisible != getVisibility()) {
616 setVisibility(newVisible);
617 mVisibilityChangedListener.run();
618 }
619 }
620
Adrian Roos51072a82018-04-10 15:17:08 -0700621 private void updateBoundingPath() {
622 int lw = mInfo.logicalWidth;
623 int lh = mInfo.logicalHeight;
624
625 boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270;
626
627 int dw = flipped ? lh : lw;
628 int dh = flipped ? lw : lh;
629
Adrian Roosa99f5d62018-04-16 16:03:04 +0200630 mBoundingPath.set(DisplayCutout.pathFromResources(getResources(), dw, dh));
Adrian Roos51072a82018-04-10 15:17:08 -0700631 Matrix m = new Matrix();
632 transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
633 mBoundingPath.transform(m);
634 }
635
636 private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation,
637 @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
638 switch (rotation) {
639 case ROTATION_0:
640 out.reset();
641 break;
642 case ROTATION_90:
643 out.setRotate(270);
644 out.postTranslate(0, physicalWidth);
645 break;
646 case ROTATION_180:
647 out.setRotate(180);
648 out.postTranslate(physicalWidth, physicalHeight);
649 break;
650 case ROTATION_270:
651 out.setRotate(90);
652 out.postTranslate(physicalHeight, 0);
653 break;
654 default:
655 throw new IllegalArgumentException("Unknown rotation: " + rotation);
656 }
657 }
658
Adrian Roos5b518852018-01-23 17:23:38 +0100659 private boolean hasCutout() {
Adrian Roos24264212018-02-19 16:26:15 +0100660 final DisplayCutout displayCutout = mInfo.displayCutout;
661 if (displayCutout == null) {
Adrian Roos5b518852018-01-23 17:23:38 +0100662 return false;
663 }
Adrian Roos5b518852018-01-23 17:23:38 +0100664 if (mStart) {
665 return displayCutout.getSafeInsetLeft() > 0
666 || displayCutout.getSafeInsetTop() > 0;
667 } else {
668 return displayCutout.getSafeInsetRight() > 0
669 || displayCutout.getSafeInsetBottom() > 0;
670 }
671 }
672
673 @Override
674 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100675 if (mBounds.isEmpty()) {
Adrian Roos5b518852018-01-23 17:23:38 +0100676 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
677 return;
678 }
679 setMeasuredDimension(
680 resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
681 resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
682 }
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100683
Jorim Jaggi0d4a9952018-06-06 17:22:56 +0200684 public static void boundsFromDirection(DisplayCutout displayCutout, int gravity,
685 Rect out) {
686 Region bounds = boundsFromDirection(displayCutout, gravity);
687 out.set(bounds.getBounds());
688 bounds.recycle();
689 }
690
691 public static Region boundsFromDirection(DisplayCutout displayCutout, int gravity) {
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100692 Region bounds = displayCutout.getBounds();
693 switch (gravity) {
694 case Gravity.TOP:
695 bounds.op(0, 0, Integer.MAX_VALUE, displayCutout.getSafeInsetTop(),
696 Region.Op.INTERSECT);
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100697 break;
698 case Gravity.LEFT:
699 bounds.op(0, 0, displayCutout.getSafeInsetLeft(), Integer.MAX_VALUE,
700 Region.Op.INTERSECT);
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100701 break;
702 case Gravity.BOTTOM:
703 bounds.op(0, displayCutout.getSafeInsetTop() + 1, Integer.MAX_VALUE,
704 Integer.MAX_VALUE, Region.Op.INTERSECT);
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100705 break;
706 case Gravity.RIGHT:
707 bounds.op(displayCutout.getSafeInsetLeft() + 1, 0, Integer.MAX_VALUE,
708 Integer.MAX_VALUE, Region.Op.INTERSECT);
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100709 break;
710 }
Jorim Jaggi0d4a9952018-06-06 17:22:56 +0200711 return bounds;
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100712 }
713
714 private void localBounds(Rect out) {
715 final DisplayCutout displayCutout = mInfo.displayCutout;
716
717 if (mStart) {
718 if (displayCutout.getSafeInsetLeft() > 0) {
719 boundsFromDirection(displayCutout, Gravity.LEFT, out);
720 } else if (displayCutout.getSafeInsetTop() > 0) {
721 boundsFromDirection(displayCutout, Gravity.TOP, out);
722 }
723 } else {
724 if (displayCutout.getSafeInsetRight() > 0) {
725 boundsFromDirection(displayCutout, Gravity.RIGHT, out);
726 } else if (displayCutout.getSafeInsetBottom() > 0) {
727 boundsFromDirection(displayCutout, Gravity.BOTTOM, out);
728 }
729 }
730 }
Evan Lairdb0506ca2018-03-15 10:34:31 -0400731
732 @Override
733 public boolean shouldInterceptTouch() {
734 return mInfo.displayCutout != null && getVisibility() == VISIBLE;
735 }
736
737 @Override
738 public Region getInterceptRegion() {
739 if (mInfo.displayCutout == null) {
740 return null;
741 }
742
Adrian Roos134e1cb2018-05-16 17:04:29 +0200743 View rootView = getRootView();
744 Region cutoutBounds = mInfo.displayCutout.getBounds();
745
746 // Transform to window's coordinate space
747 rootView.getLocationOnScreen(mLocation);
748 cutoutBounds.translate(-mLocation[0], -mLocation[1]);
749
750 // Intersect with window's frame
751 cutoutBounds.op(rootView.getLeft(), rootView.getTop(), rootView.getRight(),
752 rootView.getBottom(), Region.Op.INTERSECT);
753
754 return cutoutBounds;
Evan Lairdb0506ca2018-03-15 10:34:31 -0400755 }
Adrian Roos5b518852018-01-23 17:23:38 +0100756 }
Beverlye91f0d02018-05-15 14:40:47 -0400757
758 private boolean isLandscape(int rotation) {
759 return rotation == RotationUtils.ROTATION_LANDSCAPE || rotation ==
760 RotationUtils.ROTATION_SEASCAPE;
761 }
Adrian Roos28c25e22018-05-31 18:07:28 +0200762
763 /**
764 * A pre-draw listener, that cancels the draw and restarts the traversal with the updated
765 * window attributes.
766 */
767 private class RestartingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
768
769 private final View mView;
Adrian Roos73ab97c2018-08-02 15:56:15 +0200770 private final int mTargetRotation;
Adrian Roos28c25e22018-05-31 18:07:28 +0200771
Adrian Roos73ab97c2018-08-02 15:56:15 +0200772 private RestartingPreDrawListener(View view, int targetRotation) {
773 mView = view;
774 mTargetRotation = targetRotation;
775 }
776
777 @Override
778 public boolean onPreDraw() {
779 mView.getViewTreeObserver().removeOnPreDrawListener(this);
780
781 if (mTargetRotation == mRotation) {
782 if (DEBUG) {
783 Log.i(TAG, (mView == mOverlay ? "OverlayTop" : "OverlayBottom")
784 + " already in target rot "
785 + mTargetRotation + ", allow draw without restarting it");
786 }
787 return true;
788 }
789
790 mPendingRotationChange = false;
791 // This changes the window attributes - we need to restart the traversal for them to
792 // take effect.
793 updateOrientation();
794 if (DEBUG) {
795 Log.i(TAG, (mView == mOverlay ? "OverlayTop" : "OverlayBottom")
796 + " restarting listener fired, restarting draw for rot " + mRotation);
797 }
798 mView.invalidate();
799 return false;
800 }
801 }
802
803 /**
804 * A pre-draw listener, that validates that the rotation we draw in matches the displays
805 * rotation before continuing the draw.
806 *
807 * This is to prevent a race condition, where we have not received the display changed event
808 * yet, and would thus draw in an old orientation.
809 */
810 private class ValidatingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
811
812 private final View mView;
813
814 public ValidatingPreDrawListener(View view) {
Adrian Roos28c25e22018-05-31 18:07:28 +0200815 mView = view;
816 }
817
818 @Override
819 public boolean onPreDraw() {
Adrian Roos73ab97c2018-08-02 15:56:15 +0200820 final int displayRotation = RotationUtils.getExactRotation(mContext);
821 if (displayRotation != mRotation && !mPendingRotationChange) {
822 if (DEBUG) {
823 Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
824 + displayRotation + ". Restarting draw");
825 }
826 mView.invalidate();
827 return false;
828 }
829 return true;
Adrian Roos28c25e22018-05-31 18:07:28 +0200830 }
831 }
Adrian Roos5b518852018-01-23 17:23:38 +0100832}