Merge emulated cutout into either rounded corner overlay

This so that we can save a layer, which avoids dropping us into
GL composition during animations.

This assumes that the cutout is always at the top or bottom edge
in the device's natural orientation.

Note that the two overlays for the top and bottom rounded corners
are still separate.

Bug: 72492508
Test: enable emulated cutout, verify it still shows up
Change-Id: I895084828e0502005bfa31e37d23dd3a6f01a2ca
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
new file mode 100644
index 0000000..0b3e9e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import static com.android.systemui.tuner.TunablePadding.FLAG_START;
+import static com.android.systemui.tuner.TunablePadding.FLAG_END;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.provider.Settings.Secure;
+import android.support.annotation.VisibleForTesting;
+import android.util.DisplayMetrics;
+import android.view.DisplayCutout;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.SecureSetting;
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.tuner.TunablePadding;
+import com.android.systemui.tuner.TunerService;
+import com.android.systemui.tuner.TunerService.Tunable;
+
+/**
+ * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
+ * for antialiasing and emulation purposes.
+ */
+public class ScreenDecorations extends SystemUI implements Tunable {
+    public static final String SIZE = "sysui_rounded_size";
+    public static final String PADDING = "sysui_rounded_content_padding";
+
+    private int mRoundedDefault;
+    private View mOverlay;
+    private View mBottomOverlay;
+    private float mDensity;
+    private WindowManager mWindowManager;
+    private boolean mLandscape;
+
+    @Override
+    public void start() {
+        mWindowManager = mContext.getSystemService(WindowManager.class);
+        mRoundedDefault = mContext.getResources().getDimensionPixelSize(
+                R.dimen.rounded_corner_radius);
+        if (mRoundedDefault != 0 || shouldDrawCutout()) {
+            setupDecorations();
+        }
+        int padding = mContext.getResources().getDimensionPixelSize(
+                R.dimen.rounded_corner_content_padding);
+        if (padding != 0) {
+            setupPadding(padding);
+        }
+    }
+
+    private void setupDecorations() {
+        mOverlay = LayoutInflater.from(mContext)
+                .inflate(R.layout.rounded_corners, null);
+        ((ViewGroup)mOverlay).addView(new DisplayCutoutView(mContext, true,
+                this::updateWindowVisibilities));
+        mBottomOverlay = LayoutInflater.from(mContext)
+                .inflate(R.layout.rounded_corners, null);
+        ((ViewGroup)mBottomOverlay).addView(new DisplayCutoutView(mContext, false,
+                this::updateWindowVisibilities));
+
+        mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        mOverlay.setAlpha(0);
+
+        mBottomOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        mBottomOverlay.setAlpha(0);
+
+        updateViews();
+
+        mWindowManager.addView(mOverlay, getWindowLayoutParams());
+        mWindowManager.addView(mBottomOverlay, getBottomLayoutParams());
+
+        DisplayMetrics metrics = new DisplayMetrics();
+        mWindowManager.getDefaultDisplay().getMetrics(metrics);
+        mDensity = metrics.density;
+
+        Dependency.get(TunerService.class).addTunable(this, SIZE);
+
+        // Watch color inversion and invert the overlay as needed.
+        SecureSetting setting = new SecureSetting(mContext, Dependency.get(Dependency.MAIN_HANDLER),
+                Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) {
+            @Override
+            protected void handleValueChanged(int value, boolean observedChange) {
+                int tint = value != 0 ? Color.WHITE : Color.BLACK;
+                ColorStateList tintList = ColorStateList.valueOf(tint);
+                ((ImageView) mOverlay.findViewById(R.id.left)).setImageTintList(tintList);
+                ((ImageView) mOverlay.findViewById(R.id.right)).setImageTintList(tintList);
+                ((ImageView) mBottomOverlay.findViewById(R.id.left)).setImageTintList(tintList);
+                ((ImageView) mBottomOverlay.findViewById(R.id.right)).setImageTintList(tintList);
+            }
+        };
+        setting.setListening(true);
+        setting.onChange(false);
+
+        mOverlay.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                    int oldLeft,
+                    int oldTop, int oldRight, int oldBottom) {
+                mOverlay.removeOnLayoutChangeListener(this);
+                mOverlay.animate()
+                        .alpha(1)
+                        .setDuration(1000)
+                        .start();
+                mBottomOverlay.animate()
+                        .alpha(1)
+                        .setDuration(1000)
+                        .start();
+            }
+        });
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        boolean newLanscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
+        if (newLanscape != mLandscape) {
+            mLandscape = newLanscape;
+
+            if (mOverlay != null) {
+                updateLayoutParams();
+                updateViews();
+            }
+        }
+        if (shouldDrawCutout() && mOverlay == null) {
+            setupDecorations();
+        }
+    }
+
+    private void updateViews() {
+        View topLeft = mOverlay.findViewById(R.id.left);
+        View topRight = mOverlay.findViewById(R.id.right);
+        View bottomLeft = mBottomOverlay.findViewById(R.id.left);
+        View bottomRight = mBottomOverlay.findViewById(R.id.right);
+        if (mLandscape) {
+            // Flip corners
+            View tmp = topRight;
+            topRight = bottomLeft;
+            bottomLeft = tmp;
+        }
+        updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
+        updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90);
+        updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
+        updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
+
+        updateWindowVisibilities();
+    }
+
+    private void updateView(View v, int gravity, int rotation) {
+        ((FrameLayout.LayoutParams)v.getLayoutParams()).gravity = gravity;
+        v.setRotation(rotation);
+    }
+
+    private void updateWindowVisibilities() {
+        updateWindowVisibility(mOverlay);
+        updateWindowVisibility(mBottomOverlay);
+    }
+
+    private void updateWindowVisibility(View overlay) {
+        boolean visibleForCutout = shouldDrawCutout()
+                && overlay.findViewById(R.id.display_cutout).getVisibility() == View.VISIBLE;
+        boolean visibleForRoundedCorners = mRoundedDefault > 0;
+        overlay.setVisibility(visibleForCutout || visibleForRoundedCorners
+                ? View.VISIBLE : View.GONE);
+    }
+
+    private boolean shouldDrawCutout() {
+        return mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
+    }
+
+    private void setupPadding(int padding) {
+        // Add some padding to all the content near the edge of the screen.
+        StatusBar sb = getComponent(StatusBar.class);
+        View statusBar = (sb != null ? sb.getStatusBarWindow() : null);
+        if (statusBar != null) {
+            TunablePadding.addTunablePadding(statusBar.findViewById(R.id.keyguard_header), PADDING,
+                    padding, FLAG_END);
+
+            FragmentHostManager fragmentHostManager = FragmentHostManager.get(statusBar);
+            fragmentHostManager.addTagListener(CollapsedStatusBarFragment.TAG,
+                    new TunablePaddingTagListener(padding, R.id.status_bar));
+            fragmentHostManager.addTagListener(QS.TAG,
+                    new TunablePaddingTagListener(padding, R.id.header));
+        }
+    }
+
+    @VisibleForTesting
+    WindowManager.LayoutParams getWindowLayoutParams() {
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                LayoutParams.WRAP_CONTENT,
+                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+                        | WindowManager.LayoutParams.FLAG_SLIPPERY
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+                PixelFormat.TRANSLUCENT);
+        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
+                | WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
+        lp.setTitle("ScreenDecorOverlay");
+        lp.gravity = Gravity.TOP | Gravity.LEFT;
+        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+        if (mLandscape) {
+            lp.width = WRAP_CONTENT;
+            lp.height = MATCH_PARENT;
+        }
+        return lp;
+    }
+
+    private WindowManager.LayoutParams getBottomLayoutParams() {
+        WindowManager.LayoutParams lp = getWindowLayoutParams();
+        lp.setTitle("ScreenDecorOverlayBottom");
+        lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
+        return lp;
+    }
+
+    private void updateLayoutParams() {
+        mWindowManager.updateViewLayout(mOverlay, getWindowLayoutParams());
+        mWindowManager.updateViewLayout(mBottomOverlay, getBottomLayoutParams());
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        if (mOverlay == null) return;
+        if (SIZE.equals(key)) {
+            int size = mRoundedDefault;
+            try {
+                size = (int) (Integer.parseInt(newValue) * mDensity);
+            } catch (Exception e) {
+            }
+            setSize(mOverlay.findViewById(R.id.left), size);
+            setSize(mOverlay.findViewById(R.id.right), size);
+            setSize(mBottomOverlay.findViewById(R.id.left), size);
+            setSize(mBottomOverlay.findViewById(R.id.right), size);
+        }
+    }
+
+    private void setSize(View view, int pixelSize) {
+        LayoutParams params = view.getLayoutParams();
+        params.width = pixelSize;
+        params.height = pixelSize;
+        view.setLayoutParams(params);
+    }
+
+    @VisibleForTesting
+    static class TunablePaddingTagListener implements FragmentListener {
+
+        private final int mPadding;
+        private final int mId;
+        private TunablePadding mTunablePadding;
+
+        public TunablePaddingTagListener(int padding, int id) {
+            mPadding = padding;
+            mId = id;
+        }
+
+        @Override
+        public void onFragmentViewCreated(String tag, Fragment fragment) {
+            if (mTunablePadding != null) {
+                mTunablePadding.destroy();
+            }
+            View view = fragment.getView();
+            if (mId != 0) {
+                view = view.findViewById(mId);
+            }
+            mTunablePadding = TunablePadding.addTunablePadding(view, PADDING, mPadding,
+                    FLAG_START | FLAG_END);
+        }
+    }
+
+    public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener {
+
+        private final DisplayInfo mInfo = new DisplayInfo();
+        private final Paint mPaint = new Paint();
+        private final Rect mBoundingRect = new Rect();
+        private final Path mBoundingPath = new Path();
+        private final int[] mLocation = new int[2];
+        private final boolean mStart;
+        private final Runnable mVisibilityChangedListener;
+
+        public DisplayCutoutView(Context context, boolean start,
+                Runnable visibilityChangedListener) {
+            super(context);
+            mStart = start;
+            mVisibilityChangedListener = visibilityChangedListener;
+            setId(R.id.display_cutout);
+        }
+
+        @Override
+        protected void onAttachedToWindow() {
+            super.onAttachedToWindow();
+            mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
+                    getHandler());
+            update();
+        }
+
+        @Override
+        protected void onDetachedFromWindow() {
+            super.onDetachedFromWindow();
+            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            super.onDraw(canvas);
+            getLocationOnScreen(mLocation);
+            canvas.translate(-mLocation[0], -mLocation[1]);
+            if (!mBoundingPath.isEmpty()) {
+                mPaint.setColor(Color.BLACK);
+                mPaint.setStyle(Paint.Style.FILL);
+                canvas.drawPath(mBoundingPath, mPaint);
+            }
+        }
+
+        @Override
+        public void onDisplayAdded(int displayId) {
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+            if (displayId == getDisplay().getDisplayId()) {
+                update();
+            }
+        }
+
+        private void update() {
+            requestLayout();
+            getDisplay().getDisplayInfo(mInfo);
+            mBoundingRect.setEmpty();
+            mBoundingPath.reset();
+            int newVisible;
+            if (hasCutout()) {
+                mBoundingRect.set(mInfo.displayCutout.getBoundingRect());
+                mInfo.displayCutout.getBounds().getBoundaryPath(mBoundingPath);
+                newVisible = VISIBLE;
+            } else {
+                newVisible = GONE;
+            }
+            if (newVisible != getVisibility()) {
+                setVisibility(newVisible);
+                mVisibilityChangedListener.run();
+            }
+        }
+
+        private boolean hasCutout() {
+            if (mInfo.displayCutout == null) {
+                return false;
+            }
+            DisplayCutout displayCutout = mInfo.displayCutout.calculateRelativeTo(
+                    new Rect(0, 0, mInfo.logicalWidth, mInfo.logicalHeight));
+            if (mStart) {
+                return displayCutout.getSafeInsetLeft() > 0
+                        || displayCutout.getSafeInsetTop() > 0;
+            } else {
+                return displayCutout.getSafeInsetRight() > 0
+                        || displayCutout.getSafeInsetBottom() > 0;
+            }
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            if (mBoundingRect.isEmpty()) {
+                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+                return;
+            }
+            setMeasuredDimension(
+                    resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
+                    resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
+        }
+    }
+}