Android 9.0のdisplay cutoutは端末ベンダーにどういう選択肢を与えているか

## まえおき

![t.png](https://qiita-image-store.s3.amazonaws.com/0/74571/7ac18e37-4659-3a31-b699-4a6a2bbb53dc.png)

iPhone Xに始まった↑のような~~欠陥~~デザインは、Android 9.0でも正式に対応されたようです。みんな「ノッチ」とか言っていますが、Androidでは `display cutout` と呼ぶようです。

display cutoutは全画面表示するようなアプリの動作にも影響が出るため、開発者向けリファレンスにも、 https://developer.android.com/guide/topics/display-cutout/ こんな感じのドキュメントが公式に公開されています。

### display cutoutにはいろんな種類がある

Android 9.0エミュレータで開発者オプションを見てみると

| corner | double | tall |
|:--|:--|:--|
|![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/e16a801c-3c7d-6981-2255-7361424c1e4c.png)|![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/9c058218-413c-e95c-9c43-fa9357714149.png)|![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/5f8f1447-aa0a-08e2-e8cd-38861ad57cc6.png)|

の3種類を選ぶことができるようになっています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/74571/1b76d057-541a-3b11-1e36-f6e4cc0a5ebd.png" width=320 />


これってようするに、端末ベンダーが割と自由にdisplay cutoutできるようになってたりするんじゃない？？？

と気になったので、Androidフレームワークにどういう形でdisplay cutoutが定義されているのか調べてみました。
あらかじめ言っておきますが、**アプリ開発者には多分なんの約にも立ちません。完全に自分用のメモです。**

## 開発者オプションが何をやってるかを調べてみる

### display cutoutの形の定義

まずdisplay cutoutのシミュレート部分になにかヒントが無いかみてみます。

https://android.googlesource.com/platform/frameworks/base/+/master/packages/overlays

の下に、

* DisplayCutoutEmulationCornerOverlay/
* DisplayCutoutEmulationDoubleOverlay/
* DisplayCutoutEmulationNarrowOverlay/
* DisplayCutoutEmulationTallOverlay/
* DisplayCutoutEmulationWideOverlay/

の5つのオーバーレイデータがあって、おそらくそれが使われているのでしょう。 `res/values/config.xml` にはそれぞれ↓のようなSVGパス定義とステータスバー等の高さ定義などがあります。

```
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <!-- The bounding path of the cutout region of the main built-in display.
         Must either be empty if there is no cutout region, or a string that is parsable by
         {@link android.util.PathParser}.
         The path is assumed to be specified in display coordinates with pixel units and in
         the display's native orientation, with the origin of the coordinate system at the
         center top of the display.
         To facilitate writing device-independent emulation overlays, the marker `@dp` can be
         appended after the path string to interpret coordinates in dp instead of px units.
         Note that a physical cutout should be configured in pixels for the best results.
         -->
    <string translatable="false" name="config_mainBuiltInDisplayCutout">
        M 0,0
        L -72, 0
        L -69.9940446283, 20.0595537175
        C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0
        L 56.8, 32.0
        C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175
        L 72, 0
        Z
        @dp
    </string>
    <!-- Whether the display cutout region of the main built-in display should be forced to
         black in software (to avoid aliasing or emulate a cutout that is not physically existent).
     -->
    <bool name="config_fillMainBuiltInDisplayCutout">true</bool>
    <!-- Height of the status bar -->
    <dimen name="status_bar_height_portrait">48dp</dimen>
    <dimen name="status_bar_height_landscape">28dp</dimen>
    <!-- Height of area above QQS where battery/time go (equal to status bar height if > 48dp) -->
    <dimen name="quick_qs_offset_height">48dp</dimen>
    <!-- Total height of QQS (quick_qs_offset_height + 128) -->
    <dimen name="quick_qs_total_height">176dp</dimen>
</resources>
```

この定義が https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/DisplayCutout.java で読み込まれるらしい。

## cutoutの形を適用するためのオペレーション

```java:packages/apps/Settings/src/com/android/settings/development/EmulateDisplayCutoutPreferenceController.java

 89    private boolean setEmulationOverlay(String packageName) {
 90        OverlayInfo[] overlays = getOverlayInfos();
 91        String currentPackageName = null;
 92        for (OverlayInfo o : overlays) {
 93            if (o.isEnabled()) {
 94                currentPackageName = o.packageName;
 95            }
 96        }
 97
 98        if (TextUtils.isEmpty(packageName) && TextUtils.isEmpty(currentPackageName)
 99                || TextUtils.equals(packageName, currentPackageName)) {
100            // Already set.
101            return true;
102        }
103
104        final boolean result;
105        if (TextUtils.isEmpty(packageName)) {
106            result = mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM);
107        } else {
108            result = mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM);
109        }
110        updateState(mPreference);
111        return result;
112    }
```

**OverlayManager**とかいうAPIを叩いているだけですね・・。

私は全く知らなかったんですが、Android 8.0くらいからテーマ設定を実現するためにシステムサービスに追加されているもののようです。
https://android.googlesource.com/platform/frameworks/base/+/eabc9e95768e7ac9acc3b32dc9ac2edf99c9e2c5

### OverlayManagerService

http://androidxref.com/9.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/om/OverlayManagerService.java

このクラスは、DisplayCutoutについては何も知らないクラスで、純粋にOverlayInfoに基づいてオーバーレイするだけっぽい？

## DisplayCutoutはどうシステムに反映されるか

DisplayCutoutの利用箇所を見てみると、既存の DisplayInfoの１プロパティとして組み込まれていて、WindowManagerあたりでinsetsを計算するときに使われています。

* http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/view/DisplayInfo.java#152
* http://androidxref.com/9.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/wm/utils/WmDisplayCutout.java#44

ステータスバーとか各種コンポーネントは、空気を読んで（ `View#getRootWindowInsets()` とかいろいろ使って）描画位置をずらしているようです。

* http://androidxref.com/9.0.0_r3/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java#261
* http://androidxref.com/9.0.0_r3/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java#198

## 開発者オプションで指定がない場合のDisplayCutoutはどこからやってくるのか

DisplayInfoはDisplayManagerServiceというシステムサービスが持ってるはずなので、そのあたりを見てみます。
http://androidxref.com/9.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/display/DisplayManagerService.java
見た感じだとDisplayDeviceの情報を、起動時に `addLogicalDisplayLocked` でメモリ上にキャッシュしてるだけですね。

デフォルトのディスプレイは↓のあたりで定義されている。

```java:frameworks/base/services/core/java/com/android/server/display/LocalDisplayAdapter.java
    397                 if (mBuiltInDisplayId == SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
    398                     mInfo.name = res.getString(
    399                             com.android.internal.R.string.display_manager_built_in_display_name);
    400                     mInfo.flags |= DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY
    401                             | DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
    402                     if (res.getBoolean(com.android.internal.R.bool.config_mainBuiltInDisplayIsRound)
    403                             || (Build.IS_EMULATOR
    404                             && SystemProperties.getBoolean(PROPERTY_EMULATOR_CIRCULAR, false))) {
    405                         mInfo.flags |= DisplayDeviceInfo.FLAG_ROUND;
    406                     }
    407                     mInfo.displayCutout = DisplayCutout.fromResources(res, mInfo.width,
    408                             mInfo.height);
    409                     mInfo.type = Display.TYPE_BUILT_IN;
    410                     mInfo.densityDpi = (int)(phys.density * 160 + 0.5f);
    411                     mInfo.xDpi = phys.xDpi;
    412                     mInfo.yDpi = phys.yDpi;
    413                     mInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL;
```

* display_manager_built_in_display_name
* config_mainBuiltInDisplayIsRound

とかいろいろ面白そうなプロパティあるんだなー。へー。（どうでもいいｗ


```java:frameworks/base/core/java/android/view/DisplayCutout.java
    361     public static DisplayCutout fromResources(Resources res, int displayWidth, int displayHeight) {
    362         return fromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
    363                 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT);
    364     }
```

やはりここに行き着いた。


```xml:frameworks/base/core/res/res/values/config.xml
2913    <!-- The bounding path of the cutout region of the main built-in display.
2914         Must either be empty if there is no cutout region, or a string that is parsable by
2915         {@link android.util.PathParser}.
2916
2917         The path is assumed to be specified in display coordinates with pixel units and in
2918         the display's native orientation, with the origin of the coordinate system at the
2919         center top of the display.
2920
2921         To facilitate writing device-independent emulation overlays, the marker `@dp` can be
2922         appended after the path string to interpret coordinates in dp instead of px units.
2923         Note that a physical cutout should be configured in pixels for the best results.
2924
2925         Example for a 10px x 10px square top-center cutout:
2926                <string ...>M -5,0 L -5,10 L 5,10 L 5,0 Z</string>
2927         Example for a 10dp x 10dp square top-center cutout:
2928                <string ...>M -5,0 L -5,10 L 5,10 L 5,0 Z @dp</string>
2929
2930         @see https://www.w3.org/TR/SVG/paths.html#PathData
2931         -->
2932    <string translatable="false" name="config_mainBuiltInDisplayCutout"></string>
```

基本的にはカラの実装だけど、ベンダーが `device/hoge_vendor/fugafuga/overlay/...` オーバーレイしてねーってことなんだろう。

現状、ぐぐってでてくるのは、カスタムロム製作者と思われる人が公開している
https://github.com/phhusson/vendor_hardware_overlay/blob/master/Asus/ZenFone5Z/res/values/notch.xml
このくらいだけど、まぁSVGで好きな形を定義できるんだろう。きっと。

## 改めてパス定義をみてみる

```xml:CornerOverlay
    <string translatable="false" name="config_mainBuiltInDisplayCutout">
        M 0,0
        L -48, 0
        C -48,48 -48,48 0,48
        Z
        @dp
        @right
    </string>
```

![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/c160f3f9-be11-05cb-b969-5f1f0cb39b13.png)

　

```DoubleOverlay
    <string translatable="false" name="config_mainBuiltInDisplayCutout">
        M 0,0
        L -72, 0
        L -69.9940446283, 20.0595537175
        C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0
        L 56.8, 32.0
        C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175
        L 72, 0
        Z
        @bottom
        M 0,0
        L -72, 0
        L -69.9940446283, -20.0595537175
        C -69.1582133885, -28.4178661152 -65.2, -32.0 -56.8, -32.0
        L 56.8, -32.0
        C 65.2, -32.0 69.1582133885, -28.4178661152 69.9940446283, -20.0595537175
        L 72, 0
        Z
        @dp
    </string>
```

![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/1b9dce45-5e41-99bf-2bd8-b396943dd54a.png)

なるほどわかった。

　


## 余談: HuaweiはAndroid 8.xでもnotch対応しているらしい

なんか力ずくで頑張って実装したっぽいです。すごい。
https://developer.huawei.com/consumer/en/devservice/doc/30210


## まとめ

端末ベンダーは framework-res/res/values/config.xml の `config_mainBuiltInDisplayCutout` をoverlay定義することで、好きな形のdisplay cutoutにすることができるよ！！