[local_auth] Fix device credential only check for API < 30 (#6522)

diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md
index a26846d..c9eeed9 100644
--- a/packages/local_auth/local_auth_android/CHANGELOG.md
+++ b/packages/local_auth/local_auth_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.0.14
+
+* Fixes device credential authentication for API versions before R.
+
 ## 1.0.13
 
 * Updates imports for `prefer_relative_imports`.
diff --git a/packages/local_auth/local_auth_android/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle
index 569d7e3..6c94170 100644
--- a/packages/local_auth/local_auth_android/android/build.gradle
+++ b/packages/local_auth/local_auth_android/android/build.gradle
@@ -56,6 +56,7 @@
     api "androidx.fragment:fragment:1.5.2"
     testImplementation 'junit:junit:4.13.2'
     testImplementation 'org.mockito:mockito-inline:4.7.0'
+    testImplementation 'org.robolectric:robolectric:4.5'
     androidTestImplementation 'androidx.test:runner:1.2.0'
     androidTestImplementation 'androidx.test:rules:1.2.0'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java
index e8632c4..e545df0 100644
--- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java
+++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java
@@ -253,11 +253,16 @@
   }
 
   @VisibleForTesting
-  public boolean isDeviceSupported() {
+  public boolean isDeviceSecure() {
     if (keyguardManager == null) return false;
     return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && keyguardManager.isDeviceSecure());
   }
 
+  @VisibleForTesting
+  public boolean isDeviceSupported() {
+    return isDeviceSecure() || canAuthenticateWithBiometrics();
+  }
+
   private boolean canAuthenticateWithBiometrics() {
     if (biometricManager == null) return false;
     return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
@@ -270,7 +275,15 @@
         != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE;
   }
 
-  private boolean canAuthenticateWithDeviceCredential() {
+  @VisibleForTesting
+  public boolean canAuthenticateWithDeviceCredential() {
+    if (Build.VERSION.SDK_INT < 30) {
+      // Checking for device credential only authentication via the BiometricManager
+      // is not allowed before API level 30, so we check for presence of PIN, pattern,
+      // or password instead.
+      return isDeviceSecure();
+    }
+
     if (biometricManager == null) return false;
     return biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
         == BiometricManager.BIOMETRIC_SUCCESS;
@@ -334,4 +347,9 @@
   void setBiometricManager(BiometricManager biometricManager) {
     this.biometricManager = biometricManager;
   }
+
+  @VisibleForTesting
+  void setKeyguardManager(KeyguardManager keyguardManager) {
+    this.keyguardManager = keyguardManager;
+  }
 }
diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java
index 0eaf312..7279a3c 100644
--- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java
+++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java
@@ -17,6 +17,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
+import android.app.KeyguardManager;
 import android.app.NativeActivity;
 import android.content.Context;
 import androidx.biometric.BiometricManager;
@@ -34,8 +35,12 @@
 import java.util.Collections;
 import java.util.HashMap;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
 
+@RunWith(RobolectricTestRunner.class)
 public class LocalAuthTest {
   @Test
   public void authenticate_returnsErrorWhenAuthInProgress() {
@@ -107,14 +112,13 @@
   }
 
   @Test
+  @Config(sdk = 30)
   public void authenticate_properlyConfiguresBiometricAndDeviceCredentialAuthenticationRequest() {
     final LocalAuthPlugin plugin = spy(new LocalAuthPlugin());
     setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class)));
     when(plugin.isDeviceSupported()).thenReturn(true);
 
     final BiometricManager mockBiometricManager = mock(BiometricManager.class);
-    when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
-        .thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
     when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
         .thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
     plugin.setBiometricManager(mockBiometricManager);
@@ -135,6 +139,7 @@
   }
 
   @Test
+  @Config(sdk = 30)
   public void authenticate_properlyConfiguresDeviceCredentialOnlyAuthenticationRequest() {
     final LocalAuthPlugin plugin = spy(new LocalAuthPlugin());
     setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class)));
@@ -343,6 +348,44 @@
             });
   }
 
+  @Test
+  @Config(sdk = 22)
+  public void isDeviceSecure_returnsFalseOnBelowApi23() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    assertFalse(plugin.isDeviceSecure());
+  }
+
+  @Test
+  @Config(sdk = 23)
+  public void isDeviceSecure_returnsTrueIfDeviceIsSecure() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
+    plugin.setKeyguardManager(mockKeyguardManager);
+
+    when(mockKeyguardManager.isDeviceSecure()).thenReturn(true);
+    assertTrue(plugin.isDeviceSecure());
+
+    when(mockKeyguardManager.isDeviceSecure()).thenReturn(false);
+    assertFalse(plugin.isDeviceSecure());
+  }
+
+  @Test
+  @Config(sdk = 30)
+  public void
+      canAuthenticateWithDeviceCredential_returnsTrueIfHasBiometricManagerSupportAboveApi30() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    plugin.setBiometricManager(mockBiometricManager);
+
+    when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
+        .thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
+    assertTrue(plugin.canAuthenticateWithDeviceCredential());
+
+    when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
+        .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
+    assertFalse(plugin.canAuthenticateWithDeviceCredential());
+  }
+
   private Activity buildMockActivityWithContext(Activity mockActivity) {
     final Context mockContext = mock(Context.class);
     when(mockActivity.getBaseContext()).thenReturn(mockContext);
diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml
index 35c2d3a..99e9e2c 100644
--- a/packages/local_auth/local_auth_android/pubspec.yaml
+++ b/packages/local_auth/local_auth_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Android implementation of the local_auth plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22
-version: 1.0.13
+version: 1.0.14
 
 environment:
   sdk: ">=2.14.0 <3.0.0"