Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
7 changes: 7 additions & 0 deletions packages/local_auth/local_auth_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -101,15 +100,18 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) {
case "authenticate":
authenticate(call, result);
break;
case "getAvailableBiometrics":
getAvailableBiometrics(result);
case "getEnrolledBiometrics":
getEnrolledBiometrics(result);
break;
case "isDeviceSupported":
isDeviceSupported(result);
break;
case "stopAuthentication":
stopAuthentication(result);
break;
case "deviceSupportsBiometrics":
deviceSupportsBiometrics(result);
break;
default:
result.notImplemented();
break;
Expand Down Expand Up @@ -248,42 +250,39 @@ private void stopAuthentication(Result result) {
}
}

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;
}

Expand Down Expand Up @@ -359,4 +358,9 @@ public void onDetachedFromActivity() {
final Activity getActivity() {
return activity;
}

@VisibleForTesting
void setBiometricManager(BiometricManager biometricManager) {
this.biometricManager = biometricManager;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -31,6 +35,50 @@ public void isDeviceSupportedReturnsFalse() {
verify(mockResult).success(false);
}

@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);
Expand Down Expand Up @@ -61,4 +109,122 @@ public void onDetachedFromActivity_ShouldReleaseActivity() {
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);
}
}
23 changes: 12 additions & 11 deletions packages/local_auth/local_auth_android/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class MyApp extends StatefulWidget {

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;

Expand All @@ -38,20 +38,20 @@ class _MyAppState extends State<MyApp> {
}

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) {
return;
}

setState(() {
_canCheckBiometrics = canCheckBiometrics;
_deviceSupportsBiometrics = deviceSupportsBiometrics;
});
}

Expand All @@ -69,7 +69,7 @@ class _MyAppState extends State<MyApp> {
}

setState(() {
_availableBiometrics = availableBiometrics;
_enrolledBiometrics = availableBiometrics;
});
}

Expand Down Expand Up @@ -171,15 +171,16 @@ class _MyAppState extends State<MyApp> {
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),
Expand Down
Loading