[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,
]);
});