@@ -61,6 +61,11 @@ public class NativeBiometric extends Plugin {
6161 private static final int IRIS_AUTHENTICATION = 5 ;
6262 private static final int MULTIPLE = 6 ;
6363
64+ // AuthenticationStrength enum values
65+ private static final int AUTH_STRENGTH_NONE = 0 ;
66+ private static final int AUTH_STRENGTH_STRONG = 1 ;
67+ private static final int AUTH_STRENGTH_WEAK = 2 ;
68+
6469 private KeyStore keyStore ;
6570 private static final String ANDROID_KEY_STORE = "AndroidKeyStore" ;
6671 private static final String TRANSFORMATION = "AES/GCM/NoPadding" ;
@@ -72,82 +77,71 @@ public class NativeBiometric extends Plugin {
7277
7378 private SharedPreferences encryptedSharedPreferences ;
7479
75- private int getAvailableFeature () {
76- // default to none
77- BiometricManager biometricManager = BiometricManager .from (getContext ());
78-
79- // Check for biometric capabilities
80- int authenticators = BiometricManager .Authenticators .BIOMETRIC_STRONG ;
81- int canAuthenticate = biometricManager .canAuthenticate (authenticators );
82-
83- if (canAuthenticate == BiometricManager .BIOMETRIC_SUCCESS ) {
84- // Check specific features
85- PackageManager pm = getContext ().getPackageManager ();
86- boolean hasFinger = pm .hasSystemFeature (PackageManager .FEATURE_FINGERPRINT );
87- boolean hasIris = false ;
88- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
89- hasIris = pm .hasSystemFeature (PackageManager .FEATURE_IRIS );
90- }
91-
92- // For face, we rely on BiometricManager since it's more reliable
93- boolean hasFace = false ;
94- try {
95- // Try to create a face authentication prompt - if it succeeds, face auth is available
96- androidx .biometric .BiometricPrompt .PromptInfo promptInfo = new androidx .biometric .BiometricPrompt .PromptInfo .Builder ()
97- .setTitle ("Test" )
98- .setNegativeButtonText ("Cancel" )
99- .setAllowedAuthenticators (BiometricManager .Authenticators .BIOMETRIC_STRONG )
100- .build ();
101- hasFace = true ;
102- } catch (Exception e ) {
103- System .out .println ("Error creating face authentication prompt: " + e .getMessage ());
104- }
105-
106- // Determine the type based on available features
107- if (hasFinger && (hasFace || hasIris )) {
108- return MULTIPLE ;
109- } else if (hasFinger ) {
110- return FINGERPRINT ;
111- } else if (hasFace ) {
112- return FACE_AUTHENTICATION ;
113- } else if (hasIris ) {
114- return IRIS_AUTHENTICATION ;
115- }
116- }
117-
118- return NONE ;
119- }
120-
12180 @ PluginMethod
12281 public void isAvailable (PluginCall call ) {
12382 JSObject ret = new JSObject ();
12483
12584 boolean useFallback = Boolean .TRUE .equals (call .getBoolean ("useFallback" , false ));
12685
12786 BiometricManager biometricManager = BiometricManager .from (getContext ());
128- int authenticators = BiometricManager .Authenticators .BIOMETRIC_STRONG ;
129- if (useFallback ) {
130- authenticators |= BiometricManager .Authenticators .DEVICE_CREDENTIAL ;
131- }
132- int canAuthenticateResult = biometricManager .canAuthenticate (authenticators );
133- // Using deviceHasCredentials instead of canAuthenticate(DEVICE_CREDENTIAL)
134- // > "Developers that wish to check for the presence of a PIN, pattern, or password on these versions should instead use isDeviceSecure."
135- // @see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
87+
88+ // Check for strong biometrics first
89+ int strongAuthenticators = BiometricManager .Authenticators .BIOMETRIC_STRONG ;
90+ int strongResult = biometricManager .canAuthenticate (strongAuthenticators );
91+ boolean hasStrongBiometric = (strongResult == BiometricManager .BIOMETRIC_SUCCESS );
92+
93+ // Check for weak biometrics
94+ int weakAuthenticators = BiometricManager .Authenticators .BIOMETRIC_WEAK ;
95+ int weakResult = biometricManager .canAuthenticate (weakAuthenticators );
96+ boolean hasWeakBiometric = (weakResult == BiometricManager .BIOMETRIC_SUCCESS );
97+
98+ // Check if device has credentials (PIN/pattern/password)
13699 boolean fallbackAvailable = useFallback && this .deviceHasCredentials ();
137- if (useFallback && !fallbackAvailable ) {
138- canAuthenticateResult = BiometricManager .BIOMETRIC_ERROR_HW_UNAVAILABLE ;
139- }
140100
141- boolean isAvailable = (canAuthenticateResult == BiometricManager .BIOMETRIC_SUCCESS || fallbackAvailable );
142- ret .put ("isAvailable" , isAvailable );
101+ // Determine authentication strength
102+ int authenticationStrength = AUTH_STRENGTH_NONE ;
103+ boolean isAvailable = false ;
104+
105+ if (hasStrongBiometric ) {
106+ // Strong biometric available (fingerprints on devices that consider them strong)
107+ authenticationStrength = AUTH_STRENGTH_STRONG ;
108+ isAvailable = true ;
109+ } else if (hasWeakBiometric ) {
110+ // Only weak biometric available (face on devices that consider it weak)
111+ authenticationStrength = AUTH_STRENGTH_WEAK ;
112+ isAvailable = true ;
113+ } else if (fallbackAvailable ) {
114+ // No biometrics but fallback (PIN/pattern/password) is available
115+ // PIN/pattern/password is ALWAYS considered WEAK, never STRONG
116+ authenticationStrength = AUTH_STRENGTH_WEAK ;
117+ isAvailable = true ;
118+ }
143119
120+ // Handle error codes when authentication is not available
144121 if (!isAvailable ) {
145- // BiometricManager Error Constants use the same values as BiometricPrompt's Constants. So we can reuse our
146- int pluginErrorCode = AuthActivity .convertToPluginErrorCode (canAuthenticateResult );
122+ int biometricManagerErrorCode ;
123+
124+ // Prefer the error from strong biometric check if it failed
125+ if (strongResult != BiometricManager .BIOMETRIC_SUCCESS ) {
126+ biometricManagerErrorCode = strongResult ;
127+ } else if (weakResult != BiometricManager .BIOMETRIC_SUCCESS ) {
128+ // Otherwise use error from weak biometric check if it failed
129+ biometricManagerErrorCode = weakResult ;
130+ } else {
131+ // No biometrics available at all
132+ // BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE indicates that biometric hardware is unavailable
133+ // or cannot be accessed. This constant value may vary across Android versions, so we explicitly
134+ // use the constant rather than assuming its numeric value.
135+ biometricManagerErrorCode = BiometricManager .BIOMETRIC_ERROR_HW_UNAVAILABLE ;
136+ }
137+
138+ // Convert BiometricManager error codes to plugin error codes
139+ int pluginErrorCode = convertBiometricManagerErrorToPluginError (biometricManagerErrorCode );
147140 ret .put ("errorCode" , pluginErrorCode );
148141 }
149142
150- ret .put ("biometryType" , getAvailableFeature ());
143+ ret .put ("isAvailable" , isAvailable );
144+ ret .put ("authenticationStrength" , authenticationStrength );
151145 call .resolve (ret );
152146 }
153147
@@ -463,6 +457,25 @@ private boolean deviceHasCredentials() {
463457 return keyguardManager .isDeviceSecure ();
464458 }
465459
460+ /**
461+ * Convert BiometricManager error codes to plugin error codes
462+ * BiometricManager constants have different values than BiometricPrompt constants
463+ */
464+ private int convertBiometricManagerErrorToPluginError (int errorCode ) {
465+ switch (errorCode ) {
466+ case BiometricManager .BIOMETRIC_ERROR_HW_UNAVAILABLE :
467+ case BiometricManager .BIOMETRIC_ERROR_NO_HARDWARE :
468+ case BiometricManager .BIOMETRIC_ERROR_UNSUPPORTED :
469+ return 1 ; // BIOMETRICS_UNAVAILABLE
470+ case BiometricManager .BIOMETRIC_ERROR_NONE_ENROLLED :
471+ return 3 ; // BIOMETRICS_NOT_ENROLLED
472+ case BiometricManager .BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED :
473+ return 1 ; // BIOMETRICS_UNAVAILABLE (security update required, treat as unavailable)
474+ default :
475+ return 0 ; // UNKNOWN_ERROR
476+ }
477+ }
478+
466479 @ PluginMethod
467480 public void getPluginVersion (final PluginCall call ) {
468481 try {
0 commit comments