アプリの再表示時に生体認証を求める(Android10以下対応)

## はじめに

生体認証、便利に使っていますか？
Androidでは、セキュリティをより強固にしたいプロダクトでは、認証や操作の許可をするために生体認証を使うことが推奨されています。

今回はログインセッションの有効期限等によらず、アプリがタスク復帰した際に都度生体認証を求める動きを実装しました。

※生体認証そのものの処理は割愛しています。

## アプリ全体の構成について

私のアプリは1Activityで、その上にFragmentがスタックする形式です。
また、画面遷移にはNavigationComponentを使用しています。

## 仕様

- 複数画面があり、それぞれ画面遷移することができる。
- 先頭の画面はログイン画面で、認証が通ったあとはログイン画面に戻れない。戻るボタンやスワイプバックすると、ログイン画面には戻らずアプリが終了する（タスクにしまわれる）。
- 他アプリの割込時やタスクからの復帰時など、メモリ上にアプリが存在していたら（キルされていなければ）生体認証を求めて、認証OKだったら前に閲覧した画面が表示される
- ただしログイン画面だけは、タスク復帰したとき生体認証ではなく、ログインの前処理を行いたい。例えば、Tokenの有効期限チェックやローカルDBのマイグレーションなど。
- ログイン完了後は、どの画面に遷移していても、一度でもタスクにしまった後は、必ず生体認証を要求する

## MainActivity

今回は「生体認証をしても良いかどうか」を表すフラグを管理するため、ViewModelを使います。

```kotlin
@HiltViewModel
class MainViewModel @Inject constructor(
    private var needsBiometric: Boolean
) : ViewModel() {

    fun enableBiometric() {
        needsBiometric = true
    }

    fun disableBiometric() {
        needsBiometric = false
    }

    fun getBiometricFlag(): Boolean {
        return needsBiometric
    }
}
```

コンストラクタで定義した`needsBiometric`にフラグを与えたり、現在の値を返すだけのシンプルな存在です。
あと、DaggerHiltを使っているためアノテーションがついてます。

次にMainActivityです。
まずアプリがタスク復帰した時ですが、ライフサイクル的にはonRestart() -> onResume() の順に呼ばれます。
今回はonResume()に生体認証を呼ぶ処理を書きます。

```kotlin:MainActivity.kt
override fun onResume() {
    super.onResume()

    //アプリがフォアグラウンドに戻った時にフラグが立っていたら、生体認証を走らせる
    if (mainViewModel.getBiometricFlag()) {
        //ログイン画面でアプリを閉じていた場合、タスク復帰時には生体認証をしないでもう一度アプリ起動時の処理をさせたいため、ここで除外する
        val navHostFragment: Fragment? =
            supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
        val currentFragment = navHostFragment?.childFragmentManager?.fragments?.get(0)
        currentFragment?.let { fragment ->
            if (fragment.CLASSNAME != LoginFragment::class.java.simpleName) {
                startBiometricAuth()
                //生体認証が走ったらフラグをOFFにする
                mainViewModel.disableBiometric()
            }
        }
    }
}
```

NavigationのHostFragmentから、現在先頭にいるFragmentを判別し、ログイン画面では生体認証の処理が走らないようにしています。

次に、アプリがタスクにしまわれたり、割り込まれた時の処理です。

```kotlin:MainActivity.kt
override fun onPause() {
    super.onPause()

    //バックグラウンドに行ったら、フラグを立てて保持
    mainViewModel.enableBiometric()
}
```

これは見たまんまですね。
アプリが何らかの理由で停止した場合に、「次のタスク復帰時には生体認証をしてよ！」というフラグを立てておきます。

次に、生体認証が通ったあとの処理を考えます。
生体認証のビューというのは、Android11以降では先頭のViewの上に重なる形で表示されており、この時裏でアプリ自身は処理を続けることが可能です。生体認証が通ったあとのライフサイクルには特に影響しません。
Android10以下の場合はどうなるかというと、一度別のダイアログに遷移しているようで、アプリに再度戻ってくるような動きになっています。
繰り返しonResumeを通ってしまい、無限に生体認証が出続けてしまいました。

Android10以下の挙動に対応していきましょう。
「今生体認証から帰ってきたばかりなので、生体は求めないでね」という処理を加えます。

Android10以下では、onActivityResult（Depricatedですが）に結果が渡ってくるので、onActivityResultで「RESULT_OK,処理成功」をキャッチしたら、このあと呼ばれるonStartでlifecycleScope.launchWhenStarted{}内に書いた処理が走る（== onResumeが呼ばれる前に走る）、という流れにしていきます。

```kotlin:MainActivity.kt
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    activityResultCode = resultCode

    lifecycleScope.launchWhenStarted {
        if (resultCode == -1) {//生体認証がとおったら == RESULT_OK == -1
            //次回onResumeの時に生体認証を走らせない
            mainViewModel.disableBiometric()
        }
    }
}
```

サラッと出てきましたが、今回は「Android10以下で、生体認証を通った直後に呼ばれるonResume()では、何もしない」という処理を実装するために、lifecycleScopeを使っています。

これでアプリ復帰の度に生体認証を求めることが可能となりました。

onActivityResultは後ほど直します！！！

## 最後に

AndroidOSバージョンでライブラリの挙動に差異があり、かなり苦戦しました。
もっと良い方法があるよ！という方、ぜひ編集リクエストなどいただけると幸いです。

[Twitterはこちら](https://twitter.com/kotlin_obasan)
https://twitter.com/kotlin_obasan