Skip to content

Commit 47f3d0a

Browse files
[local_auth] Fix enum return on Android (#3780)
The Pigeon conversion in #3748 used a `List<enum>` return value, which Pigeon doesn't support correctly (see flutter/flutter#125230 for catching this earlier next time). Because the method doesn't return any values in integration test environments it wasn't caught (since the issue is in Pigeon message handling, and thus doesn't show up in unit tests of the plugin). This adds a wrapper class, since enum fields do work. Fixes flutter/flutter#125214
1 parent c459977 commit 47f3d0a

File tree

10 files changed

+141
-40
lines changed

10 files changed

+141
-40
lines changed

packages/local_auth/local_auth_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.0.24
2+
3+
* Fixes `getEnrolledBiometrics` return value handling.
4+
15
## 1.0.23
26

37
* Switches internals to Pigeon and fixes Java warnings.

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.flutter.plugin.common.PluginRegistry;
2525
import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler;
2626
import io.flutter.plugins.localauth.Messages.AuthClassification;
27+
import io.flutter.plugins.localauth.Messages.AuthClassificationWrapper;
2728
import io.flutter.plugins.localauth.Messages.AuthOptions;
2829
import io.flutter.plugins.localauth.Messages.AuthResult;
2930
import io.flutter.plugins.localauth.Messages.AuthResultWrapper;
@@ -98,19 +99,23 @@ public LocalAuthPlugin() {}
9899
return hasBiometricHardware();
99100
}
100101

101-
public @NonNull List<AuthClassification> getEnrolledBiometrics() {
102-
ArrayList<AuthClassification> biometrics = new ArrayList<>();
102+
public @NonNull List<AuthClassificationWrapper> getEnrolledBiometrics() {
103+
ArrayList<AuthClassificationWrapper> biometrics = new ArrayList<>();
103104
if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
104105
== BiometricManager.BIOMETRIC_SUCCESS) {
105-
biometrics.add(AuthClassification.WEAK);
106+
biometrics.add(wrappedBiometric(AuthClassification.WEAK));
106107
}
107108
if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
108109
== BiometricManager.BIOMETRIC_SUCCESS) {
109-
biometrics.add(AuthClassification.STRONG);
110+
biometrics.add(wrappedBiometric(AuthClassification.STRONG));
110111
}
111112
return biometrics;
112113
}
113114

115+
private @NonNull AuthClassificationWrapper wrappedBiometric(AuthClassification value) {
116+
return new AuthClassificationWrapper.Builder().setValue(value).build();
117+
}
118+
114119
public @NonNull Boolean stopAuthentication() {
115120
try {
116121
if (authHelper != null && authInProgress.get()) {

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

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,55 @@ ArrayList<Object> toList() {
538538
}
539539
}
540540

541+
/** Generated class from Pigeon that represents data sent in messages. */
542+
public static final class AuthClassificationWrapper {
543+
private @NonNull AuthClassification value;
544+
545+
public @NonNull AuthClassification getValue() {
546+
return value;
547+
}
548+
549+
public void setValue(@NonNull AuthClassification setterArg) {
550+
if (setterArg == null) {
551+
throw new IllegalStateException("Nonnull field \"value\" is null.");
552+
}
553+
this.value = setterArg;
554+
}
555+
556+
/** Constructor is non-public to enforce null safety; use Builder. */
557+
AuthClassificationWrapper() {}
558+
559+
public static final class Builder {
560+
561+
private @Nullable AuthClassification value;
562+
563+
public @NonNull Builder setValue(@NonNull AuthClassification setterArg) {
564+
this.value = setterArg;
565+
return this;
566+
}
567+
568+
public @NonNull AuthClassificationWrapper build() {
569+
AuthClassificationWrapper pigeonReturn = new AuthClassificationWrapper();
570+
pigeonReturn.setValue(value);
571+
return pigeonReturn;
572+
}
573+
}
574+
575+
@NonNull
576+
ArrayList<Object> toList() {
577+
ArrayList<Object> toListResult = new ArrayList<Object>(1);
578+
toListResult.add(value == null ? null : value.index);
579+
return toListResult;
580+
}
581+
582+
static @NonNull AuthClassificationWrapper fromList(@NonNull ArrayList<Object> list) {
583+
AuthClassificationWrapper pigeonResult = new AuthClassificationWrapper();
584+
Object value = list.get(0);
585+
pigeonResult.setValue(value == null ? null : AuthClassification.values()[(int) value]);
586+
return pigeonResult;
587+
}
588+
}
589+
541590
public interface Result<T> {
542591
@SuppressWarnings("UnknownNullness")
543592
void success(T result);
@@ -554,10 +603,12 @@ private LocalAuthApiCodec() {}
554603
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
555604
switch (type) {
556605
case (byte) 128:
557-
return AuthOptions.fromList((ArrayList<Object>) readValue(buffer));
606+
return AuthClassificationWrapper.fromList((ArrayList<Object>) readValue(buffer));
558607
case (byte) 129:
559-
return AuthResultWrapper.fromList((ArrayList<Object>) readValue(buffer));
608+
return AuthOptions.fromList((ArrayList<Object>) readValue(buffer));
560609
case (byte) 130:
610+
return AuthResultWrapper.fromList((ArrayList<Object>) readValue(buffer));
611+
case (byte) 131:
561612
return AuthStrings.fromList((ArrayList<Object>) readValue(buffer));
562613
default:
563614
return super.readValueOfType(type, buffer);
@@ -566,14 +617,17 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
566617

567618
@Override
568619
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
569-
if (value instanceof AuthOptions) {
620+
if (value instanceof AuthClassificationWrapper) {
570621
stream.write(128);
622+
writeValue(stream, ((AuthClassificationWrapper) value).toList());
623+
} else if (value instanceof AuthOptions) {
624+
stream.write(129);
571625
writeValue(stream, ((AuthOptions) value).toList());
572626
} else if (value instanceof AuthResultWrapper) {
573-
stream.write(129);
627+
stream.write(130);
574628
writeValue(stream, ((AuthResultWrapper) value).toList());
575629
} else if (value instanceof AuthStrings) {
576-
stream.write(130);
630+
stream.write(131);
577631
writeValue(stream, ((AuthStrings) value).toList());
578632
} else {
579633
super.writeValue(stream, value);
@@ -603,7 +657,7 @@ public interface LocalAuthApi {
603657
* Returns the biometric types that are enrolled, and can thus be used without additional setup.
604658
*/
605659
@NonNull
606-
List<AuthClassification> getEnrolledBiometrics();
660+
List<AuthClassificationWrapper> getEnrolledBiometrics();
607661
/**
608662
* Attempts to authenticate the user with the provided [options], and using [strings] for any
609663
* UI.
@@ -695,7 +749,7 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable LocalAuthA
695749
(message, reply) -> {
696750
ArrayList<Object> wrapped = new ArrayList<Object>();
697751
try {
698-
List<AuthClassification> output = api.getEnrolledBiometrics();
752+
List<AuthClassificationWrapper> output = api.getEnrolledBiometrics();
699753
wrapped.add(0, output);
700754
} catch (Throwable exception) {
701755
ArrayList<Object> wrappedError = wrapError(exception);

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.flutter.plugin.common.BinaryMessenger;
3131
import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler;
3232
import io.flutter.plugins.localauth.Messages.AuthClassification;
33+
import io.flutter.plugins.localauth.Messages.AuthClassificationWrapper;
3334
import io.flutter.plugins.localauth.Messages.AuthOptions;
3435
import io.flutter.plugins.localauth.Messages.AuthResult;
3536
import io.flutter.plugins.localauth.Messages.AuthResultWrapper;
@@ -291,7 +292,7 @@ public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent()
291292
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
292293
plugin.setBiometricManager(mockBiometricManager);
293294

294-
final List<AuthClassification> enrolled = plugin.getEnrolledBiometrics();
295+
final List<AuthClassificationWrapper> enrolled = plugin.getEnrolledBiometrics();
295296
assertTrue(enrolled.isEmpty());
296297
}
297298

@@ -304,7 +305,7 @@ public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled()
304305
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
305306
plugin.setBiometricManager(mockBiometricManager);
306307

307-
final List<AuthClassification> enrolled = plugin.getEnrolledBiometrics();
308+
final List<AuthClassificationWrapper> enrolled = plugin.getEnrolledBiometrics();
308309
assertTrue(enrolled.isEmpty());
309310
}
310311

@@ -319,9 +320,9 @@ public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() {
319320
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
320321
plugin.setBiometricManager(mockBiometricManager);
321322

322-
final List<AuthClassification> enrolled = plugin.getEnrolledBiometrics();
323+
final List<AuthClassificationWrapper> enrolled = plugin.getEnrolledBiometrics();
323324
assertEquals(1, enrolled.size());
324-
assertEquals(AuthClassification.WEAK, enrolled.get(0));
325+
assertEquals(AuthClassification.WEAK, enrolled.get(0).getValue());
325326
}
326327

327328
@Test
@@ -335,10 +336,10 @@ public void getEnrolledBiometrics_shouldAddStrongBiometrics() {
335336
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
336337
plugin.setBiometricManager(mockBiometricManager);
337338

338-
final List<AuthClassification> enrolled = plugin.getEnrolledBiometrics();
339+
final List<AuthClassificationWrapper> enrolled = plugin.getEnrolledBiometrics();
339340
assertEquals(2, enrolled.size());
340-
assertEquals(AuthClassification.WEAK, enrolled.get(0));
341-
assertEquals(AuthClassification.STRONG, enrolled.get(1));
341+
assertEquals(AuthClassification.WEAK, enrolled.get(0).getValue());
342+
assertEquals(AuthClassification.STRONG, enrolled.get(1).getValue());
342343
}
343344

344345
@Test

packages/local_auth/local_auth_android/lib/local_auth_android.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ class LocalAuthAndroid extends LocalAuthPlatform {
9696

9797
@override
9898
Future<List<BiometricType>> getEnrolledBiometrics() async {
99-
final List<AuthClassification?> result = await _api.getEnrolledBiometrics();
100-
return result.cast<AuthClassification>().map((AuthClassification entry) {
101-
switch (entry) {
99+
final List<AuthClassificationWrapper?> result =
100+
await _api.getEnrolledBiometrics();
101+
return result
102+
.cast<AuthClassificationWrapper>()
103+
.map((AuthClassificationWrapper entry) {
104+
switch (entry.value) {
102105
case AuthClassification.weak:
103106
return BiometricType.weak;
104107
case AuthClassification.strong:

packages/local_auth/local_auth_android/lib/src/messages.g.dart

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,19 +174,43 @@ class AuthResultWrapper {
174174
}
175175
}
176176

177+
class AuthClassificationWrapper {
178+
AuthClassificationWrapper({
179+
required this.value,
180+
});
181+
182+
AuthClassification value;
183+
184+
Object encode() {
185+
return <Object?>[
186+
value.index,
187+
];
188+
}
189+
190+
static AuthClassificationWrapper decode(Object result) {
191+
result as List<Object?>;
192+
return AuthClassificationWrapper(
193+
value: AuthClassification.values[result[0]! as int],
194+
);
195+
}
196+
}
197+
177198
class _LocalAuthApiCodec extends StandardMessageCodec {
178199
const _LocalAuthApiCodec();
179200
@override
180201
void writeValue(WriteBuffer buffer, Object? value) {
181-
if (value is AuthOptions) {
202+
if (value is AuthClassificationWrapper) {
182203
buffer.putUint8(128);
183204
writeValue(buffer, value.encode());
184-
} else if (value is AuthResultWrapper) {
205+
} else if (value is AuthOptions) {
185206
buffer.putUint8(129);
186207
writeValue(buffer, value.encode());
187-
} else if (value is AuthStrings) {
208+
} else if (value is AuthResultWrapper) {
188209
buffer.putUint8(130);
189210
writeValue(buffer, value.encode());
211+
} else if (value is AuthStrings) {
212+
buffer.putUint8(131);
213+
writeValue(buffer, value.encode());
190214
} else {
191215
super.writeValue(buffer, value);
192216
}
@@ -196,10 +220,12 @@ class _LocalAuthApiCodec extends StandardMessageCodec {
196220
Object? readValueOfType(int type, ReadBuffer buffer) {
197221
switch (type) {
198222
case 128:
199-
return AuthOptions.decode(readValue(buffer)!);
223+
return AuthClassificationWrapper.decode(readValue(buffer)!);
200224
case 129:
201-
return AuthResultWrapper.decode(readValue(buffer)!);
225+
return AuthOptions.decode(readValue(buffer)!);
202226
case 130:
227+
return AuthResultWrapper.decode(readValue(buffer)!);
228+
case 131:
203229
return AuthStrings.decode(readValue(buffer)!);
204230
default:
205231
return super.readValueOfType(type, buffer);
@@ -304,7 +330,7 @@ class LocalAuthApi {
304330

305331
/// Returns the biometric types that are enrolled, and can thus be used
306332
/// without additional setup.
307-
Future<List<AuthClassification?>> getEnrolledBiometrics() async {
333+
Future<List<AuthClassificationWrapper?>> getEnrolledBiometrics() async {
308334
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
309335
'dev.flutter.pigeon.LocalAuthApi.getEnrolledBiometrics', codec,
310336
binaryMessenger: _binaryMessenger);
@@ -326,7 +352,8 @@ class LocalAuthApi {
326352
message: 'Host platform returned null value for non-null return value.',
327353
);
328354
} else {
329-
return (replyList[0] as List<Object?>?)!.cast<AuthClassification?>();
355+
return (replyList[0] as List<Object?>?)!
356+
.cast<AuthClassificationWrapper?>();
330357
}
331358
}
332359

packages/local_auth/local_auth_android/pigeons/messages.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ class AuthResultWrapper {
9494
/// Pigeon equivalent of the subset of BiometricType used by Android.
9595
enum AuthClassification { weak, strong }
9696

97+
// TODO(stuartmorgan): Remove this when
98+
// https://github.com/flutter/flutter/issues/87307 is implemented.
99+
class AuthClassificationWrapper {
100+
AuthClassificationWrapper({required this.value});
101+
final AuthClassification value;
102+
}
103+
97104
@HostApi()
98105
abstract class LocalAuthApi {
99106
/// Returns true if this device supports authentication.
@@ -111,7 +118,7 @@ abstract class LocalAuthApi {
111118

112119
/// Returns the biometric types that are enrolled, and can thus be used
113120
/// without additional setup.
114-
List<AuthClassification> getEnrolledBiometrics();
121+
List<AuthClassificationWrapper> getEnrolledBiometrics();
115122

116123
/// Attempts to authenticate the user with the provided [options], and using
117124
/// [strings] for any UI.

packages/local_auth/local_auth_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: local_auth_android
22
description: Android implementation of the local_auth plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22
5-
version: 1.0.23
5+
version: 1.0.24
66

77
environment:
88
sdk: ">=2.17.0 <4.0.0"

packages/local_auth/local_auth_android/test/local_auth_test.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ void main() {
6565

6666
group('getEnrolledBiometrics', () {
6767
test('translates values', () async {
68-
when(api.getEnrolledBiometrics()).thenAnswer((_) async =>
69-
<AuthClassification>[
70-
AuthClassification.weak,
71-
AuthClassification.strong
72-
]);
68+
when(api.getEnrolledBiometrics())
69+
.thenAnswer((_) async => <AuthClassificationWrapper>[
70+
AuthClassificationWrapper(value: AuthClassification.weak),
71+
AuthClassificationWrapper(value: AuthClassification.strong),
72+
]);
7373

7474
final List<BiometricType> result = await plugin.getEnrolledBiometrics();
7575

@@ -81,7 +81,7 @@ void main() {
8181

8282
test('handles emtpy', () async {
8383
when(api.getEnrolledBiometrics())
84-
.thenAnswer((_) async => <AuthClassification>[]);
84+
.thenAnswer((_) async => <AuthClassificationWrapper>[]);
8585

8686
final List<BiometricType> result = await plugin.getEnrolledBiometrics();
8787

packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi {
6363
returnValue: _i3.Future<bool>.value(false),
6464
) as _i3.Future<bool>);
6565
@override
66-
_i3.Future<List<_i2.AuthClassification?>> getEnrolledBiometrics() =>
66+
_i3.Future<List<_i2.AuthClassificationWrapper?>> getEnrolledBiometrics() =>
6767
(super.noSuchMethod(
6868
Invocation.method(
6969
#getEnrolledBiometrics,
7070
[],
7171
),
72-
returnValue: _i3.Future<List<_i2.AuthClassification?>>.value(
73-
<_i2.AuthClassification?>[]),
74-
) as _i3.Future<List<_i2.AuthClassification?>>);
72+
returnValue: _i3.Future<List<_i2.AuthClassificationWrapper?>>.value(
73+
<_i2.AuthClassificationWrapper?>[]),
74+
) as _i3.Future<List<_i2.AuthClassificationWrapper?>>);
7575
@override
7676
_i3.Future<_i2.AuthResultWrapper> authenticate(
7777
_i2.AuthOptions? arg_options,

0 commit comments

Comments
 (0)