BiometricPromptをAndroid Mから使う

# はじめに

Android Pから`BiometricPrompt`というクラスが追加されました。これはバイオメトリック(生体認証)に関連する処理を引き受けてくれるもので、これを使うと今までアプリ側で実装していたダイアログの表示をシステム側で提供してくれるようになります。また、指紋認証だけでなく、虹彩認証や顔認証にも対応しており、ユーザが選択した方式を判別し、適切なダイアログを表示してくれるという優れものです。それに伴い、従来の`FingerprintManager`はAndroid API 28より非推奨になりました。こんな便利な物を使わない手がないと、早速移行してみようと思ったのですが、`BiometricPrompt`の最低サポートがAndroid API 28からなので、Min SDKバージョンが28より低い場合は完全に移行することが出来ません😇

今回はこの問題の解決方法と実装の仕方を簡単にまとめました。

# 解決方法

いきなり結論からです。`android.hardware.biometrics.BiometricPrompt`ではなく`androidx.biometrics.BiometricsPrompt`を使います。ただし、AndroidXを使うので**従来のAndroid Support Libraryとは共存出来ない**点に注意してください。

# 実装方法

まず、依存関係をGradleに追加します。

```gradle
dependencies {
    ...
    implementation 'androidx.biometric:biometric:1.0.0-alpha04'
}
```

生体認証を使う為、`Android Manifest`にパーミッションを追加します。

```xml
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
```

最後に認証ダイアログを呼び出す処理を実装します。

```kotlin
val executor = Executors.newSingleThreadExecutor()

val biometricPrompt = BiometricPrompt(this, executor,
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            super.onAuthenticationError(errorCode, errString)
            Log.d("Biometric", "Error")
        }

        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            super.onAuthenticationSucceeded(result)
            Log.d("Biometric", "Succeeded")
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            Log.d("Biometric", "Failed")
        }
    })

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Title")
    .setSubtitle("Sub title")
    .setDescription("Description")
    .setNegativeButtonText("Cancel")
    .build()

biometricPrompt.authenticate(promptInfo)
```

これだけで、認証ダイアログを表示できます。

i|ii|iii
---|---|---
![img_1](https://raw.githubusercontent.com/toranoko0518/demo-fingerprint/master/screenshots/img_1.png) | ![img_2](https://raw.githubusercontent.com/toranoko0518/demo-fingerprint/master/screenshots/img_2.png) | ![img_3](https://raw.githubusercontent.com/toranoko0518/demo-fingerprint/master/screenshots/img_3.png)

軽くコードの説明をします。

認証の結果を受け取るのは`BiometricPrompt`インスタンス生成時に渡している`BiometricPrompt.AuthenticationCallback`になります。

関数名|内容
---|---
onAuthenticationError|エラーが発生して操作が完了された時に呼び出される
onAuthenticationSucceeded|生体認証が成功した時に呼び出される
onAuthenticationFailed|生体認証が有効だが認識されない時に呼び出される

キャンセルボタンのイベントは`onAuthenticationError`メソッドで`errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON`と比較することでキャッチすることができます。`BIOMETRIC_ERROR`定数に関しては以下のものがあります。

定数名|内容
---|---
ERROR_CANCELED|生体認証が利用できない為、操作がキャンセルされた
ERROR_HW_NOT_PRESENT|端末に生体センサが搭載されていない
ERROR_HW_UNAVAILABLE|端末が利用できない
ERROR_LOCKOUT|生体認証に5回失敗した
ERROR_LOCKOUT_PERMANENT|`ERROR_LOCKOUT`の発生回数が多すぎる為、操作がキャンセルされた
ERROR_NEGATIVE_BUTTON|ユーザがネガティブボタン(ダイアログのキャンセルボタン)を押した
ERROR_NO_BIOMETRICS|ユーザの生体認証情報が登録されていない
ERROR_NO_SPACE|操作を完了するのに十分なストレージが用意されていない
ERROR_TIMEOUT|認証を実行するまでの時間が長すぎる為、タイムアウトが発生した
ERROR_UNABLE_TO_PROCESS|生体センサが画像を処理できない
ERROR_USER_CANCELED|ユーザが操作をキャンセルした
ERROR_VENDOR|いずれのエラーにも該当しない

`ERROR_CANCELED`は具体的に端末で使用するユーザを切り替えた場合(おそらく生体認証情報がリセットされる)や端末がロックされている場合、他のアプリが生体認証を利用中の場合に発生するみたいです。`ERROR_HW_UNAVAILABLE`はどういったタイミングで発生するのかがわかりませんでした。`ERROR_LOCKOUT`が発生した場合、30秒後に再度認証可能になるみたいです(知らなかった:sweat_smile:)。`ERROR_UNABLE_TO_PROCESS`はおそらく虹彩認証や顔認証などのカメラを利用する場合に発生するのかと思います。

認証ダイアログのカスタマイズは`BiometricPrompt.PromptInfo.Builder`で行います。指定できる項目は以下の通りです。

関数名|内容
---|---
setTitle|タイトルを設定(必須)
setSubtitle|サブタイトルを設定(任意)
setDescription|説明を設定(任意)
setNegativeButtonText|ネガティブボタンに表示されるテキストを設定(必須)

ダイアログの画像やメッセージといった細かい部分は流石に設定できないみたいですね。

また、認証ダイアログを表示する前に

```kotlin
private fun isBiometricEnabled(context: Context): Boolean =
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
        && context.getSystemService(KeyguardManager::class.java).isKeyguardSecure
        && context.packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
```

のように端末が生体認証を利用出来るか判定した方が良さそうです。

# まとめ

足早にですが`BiometricPrompt`をAndroid Mから使う方法を解説しました。本来は`Build.VERSION.SDK_INT`でAndroidのバージョンを取得して`FingerprintManager`との処理を切り分けるのが行儀がいいのですが、~~ぶっちゃけめんどくさい~~効率的に出来るのであれば致し方ないのかなと。また、虹彩認証と顔認証については、次期バージョンのAndroid Q(API 29)から利用出来るみたいです。今回のデモアプリを[GitHub](https://github.com/toranoko0518/demo-fingerprint)に公開しておきますので、参考にしていただければ嬉しいです。では。

# 参考文献

* Android Developers Docs: https://developer.android.com/reference/androidx/biometric/BiometricPrompt
* Android 9 features and APIs: https://developer.android.com/about/versions/pie/android-9.0
* Fingerprint Authentication using BiometricPrompt Compat: https://medium.com/mindorks/fingerprint-authentication-using-biometricprompt-compat-1466365b4795