指紋認証APIのエラーハンドリングで気をつけること

## 概要

[株式会社 Zaim](http://zaim.net/) で提供している[家計簿アプリ Zaim](https://play.google.com/store/apps/details?id=net.zaim.android) では、12/16（水）にアプリの画面ロック機能の方法として指紋認証を追加しました。

![capture.png](https://qiita-image-store.s3.amazonaws.com/0/4706/f4afc45e-018a-41b7-c877-b0696270e19d.png)


パスワードによるロックに比べて、とても使いやすいのでぜひ触ってみてください。（Android 6.0 以上の端末でのみ利用可能です。）

今回はこの機能の開発中にエラーハンドリングの部分でハマったので、注意点をまとめてみました。

## 指紋認証 API について

指紋認証APIについては、公式ブログおよび（手前味噌ですが）以下のブログなどで紹介されていますので、ここでは使い方などの説明は省きます。

- [Android 新機能のサンプル: Fingerprint API を使用したリモート サーバー認証](http://googledevjp.blogspot.jp/2015/11/android-fingerprint-api.html)
- [Android 6.0 の指紋認証 API を触ってみた感想文](http://androhi.hatenablog.com/entry/2015/10/28/031626)
- [API level 23 未満を考慮した指紋認証 API の実装](http://androhi.hatenablog.com/entry/2015/10/29/000258)

また Google のサンプルコードも公開されています。

- [FingerprintDialog](https://github.com/googlesamples/android-FingerprintDialog)

## 指紋センサーとのやり取り

指紋センサーによる読み取りを準備（warm up）するには、 `FingerprintManager#authenticate()` をコールします。

このメソッドがコールされた後、指紋センサーでスキャンすることが出来るようになります。この状態は、 AuthenticationCallback クラスの `onAuthenticationError()` か `onAuthenticationSucceeded()` が呼ばれるまで維持されます。

authenticate() メソッドの Parameter は以下となっています。

- crypto: オブジェクトの呼び出しに関連づけられる
- cancel: 認証をキャンセルできるオブジェクト
- flags: オプションフラグ（0をセットすべき）
- callback: 認証イベントを受け取るオブジェクト
- handler: callbackイベントを扱うオプションハンドラー

このうち、callback には次の4つのメソッドがあります。

- onAuthenticationError : 以下のどれにも当てはまらない復帰不可能なエラーを検出したとき
- onAuthenticationFailed : 指紋は読み取れたけど未登録と判断したとき
- onAuthenticationHelp : 復帰可能なエラーを検出したとき
- onAuthenticationSucceeded : 指紋を登録済みのものと判断したとき

## エラーハンドリングが必要なケース

この中で onAuthenticationError は、以下の場合にコールされます。
1. CancellationSignal が発動したとき
2. 所定回数、指紋の読み取りに失敗したとき

Google のサンプルでは、上記1と2の発生を区別するために、以下のように `mSelfCancelled` というフラグで処理を分岐させています。

```java
    // このメソッドはFragmentのonResume()で呼ばれます
    public void startListening(FingerprintManager.CryptoObject cryptoObject) {
        if (!isFingerprintAuthAvailable()) {
            return;
        }
        mCancellationSignal = new CancellationSignal();
        mSelfCancelled = false;
        mFingerprintManager
                .authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
        mIcon.setImageResource(R.drawable.ic_fp_40px);
    }

    // このメソッドはFragmentのonPause()で呼ばれます
    public void stopListening() {
        if (mCancellationSignal != null) {
            mSelfCancelled = true;
            mCancellationSignal.cancel();
            mCancellationSignal = null;
        }
    }
    
    @Override
    public void onAuthenticationError(int errMsgId, CharSequence errString) {
        // stopListening()が呼ばれなかったときはif文の中の処理が動きます
        if (!mSelfCancelled) {
            showError(errString);
            mIcon.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mCallback.onError();
                }
            }, ERROR_TIMEOUT_MILLIS);
        }
    }
```

ところが、1のケースには特殊なケースがありました。
authenticate() で指紋センサーの応答を待機しているときに、電源ボタンを押すと **onPause()より早いタイミングで** CancellationSignal が発動されます。

そのためサンプルコードのままだと、mSelfCancelled が false のまま onAuthenticationError() に入ってきてしまいます。

onAuthenticationError() の実装内容にもよりますが、そのことを考慮しておかないと意図しない挙動になるケースもありますので、注意した方がよさそうです。

## 回避するなら

電源ボタン押下による CancellationSignal 発動を考慮するのであれば、以下のように onAuthenticationError() を修正するのが簡単かと思います。

```java
    @Override
    public void onAuthenticationError(int errMsgId, CharSequence errString) {
        // 電源ボタンを押したときなど
        if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
            return;
        }
        
        // stopListening()が呼ばれなかったときはif文の中の処理が動きます
        if (!mSelfCancelled) {
            showError(errString);
            mIcon.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mCallback.onError();
                }
            }, ERROR_TIMEOUT_MILLIS);
        }
    }
```

## まとめ

onAuthenticationError()が呼ばれるケースは、

1. CancellationSignal が発動したとき
  - 明示的に `CancellationSignal#cancel()` を呼ぶ
  - 指紋センサー準備中に電源ボタンを押す
2. 所定回数、指紋の読み取りに失敗したとき

## 該当のリファレンス

- [FingerprintManager](http://developer.android.com/intl/ja/reference/android/hardware/fingerprint/FingerprintManager.html)
- [FingerprintManager.AuthenticationCallback](http://developer.android.com/intl/ja/reference/android/hardware/fingerprint/FingerprintManager.AuthenticationCallback.html)