From 4fe232db83d2daf4e6d7a5ce38f2c7b41765bb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20=C4=8Eurech?= Date: Mon, 25 Sep 2023 14:54:16 +0200 Subject: [PATCH] Android: Fix #558: BiometricErrorInfo contains localized reason of failure. - Fixed mapping from BiometricStatus.NOT_ENROLLED to PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED - BiometricAuthentication.getBiometricDialogResources() is now static (as intended) --- docs/PowerAuth-SDK-for-Android.md | 33 ++--- .../biometry/BiometricAuthentication.java | 14 +-- .../biometry/BiometricErrorInfo.java | 114 +++++++++++++++--- .../biometry/impl/BiometricAuthenticator.java | 47 +++----- .../biometry/impl/BiometricHelper.java | 48 ++++++-- 5 files changed, 179 insertions(+), 77 deletions(-) diff --git a/docs/PowerAuth-SDK-for-Android.md b/docs/PowerAuth-SDK-for-Android.md index b2810152..977352ff 100644 --- a/docs/PowerAuth-SDK-for-Android.md +++ b/docs/PowerAuth-SDK-for-Android.md @@ -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) } } } @@ -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. + } } } } @@ -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) { diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java index 979ec659..5b30add6 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricAuthentication.java @@ -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; } } @@ -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); } } } @@ -249,7 +249,7 @@ public void run() { final FragmentManager fragmentManager = requestData.getFragmentManager(); final BiometricDialogResources resources = requestData.getResources(); - final Pair titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(status, resources); + final Pair titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(status, resources.strings); final BiometricErrorDialogFragment dialogFragment = new BiometricErrorDialogFragment.Builder(context) .setTitle(titleDescription.first) @@ -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(); } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricErrorInfo.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricErrorInfo.java index 4f544d01..6ac148bc 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricErrorInfo.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/BiometricErrorInfo.java @@ -16,13 +16,16 @@ 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: *
    *
  • {@link PowerAuthErrorCodes#BIOMETRY_LOCKOUT}
  • *
  • {@link PowerAuthErrorCodes#BIOMETRY_NOT_AVAILABLE}
  • @@ -30,37 +33,120 @@ *
  • {@link PowerAuthErrorCodes#BIOMETRY_NOT_SUPPORTED}
  • *
  • {@link PowerAuthErrorCodes#BIOMETRY_NOT_ENROLLED}
  • *
+ * 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); + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java index 61f6fe0b..01bd32b2 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricAuthenticator.java @@ -209,46 +209,37 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString // Determine whether the error dialog should be displayed final boolean shouldDisplayError = shouldDisplayErrorDialog(requestData); - final boolean errorDialogDisabled = requestData.isErrorDialogDisabled(); - final boolean displayError; - final BiometricErrorInfo errorInfo; - - // Prepare error info in advance - if (shouldDisplayError && !errorDialogDisabled) { - // We should display error and dialog is not disabled. Our dialog will show the reason of failure. - errorInfo = BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON; - displayError = true; - } else if (shouldDisplayError) { - // We should display error, but dialog is disabled. The reason of failure was not presented yet. - errorInfo = BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON; - displayError = false; - } else { - // Error should not be displayed, so the reason was already presented in system UI. - errorInfo = BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON; - displayError = false; - } + // Error will be displayed only if it's not disabled globally. + final boolean displayError = shouldDisplayError && !requestData.isErrorDialogDisabled(); + // Display hint to application whether error should be presented. + final boolean displayHint = shouldDisplayError && requestData.isErrorDialogDisabled(); if (isLockout || isQuickCancel) { if (authenticationFailedBefore > 0) { // Too many failed attempts, we should report the "not recognized" error after all. // If `authenticationFailedBefore` is greater than 0, then it means that sensor did a multiple failed attempts // in this round. So we're pretty sure that biometric authentication dialog was properly displayed. - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED, "Biometric image was not recognized", null, errorInfo); + final BiometricErrorInfo info = new BiometricErrorInfo(PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED, displayHint, errString); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED, "Biometric image was not recognized", null, info); } else { // Too many failed attempts, but no authentication dialog was displayed in this round. It looks like that // the error was immediately reported back to us, so we can report "lockout" to the application. - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_LOCKOUT, "Too many failed attempts", null, errorInfo); + final BiometricErrorInfo info = new BiometricErrorInfo(PowerAuthErrorCodes.BIOMETRY_LOCKOUT, displayHint, errString); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_LOCKOUT, "Too many failed attempts", null, info); } } else if (notEnrolled) { // Biometry is not enrolled. - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED, "Biometry not enrolled", null, errorInfo); + final BiometricErrorInfo info = new BiometricErrorInfo(PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED, displayHint, errString); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED, "Biometry not enrolled", null, info); } else if (notAvailable) { // Biometry is not supported - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED, "Biometry not supported", null, errorInfo); + final BiometricErrorInfo info = new BiometricErrorInfo(PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED, displayHint, errString); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED, "Biometry not supported", null, info); } else { // Other error, we can use "not available" error code, due to that other // errors are mostly about an internal failures. - exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, errString.toString(), null, errorInfo); + final BiometricErrorInfo info = new BiometricErrorInfo(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, displayHint, errString); + exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, errString.toString(), null, info); } // Show error dialog first or dispatch the failure immediately. @@ -288,12 +279,10 @@ public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationRes // or due to fact, that the previously constructed cipher is not available. The right response for this state // is to remove the biometric key from the keychain, show an error dialog and then, finally report "not available" state. dispatcher.reportBiometricKeyUnavailable(); - final boolean showDialog = !requestData.isErrorDialogDisabled(); - final BiometricErrorInfo errorInfo = showDialog - ? BiometricErrorInfo.BIOMETRICS_FAILED_WITH_VISIBLE_REASON - : BiometricErrorInfo.BIOMETRICS_FAILED_WITH_NO_VISIBLE_REASON; + // Prepare BiometricErrorInfo. If error dialog is disabled, then application should report this situation to the user. + final BiometricErrorInfo errorInfo = new BiometricErrorInfo(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, requestData.isErrorDialogDisabled()); final PowerAuthErrorException exception = new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, "Failed to encrypt biometric key.", null, errorInfo); - if (showDialog) { + if (!requestData.isErrorDialogDisabled()) { // Display error dialog dispatcher.dispatchRunnable(() -> { showErrorDialogAfterSuccess(requestData, exception); @@ -516,7 +505,7 @@ private void showErrorDialogAfterSuccess( } final BiometricDialogResources resources = requestData.getResources(); - final Pair titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(BiometricStatus.NOT_AVAILABLE, resources); + final Pair titleDescription = BiometricHelper.getErrorDialogStringsForBiometricStatus(BiometricStatus.NOT_AVAILABLE, resources.strings); final FragmentManager fragmentManager = requestData.getFragmentManager(); final BiometricErrorDialogFragment dialogFragment = new BiometricErrorDialogFragment.Builder(context) diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricHelper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricHelper.java index b44a7a1f..bd3a9a5c 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricHelper.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/biometry/impl/BiometricHelper.java @@ -40,7 +40,7 @@ public class BiometricHelper { public static @NonNull PowerAuthErrorException getExceptionForBiometricStatus(@BiometricStatus int status) { switch (status) { case BiometricStatus.NOT_ENROLLED: - return new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE, "Biometric data is not enrolled on the device."); + return new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED, "Biometric data is not enrolled on the device."); case BiometricStatus.NOT_SUPPORTED: return new PowerAuthErrorException(PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED, "Biometry is not supported on the device."); case BiometricStatus.NOT_AVAILABLE: @@ -53,32 +53,56 @@ public class BiometricHelper { } /** - * Translate {@link BiometricStatus} into pair of string resources, representing title and description for error dialog. + * Translate {@link BiometricStatus} into a pair of string resources, representing title and description for error dialog. * * @param status Status to be translated to error dialog resources. - * @param resources {@link BiometricDialogResources} object with resource identifiers. + * @param strings {@link BiometricDialogResources.Strings} object with resource identifiers. * @return Pair of string resource identifiers, with appropriate title and description. */ - public static @NonNull Pair getErrorDialogStringsForBiometricStatus(@BiometricStatus int status, @NonNull BiometricDialogResources resources) { + public static @NonNull Pair getErrorDialogStringsForBiometricStatus(@BiometricStatus int status, @NonNull BiometricDialogResources.Strings strings) { final @StringRes int errorTitle; final @StringRes int errorDescription; if (status == BiometricStatus.NOT_ENROLLED) { // User must enroll at least one fingerprint - errorTitle = resources.strings.errorEnrollFingerprintTitle; - errorDescription = resources.strings.errorEnrollFingerprintDescription; + errorTitle = strings.errorEnrollFingerprintTitle; + errorDescription = strings.errorEnrollFingerprintDescription; } else if (status == BiometricStatus.NOT_SUPPORTED) { // Fingerprint scanner is not supported on the authenticator - errorTitle = resources.strings.errorNoFingerprintScannerTitle; - errorDescription = resources.strings.errorNoFingerprintScannerDescription; + errorTitle = strings.errorNoFingerprintScannerTitle; + errorDescription = strings.errorNoFingerprintScannerDescription; } else if (status == BiometricStatus.NOT_AVAILABLE) { // Fingerprint scanner is disabled in the system, or permission was not granted. - errorTitle = resources.strings.errorFingerprintDisabledTitle; - errorDescription = resources.strings.errorFingerprintDisabledDescription; + errorTitle = strings.errorFingerprintDisabledTitle; + errorDescription = strings.errorFingerprintDisabledDescription; } else { // Fallback... - errorTitle = resources.strings.errorFingerprintDisabledTitle; - errorDescription = resources.strings.errorFingerprintDisabledDescription; + errorTitle = strings.errorFingerprintDisabledTitle; + errorDescription = strings.errorFingerprintDisabledDescription; } return Pair.create(errorTitle, errorDescription); } + + /** + * Translate {@link PowerAuthErrorCodes} into the string resource identifier with the reason of biometric authentication failure. + * @param errorCode Error code to be translated. + * @param strings {@link BiometricDialogResources.Strings} object with resource identifiers. + * @return String resource identifier. + */ + public static @StringRes int getErrorDialogStringForBiometricErrorCode(@PowerAuthErrorCodes int errorCode, @NonNull BiometricDialogResources.Strings strings) { + switch (errorCode) { + case PowerAuthErrorCodes.BIOMETRY_LOCKOUT: + return strings.errorCodeLockout; + case PowerAuthErrorCodes.BIOMETRY_NOT_ENROLLED: + return strings.errorEnrollFingerprintDescription; + case PowerAuthErrorCodes.BIOMETRY_NOT_SUPPORTED: + return strings.errorNoFingerprintScannerDescription; + case PowerAuthErrorCodes.BIOMETRY_NOT_AVAILABLE: + return strings.errorFingerprintDisabledDescription; + case PowerAuthErrorCodes.BIOMETRY_NOT_RECOGNIZED: + // NOT-RECOGNIZED may be reported only during biometric setup. + return strings.errorCodeLockout; + default: + return strings.errorCodeGeneric; + } + } }