1. 程式人生 > >Android指紋識別API講解,一種更快更好的使用者體驗

Android指紋識別API講解,一種更快更好的使用者體驗

 我發現了一個比較怪的現象。在iPhone上使用十分普遍的指紋認證功能,在Android手機上卻鮮有APP使用,我簡單觀察了一下,發現Android手機上基本上只有支付寶、微信和極少APP支援指紋認證功能,就連銀行和金融類的應用都基本不支援,甚至很多開發者都不知道Android系統是有指紋認證的官方API的。

事實上,Android從6.0系統開始就支援指紋認證功能了,但是指紋功能還需要有硬體支援才行,而Android手機的硬體都是由各廠商生產的,手機檔次也參差不齊,因此不能像iPhone那樣保證所有的手機都是支援指紋認證功能的。所以,可能很多開發者就覺得,即使做了指紋認證功能,也無法相容所有的手機,還是要配合圖案解鎖或密碼等功能一起使用才行,那麼索性就只用圖案和密碼好了,一勞永逸。

看似這樣解釋好像也合情合理,但其實受傷的是數以億計的Android手機使用者。明明有更輕鬆更快捷的使用方式,卻因為APP不予支援,最終只能使用更加原始和笨拙的方式。在國內,絕大多數Android手機的指紋認證功能都僅僅只侷限於用來解鎖手機而已,很少有使用到APP的功能邏輯當中。

其實將指紋認證功能使用到APP的功能邏輯當中是有很多功能場景的,比如說金融銀行類APP可以使用指紋認證來快速登入,應用商店類APP可以使用指紋認證來下載安裝軟體,股票證券類APP可以使用指紋認證來操作和交易等等。

雖然有了應用場景,還有很多開發者可能會擔心,指紋認證功能實現起來會不會很複雜?因為畢竟支援的裝置有限,還要配合圖案和密碼來使用才行,如果實現起來非常複雜,又只能支援部分裝置的話,那投入產出比就太低了,或許這也是很多APP不肯去實現指紋認證功能的原因。這裡我不得不說,Android官方提供的指紋認證Demo的確是挺複雜的,看著讓人望而卻步。但是大家不用擔心,本篇文章中我會帶著大家一起去實現一個最簡版的指紋認證Demo,直接複製貼上本文中的程式碼到大家各自的專案中,即可一步整合指紋認證功能。

那麼話不多說,首先新建一個FingerprintTest專案,並選擇新增一個Empty Activity。然後修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="已進入App主介面"
        android:textSize="18sp"
        android:layout_gravity="center"
        />

</FrameLayout>

這裡我們修改了MainActivity中的佈局檔案,在介面上添加了一個 已進入App主介面 的TextView,待會在指紋認證通過之後,就會讓APP跳轉到此介面。

接下來我們開始編寫指紋認證介面,新建fingerprint_dialog.xml,程式碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:src="@drawable/ic_fp_40px"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="20dp"
        android:text="請驗證指紋解鎖"
        android:textColor="#000"
        android:textSize="16sp"
        />

    <TextView
        android:id="@+id/error_msg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="5dp"
        android:maxLines="1"
        android:textSize="12sp"
        android:textColor="#f45"
        />

    <View
        android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:layout_marginTop="10dp"
        android:background="#ccc"
        />

    <TextView
        android:id="@+id/cancel"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:text="取消"
        android:textColor="#5d7883"
        android:textSize="16sp"
        />

</LinearLayout>

這是一個非常簡易的指紋認證介面,相信沒什麼需要解釋的地方。介面大致樣式如下圖所示。

注意,通常為了讓使用者清楚的知道現在需要進行指紋認證,Google官方建議最好使用一個通用的指紋圖示,而不應該由各APP製作自己的指紋圖示。為此,Google也特意提供了一套指紋認證的組圖,可以 點選這裡 檢視和下載。

接著我們建立一個FingerprintDialogFragment類,並讓它繼承自DialogFragment,用於作為提示使用者進行指紋認證的對話方塊,程式碼如下所示:

@TargetApi(23)
public class FingerprintDialogFragment extends DialogFragment {

    private FingerprintManager fingerprintManager;

    private CancellationSignal mCancellationSignal;

    private Cipher mCipher;

    private LoginActivity mActivity;

    private TextView errorMsg;

    /**
     * 標識是否是使用者主動取消的認證。
     */
    private boolean isSelfCancelled;

    public void setCipher(Cipher cipher) {
        mCipher = cipher;
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mActivity = (LoginActivity) getActivity();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        fingerprintManager = getContext().getSystemService(FingerprintManager.class);
        setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fingerprint_dialog, container, false);
        errorMsg = v.findViewById(R.id.error_msg);
        TextView cancel = v.findViewById(R.id.cancel);
        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
                stopListening();
            }
        });
        return v;
    }

    @Override
    public void onResume() {
        super.onResume();
        // 開始指紋認證監聽
        startListening(mCipher);
    }

    @Override
    public void onPause() {
        super.onPause();
        // 停止指紋認證監聽
        stopListening();
    }

    private void startListening(Cipher cipher) {
        isSelfCancelled = false;
        mCancellationSignal = new CancellationSignal();
        fingerprintManager.authenticate(new FingerprintManager.CryptoObject(cipher), mCancellationSignal, 0, new FingerprintManager.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                if (!isSelfCancelled) {
                    errorMsg.setText(errString);
                    if (errorCode == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
                        Toast.makeText(mActivity, errString, Toast.LENGTH_SHORT).show();
                        dismiss();
                    }
                }
            }

            @Override
            public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
                errorMsg.setText(helpString);
            }

            @Override
            public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
                Toast.makeText(mActivity, "指紋認證成功", Toast.LENGTH_SHORT).show();
                mActivity.onAuthenticated();
            }

            @Override
            public void onAuthenticationFailed() {
                errorMsg.setText("指紋認證失敗,請再試一次");
            }
        }, null);
    }

    private void stopListening() {
        if (mCancellationSignal != null) {
            mCancellationSignal.cancel();
            mCancellationSignal = null;
            isSelfCancelled = true;
        }
    }

}

說了是實現一個最簡版的指紋認證Demo,因此這裡的程式碼也都是非常簡單的,基本上就是一個Fragment類的最普通實現,下面我帶大家簡單解析一下。

首先setCipher()方法用於接受一個Cipher物件,這個引數在待會進行指紋認證的時候會用到。

接下來幾個生命週期方法都很簡單,在onAttach()方法中獲取了Activity的例項,在onCreate()方法獲取了FingerprintManager的例項,在onCreateView()方法中載入了我們剛剛建立的fingerprint_dialog.xml佈局,都是一些常規操作。

緊接著重點的要來了,在onResume()方法中呼叫了startListening()方法開始指紋認證監聽,在onPause()方法中呼叫了stopListening()方法停止指紋認證監聽。為什麼要這麼做呢?因為指紋感測器和攝像頭類似,是不能多個程式同時使用的,因此任何一個程式都不應該在非前臺時刻佔用著指紋感測器的資源,所以需要在onPause()方法中及時釋放資源。

那麼,現在我們只需要把所有的目光都放在startListening()和stopListening()這兩個方法上就可以了。在startListening()方法中,呼叫了FingerprintManager的authenticate()方法來開啟指紋指紋監聽。authenticate()方法接收五個引數,第一個引數是CryptoObject物件,這裡我們只需要將剛才傳入的Cipher物件包裝成CryptoObject物件就可以了。第二個引數是CancellationSignal物件,可以使用它來取消指紋認證操作。第三個引數是可選引數,官方的建議是直接傳0就可以了。第四個引數用於接收指紋認證的回撥,上述程式碼中我將所有的回撥可能都進行了介面提示,方便大家觀察。第五個引數用於指定處理回撥的Handler,這裡直接傳null表示回撥到主執行緒即可。

而在stopListening()方法中的邏輯則簡單得多了,我們只需要呼叫CancellationSignal的cancel()方法將指紋認證操作取消就可以了。

這樣我們就將FingerprintDialogFragment中的程式碼全部完成了,這段程式碼可以直接複製到任意專案當中來作為指紋認證提醒對話方塊。

最後,我們再來編寫一個簡單的登入介面,整個指紋認證過程就完整了。建立LoginActivity,程式碼如下所示:

public class LoginActivity extends AppCompatActivity {

    private static final String DEFAULT_KEY_NAME = "default_key";

    KeyStore keyStore;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        if (supportFingerprint()) {
            initKey();
            initCipher();
        }
    }

    public boolean supportFingerprint() {
        if (Build.VERSION.SDK_INT < 23) {
            Toast.makeText(this, "您的系統版本過低,不支援指紋功能", Toast.LENGTH_SHORT).show();
            return false;
        } else {
            KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
            FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class);
            if (!fingerprintManager.isHardwareDetected()) {
                Toast.makeText(this, "您的手機不支援指紋功能", Toast.LENGTH_SHORT).show();
                return false;
            } else if (!keyguardManager.isKeyguardSecure()) {
                Toast.makeText(this, "您還未設定鎖屏,請先設定鎖屏並新增一個指紋", Toast.LENGTH_SHORT).show();
                return false;
            } else if (!fingerprintManager.hasEnrolledFingerprints()) {
                Toast.makeText(this, "您至少需要在系統設定中新增一個指紋", Toast.LENGTH_SHORT).show();
                return false;
            }
        }
        return true;
    }

    @TargetApi(23)
    private void initKey() {
        try {
            keyStore = KeyStore.getInstance("AndroidKeyStore");
            keyStore.load(null);
            KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
            KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(DEFAULT_KEY_NAME,
                    KeyProperties.PURPOSE_ENCRYPT |
                            KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setUserAuthenticationRequired(true)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
            keyGenerator.init(builder.build());
            keyGenerator.generateKey();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @TargetApi(23)
    private void initCipher() {
        try {
            SecretKey key = (SecretKey) keyStore.getKey(DEFAULT_KEY_NAME, null);
            Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                    + KeyProperties.BLOCK_MODE_CBC + "/"
                    + KeyProperties.ENCRYPTION_PADDING_PKCS7);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            showFingerPrintDialog(cipher);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void showFingerPrintDialog(Cipher cipher) {
        FingerprintDialogFragment fragment = new FingerprintDialogFragment();
        fragment.setCipher(cipher);
        fragment.show(getFragmentManager(), "fingerprint");
    }

    public void onAuthenticated() {
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        finish();
    }

}

首先在onCreate()方法中,呼叫了supportFingerprint()方法來判斷當前裝置是否支援指紋認證功能。這一點是非常重要的,因為當裝置不支援指紋認證的時候,還需要及時切換到如圖案、密碼等其他的認證方式。

當裝置支援指紋認證的時候,再分為兩步,第一步生成一個對稱加密的Key,第二步生成一個Cipher物件,這都是Android指紋認證API要求的標準用法。得到了Cipher物件之後,我們建立FingerprintDialogFragment的例項,並將Cipher物件傳入,再將FingerprintDialogFragment顯示出來就可以了。

最後的最後,當指紋認證成功之後,會在FingerprintDialogFragment的回調當中呼叫LoginActivity的onAuthenticated()方法,然後介面會跳轉到MainActivity,整個指紋認證過程就此結束。

總共就這些程式碼了,總體來說還是相當簡單的,現在我們來執行一下看看實際的效果吧。開啟應用之後會立刻彈出指紋認證對話方塊,此時先使用錯誤的手指來進行認證:

可以看到,當指紋驗證失敗的時候,會在介面上顯示相應的錯誤提示資訊。

接下來使用正確的手指來進行認證:

OK,指紋驗證成功,並自動跳轉到了MainActivity介面。

這樣一個最簡版的指紋認證Demo就此完成,大家如果想要在自己的APP中整合指紋認證功能,只需要複製貼上本文中的程式碼就可以輕鬆實現了。如果想要下載完整的Demo原始碼,點選這裡 即可下載。

在文章的結尾我還想再補充幾句,雖然本文中的指紋認證Demo實現過程很簡單,但是切記它是不能單獨使用的,必須要配合著圖案或其他認證方式一起來使用,因為一定要提供一個在裝置不支援指紋情況下的其他認證方式。

另外,比較遺憾的是,雖然是剛剛寫出來的文章,但是FingerprintManager在最新的Android 9.0系統上已經被廢棄了。因為Android 9.0系統提供了更加強大的生物識別認證功能,包括指紋識別、面部識別、甚至是虹膜識別等等,因此僅僅只能用於指紋識別的FingerprintManager已經不能滿足新系統的強大需求了。

不過大家也不用擔心,雖然被標為廢棄,但是至少在較長一段時間內,FingerprintManager還是可以正常使用的。而我過段時間也會針對Android 9.0的生物識別功能專門再寫一篇文章,敬請期待吧。