FingerprintHelperFragment.java
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.biometrics;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.RestrictTo;
import androidx.biometric.R;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import androidx.fragment.app.Fragment;
import java.util.concurrent.Executor;
/**
* A fragment that wraps the FingerprintManagerCompat and has the ability to continue authentication
* across device configuration changes. This class is not meant to be preserved after process death;
* for security reasons, the BiometricPromptCompat will automatically stop authentication when the
* activity is no longer in the foreground.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class FingerprintHelperFragment extends Fragment {
private static final String TAG = "FingerprintHelperFragment";
protected static final int USER_CANCELED_FROM_NONE = 0;
protected static final int USER_CANCELED_FROM_USER = 1;
protected static final int USER_CANCELED_FROM_NEGATIVE_BUTTON = 2;
// Re-set by the application, through BiometricPromptCompat upon orientation changes.
Executor mExecutor;
BiometricPrompt.AuthenticationCallback mClientAuthenticationCallback;
// Re-set by BiometricPromptCompat upon orientation changes. This handler is used to send
// messages from the AuthenticationCallbacks to the UI.
Handler mHandler;
// Set once and retained.
private BiometricPrompt.CryptoObject mCryptoObject;
// Created once and retained.
Context mContext;
int mCanceledFrom;
private CancellationSignal mCancellationSignal;
// Also created once and retained.
private final FingerprintManagerCompat.AuthenticationCallback mAuthenticationCallback =
new FingerprintManagerCompat.AuthenticationCallback() {
@Override
public void onAuthenticationError(final int errMsgId,
final CharSequence errString) {
if (errMsgId == BiometricPrompt.ERROR_CANCELED) {
if (mCanceledFrom == USER_CANCELED_FROM_NONE) {
mHandler.obtainMessage(FingerprintDialogFragment.MSG_DISMISS_DIALOG)
.sendToTarget();
mExecutor.execute(new Runnable() {
@Override
public void run() {
mClientAuthenticationCallback
.onAuthenticationError(errMsgId, errString);
}
});
}
} else {
mHandler.obtainMessage(FingerprintDialogFragment.MSG_SHOW_ERROR, errMsgId,
0,
errString).sendToTarget();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mClientAuthenticationCallback.onAuthenticationError(
errMsgId,
errString);
}
});
}
}, FingerprintDialogFragment.HIDE_DIALOG_DELAY);
}
cleanup();
}
@Override
public void onAuthenticationHelp(final int helpMsgId,
final CharSequence helpString) {
mHandler.obtainMessage(FingerprintDialogFragment.MSG_SHOW_HELP, helpString)
.sendToTarget();
// Don't forward the result to the client, since the dialog takes care of it.
}
@Override
public void onAuthenticationSucceeded(
final FingerprintManagerCompat.AuthenticationResult result) {
mHandler.obtainMessage(
FingerprintDialogFragment.MSG_DISMISS_DIALOG).sendToTarget();
mExecutor.execute(new Runnable() {
@Override
public void run() {
mClientAuthenticationCallback.onAuthenticationSucceeded(
new BiometricPrompt.AuthenticationResult(
unwrapCryptoObject(result.getCryptoObject())));
}
});
cleanup();
}
@Override
public void onAuthenticationFailed() {
mHandler.obtainMessage(FingerprintDialogFragment.MSG_SHOW_HELP,
mContext.getResources().getString(R.string.fingerprint_not_recognized))
.sendToTarget();
mExecutor.execute(new Runnable() {
@Override
public void run() {
mClientAuthenticationCallback.onAuthenticationFailed();
}
});
}
};
/**
* Creates a new instance of the {@link FingerprintHelperFragment}.
*/
public static FingerprintHelperFragment newInstance() {
return new FingerprintHelperFragment();
}
/**
* Sets the crypto object to be associated with the authentication. Should be called before
* adding the fragment to guarantee that it's ready in onCreate().
*/
public void setCryptoObject(BiometricPrompt.CryptoObject crypto) {
mCryptoObject = crypto;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
mContext = getContext();
mCancellationSignal = new CancellationSignal();
FingerprintManagerCompat fingerprintManagerCompat = FingerprintManagerCompat.from(mContext);
if (handlePreAuthenticationErrors(fingerprintManagerCompat)) {
mHandler.obtainMessage(FingerprintDialogFragment.MSG_DISMISS_DIALOG).sendToTarget();
cleanup();
return;
}
mCanceledFrom = USER_CANCELED_FROM_NONE;
fingerprintManagerCompat.authenticate(
wrapCryptoObject(mCryptoObject),
0 /* flags */,
mCancellationSignal,
mAuthenticationCallback,
null /* handler */);
}
/**
* Sets the client's callback. This should be done whenever the lifecycle changes (orientation
* changes).
*/
protected void setCallback(Executor executor,
BiometricPrompt.AuthenticationCallback callback) {
mExecutor = executor;
mClientAuthenticationCallback = callback;
}
/**
* Pass a reference to the handler used by FingerprintDialogFragment to update the UI.
*/
protected void setHandler(Handler handler) {
mHandler = handler;
}
/**
* Cancel the authentication.
* @param canceledFrom one of the USER_CANCELED_FROM* constants
*/
protected void cancel(int canceledFrom) {
mCanceledFrom = canceledFrom;
if (canceledFrom == USER_CANCELED_FROM_USER) {
sendErrorToClient(BiometricPrompt.ERROR_USER_CANCELED);
}
if (mCancellationSignal != null) {
mCancellationSignal.cancel();
}
cleanup();
}
/**
* Remove the fragment so that resources can be freed.
*/
void cleanup() {
if (getActivity() != null) {
getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit();
}
}
/**
* Check before starting authentication for basic conditions, notifies client and returns true
* if conditions are not met
*/
private boolean handlePreAuthenticationErrors(FingerprintManagerCompat fingerprintManager) {
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
sendErrorToClient(BiometricPrompt.ERROR_HW_NOT_PRESENT);
return true;
} else if (!fingerprintManager.isHardwareDetected()) {
sendErrorToClient(BiometricPrompt.ERROR_HW_UNAVAILABLE);
return true;
} else if (!fingerprintManager.hasEnrolledFingerprints()) {
sendErrorToClient(BiometricPrompt.ERROR_NO_BIOMETRICS);
return true;
}
return false;
}
/**
* Bypasses the FingerprintManager authentication callback wrapper and sends it directly to the
* client's callback, since the UI is not even showing yet.
* @param error
*/
private void sendErrorToClient(final int error) {
mClientAuthenticationCallback.onAuthenticationError(error, getErrorString(mContext, error));
}
/**
* Only needs to provide a subset of the fingerprint error strings since the rest are translated
* in FingerprintManager
*/
private String getErrorString(Context context, int errorCode) {
switch (errorCode) {
case BiometricPrompt.ERROR_HW_NOT_PRESENT:
return context.getString(R.string.fingerprint_error_hw_not_present);
case BiometricPrompt.ERROR_HW_UNAVAILABLE:
return context.getString(R.string.fingerprint_error_hw_not_available);
case BiometricPrompt.ERROR_NO_BIOMETRICS:
return context.getString(R.string.fingerprint_error_no_fingerprints);
case BiometricPrompt.ERROR_USER_CANCELED:
return context.getString(R.string.fingerprint_error_user_canceled);
}
return null;
}
static BiometricPrompt.CryptoObject unwrapCryptoObject(
FingerprintManagerCompat.CryptoObject cryptoObject) {
if (cryptoObject == null) {
return null;
} else if (cryptoObject.getCipher() != null) {
return new BiometricPrompt.CryptoObject(cryptoObject.getCipher());
} else if (cryptoObject.getSignature() != null) {
return new BiometricPrompt.CryptoObject(cryptoObject.getSignature());
} else if (cryptoObject.getMac() != null) {
return new BiometricPrompt.CryptoObject(cryptoObject.getMac());
} else {
return null;
}
}
static FingerprintManagerCompat.CryptoObject wrapCryptoObject(
BiometricPrompt.CryptoObject cryptoObject) {
if (cryptoObject == null) {
return null;
} else if (cryptoObject.getCipher() != null) {
return new FingerprintManagerCompat.CryptoObject(cryptoObject.getCipher());
} else if (cryptoObject.getSignature() != null) {
return new FingerprintManagerCompat.CryptoObject(cryptoObject.getSignature());
} else if (cryptoObject.getMac() != null) {
return new FingerprintManagerCompat.CryptoObject(cryptoObject.getMac());
} else {
return null;
}
}
}