blob: 12ecf70e82524290c96e59762747af3248b77cf6 [file] [log] [blame]
/*
* Copyright 2020 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.biometric;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
/**
* A container for data associated with an ongoing authentication session, including intermediate
* values needed to display the prompt UI.
*
* <p>This model and all of its data is persisted over the lifetime of the client activity that
* hosts the {@link BiometricPrompt}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class BiometricViewModel extends ViewModel {
/**
* The default executor provided when {@link #getClientExecutor()} is called before
* {@link #setClientExecutor(Executor)}.
*/
private static class DefaultExecutor implements Executor {
private final Handler mHandler = new Handler(Looper.getMainLooper());
@SuppressWarnings("WeakerAccess") /* synthetic access */
DefaultExecutor() {}
@Override
public void execute(Runnable runnable) {
mHandler.post(runnable);
}
}
/**
* The authentication callback listener passed to {@link AuthenticationCallbackProvider} when
* {@link #getAuthenticationCallbackProvider()} is called.
*/
private static final class CallbackListener extends AuthenticationCallbackProvider.Listener {
@NonNull private final WeakReference<BiometricViewModel> mViewModelRef;
/**
* Creates a callback listener with a weak reference to the given view model.
*
* @param viewModel The view model instance to hold a weak reference to.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
CallbackListener(@Nullable BiometricViewModel viewModel) {
mViewModelRef = new WeakReference<>(viewModel);
}
@Override
void onSuccess(@NonNull BiometricPrompt.AuthenticationResult result) {
if (mViewModelRef.get() != null && mViewModelRef.get().isAwaitingResult()) {
// Try to infer the authentication type if unknown.
if (result.getAuthenticationType()
== BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN) {
result = new BiometricPrompt.AuthenticationResult(
result.getCryptoObject(),
mViewModelRef.get().getInferredAuthenticationResultType());
}
mViewModelRef.get().setAuthenticationResult(result);
}
}
@Override
void onError(int errorCode, @Nullable CharSequence errorMessage) {
if (mViewModelRef.get() != null
&& !mViewModelRef.get().isConfirmingDeviceCredential()
&& mViewModelRef.get().isAwaitingResult()) {
mViewModelRef.get().setAuthenticationError(
new BiometricErrorData(errorCode, errorMessage));
}
}
@Override
void onHelp(@Nullable CharSequence helpMessage) {
if (mViewModelRef.get() != null) {
mViewModelRef.get().setAuthenticationHelpMessage(helpMessage);
}
}
@Override
void onFailure() {
if (mViewModelRef.get() != null && mViewModelRef.get().isAwaitingResult()) {
mViewModelRef.get().setAuthenticationFailurePending(true);
}
}
}
/**
* The dialog listener that is returned by {@link #getNegativeButtonListener()}.
*/
private static class NegativeButtonListener implements DialogInterface.OnClickListener {
@NonNull private final WeakReference<BiometricViewModel> mViewModelRef;
/**
* Creates a negative button listener with a weak reference to the given view model.
*
* @param viewModel The view model instance to hold a weak reference to.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
NegativeButtonListener(@Nullable BiometricViewModel viewModel) {
mViewModelRef = new WeakReference<>(viewModel);
}
@Override
public void onClick(DialogInterface dialogInterface, int which) {
if (mViewModelRef.get() != null) {
mViewModelRef.get().setNegativeButtonPressPending(true);
}
}
}
/**
* The executor that will run authentication callback methods.
*
* <p>If unset, callbacks are invoked on the main thread with {@link Looper#getMainLooper()}.
*/
@Nullable private Executor mClientExecutor;
/**
* The callback object that will receive authentication events.
*/
@Nullable private BiometricPrompt.AuthenticationCallback mClientCallback;
/**
* Reference to latest {@link androidx.fragment.app.FragmentActivity} hosting BiometricPrompt
*/
@NonNull private WeakReference<FragmentActivity> mClientActivity;
/**
* Info about the appearance and behavior of the prompt provided by the client application.
*/
@Nullable private BiometricPrompt.PromptInfo mPromptInfo;
/**
* The crypto object associated with the current authentication session.
*/
@Nullable private BiometricPrompt.CryptoObject mCryptoObject;
/**
* A provider for cross-platform compatible authentication callbacks.
*/
@Nullable private AuthenticationCallbackProvider mAuthenticationCallbackProvider;
/**
* A provider for cross-platform compatible cancellation signal objects.
*/
@Nullable private CancellationSignalProvider mCancellationSignalProvider;
/**
* A dialog listener for the negative button shown on the prompt.
*/
@Nullable private DialogInterface.OnClickListener mNegativeButtonListener;
/**
* A label for the negative button shown on the prompt.
*
* <p>If set, this value overrides the one returned by
* {@link BiometricPrompt.PromptInfo#getNegativeButtonText()}.
*/
@Nullable private CharSequence mNegativeButtonTextOverride;
/**
* An integer indicating where the dialog was last canceled from.
*/
@BiometricFragment.CanceledFrom
private int mCanceledFrom = BiometricFragment.CANCELED_FROM_INTERNAL;
/**
* Whether the prompt is currently showing.
*/
private boolean mIsPromptShowing;
/**
* Whether the client callback is awaiting an authentication result.
*/
private boolean mIsAwaitingResult;
/**
* Whether the user is currently authenticating with their PIN, pattern, or password.
*/
private boolean mIsConfirmingDeviceCredential;
/**
* Whether the prompt should delay showing the authentication UI.
*/
private boolean mIsDelayingPrompt;
/**
* Whether the prompt should ignore cancel requests not initiated by the client.
*/
private boolean mIsIgnoringCancel;
/**
* Information associated with a successful authentication attempt.
*/
@Nullable private MutableLiveData<BiometricPrompt.AuthenticationResult> mAuthenticationResult;
/**
* Information associated with an unrecoverable authentication error.
*/
@Nullable private MutableLiveData<BiometricErrorData> mAuthenticationError;
/**
* A human-readable message describing a recoverable authentication error or event.
*/
@Nullable private MutableLiveData<CharSequence> mAuthenticationHelpMessage;
/**
* Whether an unrecognized biometric has been presented.
*/
@Nullable private MutableLiveData<Boolean> mIsAuthenticationFailurePending;
/**
* Whether the user has pressed the negative button on the prompt.
*/
@Nullable private MutableLiveData<Boolean> mIsNegativeButtonPressPending;
/**
* Whether the fingerprint dialog should always be dismissed instantly.
*/
private boolean mIsFingerprintDialogDismissedInstantly = true;
/**
* Whether the user has manually canceled out of the fingerprint dialog.
*/
@Nullable private MutableLiveData<Boolean> mIsFingerprintDialogCancelPending;
/**
* The previous state of the fingerprint dialog UI.
*/
@FingerprintDialogFragment.State
private int mFingerprintDialogPreviousState = FingerprintDialogFragment.STATE_NONE;
/**
* The current state of the fingerprint dialog UI.
*/
@Nullable private MutableLiveData<Integer> mFingerprintDialogState;
/**
* A human-readable message to be displayed below the icon on the fingerprint dialog.
*/
@Nullable private MutableLiveData<CharSequence> mFingerprintDialogHelpMessage;
@NonNull
Executor getClientExecutor() {
return mClientExecutor != null ? mClientExecutor : new DefaultExecutor();
}
void setClientExecutor(@NonNull Executor clientExecutor) {
mClientExecutor = clientExecutor;
}
@NonNull
BiometricPrompt.AuthenticationCallback getClientCallback() {
if (mClientCallback == null) {
mClientCallback = new BiometricPrompt.AuthenticationCallback() {};
}
return mClientCallback;
}
void setClientCallback(@NonNull BiometricPrompt.AuthenticationCallback clientCallback) {
mClientCallback = clientCallback;
}
/**
* Returns reference to latest activity hosting BiometricPrompt or null if activity has
* already been destroyed
* @return Reference to latest activity hosting BiometricPrompt
*/
@Nullable
public FragmentActivity getClientActivity() {
return mClientActivity.get();
}
/**
* Updates reference to latest activity hosting BiometricPrompt
* @param clientActivity Reference to latest activity hosting BiometricPrompt
*/
void setClientActivity(@NonNull FragmentActivity clientActivity) {
mClientActivity = new WeakReference<>(clientActivity);
}
void setPromptInfo(@Nullable BiometricPrompt.PromptInfo promptInfo) {
mPromptInfo = promptInfo;
}
/**
* Gets the title to be shown on the biometric prompt.
*
* <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
* {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
*
* @return The title for the prompt, or {@code null} if not set.
*/
@Nullable
CharSequence getTitle() {
return mPromptInfo != null ? mPromptInfo.getTitle() : null;
}
/**
* Gets the subtitle to be shown on the biometric prompt.
*
* <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
* {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
*
* @return The subtitle for the prompt, or {@code null} if not set.
*/
@Nullable
CharSequence getSubtitle() {
return mPromptInfo != null ? mPromptInfo.getSubtitle() : null;
}
/**
* Gets the description to be shown on the biometric prompt.
*
* <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
* {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
*
* @return The description for the prompt, or {@code null} if not set.
*/
@Nullable
CharSequence getDescription() {
return mPromptInfo != null ? mPromptInfo.getDescription() : null;
}
/**
* Gets the text that should be shown for the negative button on the biometric prompt.
*
* <p>If non-null, the value set by {@link #setNegativeButtonTextOverride(CharSequence)} is
* used. Otherwise, falls back to the value returned by
* {@link BiometricPrompt.PromptInfo#getNegativeButtonText()}, or {@code null} if a non-null
* {@link BiometricPrompt.PromptInfo} has not been set by
* {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
*
* @return The negative button text for the prompt, or {@code null} if not set.
*/
@Nullable
CharSequence getNegativeButtonText() {
if (mNegativeButtonTextOverride != null) {
return mNegativeButtonTextOverride;
} else if (mPromptInfo != null) {
return mPromptInfo.getNegativeButtonText();
} else {
return null;
}
}
/**
* Checks if the confirmation required option is enabled for the biometric prompt.
*
* <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
* {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
*
* @return Whether the confirmation required option is enabled.
*/
boolean isConfirmationRequired() {
return mPromptInfo == null || mPromptInfo.isConfirmationRequired();
}
/**
* Gets the type(s) of authenticators that may be invoked by the biometric prompt.
*
* <p>If a non-null {@link BiometricPrompt.PromptInfo} has been set by
* {@link #setPromptInfo(BiometricPrompt.PromptInfo)}, this is the single consolidated set of
* authenticators allowed by the prompt, taking into account the values of
* {@link BiometricPrompt.PromptInfo#getAllowedAuthenticators()},
* {@link BiometricPrompt.PromptInfo#isDeviceCredentialAllowed()}, and
* {@link #getCryptoObject()}.
*
* @return A bit field representing all valid authenticator types that may be invoked by
* the prompt, or 0 if not set.
*/
@SuppressWarnings("deprecation")
@BiometricManager.AuthenticatorTypes
int getAllowedAuthenticators() {
return mPromptInfo != null
? AuthenticatorUtils.getConsolidatedAuthenticators(mPromptInfo, mCryptoObject)
: 0;
}
@Nullable
BiometricPrompt.CryptoObject getCryptoObject() {
return mCryptoObject;
}
void setCryptoObject(@Nullable BiometricPrompt.CryptoObject cryptoObject) {
mCryptoObject = cryptoObject;
}
@NonNull
AuthenticationCallbackProvider getAuthenticationCallbackProvider() {
if (mAuthenticationCallbackProvider == null) {
mAuthenticationCallbackProvider =
new AuthenticationCallbackProvider(new CallbackListener(this));
}
return mAuthenticationCallbackProvider;
}
@NonNull
CancellationSignalProvider getCancellationSignalProvider() {
if (mCancellationSignalProvider == null) {
mCancellationSignalProvider = new CancellationSignalProvider();
}
return mCancellationSignalProvider;
}
@NonNull
DialogInterface.OnClickListener getNegativeButtonListener() {
if (mNegativeButtonListener == null) {
mNegativeButtonListener = new NegativeButtonListener(this);
}
return mNegativeButtonListener;
}
void setNegativeButtonTextOverride(@Nullable CharSequence negativeButtonTextOverride) {
mNegativeButtonTextOverride = negativeButtonTextOverride;
}
int getCanceledFrom() {
return mCanceledFrom;
}
void setCanceledFrom(int canceledFrom) {
mCanceledFrom = canceledFrom;
}
boolean isPromptShowing() {
return mIsPromptShowing;
}
void setPromptShowing(boolean promptShowing) {
mIsPromptShowing = promptShowing;
}
boolean isAwaitingResult() {
return mIsAwaitingResult;
}
void setAwaitingResult(boolean awaitingResult) {
mIsAwaitingResult = awaitingResult;
}
boolean isConfirmingDeviceCredential() {
return mIsConfirmingDeviceCredential;
}
void setConfirmingDeviceCredential(boolean confirmingDeviceCredential) {
mIsConfirmingDeviceCredential = confirmingDeviceCredential;
}
boolean isDelayingPrompt() {
return mIsDelayingPrompt;
}
void setDelayingPrompt(boolean delayingPrompt) {
mIsDelayingPrompt = delayingPrompt;
}
boolean isIgnoringCancel() {
return mIsIgnoringCancel;
}
void setIgnoringCancel(boolean ignoringCancel) {
mIsIgnoringCancel = ignoringCancel;
}
@NonNull
LiveData<BiometricPrompt.AuthenticationResult> getAuthenticationResult() {
if (mAuthenticationResult == null) {
mAuthenticationResult = new MutableLiveData<>();
}
return mAuthenticationResult;
}
void setAuthenticationResult(
@Nullable BiometricPrompt.AuthenticationResult authenticationResult) {
if (mAuthenticationResult == null) {
mAuthenticationResult = new MutableLiveData<>();
}
updateValue(mAuthenticationResult, authenticationResult);
}
@NonNull
MutableLiveData<BiometricErrorData> getAuthenticationError() {
if (mAuthenticationError == null) {
mAuthenticationError = new MutableLiveData<>();
}
return mAuthenticationError;
}
void setAuthenticationError(@Nullable BiometricErrorData authenticationError) {
if (mAuthenticationError == null) {
mAuthenticationError = new MutableLiveData<>();
}
updateValue(mAuthenticationError, authenticationError);
}
@NonNull
LiveData<CharSequence> getAuthenticationHelpMessage() {
if (mAuthenticationHelpMessage == null) {
mAuthenticationHelpMessage = new MutableLiveData<>();
}
return mAuthenticationHelpMessage;
}
void setAuthenticationHelpMessage(
@Nullable CharSequence authenticationHelpMessage) {
if (mAuthenticationHelpMessage == null) {
mAuthenticationHelpMessage = new MutableLiveData<>();
}
updateValue(mAuthenticationHelpMessage, authenticationHelpMessage);
}
@NonNull
LiveData<Boolean> isAuthenticationFailurePending() {
if (mIsAuthenticationFailurePending == null) {
mIsAuthenticationFailurePending = new MutableLiveData<>();
}
return mIsAuthenticationFailurePending;
}
void setAuthenticationFailurePending(boolean authenticationFailurePending) {
if (mIsAuthenticationFailurePending == null) {
mIsAuthenticationFailurePending = new MutableLiveData<>();
}
updateValue(mIsAuthenticationFailurePending, authenticationFailurePending);
}
@NonNull
LiveData<Boolean> isNegativeButtonPressPending() {
if (mIsNegativeButtonPressPending == null) {
mIsNegativeButtonPressPending = new MutableLiveData<>();
}
return mIsNegativeButtonPressPending;
}
void setNegativeButtonPressPending(boolean negativeButtonPressPending) {
if (mIsNegativeButtonPressPending == null) {
mIsNegativeButtonPressPending = new MutableLiveData<>();
}
updateValue(mIsNegativeButtonPressPending, negativeButtonPressPending);
}
boolean isFingerprintDialogDismissedInstantly() {
return mIsFingerprintDialogDismissedInstantly;
}
void setFingerprintDialogDismissedInstantly(
boolean fingerprintDialogDismissedInstantly) {
mIsFingerprintDialogDismissedInstantly = fingerprintDialogDismissedInstantly;
}
@NonNull
LiveData<Boolean> isFingerprintDialogCancelPending() {
if (mIsFingerprintDialogCancelPending == null) {
mIsFingerprintDialogCancelPending = new MutableLiveData<>();
}
return mIsFingerprintDialogCancelPending;
}
void setFingerprintDialogCancelPending(boolean fingerprintDialogCancelPending) {
if (mIsFingerprintDialogCancelPending == null) {
mIsFingerprintDialogCancelPending = new MutableLiveData<>();
}
updateValue(mIsFingerprintDialogCancelPending, fingerprintDialogCancelPending);
}
@FingerprintDialogFragment.State
int getFingerprintDialogPreviousState() {
return mFingerprintDialogPreviousState;
}
void setFingerprintDialogPreviousState(
@FingerprintDialogFragment.State int fingerprintDialogPreviousState) {
mFingerprintDialogPreviousState = fingerprintDialogPreviousState;
}
@NonNull
LiveData<Integer> getFingerprintDialogState() {
if (mFingerprintDialogState == null) {
mFingerprintDialogState = new MutableLiveData<>();
}
return mFingerprintDialogState;
}
void setFingerprintDialogState(
@FingerprintDialogFragment.State int fingerprintDialogState) {
if (mFingerprintDialogState == null) {
mFingerprintDialogState = new MutableLiveData<>();
}
updateValue(mFingerprintDialogState, fingerprintDialogState);
}
@NonNull
LiveData<CharSequence> getFingerprintDialogHelpMessage() {
if (mFingerprintDialogHelpMessage == null) {
mFingerprintDialogHelpMessage = new MutableLiveData<>();
}
return mFingerprintDialogHelpMessage;
}
void setFingerprintDialogHelpMessage(
@NonNull CharSequence fingerprintDialogHelpMessage) {
if (mFingerprintDialogHelpMessage == null) {
mFingerprintDialogHelpMessage = new MutableLiveData<>();
}
updateValue(mFingerprintDialogHelpMessage, fingerprintDialogHelpMessage);
}
/**
* Attempts to infer the type of authenticator that was used to authenticate the user.
*
* @return The inferred authentication type, or
* {@link BiometricPrompt#AUTHENTICATION_RESULT_TYPE_UNKNOWN} if unknown.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
@BiometricPrompt.AuthenticationResultType
int getInferredAuthenticationResultType() {
@BiometricManager.AuthenticatorTypes final int authenticators = getAllowedAuthenticators();
if (AuthenticatorUtils.isSomeBiometricAllowed(authenticators)
&& !AuthenticatorUtils.isDeviceCredentialAllowed(authenticators)) {
return BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC;
}
return BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN;
}
/**
* Ensures the value of a given mutable live data object is updated on the main thread.
*
* @param liveData The mutable live data object whose value should be updated.
* @param value The new value to be set for the mutable live data object.
*/
private static <T> void updateValue(MutableLiveData<T> liveData, T value) {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
liveData.setValue(value);
} else {
liveData.postValue(value);
}
}
}