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