android:enableOnBackInvokedCallback="true"にしたときのバックキーのKeyEventの変化

予測型「戻る」ジェスチャーをオプトインしていますか？

オプトインした場合のDialogでのバックキー検出の問題に遭遇しました。

https://developer.android.com/guide/navigation/predictive-back-gesture?hl=ja

予測型「戻る」ジェスチャーをオプトインするには、 `AndroidManifest.xml` の `<application>` タグ内で、 `android:enableOnBackInvokedCallback` フラグを `true` に設定します。

```AndroidManifest.xml
<application
    ...
    android:enableOnBackInvokedCallback="true"
    ... >
...
</application>
```

# AppCompatActivityでの変化

このとき、

> 注: OnBackPressedCallback は、android:enableOnBackInvokedCallback の値に関係なく常に呼び出されます。つまり、システム アニメーションを無効にしても、OnBackPressedCallback を使用している場合、アプリの戻る処理のロジックには影響しません。

とある通り、`ComponentActivity` の `OnBackPressedCallback` は、`android:enableOnBackInvokedCallback`の値に関係なく呼び出されます。
一方、`android:enableOnBackInvokedCallback="true"` にした場合、`Activity`の `onBackPressed()` もコールされなくなりますし、`onKeyUp()` などもバックキーのイベントが通知されなくなります。

```MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
...
    onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // android:enableOnBackInvokedCallback の値にかかわらずコールされる
        }
    })
}

override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
    // android:enableOnBackInvokedCallback="true"ならバックキーでコールされない
    return super.onKeyUp(keyCode, event)
}

override fun onBackPressed() {
    // android:enableOnBackInvokedCallback="true" ならコールされない
    super.onBackPressed()
}
```

バックキーのハンドリングは `OnBackPressedCallback` で行うようにしておけば問題無いでしょう。

# Dialogでの変化

一方、Dialogの場合、ちょっと事情が異なります。
DialogFragmentではDialogに対して、OnKeyListenerを登録することでバックキーのイベントを拾うことができます。
また、API 33以上では、 `onBackInvokedDispatcher` に `OnBackInvokedCallback` を登録することができます。
この動作が、　`android:enableOnBackInvokedCallback` の値によって排他的な動作になっているので注意が必要です。
`android:enableOnBackInvokedCallback="false"` の場合、 `OnKeyListener` がコールされますが、`OnBackInvokedCallback` がコールされません。
`OnBackInvokedCallback` で検出したい場合、 `android:enableOnBackInvokedCallback="true"` にする必要があります。こうすると `OnKeyListener` がコールされなくなります。

```MyDialogFragment.kt
override fun onStart() {
    super.onStart()
    requireDialog().setOnKeyListener { dialog, keyCode, event ->
        if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
            // android:enableOnBackInvokedCallback="true" ならバックキーでコールされない
        }
        false
    }
    if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
        requireDialog().onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) {
            // android:enableOnBackInvokedCallback="false" ならコールされない
        }
    }
}
```

---

どうやら `ViewRootImpl` でイベントが排他的に振り分けられているようです。

https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/ViewRootImpl.java


```ViewRootImpl.java
@Override
protected int onProcess(QueuedInputEvent q) {
    if (q.mEvent instanceof KeyEvent) {
        final KeyEvent event = (KeyEvent) q.mEvent;

        // If the new back dispatch is enabled, intercept KEYCODE_BACK before it reaches the
        // view tree or IME, and invoke the appropriate {@link OnBackInvokedCallback}.
        if (isBack(event)
                && mContext != null
                && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
            OnBackInvokedCallback topCallback =
                    getOnBackInvokedDispatcher().getTopCallback();
            if (event.getAction() == KeyEvent.ACTION_UP) {
                if (topCallback != null) {
                    topCallback.onBackInvoked();
                    return FINISH_HANDLED;
                }
            } else {
                // Drop other actions such as {@link KeyEvent.ACTION_DOWN}.
                return FINISH_NOT_HANDLED;
            }
        }
    }

    if (mInputQueue != null && q.mEvent instanceof KeyEvent) {
        mInputQueue.sendInputEvent(q.mEvent, q, true, this);
        return DEFER;
    }
    return FORWARD;
}
```

`android:enableOnBackInvokedCallback="true"` の場合 `WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)` がtrueを返すので、`onBackInvoked()` がコールされて、`FINISH_HANDLED`が返却され、イベントがここで消費されてしまうので、onKeyListenerがコールされなくなるようです。

# まとめ

ようするに、
`OnBackInvokedCallback` でバックキーイベントを拾うには、 `android:enableOnBackInvokedCallback="true"` である必要があり、この場合、`OnKeyListner` は呼ばれなくなる。
`OnKeyListner` でバックキーイベントを拾うには、`android:enableOnBackInvokedCallback="false"` である必要があり、この場合、`OnBackInvokedCallback` は呼ばれなくなる。
ということでした。まあ、書き下してみればおかしな挙動というわけでもない気がしますが、前提知識が無いと混乱しそうなのでご注意を。