[local_auth] Fix getEnrolledBiometrics returning non-enrolled biometrics on Android. (#5309)

diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md
index 121daf9..cbf686a 100644
--- a/packages/local_auth/local_auth_android/CHANGELOG.md
+++ b/packages/local_auth/local_auth_android/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 1.0.2
+
+* Fixes `getEnrolledBiometrics` to match documented behaviour:
+  Present biometrics that are not enrolled are no longer returned.
+* `getEnrolledBiometrics` now only returns `weak` and `strong` biometric types.
+* `deviceSupportsBiometrics` now returns the correct value regardless of enrollment state.
+
 ## 1.0.1
 
 * Adopts `Object.hash`.
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 49a6b78..3c5ecad 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
@@ -11,7 +11,6 @@
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.Build;
 import androidx.annotation.NonNull;
@@ -101,8 +100,8 @@
       case "authenticate":
         authenticate(call, result);
         break;
-      case "getAvailableBiometrics":
-        getAvailableBiometrics(result);
+      case "getEnrolledBiometrics":
+        getEnrolledBiometrics(result);
         break;
       case "isDeviceSupported":
         isDeviceSupported(result);
@@ -110,6 +109,9 @@
       case "stopAuthentication":
         stopAuthentication(result);
         break;
+      case "deviceSupportsBiometrics":
+        deviceSupportsBiometrics(result);
+        break;
       default:
         result.notImplemented();
         break;
@@ -248,42 +250,39 @@
     }
   }
 
+  private void deviceSupportsBiometrics(final Result result) {
+    result.success(hasBiometricHardware());
+  }
+
   /*
-   * Returns biometric types available on device
+   * Returns enrolled biometric types available on device.
    */
-  private void getAvailableBiometrics(final Result result) {
+  private void getEnrolledBiometrics(final Result result) {
     try {
       if (activity == null || activity.isFinishing()) {
         result.error("no_activity", "local_auth plugin requires a foreground activity", null);
         return;
       }
-      ArrayList<String> biometrics = getAvailableBiometrics();
+      ArrayList<String> biometrics = getEnrolledBiometrics();
       result.success(biometrics);
     } catch (Exception e) {
       result.error("no_biometrics_available", e.getMessage(), null);
     }
   }
 
-  private ArrayList<String> getAvailableBiometrics() {
+  private ArrayList<String> getEnrolledBiometrics() {
     ArrayList<String> biometrics = new ArrayList<>();
     if (activity == null || activity.isFinishing()) {
       return biometrics;
     }
-    PackageManager packageManager = activity.getPackageManager();
-    if (Build.VERSION.SDK_INT >= 23) {
-      if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
-        biometrics.add("fingerprint");
-      }
+    if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
+        == BiometricManager.BIOMETRIC_SUCCESS) {
+      biometrics.add("weak");
     }
-    if (Build.VERSION.SDK_INT >= 29) {
-      if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
-        biometrics.add("face");
-      }
-      if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
-        biometrics.add("iris");
-      }
+    if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+        == BiometricManager.BIOMETRIC_SUCCESS) {
+      biometrics.add("strong");
     }
-
     return biometrics;
   }
 
@@ -359,4 +358,9 @@
   final Activity getActivity() {
     return activity;
   }
+
+  @VisibleForTesting
+  void setBiometricManager(BiometricManager biometricManager) {
+    this.biometricManager = biometricManager;
+  }
 }
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 41868e6..5fbda46 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
@@ -6,12 +6,14 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.content.Context;
+import androidx.biometric.BiometricManager;
 import androidx.lifecycle.Lifecycle;
 import io.flutter.embedding.engine.FlutterEngine;
 import io.flutter.embedding.engine.dart.DartExecutor;
@@ -20,6 +22,8 @@
 import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
+import java.util.ArrayList;
+import java.util.Collections;
 import org.junit.Test;
 
 public class LocalAuthTest {
@@ -32,6 +36,50 @@
   }
 
   @Test
+  public void deviceSupportsBiometrics_returnsTrueForPresentNonEnrolledBiometrics() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    when(mockBiometricManager.canAuthenticate())
+        .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
+    plugin.setBiometricManager(mockBiometricManager);
+    plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
+    verify(mockResult).success(true);
+  }
+
+  @Test
+  public void deviceSupportsBiometrics_returnsTrueForPresentEnrolledBiometrics() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    when(mockBiometricManager.canAuthenticate()).thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
+    plugin.setBiometricManager(mockBiometricManager);
+    plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
+    verify(mockResult).success(true);
+  }
+
+  @Test
+  public void deviceSupportsBiometrics_returnsFalseForNoBiometricHardware() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    when(mockBiometricManager.canAuthenticate())
+        .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
+    plugin.setBiometricManager(mockBiometricManager);
+    plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
+    verify(mockResult).success(false);
+  }
+
+  @Test
+  public void deviceSupportsBiometrics_returnsFalseForNullBiometricManager() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    plugin.setBiometricManager(null);
+    plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
+    verify(mockResult).success(false);
+  }
+
+  @Test
   public void onDetachedFromActivity_ShouldReleaseActivity() {
     final Activity mockActivity = mock(Activity.class);
     final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class);
@@ -61,4 +109,122 @@
     plugin.onDetachedFromActivity();
     assertNull(plugin.getActivity());
   }
+
+  @Test
+  public void getEnrolledBiometrics_shouldReturnError_whenNoActivity() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+
+    plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
+    verify(mockResult)
+        .error("no_activity", "local_auth plugin requires a foreground activity", null);
+  }
+
+  @Test
+  public void getEnrolledBiometrics_shouldReturnError_whenFinishingActivity() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final Activity mockActivity = buildMockActivity();
+    when(mockActivity.isFinishing()).thenReturn(true);
+    setPluginActivity(plugin, mockActivity);
+
+    plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
+    verify(mockResult)
+        .error("no_activity", "local_auth plugin requires a foreground activity", null);
+  }
+
+  @Test
+  public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    setPluginActivity(plugin, buildMockActivity());
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    when(mockBiometricManager.canAuthenticate(anyInt()))
+        .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
+    plugin.setBiometricManager(mockBiometricManager);
+
+    plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
+    verify(mockResult).success(Collections.emptyList());
+  }
+
+  @Test
+  public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    setPluginActivity(plugin, buildMockActivity());
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    when(mockBiometricManager.canAuthenticate(anyInt()))
+        .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
+    plugin.setBiometricManager(mockBiometricManager);
+
+    plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
+    verify(mockResult).success(Collections.emptyList());
+  }
+
+  @Test
+  public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    setPluginActivity(plugin, buildMockActivity());
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
+        .thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
+    when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
+        .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
+    plugin.setBiometricManager(mockBiometricManager);
+
+    plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
+    verify(mockResult)
+        .success(
+            new ArrayList<String>() {
+              {
+                add("weak");
+              }
+            });
+  }
+
+  @Test
+  public void getEnrolledBiometrics_shouldAddStrongBiometrics() {
+    final LocalAuthPlugin plugin = new LocalAuthPlugin();
+    setPluginActivity(plugin, buildMockActivity());
+    final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+    final BiometricManager mockBiometricManager = mock(BiometricManager.class);
+    when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
+        .thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
+    when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
+        .thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
+    plugin.setBiometricManager(mockBiometricManager);
+
+    plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
+    verify(mockResult)
+        .success(
+            new ArrayList<String>() {
+              {
+                add("weak");
+                add("strong");
+              }
+            });
+  }
+
+  private Activity buildMockActivity() {
+    final Activity mockActivity = mock(Activity.class);
+    final Context mockContext = mock(Context.class);
+    when(mockActivity.getBaseContext()).thenReturn(mockContext);
+    when(mockActivity.getApplicationContext()).thenReturn(mockContext);
+    return mockActivity;
+  }
+
+  private void setPluginActivity(LocalAuthPlugin plugin, Activity activity) {
+    final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class);
+    final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class);
+    final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class);
+    final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class);
+    final DartExecutor mockDartExecutor = mock(DartExecutor.class);
+    when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine);
+    when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor);
+    when(mockActivityBinding.getActivity()).thenReturn(activity);
+    when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference);
+    plugin.onAttachedToEngine(mockPluginBinding);
+    plugin.onAttachedToActivity(mockActivityBinding);
+  }
 }
diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart
index 4c04521..29b1d66 100644
--- a/packages/local_auth/local_auth_android/example/lib/main.dart
+++ b/packages/local_auth/local_auth_android/example/lib/main.dart
@@ -22,8 +22,8 @@
 
 class _MyAppState extends State<MyApp> {
   _SupportState _supportState = _SupportState.unknown;
-  bool? _canCheckBiometrics;
-  List<BiometricType>? _availableBiometrics;
+  bool? _deviceSupportsBiometrics;
+  List<BiometricType>? _enrolledBiometrics;
   String _authorized = 'Not Authorized';
   bool _isAuthenticating = false;
 
@@ -38,12 +38,12 @@
   }
 
   Future<void> _checkBiometrics() async {
-    late bool canCheckBiometrics;
+    late bool deviceSupportsBiometrics;
     try {
-      canCheckBiometrics =
-          (await LocalAuthPlatform.instance.getEnrolledBiometrics()).isNotEmpty;
+      deviceSupportsBiometrics =
+          await LocalAuthPlatform.instance.deviceSupportsBiometrics();
     } on PlatformException catch (e) {
-      canCheckBiometrics = false;
+      deviceSupportsBiometrics = false;
       print(e);
     }
     if (!mounted) {
@@ -51,7 +51,7 @@
     }
 
     setState(() {
-      _canCheckBiometrics = canCheckBiometrics;
+      _deviceSupportsBiometrics = deviceSupportsBiometrics;
     });
   }
 
@@ -69,7 +69,7 @@
     }
 
     setState(() {
-      _availableBiometrics = availableBiometrics;
+      _enrolledBiometrics = availableBiometrics;
     });
   }
 
@@ -171,15 +171,16 @@
                 else
                   const Text('This device is not supported'),
                 const Divider(height: 100),
-                Text('Can check biometrics: $_canCheckBiometrics\n'),
+                Text(
+                    'Device supports biometrics: $_deviceSupportsBiometrics\n'),
                 ElevatedButton(
                   child: const Text('Check biometrics'),
                   onPressed: _checkBiometrics,
                 ),
                 const Divider(height: 100),
-                Text('Available biometrics: $_availableBiometrics\n'),
+                Text('Enrolled biometrics: $_enrolledBiometrics\n'),
                 ElevatedButton(
-                  child: const Text('Get available biometrics'),
+                  child: const Text('Get enrolled biometrics'),
                   onPressed: _getEnrolledBiometrics,
                 ),
                 const Divider(height: 100),
diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart
index a3f314e..2ce0f88 100644
--- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart
+++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart
@@ -49,28 +49,24 @@
 
   @override
   Future<bool> deviceSupportsBiometrics() async {
-    return (await getEnrolledBiometrics()).isNotEmpty;
+    return (await _channel.invokeMethod<bool>('deviceSupportsBiometrics')) ??
+        false;
   }
 
   @override
   Future<List<BiometricType>> getEnrolledBiometrics() async {
     final List<String> result = (await _channel.invokeListMethod<String>(
-          'getAvailableBiometrics',
+          'getEnrolledBiometrics',
         )) ??
         <String>[];
     final List<BiometricType> biometrics = <BiometricType>[];
     for (final String value in result) {
       switch (value) {
-        case 'face':
-          biometrics.add(BiometricType.face);
+        case 'weak':
+          biometrics.add(BiometricType.weak);
           break;
-        case 'fingerprint':
-          biometrics.add(BiometricType.fingerprint);
-          break;
-        case 'iris':
-          biometrics.add(BiometricType.iris);
-          break;
-        case 'undefined':
+        case 'strong':
+          biometrics.add(BiometricType.strong);
           break;
       }
     }
diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml
index 5734843..aad0b9b 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/master/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.1
+version: 1.0.2
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart
index 31f5e57..55ac926 100644
--- a/packages/local_auth/local_auth_android/test/local_auth_test.dart
+++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart
@@ -24,9 +24,8 @@
       channel.setMockMethodCallHandler((MethodCall methodCall) {
         log.add(methodCall);
         switch (methodCall.method) {
-          case 'getAvailableBiometrics':
-            return Future<List<String>>.value(
-                <String>['face', 'fingerprint', 'iris', 'undefined']);
+          case 'getEnrolledBiometrics':
+            return Future<List<String>>.value(<String>['weak', 'strong']);
           default:
             return Future<dynamic>.value(true);
         }
@@ -35,13 +34,13 @@
       log.clear();
     });
 
-    test('deviceSupportsBiometrics calls getEnrolledBiometrics', () async {
+    test('deviceSupportsBiometrics calls platform', () async {
       final bool result = await localAuthentication.deviceSupportsBiometrics();
 
       expect(
         log,
         <Matcher>[
-          isMethodCall('getAvailableBiometrics', arguments: null),
+          isMethodCall('deviceSupportsBiometrics', arguments: null),
         ],
       );
       expect(result, true);
@@ -54,13 +53,12 @@
       expect(
         log,
         <Matcher>[
-          isMethodCall('getAvailableBiometrics', arguments: null),
+          isMethodCall('getEnrolledBiometrics', arguments: null),
         ],
       );
       expect(result, <BiometricType>[
-        BiometricType.face,
-        BiometricType.fingerprint,
-        BiometricType.iris
+        BiometricType.weak,
+        BiometricType.strong,
       ]);
     });