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