blob: a0fa69e61f20304e972ff14261328a79c3518d7d [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
17import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
18import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
19import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
20
21import static com.android.systemui.tuner.TunablePadding.FLAG_START;
22import static com.android.systemui.tuner.TunablePadding.FLAG_END;
23
24import android.app.Fragment;
25import android.content.Context;
26import android.content.res.ColorStateList;
27import android.content.res.Configuration;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Paint;
31import android.graphics.Path;
32import android.graphics.PixelFormat;
33import android.graphics.Rect;
Adrian Roos6a4fa0e2018-03-05 19:50:16 +010034import android.graphics.Region;
Adrian Roos5b518852018-01-23 17:23:38 +010035import android.hardware.display.DisplayManager;
Adrian Roos56d1a2c2018-03-08 23:22:19 +010036import android.os.SystemProperties;
Adrian Roos5b518852018-01-23 17:23:38 +010037import android.provider.Settings.Secure;
38import android.support.annotation.VisibleForTesting;
39import android.util.DisplayMetrics;
40import android.view.DisplayCutout;
41import android.view.DisplayInfo;
42import android.view.Gravity;
43import android.view.LayoutInflater;
44import android.view.View;
45import android.view.View.OnLayoutChangeListener;
46import android.view.ViewGroup;
47import android.view.ViewGroup.LayoutParams;
48import android.view.WindowManager;
49import android.widget.FrameLayout;
50import android.widget.ImageView;
51
Evan Lairdb0506ca2018-03-15 10:34:31 -040052import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView;
Adrian Roos5b518852018-01-23 17:23:38 +010053import com.android.systemui.fragments.FragmentHostManager;
54import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
55import com.android.systemui.plugins.qs.QS;
56import com.android.systemui.qs.SecureSetting;
57import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
58import com.android.systemui.statusbar.phone.StatusBar;
59import com.android.systemui.tuner.TunablePadding;
60import com.android.systemui.tuner.TunerService;
61import com.android.systemui.tuner.TunerService.Tunable;
62
63/**
64 * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
65 * for antialiasing and emulation purposes.
66 */
67public class ScreenDecorations extends SystemUI implements Tunable {
68 public static final String SIZE = "sysui_rounded_size";
69 public static final String PADDING = "sysui_rounded_content_padding";
Adrian Roos56d1a2c2018-03-08 23:22:19 +010070 private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
71 SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
Adrian Roos5b518852018-01-23 17:23:38 +010072
73 private int mRoundedDefault;
74 private View mOverlay;
75 private View mBottomOverlay;
76 private float mDensity;
77 private WindowManager mWindowManager;
78 private boolean mLandscape;
79
80 @Override
81 public void start() {
82 mWindowManager = mContext.getSystemService(WindowManager.class);
83 mRoundedDefault = mContext.getResources().getDimensionPixelSize(
84 R.dimen.rounded_corner_radius);
85 if (mRoundedDefault != 0 || shouldDrawCutout()) {
86 setupDecorations();
87 }
88 int padding = mContext.getResources().getDimensionPixelSize(
89 R.dimen.rounded_corner_content_padding);
90 if (padding != 0) {
91 setupPadding(padding);
92 }
93 }
94
95 private void setupDecorations() {
96 mOverlay = LayoutInflater.from(mContext)
97 .inflate(R.layout.rounded_corners, null);
98 ((ViewGroup)mOverlay).addView(new DisplayCutoutView(mContext, true,
99 this::updateWindowVisibilities));
100 mBottomOverlay = LayoutInflater.from(mContext)
101 .inflate(R.layout.rounded_corners, null);
102 ((ViewGroup)mBottomOverlay).addView(new DisplayCutoutView(mContext, false,
103 this::updateWindowVisibilities));
104
105 mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
106 mOverlay.setAlpha(0);
107
108 mBottomOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
109 mBottomOverlay.setAlpha(0);
110
111 updateViews();
112
113 mWindowManager.addView(mOverlay, getWindowLayoutParams());
114 mWindowManager.addView(mBottomOverlay, getBottomLayoutParams());
115
116 DisplayMetrics metrics = new DisplayMetrics();
117 mWindowManager.getDefaultDisplay().getMetrics(metrics);
118 mDensity = metrics.density;
119
120 Dependency.get(TunerService.class).addTunable(this, SIZE);
121
122 // Watch color inversion and invert the overlay as needed.
123 SecureSetting setting = new SecureSetting(mContext, Dependency.get(Dependency.MAIN_HANDLER),
124 Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
125 @Override
126 protected void handleValueChanged(int value, boolean observedChange) {
127 int tint = value != 0 ? Color.WHITE : Color.BLACK;
128 ColorStateList tintList = ColorStateList.valueOf(tint);
129 ((ImageView) mOverlay.findViewById(R.id.left)).setImageTintList(tintList);
130 ((ImageView) mOverlay.findViewById(R.id.right)).setImageTintList(tintList);
131 ((ImageView) mBottomOverlay.findViewById(R.id.left)).setImageTintList(tintList);
132 ((ImageView) mBottomOverlay.findViewById(R.id.right)).setImageTintList(tintList);
133 }
134 };
135 setting.setListening(true);
136 setting.onChange(false);
137
138 mOverlay.addOnLayoutChangeListener(new OnLayoutChangeListener() {
139 @Override
140 public void onLayoutChange(View v, int left, int top, int right, int bottom,
141 int oldLeft,
142 int oldTop, int oldRight, int oldBottom) {
143 mOverlay.removeOnLayoutChangeListener(this);
144 mOverlay.animate()
145 .alpha(1)
146 .setDuration(1000)
147 .start();
148 mBottomOverlay.animate()
149 .alpha(1)
150 .setDuration(1000)
151 .start();
152 }
153 });
154 }
155
156 @Override
157 protected void onConfigurationChanged(Configuration newConfig) {
158 boolean newLanscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
159 if (newLanscape != mLandscape) {
160 mLandscape = newLanscape;
161
162 if (mOverlay != null) {
163 updateLayoutParams();
164 updateViews();
165 }
166 }
167 if (shouldDrawCutout() && mOverlay == null) {
168 setupDecorations();
169 }
170 }
171
172 private void updateViews() {
173 View topLeft = mOverlay.findViewById(R.id.left);
174 View topRight = mOverlay.findViewById(R.id.right);
175 View bottomLeft = mBottomOverlay.findViewById(R.id.left);
176 View bottomRight = mBottomOverlay.findViewById(R.id.right);
177 if (mLandscape) {
178 // Flip corners
179 View tmp = topRight;
180 topRight = bottomLeft;
181 bottomLeft = tmp;
182 }
183 updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
184 updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90);
185 updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
186 updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
187
188 updateWindowVisibilities();
189 }
190
191 private void updateView(View v, int gravity, int rotation) {
192 ((FrameLayout.LayoutParams)v.getLayoutParams()).gravity = gravity;
193 v.setRotation(rotation);
194 }
195
196 private void updateWindowVisibilities() {
197 updateWindowVisibility(mOverlay);
198 updateWindowVisibility(mBottomOverlay);
199 }
200
201 private void updateWindowVisibility(View overlay) {
202 boolean visibleForCutout = shouldDrawCutout()
203 && overlay.findViewById(R.id.display_cutout).getVisibility() == View.VISIBLE;
204 boolean visibleForRoundedCorners = mRoundedDefault > 0;
205 overlay.setVisibility(visibleForCutout || visibleForRoundedCorners
206 ? View.VISIBLE : View.GONE);
207 }
208
209 private boolean shouldDrawCutout() {
210 return mContext.getResources().getBoolean(
211 com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
212 }
213
214 private void setupPadding(int padding) {
215 // Add some padding to all the content near the edge of the screen.
216 StatusBar sb = getComponent(StatusBar.class);
217 View statusBar = (sb != null ? sb.getStatusBarWindow() : null);
218 if (statusBar != null) {
219 TunablePadding.addTunablePadding(statusBar.findViewById(R.id.keyguard_header), PADDING,
220 padding, FLAG_END);
221
222 FragmentHostManager fragmentHostManager = FragmentHostManager.get(statusBar);
223 fragmentHostManager.addTagListener(CollapsedStatusBarFragment.TAG,
224 new TunablePaddingTagListener(padding, R.id.status_bar));
225 fragmentHostManager.addTagListener(QS.TAG,
226 new TunablePaddingTagListener(padding, R.id.header));
227 }
228 }
229
230 @VisibleForTesting
231 WindowManager.LayoutParams getWindowLayoutParams() {
232 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
233 ViewGroup.LayoutParams.MATCH_PARENT,
234 LayoutParams.WRAP_CONTENT,
235 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
Evan Lairdb0506ca2018-03-15 10:34:31 -0400236 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
Adrian Roos5b518852018-01-23 17:23:38 +0100237 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
238 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
239 | WindowManager.LayoutParams.FLAG_SLIPPERY
240 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
241 PixelFormat.TRANSLUCENT);
Robert Carr772e8bc2018-03-14 11:51:23 -0700242 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
243 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
244
Adrian Roos56d1a2c2018-03-08 23:22:19 +0100245 if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS) {
246 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
247 }
Robert Carr772e8bc2018-03-14 11:51:23 -0700248
Adrian Roos5b518852018-01-23 17:23:38 +0100249 lp.setTitle("ScreenDecorOverlay");
250 lp.gravity = Gravity.TOP | Gravity.LEFT;
251 lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
252 if (mLandscape) {
253 lp.width = WRAP_CONTENT;
254 lp.height = MATCH_PARENT;
255 }
256 return lp;
257 }
258
259 private WindowManager.LayoutParams getBottomLayoutParams() {
260 WindowManager.LayoutParams lp = getWindowLayoutParams();
261 lp.setTitle("ScreenDecorOverlayBottom");
262 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
263 return lp;
264 }
265
266 private void updateLayoutParams() {
267 mWindowManager.updateViewLayout(mOverlay, getWindowLayoutParams());
268 mWindowManager.updateViewLayout(mBottomOverlay, getBottomLayoutParams());
269 }
270
271 @Override
272 public void onTuningChanged(String key, String newValue) {
273 if (mOverlay == null) return;
274 if (SIZE.equals(key)) {
275 int size = mRoundedDefault;
276 try {
277 size = (int) (Integer.parseInt(newValue) * mDensity);
278 } catch (Exception e) {
279 }
280 setSize(mOverlay.findViewById(R.id.left), size);
281 setSize(mOverlay.findViewById(R.id.right), size);
282 setSize(mBottomOverlay.findViewById(R.id.left), size);
283 setSize(mBottomOverlay.findViewById(R.id.right), size);
284 }
285 }
286
287 private void setSize(View view, int pixelSize) {
288 LayoutParams params = view.getLayoutParams();
289 params.width = pixelSize;
290 params.height = pixelSize;
291 view.setLayoutParams(params);
292 }
293
294 @VisibleForTesting
295 static class TunablePaddingTagListener implements FragmentListener {
296
297 private final int mPadding;
298 private final int mId;
299 private TunablePadding mTunablePadding;
300
301 public TunablePaddingTagListener(int padding, int id) {
302 mPadding = padding;
303 mId = id;
304 }
305
306 @Override
307 public void onFragmentViewCreated(String tag, Fragment fragment) {
308 if (mTunablePadding != null) {
309 mTunablePadding.destroy();
310 }
311 View view = fragment.getView();
312 if (mId != 0) {
313 view = view.findViewById(mId);
314 }
315 mTunablePadding = TunablePadding.addTunablePadding(view, PADDING, mPadding,
316 FLAG_START | FLAG_END);
317 }
318 }
319
Evan Lairdb0506ca2018-03-15 10:34:31 -0400320 public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener,
321 RegionInterceptableView {
Adrian Roos5b518852018-01-23 17:23:38 +0100322
323 private final DisplayInfo mInfo = new DisplayInfo();
324 private final Paint mPaint = new Paint();
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100325 private final Region mBounds = new Region();
Adrian Roos5b518852018-01-23 17:23:38 +0100326 private final Rect mBoundingRect = new Rect();
327 private final Path mBoundingPath = new Path();
328 private final int[] mLocation = new int[2];
329 private final boolean mStart;
330 private final Runnable mVisibilityChangedListener;
331
332 public DisplayCutoutView(Context context, boolean start,
333 Runnable visibilityChangedListener) {
334 super(context);
335 mStart = start;
336 mVisibilityChangedListener = visibilityChangedListener;
337 setId(R.id.display_cutout);
338 }
339
340 @Override
341 protected void onAttachedToWindow() {
342 super.onAttachedToWindow();
343 mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
344 getHandler());
345 update();
346 }
347
348 @Override
349 protected void onDetachedFromWindow() {
350 super.onDetachedFromWindow();
351 mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
352 }
353
354 @Override
355 protected void onDraw(Canvas canvas) {
356 super.onDraw(canvas);
357 getLocationOnScreen(mLocation);
358 canvas.translate(-mLocation[0], -mLocation[1]);
359 if (!mBoundingPath.isEmpty()) {
360 mPaint.setColor(Color.BLACK);
361 mPaint.setStyle(Paint.Style.FILL);
362 canvas.drawPath(mBoundingPath, mPaint);
363 }
364 }
365
366 @Override
367 public void onDisplayAdded(int displayId) {
368 }
369
370 @Override
371 public void onDisplayRemoved(int displayId) {
372 }
373
374 @Override
375 public void onDisplayChanged(int displayId) {
376 if (displayId == getDisplay().getDisplayId()) {
377 update();
378 }
379 }
380
381 private void update() {
382 requestLayout();
383 getDisplay().getDisplayInfo(mInfo);
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100384 mBounds.setEmpty();
Adrian Roos5b518852018-01-23 17:23:38 +0100385 mBoundingRect.setEmpty();
386 mBoundingPath.reset();
387 int newVisible;
388 if (hasCutout()) {
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100389 mBounds.set(mInfo.displayCutout.getBounds());
390 localBounds(mBoundingRect);
Adrian Roos5b518852018-01-23 17:23:38 +0100391 mInfo.displayCutout.getBounds().getBoundaryPath(mBoundingPath);
Adrian Roos1b028282018-03-14 14:43:03 +0100392 invalidate();
Adrian Roos5b518852018-01-23 17:23:38 +0100393 newVisible = VISIBLE;
394 } else {
395 newVisible = GONE;
396 }
397 if (newVisible != getVisibility()) {
398 setVisibility(newVisible);
399 mVisibilityChangedListener.run();
400 }
401 }
402
403 private boolean hasCutout() {
Adrian Roos24264212018-02-19 16:26:15 +0100404 final DisplayCutout displayCutout = mInfo.displayCutout;
405 if (displayCutout == null) {
Adrian Roos5b518852018-01-23 17:23:38 +0100406 return false;
407 }
Adrian Roos5b518852018-01-23 17:23:38 +0100408 if (mStart) {
409 return displayCutout.getSafeInsetLeft() > 0
410 || displayCutout.getSafeInsetTop() > 0;
411 } else {
412 return displayCutout.getSafeInsetRight() > 0
413 || displayCutout.getSafeInsetBottom() > 0;
414 }
415 }
416
417 @Override
418 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100419 if (mBounds.isEmpty()) {
Adrian Roos5b518852018-01-23 17:23:38 +0100420 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
421 return;
422 }
423 setMeasuredDimension(
424 resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
425 resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
426 }
Adrian Roos6a4fa0e2018-03-05 19:50:16 +0100427
428 public static void boundsFromDirection(DisplayCutout displayCutout, int gravity, Rect out) {
429 Region bounds = displayCutout.getBounds();
430 switch (gravity) {
431 case Gravity.TOP:
432 bounds.op(0, 0, Integer.MAX_VALUE, displayCutout.getSafeInsetTop(),
433 Region.Op.INTERSECT);
434 out.set(bounds.getBounds());
435 break;
436 case Gravity.LEFT:
437 bounds.op(0, 0, displayCutout.getSafeInsetLeft(), Integer.MAX_VALUE,
438 Region.Op.INTERSECT);
439 out.set(bounds.getBounds());
440 break;
441 case Gravity.BOTTOM:
442 bounds.op(0, displayCutout.getSafeInsetTop() + 1, Integer.MAX_VALUE,
443 Integer.MAX_VALUE, Region.Op.INTERSECT);
444 out.set(bounds.getBounds());
445 break;
446 case Gravity.RIGHT:
447 bounds.op(displayCutout.getSafeInsetLeft() + 1, 0, Integer.MAX_VALUE,
448 Integer.MAX_VALUE, Region.Op.INTERSECT);
449 out.set(bounds.getBounds());
450 break;
451 }
452 bounds.recycle();
453 }
454
455 private void localBounds(Rect out) {
456 final DisplayCutout displayCutout = mInfo.displayCutout;
457
458 if (mStart) {
459 if (displayCutout.getSafeInsetLeft() > 0) {
460 boundsFromDirection(displayCutout, Gravity.LEFT, out);
461 } else if (displayCutout.getSafeInsetTop() > 0) {
462 boundsFromDirection(displayCutout, Gravity.TOP, out);
463 }
464 } else {
465 if (displayCutout.getSafeInsetRight() > 0) {
466 boundsFromDirection(displayCutout, Gravity.RIGHT, out);
467 } else if (displayCutout.getSafeInsetBottom() > 0) {
468 boundsFromDirection(displayCutout, Gravity.BOTTOM, out);
469 }
470 }
471 }
Evan Lairdb0506ca2018-03-15 10:34:31 -0400472
473 @Override
474 public boolean shouldInterceptTouch() {
475 return mInfo.displayCutout != null && getVisibility() == VISIBLE;
476 }
477
478 @Override
479 public Region getInterceptRegion() {
480 if (mInfo.displayCutout == null) {
481 return null;
482 }
483
484 return mInfo.displayCutout.getBounds();
485 }
Adrian Roos5b518852018-01-23 17:23:38 +0100486 }
487}