Display Cutout: Add emulation

Adds an overlay to SystemUI that draws an emulated
cutout in the bounding polygon that the window manager
supplies.

Bug: 65689439
Test: adb shell settings put global emulate_display_cutout 2
Change-Id: I91e6832d7e4594e995241d29d6f1ed0d918d59a0
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 1d4477a..6b1632a 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10164,6 +10164,16 @@
         public static final String POLICY_CONTROL = "policy_control";
 
         /**
+         * {@link android.view.DisplayCutout DisplayCutout} emulation mode.
+         *
+         * @hide
+         */
+        public static final String EMULATE_DISPLAY_CUTOUT = "emulate_display_cutout";
+
+        /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_OFF = 0;
+        /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_ON = 1;
+
+        /**
          * Defines global zen mode.  ZEN_MODE_OFF, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
          * or ZEN_MODE_NO_INTERRUPTIONS.
          *
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 77c6c3e..0982a4b 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -194,6 +194,7 @@
                     Settings.Global.DROPBOX_RESERVE_PERCENT,
                     Settings.Global.DROPBOX_TAG_PREFIX,
                     Settings.Global.EMERGENCY_AFFORDANCE_NEEDED,
+                    Settings.Global.EMULATE_DISPLAY_CUTOUT,
                     Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED,
                     Settings.Global.ENABLE_CACHE_QUOTA_CALCULATION,
                     Settings.Global.ENABLE_CELLULAR_ON_BOOT,
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 3eee4da..5e7f9c6 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -337,6 +337,7 @@
         <item>com.android.systemui.LatencyTester</item>
         <item>com.android.systemui.globalactions.GlobalActionsComponent</item>
         <item>com.android.systemui.RoundedCorners</item>
+        <item>com.android.systemui.EmulatedDisplayCutout</item>
     </string-array>
 
     <!-- SystemUI vender service, used in config_systemUIServiceComponents. -->
diff --git a/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java b/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java
new file mode 100644
index 0000000..edd1748
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java
@@ -0,0 +1,140 @@
+/*
+ * 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 android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import java.util.ArrayList;
+
+/**
+ * Emulates a display cutout by drawing its shape in an overlay as supplied by
+ * {@link DisplayCutout}.
+ */
+public class EmulatedDisplayCutout extends SystemUI {
+    private View mOverlay;
+    private boolean mAttached;
+    private WindowManager mWindowManager;
+
+    @Override
+    public void start() {
+        mWindowManager = mContext.getSystemService(WindowManager.class);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.EMULATE_DISPLAY_CUTOUT),
+                false, mObserver, UserHandle.USER_ALL);
+        mObserver.onChange(false);
+    }
+
+    private void setAttached(boolean attached) {
+        if (attached && !mAttached) {
+            if (mOverlay == null) {
+                mOverlay = new CutoutView(mContext);
+                mOverlay.setLayoutParams(getLayoutParams());
+            }
+            mWindowManager.addView(mOverlay, mOverlay.getLayoutParams());
+            mAttached = true;
+        } else if (!attached && mAttached) {
+            mWindowManager.removeView(mOverlay);
+            mAttached = false;
+        }
+    }
+
+    private WindowManager.LayoutParams getLayoutParams() {
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT,
+                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
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR,
+                PixelFormat.TRANSLUCENT);
+        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
+                | WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
+        lp.setTitle("EmulatedDisplayCutout");
+        lp.gravity = Gravity.TOP;
+        return lp;
+    }
+
+    private ContentObserver mObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+        @Override
+        public void onChange(boolean selfChange) {
+            boolean emulateCutout = Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.EMULATE_DISPLAY_CUTOUT,
+                    Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF)
+                    != Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF;
+            setAttached(emulateCutout);
+        }
+    };
+
+    private static class CutoutView extends View {
+        private Paint mPaint = new Paint();
+        private Path mPath = new Path();
+        private ArrayList<Point> mBoundingPolygon = new ArrayList<>();
+
+        CutoutView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+            insets.getDisplayCutout().getBoundingPolygon(mBoundingPolygon);
+            invalidate();
+            return insets.consumeCutout();
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            if (!mBoundingPolygon.isEmpty()) {
+                mPaint.setColor(Color.DKGRAY);
+                mPaint.setStyle(Paint.Style.FILL);
+
+                mPath.reset();
+                for (int i = 0; i < mBoundingPolygon.size(); i++) {
+                    Point point = mBoundingPolygon.get(i);
+                    if (i == 0) {
+                        mPath.moveTo(point.x, point.y);
+                    } else {
+                        mPath.lineTo(point.x, point.y);
+                    }
+                }
+                mPath.close();
+                canvas.drawPath(mPath, mPaint);
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 06d7749..3538327 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -37,9 +37,7 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.media.RingtonePlayer;
 import com.android.systemui.pip.PipUI;
-import com.android.systemui.plugins.GlobalActions;
 import com.android.systemui.plugins.OverlayPlugin;
-import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.power.PowerUI;
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9a7e72a..22e2c47 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -607,6 +607,8 @@
 
     PointerLocationView mPointerLocationView;
 
+    boolean mEmulateDisplayCutout = false;
+
     // During layout, the layer at which the doc window is placed.
     int mDockLayer;
     // During layout, this is the layer of the status bar.
@@ -965,6 +967,9 @@
             resolver.registerContentObserver(Settings.Global.getUriFor(
                     Settings.Global.POLICY_CONTROL), false, this,
                     UserHandle.USER_ALL);
+            resolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.EMULATE_DISPLAY_CUTOUT), false, this,
+                    UserHandle.USER_ALL);
             updateSettings();
         }
 
@@ -2396,6 +2401,10 @@
             if (mImmersiveModeConfirmation != null) {
                 mImmersiveModeConfirmation.loadSetting(mCurrentUserId);
             }
+            mEmulateDisplayCutout = Settings.Global.getInt(resolver,
+                    Settings.Global.EMULATE_DISPLAY_CUTOUT,
+                    Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF)
+                    != Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF;
         }
         synchronized (mWindowManagerFuncs.getWindowManagerLock()) {
             PolicyControl.reloadFromSetting(mContext);
@@ -4436,7 +4445,7 @@
     /** {@inheritDoc} */
     @Override
     public void beginLayoutLw(DisplayFrames displayFrames, int uiMode) {
-        displayFrames.onBeginLayout();
+        displayFrames.onBeginLayout(mEmulateDisplayCutout, mStatusBarHeight);
         // TODO(multi-display): This doesn't seem right...Maybe only apply to default display?
         mSystemGestures.screenWidth = displayFrames.mUnrestricted.width();
         mSystemGestures.screenHeight = displayFrames.mUnrestricted.height();
diff --git a/services/core/java/com/android/server/wm/DisplayFrames.java b/services/core/java/com/android/server/wm/DisplayFrames.java
index 209ce3f..0155712 100644
--- a/services/core/java/com/android/server/wm/DisplayFrames.java
+++ b/services/core/java/com/android/server/wm/DisplayFrames.java
@@ -22,12 +22,16 @@
 import static com.android.server.wm.proto.DisplayFramesProto.STABLE_BOUNDS;
 
 import android.annotation.NonNull;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.PrintWriter;
+import java.util.Arrays;
 
 /**
  * Container class for all the display frames that affect how we do window layout on a display.
@@ -124,7 +128,7 @@
                 info.overscanLeft, info.overscanTop, info.overscanRight, info.overscanBottom);
     }
 
-    public void onBeginLayout() {
+    public void onBeginLayout(boolean emulateDisplayCutout, int statusBarHeight) {
         switch (mRotation) {
             case ROTATION_90:
                 mRotatedDisplayInfoOverscan.left = mDisplayInfoOverscan.top;
@@ -165,12 +169,64 @@
         mDisplayCutout = DisplayCutout.NO_CUTOUT;
         mDisplayCutoutSafe.set(Integer.MIN_VALUE, Integer.MIN_VALUE,
                 Integer.MAX_VALUE, Integer.MAX_VALUE);
+        if (emulateDisplayCutout) {
+            setEmulatedDisplayCutout((int) (statusBarHeight * 0.8));
+        }
     }
 
     public int getInputMethodWindowVisibleHeight() {
         return mDock.bottom - mCurrent.bottom;
     }
 
+    private void setEmulatedDisplayCutout(int height) {
+        final boolean swappedDimensions = mRotation == ROTATION_90 || mRotation == ROTATION_270;
+
+        final int screenWidth = swappedDimensions ? mDisplayHeight : mDisplayWidth;
+        final int screenHeight = swappedDimensions ? mDisplayWidth : mDisplayHeight;
+
+        final int widthTop = (int) (screenWidth * 0.3);
+        final int widthBottom = widthTop - height;
+
+        switch (mRotation) {
+            case ROTATION_90:
+                mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+                        new Point(0, (screenWidth - widthTop) / 2),
+                        new Point(height, (screenWidth - widthBottom) / 2),
+                        new Point(height, (screenWidth + widthBottom) / 2),
+                        new Point(0, (screenWidth + widthTop) / 2)
+                )).calculateRelativeTo(mUnrestricted);
+                mDisplayCutoutSafe.left = height;
+                break;
+            case ROTATION_180:
+                mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+                        new Point((screenWidth - widthTop) / 2, screenHeight),
+                        new Point((screenWidth - widthBottom) / 2, screenHeight - height),
+                        new Point((screenWidth + widthBottom) / 2, screenHeight - height),
+                        new Point((screenWidth + widthTop) / 2, screenHeight)
+                )).calculateRelativeTo(mUnrestricted);
+                mDisplayCutoutSafe.bottom = screenHeight - height;
+                break;
+            case ROTATION_270:
+                mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+                        new Point(screenHeight, (screenWidth - widthTop) / 2),
+                        new Point(screenHeight - height, (screenWidth - widthBottom) / 2),
+                        new Point(screenHeight - height, (screenWidth + widthBottom) / 2),
+                        new Point(screenHeight, (screenWidth + widthTop) / 2)
+                )).calculateRelativeTo(mUnrestricted);
+                mDisplayCutoutSafe.right = screenHeight - height;
+                break;
+            default:
+                mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+                        new Point((screenWidth - widthTop) / 2, 0),
+                        new Point((screenWidth - widthBottom) / 2, height),
+                        new Point((screenWidth + widthBottom) / 2, height),
+                        new Point((screenWidth + widthTop) / 2, 0)
+                )).calculateRelativeTo(mUnrestricted);
+                mDisplayCutoutSafe.top = height;
+                break;
+        }
+    }
+
     public void writeToProto(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
         mStable.writeToProto(proto, STABLE_BOUNDS);