Androidアプリに生体認証を導入する Compose編

## 概要

[生体認証ダイアログを表示する Android Developers](https://developer.android.com/identity/sign-in/biometric-auth?hl=ja)

・公式サイトにもあるような、生体認証ダイアログを用いた認証を作成します。
このダイアログでは
 * 指紋認証
 * 顔認証
 * 虹彩認証

など、端末のロック解除方法に登録してあるものを使用して、ユーザ認証が行えます。

・公式サイト内ではAndroid Viewを使用していますが、今回はせっかくなのでJetpack Composeで作成します。

## 開発環境
・Macbook Pro (Apple M2 Pro) 
・macOS Sonoma
・Android Studio Ladybug 2024.2.1 Patch2
実行端末
・Google Pixel9 (Android 14)

:::note warn
エミュレータではテストができないため、Android実機端末が必要です
:::
※完成画像などが使用できていないのもこのためです。実機のスクショがうまく出来ませんでした...

## 実装方法

プロジェクトの作成などは各自で行ってください。

### 1.必要なライブラリを追加する
・認証機能の実装には、[androidx.biometric](https://developer.android.com/jetpack/androidx/releases/biometric?hl=ja#declaring_dependencies)が必要なので、これを追加します。

build.gradle.kts
```kotlin
 implementation("androidx.biometric:biometric-ktx:1.4.0-alpha02")
```
※2024/12/02時点の最新版(安定版は1.1.0)

### 2.認証ダイアログの作成

Composableで作成します
```kotlin
@Composable
fun BiometricAuthenticationDialog(
    onAuthSuccess: () -> Unit,
    onAuthError: (String) -> Unit
) {
    val context = LocalContext.current
    val executor = ContextCompat.getMainExecutor(context)

    
    val biometricPrompt = remember {
        BiometricPrompt(
            context as FragmentActivity,
            executor,
            object : BiometricPrompt.AuthenticationCallback() {
                //if auth is successful.
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    onAuthSuccess()
                }
                //if auth is error.
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                    onAuthError(errString.toString())
                }
                //if auth is failed.
                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    onAuthError("Authentication failed.")
                }
            }
        )
    }

    //ダイアログに表示するタイトルやテキストをセットする
    LaunchedEffect(Unit) {
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Authentication")
            .setSubtitle("Please touch your finger")
            .setNegativeButtonText("Cancel")
            .build()
        biometricPrompt.authenticate(promptInfo)
    }
}
```

これに加えて、この関数を呼び出す親のアクティビティの継承元を
`ComponentActivity`から`FragmentActivity`に変更しておきます。(理由は後述)

```kotlin
class MainActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Favorite_actor_appTheme {
                MyApp()
            }
        }
    }
}
```


### 解説
作成した`BiometricAuthenticationDialog`は二つの引数を持たせます。

*  `onAuthSuccess`: 認証が成功した時の動作を定義する関数
*  `onAuthError`:認証に失敗した時の動作を定義する関数

`onAuthError`には、エラー内容を渡すため、String型の引数を定義しておきます

```kotlin
 val context = LocalContext.current
 val executor = ContextCompat.getMainExecutor(context)
```

この後作成する認証時の動作を定義する`BIometricPrompt`の引数として`FragmentActivity`と`executor`が必要なため、あらかじめ定義しておきます。

`exexutor`は処理を行うスレッドやタスクを管理するためのもので、今回はメインスレッドで処理を行う必要があるため、上記のように定義しておきます。

```kotlin
val biometricPrompt = remember {
        BiometricPrompt(
            context as FragmentActivity,
            executor,
            object : BiometricPrompt.AuthenticationCallback() {
                //if auth is successful.
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    onAuthSuccess()
                }
                //if auth is error.
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                    onAuthError(errString.toString())
                }
                //if auth is failed.
                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    onAuthError("Authentication failed.")
                }
            }
        )
    }
```

生体認証の動作の核となる`BiometricPrompt`を定義します

`BiometricPrompt`メソッドの実装は以下のようになっています

```java
 public BiometricPrompt(
            @NonNull FragmentActivity activity,
            @NonNull Executor executor,
            @NonNull AuthenticationCallback callback) {

        if (activity == null) {
            throw new IllegalArgumentException("FragmentActivity must not be null.");
        }
        if (executor == null) {
            throw new IllegalArgumentException("Executor must not be null.");
        }
        if (callback == null) {
            throw new IllegalArgumentException("AuthenticationCallback must not be null.");
        }

        final FragmentManager fragmentManager = activity.getSupportFragmentManager();
        final BiometricViewModel viewModel =
                new ViewModelProvider(activity).get(BiometricViewModel.class);
        init(true /* hostedInActivity */, fragmentManager, viewModel, executor, callback);
    }
```

このため、まず第1引数として`FragmentActivity`を渡す必要があります。
すでに先ほど現在のコンテキストを取得しているため、

```kotlin
context as FragmentActivity,
```
として渡します。

先ほど、呼び出し先の親アクティビティを`FragmentActivity`に変更したのはこのためです。
ここで親アクティビティが`ComponentActivity`を使用していると、実行時にエラーが発生します。

第2引数には先ほど定義したexeutorを渡します。

そして最後に第3引数として、認証時の動作を定義するコールバックを作成します。

```kotlin
object : BiometricPrompt.AuthenticationCallback() {
                //if auth is successful.
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    onAuthSuccess()
                }
                //if auth is error.
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                    onAuthError(errString.toString())
                }
                //if auth is failed.
                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    onAuthError("Authentication failed.")
                }
            }
```
見ての通り、`BiometricPrompt.AuthenticationCallback`には3つのメソッドがあり、
* `onAuthenticationSucceeded`には成功時の動作
* `onAuthenticationError`にはなんらかのエラーが発生した時の動作
* `onAuthenticatoinFailed`には認証失敗時の動作

をそれぞれ定義します。

あとは、認証ダイアログに表示される文字などを定義して、ダイアログを表示させます。

```kotlin
 LaunchedEffect(Unit) {
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            //ダイアログ上部に表示されるタイトル文字列
            .setTitle("Authentication")
            //タイトル下に表示される文字列
            .setSubtitle("Please touch your finger")
            //ダイアログ左下に表示されるネガティブボタンの文字(主にキャンセル用)
            .setNegativeButtonText("Cancel")
            .build()
        //ダイアログを表示
        biometricPrompt.authenticate(promptInfo)
    }
```

### 3.呼び出し
ここまでこれば、作成した`BiometricAuthenticationDialog`Composableを呼び出すだけです。
```kotlin
@Composable
fun TopPage(modifier: Modifier = Modifier) {

    var showBiometricDialog by remember { mutableStateOf(false) }
    
    var biometricResult by remember { mutableStateOf<String?>(null) }

    if (showBiometricDialog) {
        BiometricAuthenticationDialog(
            onAuthSuccess = { biometricResult = "Success" },
            onAuthError = { error ->
                biometricResult = error
                showBiometricDialog = false
            }
        )
    }

    Box(
        modifier = modifier.fillMaxSize(),
    ) {
        Image(
            painter = painterResource(R.drawable.actor),
            contentDescription = "background",
            contentScale = ContentScale.Crop
        )
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(color = colorResource(R.color.dark_effect))
        )
        Text(
            "",
            style = TextStyle(color = colorResource(R.color.white), fontSize = 30.sp),
            modifier = Modifier.align(
                Alignment.Center
            )
        )
        Button(
            onClick = { showBiometricDialog = true }, modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 200.dp)
        ) {
            Text("Authorization and Start")
        }

    }
}
```

この例では、ボタンが押されると`showBiometricDialog`変数が`true`になり、ダイアログが呼び出されます。
また、認証結果が成功の場合は`biometricResult`に`Success`が代入され、失敗時は受け取ったエラー文が代入されるようになっています。

実際にはこの後`navController`などを使用して画面遷移などを行うことになると思います。

実装は以上です。

## まとめ
作成してみた感想としては、意外と簡単でシンプルな印象を持ちました。
インターネット接続なども必要なく、お手軽ながらもしっかりした認証を作成できて自分はとても好印象でした！

## 参考にしたサイトなど
[生体認証ダイアログを表示する Android Developers](https://developer.android.com/identity/sign-in/biometric-auth?hl=ja)

[Biometric | Jetpack| Android Developers](https://developer.android.com/jetpack/androidx/releases/biometric?hl=ja#declaring_dependencies)