Skip to content

Commit

Permalink
Android: Fix #558: BiometricErrorInfo contains localized reason of fa…
Browse files Browse the repository at this point in the history
…ilure.

- Fixed mapping from BiometricStatus.NOT_ENROLLED to PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED
- BiometricAuthentication.getBiometricDialogResources() is now static (as intended)
  • Loading branch information
hvge committed Sep 25, 2023
1 parent b27e804 commit 4fe232d
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 77 deletions.
33 changes: 18 additions & 15 deletions docs/PowerAuth-SDK-for-Android.md
Original file line number Diff line number Diff line change
Expand Up @@ -1909,14 +1909,11 @@ powerAuthSDK.authenticateUsingBiometrics(context, fragment, "Sign in", "Use the
}

override fun onBiometricDialogFailed(error: PowerAuthErrorException) {
if (error.additionalInformation == BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON) {
// Application should display error in its own UI
when (error.powerAuthErrorCode) {
PowerAuthErrorCodes.BIOMETRY_LOCKOUT -> println("Lockout")
PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE -> println("Not available, try later")
PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED -> println("Fingerprint or face not recognized") // check inline documentation for more details
PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED -> println("Device has no biometry sensor")
PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED -> println("Device has no biometry data enrolled")
val biometricErrorInfo = error.additionalInformation as? BiometricErrorInfo
if (biometricErrorInfo != null) {
if (biometricErrorInfo.isErrorPresentationRequired) {
// Application should present reason of biometric authentication failure to the user
val localizedMessage = biometricErrorInfo.getLocalizedErrorMessage(context, null)
}
}
}
Expand Down Expand Up @@ -2615,10 +2612,13 @@ when (t) {
PowerAuthErrorCodes.TIME_SYNCHRONIZATION -> Log.d(TAG, "Failed to synchronize time with the server.")
}
// Process additional information
when (t.additionalInformation) {
BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON -> {
// Application should display error dialog after failed biometric authentication. This is relevant only
// if you disabled the biometric error dialog provided by PowerAuth mobile SDK.
val additionalInfo = error.additionalInformation
when (additionalInfo) {
is BiometricErrorInfo -> {
if (additionalInfo.isErrorPresentationRequired) {
// Application should display error dialog after failed biometric authentication. This is relevant only
// if you disabled the biometric error dialog provided by PowerAuth mobile SDK.
}
}
}
}
Expand Down Expand Up @@ -2681,9 +2681,12 @@ if (t instanceof PowerAuthErrorException) {
android.util.Log.d(TAG,"Failed to synchronize time with the server."); break;
}
// Process additional information
if (BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON.equals(exception.getAdditionalInformation())) {
// Application should display error dialog after failed biometric authentication. This is relevant only
// if you disabled the biometric error dialog provided by PowerAuth mobile SDK.
if (exception.getAdditionalInformation() instanceof BiometricErrorInfo) {
BiometricErrorInfo biometricErrorInfo = (BiometricErrorInfo) exception.getAdditionalInformation();
if (biometricErrorInfo.isErrorPresentationRequired()) {
// Application should display error dialog after failed biometric authentication. This is relevant only
// if you disabled the biometric error dialog provided by PowerAuth mobile SDK.
}
}

} else if (t instanceof ErrorResponseApiException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void onBiometricKeyUnavailable() {
} catch (IllegalArgumentException e) {
// Failed to authenticate due to a wrong configuration.
PowerAuthLog.e("BiometricAuthentication.authenticate() failed with exception: " + e.getMessage());
exception = new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, e.getMessage());
exception = new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, e.getMessage(), e);
status = BiometricStatus.NOT_AVAILABLE;
}
}
Expand All @@ -177,12 +177,12 @@ public void onBiometricKeyUnavailable() {
exception = BiometricHelper.getExceptionForBiometricStatus(status);
}
if (requestData.isErrorDialogDisabled()) {
// Error dialog is disabled, so report the error immediately. Use "no visible reason" hint.
dispatcher.dispatchError(BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON.addToException(exception));
// Error dialog is disabled, so report the error immediately. Use hint that error should be presented.
dispatcher.dispatchError(BiometricErrorInfo.addToException(exception, true));
return dispatcher.getCancelableTask();
} else {
// Error dialog is not disabled, so we can show it. Use "visible reason" hint.
return showErrorDialog(status, BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON.addToException(exception), context, requestData);
// Error dialog is not disabled, so we can show it. Use hint that error was already presented.
return showErrorDialog(status, BiometricErrorInfo.addToException(exception, false), context, requestData);
}
}
}
Expand Down Expand Up @@ -249,7 +249,7 @@ public void run() {
final FragmentManager fragmentManager = requestData.getFragmentManager();

final BiometricDialogResources resources = requestData.getResources();
final Pair<Integer, Integer> titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(status, resources);
final Pair<Integer, Integer> titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(status, resources.strings);

final BiometricErrorDialogFragment dialogFragment = new BiometricErrorDialogFragment.Builder(context)
.setTitle(titleDescription.first)
Expand Down Expand Up @@ -311,7 +311,7 @@ public static void setBiometricDialogResources(@NonNull BiometricDialogResources
/**
* @return Shared instance of {@link BiometricDialogResources} object.
*/
public @NonNull BiometricDialogResources getBiometricDialogResources() {
public static @NonNull BiometricDialogResources getBiometricDialogResources() {
synchronized (SharedContext.class) {
return getContext().getBiometricDialogResources();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,137 @@

package io.getlime.security.powerauth.biometry;

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.getlime.security.powerauth.biometry.impl.BiometricHelper;
import io.getlime.security.powerauth.exception.PowerAuthErrorCodes;
import io.getlime.security.powerauth.exception.PowerAuthErrorException;

/**
* The {@code BiometricErrorInfo} enumeration contains an information associated with {@link PowerAuthErrorException}.
* The enumeration is available only if the exception's error code is one of:
* The {@code BiometricErrorInfo} class contains an information associated with {@link PowerAuthErrorException}.
* The class is available only if the exception's error code is one of:
* <ul>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_LOCKOUT}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_AVAILABLE}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_RECOGNIZED}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_SUPPORTED}</li>
* <li>{@link PowerAuthErrorCodes#BIOMETRY_NOT_ENROLLED}</li>
* </ul>
* The information is typically available in {@link PowerAuthErrorException#getAdditionalInformation()}.
*/
public enum BiometricErrorInfo {
public class BiometricErrorInfo {
/**
* The biometric authentication failed and the reason of failure was already displayed in the authentication dialog.
* Error code that will be used to determine the localized message.
*/
BIOMETRICS_FAILED_WITH_VISIBLE_REASON,
private final @PowerAuthErrorCodes int errorCode;
/**
* The biometric authentication failed and the reason of failure was not displayed in the authentication dialog.
* In this case, application should properly investigate the reason of the failure and display an appropriate
* error information.
* Information whether application should present error to the user.
*/
BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON
;
private final boolean errorPresentationIsRequired;
/**
* Optional error message.
*/
private final @Nullable String errorMessage;

/**
* @return Contains {@code true} if the reason of biometric authentication failure was not properly communicated
* to the user in the authentication dialog.
*/
public boolean isErrorPresentationRequired() {
return errorPresentationIsRequired;
}

/**
* @return Contains optional error message retrieved from {@link androidx.biometric.BiometricPrompt.AuthenticationCallback}.
* The error message may not be available in case when the operation failed before the biometric prompt was created.
*/
@Nullable
public String getErrorMessage() {
return errorMessage;
}

/**
* Return localized error message. If {@link #getErrorMessage()} contains valid string, then returns this string, otherwise
* the strings from provided dialog resources are used.
* @param context Android context.
* @param dialogResources {@link BiometricDialogResources} class with strings. If {@code null} is provided, then the
* string resources used by {@link BiometricAuthentication} is used.
* @return Localized error message.
*/
@NonNull
public String getLocalizedErrorMessage(@NonNull Context context, @Nullable BiometricDialogResources.Strings dialogResources) {
if (errorMessage != null) {
return errorMessage;
}
if (dialogResources == null) {
dialogResources = BiometricAuthentication.getBiometricDialogResources().strings;
}
return context.getString(BiometricHelper.getErrorDialogStringForBiometricErrorCode(errorCode, dialogResources));
}

// Object construction

/**
* Construct biometric error info object with error code, hint to application and optional localized message.
* @param errorCode Biometric error code.
* @param errorPresentationIsRequired Hint to application, whether error should be presented to the user.
* @param errorMessage Optional localized error message from {@code BiometricPrompt}.
*/
public BiometricErrorInfo(
@PowerAuthErrorCodes int errorCode,
boolean errorPresentationIsRequired,
@Nullable CharSequence errorMessage) {
this.errorCode = errorCode;
this.errorPresentationIsRequired = errorPresentationIsRequired;
this.errorMessage = errorMessage != null ? errorMessage.toString() : null;
}

/**
* Construct biometric error info object with error code and hint to application.
* @param errorCode Biometric error code.
* @param errorPresentationIsRequired Hint to application, whether error should be presented to the user.
*/
public BiometricErrorInfo(
@PowerAuthErrorCodes int errorCode,
boolean errorPresentationIsRequired) {
this.errorCode = errorCode;
this.errorPresentationIsRequired = errorPresentationIsRequired;
this.errorMessage = null;
}

/**
* If the provided exception is biometry-related, then create a new instance of {@link PowerAuthErrorException}
* with the same error code, message and cause and use this enumeration as a source of additional information.
* THe additional information can be later retrieved with {@link PowerAuthErrorException#getAdditionalInformation()}.
* with the same error code, message and cause and use {@code BiometricErrorInfo} class as a source of additional information.
* The additional information can be later retrieved with {@link PowerAuthErrorException#getAdditionalInformation()}.
* @param exception Exception to enhance.
* @param errorPresentationIsRequired {@code true} if reason of failure was not communicated to the user in the authentication dialog.
* @param errorMessage Optional message, available only if the biometric authentication was performed and then failed.
* @return new exception enhanced with additional information or the original exception if it's not biometry-related.
*/
@NonNull
public PowerAuthErrorException addToException(@NonNull PowerAuthErrorException exception) {
public static PowerAuthErrorException addToException(@NonNull PowerAuthErrorException exception, boolean errorPresentationIsRequired, @Nullable CharSequence errorMessage) {
final int errorCode = exception.getPowerAuthErrorCode();
if (errorCode == PowerAuthErrorCodes.BIOMETRY_LOCKOUT ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED ||
errorCode == PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED) {
return new PowerAuthErrorException(errorCode, exception.getMessage(), exception.getCause(), this);
final BiometricErrorInfo info = new BiometricErrorInfo(errorCode, errorPresentationIsRequired, errorMessage);
return new PowerAuthErrorException(errorCode, exception.getMessage(), exception.getCause(), info);
}
return exception;
}

/**
* If the provided exception is biometry-related, then create a new instance of {@link PowerAuthErrorException}
* with the same error code, message and cause and use {@code BiometricErrorInfo} class as a source of additional information.
* The additional information can be later retrieved with {@link PowerAuthErrorException#getAdditionalInformation()}.
* @param exception Exception to enhance.
* @param errorPresentationIsRequired {@code true} if reason of failure was not communicated to the user in the authentication dialog.
* @return new exception enhanced with additional information or the original exception if it's not biometry-related.
*/
public static PowerAuthErrorException addToException(@NonNull PowerAuthErrorException exception, boolean errorPresentationIsRequired) {
return addToException(exception, errorPresentationIsRequired, null);
}
}
Loading

0 comments on commit 4fe232d

Please sign in to comment.