blob: 7042d22497e67bc459a762982cde840dfd81e24b [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
25import static com.android.systemui.tuner.TunablePadding.FLAG_START;
26import static com.android.systemui.tuner.TunablePadding.FLAG_END;
27
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 Roos56d1a2c2018-03-08 23:22:19 +010042import android.os.SystemProperties;
Adrian Roos5b518852018-01-23 17:23:38 +010043import android.provider.Settings.Secure;
44import android.support.annotation.VisibleForTesting;
45import android.util.DisplayMetrics;
46import android.view.DisplayCutout;
47import android.view.DisplayInfo;
48import android.view.Gravity;
49import android.view.LayoutInflater;
Adrian Roos51072a82018-04-10 15:17:08 -070050import android.view.Surface;
Adrian Roos5b518852018-01-23 17:23:38 +010051import android.view.View;
52import android.view.View.OnLayoutChangeListener;
53import android.view.ViewGroup;
54import android.view.ViewGroup.LayoutParams;
55import android.view.WindowManager;
56import android.widget.FrameLayout;
57import android.widget.ImageView;
58
Evan Lairdb0506ca2018-03-15 10:34:31 -040059import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView;
Adrian Roos5b518852018-01-23 17:23:38 +010060import com.android.systemui.fragments.FragmentHostManager;
61import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
62import com.android.systemui.plugins.qs.QS;
63import com.android.systemui.qs.SecureSetting;
64import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
65import com.android.systemui.statusbar.phone.StatusBar;
66import com.android.systemui.tuner.TunablePadding;
67import com.android.systemui.tuner.TunerService;
68import com.android.systemui.tuner.TunerService.Tunable;
69
70/**
71 * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
72 * for antialiasing and emulation purposes.
73 */
74public class ScreenDecorations extends SystemUI implements Tunable {
75 public static final String SIZE = "sysui_rounded_size";
76 public static final String PADDING = "sysui_rounded_content_padding";
Adrian Roos56d1a2c2018-03-08 23:22:19 +010077 private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
78 SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
Adrian Roos5b518852018-01-23 17:23:38 +010079
80 private int mRoundedDefault;
81 private View mOverlay;
82 private View mBottomOverlay;
83 private float mDensity;
84 private WindowManager mWindowManager;
85 private boolean mLandscape;
86
87 @Override
88 public void start() {
89 mWindowManager = mContext.getSystemService(WindowManager.class);
90 mRoundedDefault = mContext.getResources().getDimensionPixelSize(
91 R.dimen.rounded_corner_radius);
92 if (mRoundedDefault != 0 || shouldDrawCutout()) {
93 setupDecorations();
94 }
95 int padding = mContext.getResources().getDimensionPixelSize(
96 R.dimen.rounded_corner_content_padding);
97 if (padding != 0) {
98 setupPadding(padding);
99 }
100 }
101
102 private void setupDecorations() {
103 mOverlay = LayoutInflater.from(mContext)
104 .inflate(R.layout.rounded_corners, null);
105 ((ViewGroup)mOverlay).addView(new DisplayCutoutView(mContext, true,
106 this::updateWindowVisibilities));
107 mBottomOverlay = LayoutInflater.from(mContext)
108 .inflate(R.layout.rounded_corners, null);
109 ((ViewGroup)mBottomOverlay).addView(new DisplayCutoutView(mContext, false,
110 this::updateWindowVisibilities));
111
112 mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
113 mOverlay.setAlpha(0);
114
115 mBottomOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
116 mBottomOverlay.setAlpha(0);
117
118 updateViews();
119
120 mWindowManager.addView(mOverlay, getWindowLayoutParams());
121 mWindowManager.addView(mBottomOverlay, getBottomLayoutParams());
122
123 DisplayMetrics metrics = new DisplayMetrics();
124 mWindowManager.getDefaultDisplay().getMetrics(metrics);
125 mDensity = metrics.density;
126
127 Dependency.get(TunerService.class).addTunable(this, SIZE);
128
129 // Watch color inversion and invert the overlay as needed.
130 SecureSetting setting = new SecureSetting(mContext, Dependency.get(Dependency.MAIN_HANDLER),
131 Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
132 @Override
133 protected void handleValueChanged(int value, boolean observedChange) {
134 int tint = value != 0 ? Color.WHITE : Color.BLACK;
135 ColorStateList tintList = ColorStateList.valueOf(tint);
136 ((ImageView) mOverlay.findViewById(R.id.left)).setImageTintList(tintList);
137 ((ImageView) mOverlay.findViewById(R.id.right)).setImageTintList(tintList);
138 ((ImageView) mBottomOverlay.findViewById(R.id.left)).setImageTintList(tintList);
139 ((ImageView) mBottomOverlay.findViewById(R.id.right)).setImageTintList(tintList);
140 }
141 };
142 setting.setListening(true);
143 setting.onChange(false);
144
145 mOverlay.addOnLayoutChangeListener(new OnLayoutChangeListener() {
146 @Override
147 public void onLayoutChange(View v, int left, int top, int right, int bottom,
148 int oldLeft,
149 int oldTop, int oldRight, int oldBottom) {
150 mOverlay.removeOnLayoutChangeListener(this);
151 mOverlay.animate()
152 .alpha(1)
153 .setDuration(1000)
154 .start();
155 mBottomOverlay.animate()
156 .alpha(1)
157 .setDuration(1000)
158 .start();
159 }
160 });
161 }
162
163 @Override
164 protected void onConfigurationChanged(Configuration newConfig) {
165 boolean newLanscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
166 if (newLanscape != mLandscape) {
167 mLandscape = newLanscape;
168
169 if (mOverlay != null) {
170 updateLayoutParams();
171 updateViews();
172 }
173 }
174 if (shouldDrawCutout() && mOverlay == null) {
175 setupDecorations();
176 }
177 }
178
179 private void updateViews() {
180 View topLeft = mOverlay.findViewById(R.id.left);
181 View topRight = mOverlay.findViewById(R.id.right);
182 View bottomLeft = mBottomOverlay.findViewById(R.id.left);
183 View bottomRight = mBottomOverlay.findViewById(R.id.right);
184 if (mLandscape) {
185 // Flip corners
186 View tmp = topRight;
187 topRight = bottomLeft;
188 bottomLeft = tmp;
189 }
190 updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
191 updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90);
192 updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
193 updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
194
195 updateWindowVisibilities();
196 }
197
198 private void updateView(View v, int gravity, int rotation) {
199 ((FrameLayout.LayoutParams)v.getLayoutParams()).gravity = gravity;
200 v.setRotation(rotation);
201 }
202
203 private void updateWindowVisibilities() {
204 updateWindowVisibility(mOverlay);
205 updateWindowVisibility(mBottomOverlay);
206 }
207
208 private void updateWindowVisibility(View overlay) {
209 boolean visibleForCutout = shouldDrawCutout()
210 && overlay.findViewById(R.id.display_cutout).getVisibility() == View.VISIBLE;
211 boolean visibleForRoundedCorners = mRoundedDefault > 0;
212 overlay.setVisibility(visibleForCutout || visibleForRoundedCorners
213 ? View.VISIBLE : View.GONE);
214 }
215
216 private boolean shouldDrawCutout() {
Adrian Roosc41b32b2018-04-12 10:13:48 +0200217 return shouldDrawCutout(mContext);
218 }
219
220 static boolean shouldDrawCutout(Context context) {
221 return context.getResources().getBoolean(
Adrian Roos5b518852018-01-23 17:23:38 +0100222 com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
223 }
224
225 private void setupPadding(int padding) {
226 // Add some padding to all the content near the edge of the screen.
227 StatusBar sb = getComponent(StatusBar.class);
228 View statusBar = (sb != null ? sb.getStatusBarWindow() : null);
229 if (statusBar != null) {
230 TunablePadding.addTunablePadding(statusBar.findViewById(R.id.keyguard_header), PADDING,
231 padding, FLAG_END);
232
233 FragmentHostManager fragmentHostManager = FragmentHostManager.get(statusBar);
234 fragmentHostManager.addTagListener(CollapsedStatusBarFragment.TAG,
235 new TunablePaddingTagListener(padding, R.id.status_bar));
236 fragmentHostManager.addTagListener(QS.TAG,
237 new TunablePaddingTagListener(padding, R.id.header));
238 }
239 }
240
241 @VisibleForTesting
242 WindowManager.LayoutParams getWindowLayoutParams() {
243 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
244 ViewGroup.LayoutParams.MATCH_PARENT,
245 LayoutParams.WRAP_CONTENT,
246 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
Evan Lairdb0506ca2018-03-15 10:34:31 -0400247 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
Adrian Roos5b518852018-01-23 17:23:38 +0100248 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
249 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
250 | WindowManager.LayoutParams.FLAG_SLIPPERY
251 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
252 PixelFormat.TRANSLUCENT);
Robert Carr772e8bc2018-03-14 11:51:23 -0700253 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
254 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
255
Adrian Roos56d1a2c2018-03-08 23:22:19 +0100256 if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS) {
257 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
258 }
Robert Carr772e8bc2018-03-14 11:51:23 -0700259
Adrian Roos5b518852018-01-23 17:23:38 +0100260 lp.setTitle("ScreenDecorOverlay");
261 lp.gravity = Gravity.TOP | Gravity.LEFT;
262 lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
263 if (mLandscape) {
264 lp.width = WRAP_CONTENT;
265 lp.height = MATCH_PARENT;
266 }
267 return lp;
268 }
269
270 private WindowManager.LayoutParams getBottomLayoutParams() {
271 WindowManager.LayoutParams lp = getWindowLayoutParams();
272 lp.setTitle("ScreenDecorOverlayBottom");
273 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
274 return lp;
275 }
276
277 private void updateLayoutParams() {
278 mWindowManager.updateViewLayout(mOverlay, getWindowLayoutParams());
279 mWindowManager.updateViewLayout(mBottomOverlay, getBottomLayoutParams());
280 }
281
282 @Override
283 public void onTuningChanged(String key, String newValue) {
284 if (mOverlay == null) return;
285 if (SIZE.equals(key)) {
286 int size = mRoundedDefault;
287 try {
288 size = (int) (Integer.parseInt(newValue) * mDensity);
289 } catch (Exception e) {
290 }
291 setSize(mOverlay.findViewById(R.id.left), size);
292 setSize(mOverlay.findViewById(R.id.right), size);
293 setSize(mBottomOverlay.findViewById(R.id.left), size);
294 setSize(mBottomOverlay.findViewById(R.id.right), size);
295 }
296 }
297
298 private void setSize(View view, int pixelSize) {
299 LayoutParams params = view.getLayoutParams();
300 params.width = pixelSize;
301 params.height = pixelSize;
302 view.setLayoutParams(params);
303 }
304
305 @VisibleForTesting
306 static class TunablePaddingTagListener implements FragmentListener {
307
308 private final int mPadding;
309 private final int mId;
310 private TunablePadding mTunablePadding;
311
312 public TunablePaddingTagListener(int padding, int id) {
313 mPadding = padding;
314 mId = id;
315 }
316
317 @Override
318 public void onFragmentViewCreated(String tag, Fragment fragment) {
319 if (mTunablePadding != null) {
320 mTunablePadding.destroy();
321 }
322 View view = fragment.getView();
323 if (mId != 0) {
324 view = view.findViewById(mId);
325 }
326 mTunablePadding = TunablePadding.addTunablePadding(view, PADDING, mPadding,
327 FLAG_START | FLAG_END);
328 }
329 }
330
Evan Lairdb0506ca2018-03-15 10:34:31 -0400331 public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener,
332 RegionInterceptableView {
Adrian Roos5b518852018-01-23 17:23:38 +0100333
334 private final DisplayInfo mInfo = new DisplayInfo();
335 private final Paint mPaint = new Paint();
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100336 private final Region mBounds = new Region();
Adrian Roos5b518852018-01-23 17:23:38 +0100337 private final Rect mBoundingRect = new Rect();
338 private final Path mBoundingPath = new Path();
339 private final int[] mLocation = new int[2];
340 private final boolean mStart;
341 private final Runnable mVisibilityChangedListener;
342
343 public DisplayCutoutView(Context context, boolean start,
344 Runnable visibilityChangedListener) {
345 super(context);
346 mStart = start;
347 mVisibilityChangedListener = visibilityChangedListener;
348 setId(R.id.display_cutout);
349 }
350
351 @Override
352 protected void onAttachedToWindow() {
353 super.onAttachedToWindow();
354 mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
355 getHandler());
356 update();
357 }
358
359 @Override
360 protected void onDetachedFromWindow() {
361 super.onDetachedFromWindow();
362 mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
363 }
364
365 @Override
366 protected void onDraw(Canvas canvas) {
367 super.onDraw(canvas);
368 getLocationOnScreen(mLocation);
369 canvas.translate(-mLocation[0], -mLocation[1]);
370 if (!mBoundingPath.isEmpty()) {
371 mPaint.setColor(Color.BLACK);
372 mPaint.setStyle(Paint.Style.FILL);
Adrian Roos51072a82018-04-10 15:17:08 -0700373 mPaint.setAntiAlias(true);
Adrian Roos5b518852018-01-23 17:23:38 +0100374 canvas.drawPath(mBoundingPath, mPaint);
375 }
376 }
377
378 @Override
379 public void onDisplayAdded(int displayId) {
380 }
381
382 @Override
383 public void onDisplayRemoved(int displayId) {
384 }
385
386 @Override
387 public void onDisplayChanged(int displayId) {
388 if (displayId == getDisplay().getDisplayId()) {
389 update();
390 }
391 }
392
393 private void update() {
394 requestLayout();
395 getDisplay().getDisplayInfo(mInfo);
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100396 mBounds.setEmpty();
Adrian Roos5b518852018-01-23 17:23:38 +0100397 mBoundingRect.setEmpty();
398 mBoundingPath.reset();
399 int newVisible;
Adrian Roosc41b32b2018-04-12 10:13:48 +0200400 if (shouldDrawCutout(getContext()) && hasCutout()) {
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100401 mBounds.set(mInfo.displayCutout.getBounds());
402 localBounds(mBoundingRect);
Adrian Roos51072a82018-04-10 15:17:08 -0700403 updateBoundingPath();
Adrian Roos1b028282018-03-14 14:43:03 +0100404 invalidate();
Adrian Roos5b518852018-01-23 17:23:38 +0100405 newVisible = VISIBLE;
406 } else {
407 newVisible = GONE;
408 }
409 if (newVisible != getVisibility()) {
410 setVisibility(newVisible);
411 mVisibilityChangedListener.run();
412 }
413 }
414
Adrian Roos51072a82018-04-10 15:17:08 -0700415 private void updateBoundingPath() {
416 int lw = mInfo.logicalWidth;
417 int lh = mInfo.logicalHeight;
418
419 boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270;
420
421 int dw = flipped ? lh : lw;
422 int dh = flipped ? lw : lh;
423
424 mBoundingPath.set(DisplayCutout.pathFromResources(getResources(), lw, lh));
425 Matrix m = new Matrix();
426 transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
427 mBoundingPath.transform(m);
428 }
429
430 private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation,
431 @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
432 switch (rotation) {
433 case ROTATION_0:
434 out.reset();
435 break;
436 case ROTATION_90:
437 out.setRotate(270);
438 out.postTranslate(0, physicalWidth);
439 break;
440 case ROTATION_180:
441 out.setRotate(180);
442 out.postTranslate(physicalWidth, physicalHeight);
443 break;
444 case ROTATION_270:
445 out.setRotate(90);
446 out.postTranslate(physicalHeight, 0);
447 break;
448 default:
449 throw new IllegalArgumentException("Unknown rotation: " + rotation);
450 }
451 }
452
Adrian Roos5b518852018-01-23 17:23:38 +0100453 private boolean hasCutout() {
Adrian Roos24264212018-02-19 16:26:15 +0100454 final DisplayCutout displayCutout = mInfo.displayCutout;
455 if (displayCutout == null) {
Adrian Roos5b518852018-01-23 17:23:38 +0100456 return false;
457 }
Adrian Roos5b518852018-01-23 17:23:38 +0100458 if (mStart) {
459 return displayCutout.getSafeInsetLeft() > 0
460 || displayCutout.getSafeInsetTop() > 0;
461 } else {
462 return displayCutout.getSafeInsetRight() > 0
463 || displayCutout.getSafeInsetBottom() > 0;
464 }
465 }
466
467 @Override
468 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100469 if (mBounds.isEmpty()) {
Adrian Roos5b518852018-01-23 17:23:38 +0100470 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
471 return;
472 }
473 setMeasuredDimension(
474 resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
475 resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
476 }
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100477
478 public static void boundsFromDirection(DisplayCutout displayCutout, int gravity, Rect out) {
479 Region bounds = displayCutout.getBounds();
480 switch (gravity) {
481 case Gravity.TOP:
482 bounds.op(0, 0, Integer.MAX_VALUE, displayCutout.getSafeInsetTop(),
483 Region.Op.INTERSECT);
484 out.set(bounds.getBounds());
485 break;
486 case Gravity.LEFT:
487 bounds.op(0, 0, displayCutout.getSafeInsetLeft(), Integer.MAX_VALUE,
488 Region.Op.INTERSECT);
489 out.set(bounds.getBounds());
490 break;
491 case Gravity.BOTTOM:
492 bounds.op(0, displayCutout.getSafeInsetTop() + 1, Integer.MAX_VALUE,
493 Integer.MAX_VALUE, Region.Op.INTERSECT);
494 out.set(bounds.getBounds());
495 break;
496 case Gravity.RIGHT:
497 bounds.op(displayCutout.getSafeInsetLeft() + 1, 0, Integer.MAX_VALUE,
498 Integer.MAX_VALUE, Region.Op.INTERSECT);
499 out.set(bounds.getBounds());
500 break;
501 }
502 bounds.recycle();
503 }
504
505 private void localBounds(Rect out) {
506 final DisplayCutout displayCutout = mInfo.displayCutout;
507
508 if (mStart) {
509 if (displayCutout.getSafeInsetLeft() > 0) {
510 boundsFromDirection(displayCutout, Gravity.LEFT, out);
511 } else if (displayCutout.getSafeInsetTop() > 0) {
512 boundsFromDirection(displayCutout, Gravity.TOP, out);
513 }
514 } else {
515 if (displayCutout.getSafeInsetRight() > 0) {
516 boundsFromDirection(displayCutout, Gravity.RIGHT, out);
517 } else if (displayCutout.getSafeInsetBottom() > 0) {
518 boundsFromDirection(displayCutout, Gravity.BOTTOM, out);
519 }
520 }
521 }
Evan Lairdb0506ca2018-03-15 10:34:31 -0400522
523 @Override
524 public boolean shouldInterceptTouch() {
525 return mInfo.displayCutout != null && getVisibility() == VISIBLE;
526 }
527
528 @Override
529 public Region getInterceptRegion() {
530 if (mInfo.displayCutout == null) {
531 return null;
532 }
533
534 return mInfo.displayCutout.getBounds();
535 }
Adrian Roos5b518852018-01-23 17:23:38 +0100536 }
537}