Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit bde713a

Browse files
authored
[local_auth] Fix getEnrolledBiometrics returning non-enrolled biometrics on Android. (#5309)
1 parent 83269f3 commit bde713a

7 files changed

Lines changed: 224 additions & 52 deletions

File tree

packages/local_auth/local_auth_android/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 1.0.2
2+
3+
* Fixes `getEnrolledBiometrics` to match documented behaviour:
4+
Present biometrics that are not enrolled are no longer returned.
5+
* `getEnrolledBiometrics` now only returns `weak` and `strong` biometric types.
6+
* `deviceSupportsBiometrics` now returns the correct value regardless of enrollment state.
7+
18
## 1.0.1
29

310
* Adopts `Object.hash`.

packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import android.app.KeyguardManager;
1212
import android.content.Context;
1313
import android.content.Intent;
14-
import android.content.pm.PackageManager;
1514
import android.hardware.fingerprint.FingerprintManager;
1615
import android.os.Build;
1716
import androidx.annotation.NonNull;
@@ -101,15 +100,18 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) {
101100
case "authenticate":
102101
authenticate(call, result);
103102
break;
104-
case "getAvailableBiometrics":
105-
getAvailableBiometrics(result);
103+
case "getEnrolledBiometrics":
104+
getEnrolledBiometrics(result);
106105
break;
107106
case "isDeviceSupported":
108107
isDeviceSupported(result);
109108
break;
110109
case "stopAuthentication":
111110
stopAuthentication(result);
112111
break;
112+
case "deviceSupportsBiometrics":
113+
deviceSupportsBiometrics(result);
114+
break;
113115
default:
114116
result.notImplemented();
115117
break;
@@ -248,42 +250,39 @@ private void stopAuthentication(Result result) {
248250
}
249251
}
250252

253+
private void deviceSupportsBiometrics(final Result result) {
254+
result.success(hasBiometricHardware());
255+
}
256+
251257
/*
252-
* Returns biometric types available on device
258+
* Returns enrolled biometric types available on device.
253259
*/
254-
private void getAvailableBiometrics(final Result result) {
260+
private void getEnrolledBiometrics(final Result result) {
255261
try {
256262
if (activity == null || activity.isFinishing()) {
257263
result.error("no_activity", "local_auth plugin requires a foreground activity", null);
258264
return;
259265
}
260-
ArrayList<String> biometrics = getAvailableBiometrics();
266+
ArrayList<String> biometrics = getEnrolledBiometrics();
261267
result.success(biometrics);
262268
} catch (Exception e) {
263269
result.error("no_biometrics_available", e.getMessage(), null);
264270
}
265271
}
266272

267-
private ArrayList<String> getAvailableBiometrics() {
273+
private ArrayList<String> getEnrolledBiometrics() {
268274
ArrayList<String> biometrics = new ArrayList<>();
269275
if (activity == null || activity.isFinishing()) {
270276
return biometrics;
271277
}
272-
PackageManager packageManager = activity.getPackageManager();
273-
if (Build.VERSION.SDK_INT >= 23) {
274-
if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
275-
biometrics.add("fingerprint");
276-
}
278+
if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
279+
== BiometricManager.BIOMETRIC_SUCCESS) {
280+
biometrics.add("weak");
277281
}
278-
if (Build.VERSION.SDK_INT >= 29) {
279-
if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
280-
biometrics.add("face");
281-
}
282-
if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
283-
biometrics.add("iris");
284-
}
282+
if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
283+
== BiometricManager.BIOMETRIC_SUCCESS) {
284+
biometrics.add("strong");
285285
}
286-
287286
return biometrics;
288287
}
289288

@@ -359,4 +358,9 @@ public void onDetachedFromActivity() {
359358
final Activity getActivity() {
360359
return activity;
361360
}
361+
362+
@VisibleForTesting
363+
void setBiometricManager(BiometricManager biometricManager) {
364+
this.biometricManager = biometricManager;
365+
}
362366
}

packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import static org.junit.Assert.assertNotNull;
88
import static org.junit.Assert.assertNull;
9+
import static org.mockito.ArgumentMatchers.anyInt;
910
import static org.mockito.Mockito.mock;
1011
import static org.mockito.Mockito.verify;
1112
import static org.mockito.Mockito.when;
1213

1314
import android.app.Activity;
1415
import android.content.Context;
16+
import androidx.biometric.BiometricManager;
1517
import androidx.lifecycle.Lifecycle;
1618
import io.flutter.embedding.engine.FlutterEngine;
1719
import io.flutter.embedding.engine.dart.DartExecutor;
@@ -20,6 +22,8 @@
2022
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
2123
import io.flutter.plugin.common.MethodCall;
2224
import io.flutter.plugin.common.MethodChannel;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
2327
import org.junit.Test;
2428

2529
public class LocalAuthTest {
@@ -31,6 +35,50 @@ public void isDeviceSupportedReturnsFalse() {
3135
verify(mockResult).success(false);
3236
}
3337

38+
@Test
39+
public void deviceSupportsBiometrics_returnsTrueForPresentNonEnrolledBiometrics() {
40+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
41+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
42+
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
43+
when(mockBiometricManager.canAuthenticate())
44+
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
45+
plugin.setBiometricManager(mockBiometricManager);
46+
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
47+
verify(mockResult).success(true);
48+
}
49+
50+
@Test
51+
public void deviceSupportsBiometrics_returnsTrueForPresentEnrolledBiometrics() {
52+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
53+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
54+
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
55+
when(mockBiometricManager.canAuthenticate()).thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
56+
plugin.setBiometricManager(mockBiometricManager);
57+
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
58+
verify(mockResult).success(true);
59+
}
60+
61+
@Test
62+
public void deviceSupportsBiometrics_returnsFalseForNoBiometricHardware() {
63+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
64+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
65+
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
66+
when(mockBiometricManager.canAuthenticate())
67+
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
68+
plugin.setBiometricManager(mockBiometricManager);
69+
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
70+
verify(mockResult).success(false);
71+
}
72+
73+
@Test
74+
public void deviceSupportsBiometrics_returnsFalseForNullBiometricManager() {
75+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
76+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
77+
plugin.setBiometricManager(null);
78+
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
79+
verify(mockResult).success(false);
80+
}
81+
3482
@Test
3583
public void onDetachedFromActivity_ShouldReleaseActivity() {
3684
final Activity mockActivity = mock(Activity.class);
@@ -61,4 +109,122 @@ public void onDetachedFromActivity_ShouldReleaseActivity() {
61109
plugin.onDetachedFromActivity();
62110
assertNull(plugin.getActivity());
63111
}
112+
113+
@Test
114+
public void getEnrolledBiometrics_shouldReturnError_whenNoActivity() {
115+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
116+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
117+
118+
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
119+
verify(mockResult)
120+
.error("no_activity", "local_auth plugin requires a foreground activity", null);
121+
}
122+
123+
@Test
124+
public void getEnrolledBiometrics_shouldReturnError_whenFinishingActivity() {
125+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
126+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
127+
final Activity mockActivity = buildMockActivity();
128+
when(mockActivity.isFinishing()).thenReturn(true);
129+
setPluginActivity(plugin, mockActivity);
130+
131+
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
132+
verify(mockResult)
133+
.error("no_activity", "local_auth plugin requires a foreground activity", null);
134+
}
135+
136+
@Test
137+
public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() {
138+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
139+
setPluginActivity(plugin, buildMockActivity());
140+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
141+
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
142+
when(mockBiometricManager.canAuthenticate(anyInt()))
143+
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
144+
plugin.setBiometricManager(mockBiometricManager);
145+
146+
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
147+
verify(mockResult).success(Collections.emptyList());
148+
}
149+
150+
@Test
151+
public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled() {
152+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
153+
setPluginActivity(plugin, buildMockActivity());
154+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
155+
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
156+
when(mockBiometricManager.canAuthenticate(anyInt()))
157+
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
158+
plugin.setBiometricManager(mockBiometricManager);
159+
160+
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
161+
verify(mockResult).success(Collections.emptyList());
162+
}
163+
164+
@Test
165+
public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() {
166+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
167+
setPluginActivity(plugin, buildMockActivity());
168+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
169+
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
170+
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
171+
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
172+
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
173+
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
174+
plugin.setBiometricManager(mockBiometricManager);
175+
176+
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
177+
verify(mockResult)
178+
.success(
179+
new ArrayList<String>() {
180+
{
181+
add("weak");
182+
}
183+
});
184+
}
185+
186+
@Test
187+
public void getEnrolledBiometrics_shouldAddStrongBiometrics() {
188+
final LocalAuthPlugin plugin = new LocalAuthPlugin();
189+
setPluginActivity(plugin, buildMockActivity());
190+
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
191+
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
192+
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
193+
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
194+
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
195+
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
196+
plugin.setBiometricManager(mockBiometricManager);
197+
198+
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
199+
verify(mockResult)
200+
.success(
201+
new ArrayList<String>() {
202+
{
203+
add("weak");
204+
add("strong");
205+
}
206+
});
207+
}
208+
209+
private Activity buildMockActivity() {
210+
final Activity mockActivity = mock(Activity.class);
211+
final Context mockContext = mock(Context.class);
212+
when(mockActivity.getBaseContext()).thenReturn(mockContext);
213+
when(mockActivity.getApplicationContext()).thenReturn(mockContext);
214+
return mockActivity;
215+
}
216+
217+
private void setPluginActivity(LocalAuthPlugin plugin, Activity activity) {
218+
final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class);
219+
final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class);
220+
final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class);
221+
final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class);
222+
final DartExecutor mockDartExecutor = mock(DartExecutor.class);
223+
when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine);
224+
when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor);
225+
when(mockActivityBinding.getActivity()).thenReturn(activity);
226+
when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference);
227+
plugin.onAttachedToEngine(mockPluginBinding);
228+
plugin.onAttachedToActivity(mockActivityBinding);
229+
}
64230
}

packages/local_auth/local_auth_android/example/lib/main.dart

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ class MyApp extends StatefulWidget {
2222

2323
class _MyAppState extends State<MyApp> {
2424
_SupportState _supportState = _SupportState.unknown;
25-
bool? _canCheckBiometrics;
26-
List<BiometricType>? _availableBiometrics;
25+
bool? _deviceSupportsBiometrics;
26+
List<BiometricType>? _enrolledBiometrics;
2727
String _authorized = 'Not Authorized';
2828
bool _isAuthenticating = false;
2929

@@ -38,20 +38,20 @@ class _MyAppState extends State<MyApp> {
3838
}
3939

4040
Future<void> _checkBiometrics() async {
41-
late bool canCheckBiometrics;
41+
late bool deviceSupportsBiometrics;
4242
try {
43-
canCheckBiometrics =
44-
(await LocalAuthPlatform.instance.getEnrolledBiometrics()).isNotEmpty;
43+
deviceSupportsBiometrics =
44+
await LocalAuthPlatform.instance.deviceSupportsBiometrics();
4545
} on PlatformException catch (e) {
46-
canCheckBiometrics = false;
46+
deviceSupportsBiometrics = false;
4747
print(e);
4848
}
4949
if (!mounted) {
5050
return;
5151
}
5252

5353
setState(() {
54-
_canCheckBiometrics = canCheckBiometrics;
54+
_deviceSupportsBiometrics = deviceSupportsBiometrics;
5555
});
5656
}
5757

@@ -69,7 +69,7 @@ class _MyAppState extends State<MyApp> {
6969
}
7070

7171
setState(() {
72-
_availableBiometrics = availableBiometrics;
72+
_enrolledBiometrics = availableBiometrics;
7373
});
7474
}
7575

@@ -171,15 +171,16 @@ class _MyAppState extends State<MyApp> {
171171
else
172172
const Text('This device is not supported'),
173173
const Divider(height: 100),
174-
Text('Can check biometrics: $_canCheckBiometrics\n'),
174+
Text(
175+
'Device supports biometrics: $_deviceSupportsBiometrics\n'),
175176
ElevatedButton(
176177
child: const Text('Check biometrics'),
177178
onPressed: _checkBiometrics,
178179
),
179180
const Divider(height: 100),
180-
Text('Available biometrics: $_availableBiometrics\n'),
181+
Text('Enrolled biometrics: $_enrolledBiometrics\n'),
181182
ElevatedButton(
182-
child: const Text('Get available biometrics'),
183+
child: const Text('Get enrolled biometrics'),
183184
onPressed: _getEnrolledBiometrics,
184185
),
185186
const Divider(height: 100),

0 commit comments

Comments
 (0)