From a07a4a21fe519ee635701447d89af5f4aab3d828 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 9 Apr 2026 15:49:09 -0400 Subject: [PATCH 1/3] [local_auth] Switch to Kotlin Pigeon Replaces the Java Pigeon generator with the Kotlin Pigeon generator, and adjusts the project accordingly: - Adds Kotlin build setings to Gradle. - Updates API signatures for Kotlin/Java differences. - Adds generic Java/Kotlin compat shim to create Result objects from Java, since those haven't been added to the Pigeon generator yet. - Updates tests to use constructors instead of builders, since the Kotlin generator doesn't create builders. - Updates tests to use a Java/Kotlin compat shim to read Kotlin Result values, instead of mocking the Java Pigeon response object. Part of https://github.com/flutter/flutter/issues/158287 --- .../local_auth_android/CHANGELOG.md | 4 + .../android/build.gradle.kts | 11 + .../localauth/AuthenticationHelper.java | 13 +- .../plugins/localauth/LocalAuthPlugin.java | 50 +- .../flutter/plugins/localauth/Messages.java | 745 ------------------ .../io/flutter/plugins/localauth/Messages.kt | 454 +++++++++++ .../flutter/plugins/localauth/ResultUtils.kt | 13 + .../localauth/AuthenticationHelperTest.java | 105 +-- .../plugins/localauth/LocalAuthTest.java | 134 ++-- .../plugins/localauth/TestResultUtils.kt | 35 + .../lib/src/messages.g.dart | 234 +++--- .../local_auth_android/pigeons/messages.dart | 5 +- .../local_auth_android/pubspec.yaml | 3 +- 13 files changed, 740 insertions(+), 1066 deletions(-) delete mode 100644 packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java create mode 100644 packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt create mode 100644 packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/ResultUtils.kt create mode 100644 packages/local_auth/local_auth_android/android/src/test/kotlin/io/flutter/plugins/localauth/TestResultUtils.kt diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 5e329a9a2d26..3cba0eb61970 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.8 + +* Updates internal implementation to use Kotlin Pigeon. + ## 2.0.7 * Updates build files from Groovy to Kotlin. diff --git a/packages/local_auth/local_auth_android/android/build.gradle.kts b/packages/local_auth/local_auth_android/android/build.gradle.kts index 11af0e4afa4e..e9321f91a6d9 100644 --- a/packages/local_auth/local_auth_android/android/build.gradle.kts +++ b/packages/local_auth/local_auth_android/android/build.gradle.kts @@ -1,7 +1,10 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + group = "io.flutter.plugins.localauth" version = "1.0-SNAPSHOT" buildscript { + val kotlinVersion = "2.3.20" repositories { google() mavenCentral() @@ -9,6 +12,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:8.13.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } @@ -21,6 +25,13 @@ rootProject.allprojects { plugins { id("com.android.library") + id("kotlin-android") +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.fromTarget(JavaVersion.VERSION_17.toString()) + } } android { diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index 4da8d5f2c361..216791a9d6d1 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -16,8 +16,6 @@ import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; -import io.flutter.plugins.localauth.Messages.AuthResult; -import io.flutter.plugins.localauth.Messages.AuthResultCode; import java.util.concurrent.Executor; /** @@ -37,7 +35,6 @@ interface AuthCompletionHandler { private final Lifecycle lifecycle; private final FragmentActivity activity; private final AuthCompletionHandler completionHandler; - private final Messages.AuthStrings strings; private final BiometricPrompt.PromptInfo promptInfo; private final boolean isAuthSticky; private final UiThreadExecutor uiThreadExecutor; @@ -47,14 +44,13 @@ interface AuthCompletionHandler { AuthenticationHelper( Lifecycle lifecycle, FragmentActivity activity, - @NonNull Messages.AuthOptions options, - @NonNull Messages.AuthStrings strings, + @NonNull AuthOptions options, + @NonNull AuthStrings strings, @NonNull AuthCompletionHandler completionHandler, boolean allowCredentials) { this.lifecycle = lifecycle; this.activity = activity; this.completionHandler = completionHandler; - this.strings = strings; this.isAuthSticky = options.getSticky(); this.uiThreadExecutor = new UiThreadExecutor(); @@ -157,14 +153,13 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString code = AuthResultCode.UNKNOWN_ERROR; break; } - completionHandler.complete( - new AuthResult.Builder().setCode(code).setErrorMessage(errString.toString()).build()); + completionHandler.complete(new AuthResult(code, errString.toString())); stop(); } @Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { - completionHandler.complete(new AuthResult.Builder().setCode(AuthResultCode.SUCCESS).build()); + completionHandler.complete(new AuthResult(AuthResultCode.SUCCESS, null)); stop(); } 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 7b02af92382e..119fe2402824 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,6 +11,7 @@ import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.biometric.BiometricManager; import androidx.fragment.app.FragmentActivity; @@ -20,16 +21,13 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; -import io.flutter.plugins.localauth.Messages.AuthClassification; -import io.flutter.plugins.localauth.Messages.AuthOptions; -import io.flutter.plugins.localauth.Messages.AuthResult; -import io.flutter.plugins.localauth.Messages.AuthResultCode; -import io.flutter.plugins.localauth.Messages.AuthStrings; -import io.flutter.plugins.localauth.Messages.LocalAuthApi; -import io.flutter.plugins.localauth.Messages.Result; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import kotlin.Result; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import org.jetbrains.annotations.NotNull; /** * Flutter plugin providing access to local authentication. @@ -54,17 +52,17 @@ public class LocalAuthPlugin implements FlutterPlugin, ActivityAware, LocalAuthA public LocalAuthPlugin() {} @Override - public @NonNull Boolean isDeviceSupported() { + public boolean isDeviceSupported() { return isDeviceSecure() || canAuthenticateWithBiometrics(); } @Override - public @NonNull Boolean deviceCanSupportBiometrics() { + public boolean deviceCanSupportBiometrics() { return hasBiometricHardware(); } @Override - public @NonNull List getEnrolledBiometrics() { + public @Nullable List getEnrolledBiometrics() { if (biometricManager == null) { return null; } @@ -81,7 +79,7 @@ public LocalAuthPlugin() {} } @Override - public @NonNull Boolean stopAuthentication() { + public boolean stopAuthentication() { try { if (authHelper != null && authInProgress.get()) { authHelper.stopAuthentication(); @@ -98,30 +96,32 @@ public LocalAuthPlugin() {} public void authenticate( @NonNull AuthOptions options, @NonNull AuthStrings strings, - @NonNull Result result) { + @NonNull Function1, @NotNull Unit> callback) { if (authInProgress.get()) { - result.success(new AuthResult.Builder().setCode(AuthResultCode.ALREADY_IN_PROGRESS).build()); + ResultUtilsKt.completeWithValue( + callback, new AuthResult(AuthResultCode.ALREADY_IN_PROGRESS, null)); return; } if (activity == null || activity.isFinishing()) { - result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_ACTIVITY).build()); + ResultUtilsKt.completeWithValue(callback, new AuthResult(AuthResultCode.NO_ACTIVITY, null)); return; } if (!(activity instanceof FragmentActivity)) { - result.success( - new AuthResult.Builder().setCode(AuthResultCode.NOT_FRAGMENT_ACTIVITY).build()); + ResultUtilsKt.completeWithValue( + callback, new AuthResult(AuthResultCode.NOT_FRAGMENT_ACTIVITY, null)); return; } if (!isDeviceSupported()) { - result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_CREDENTIALS).build()); + ResultUtilsKt.completeWithValue( + callback, new AuthResult(AuthResultCode.NO_CREDENTIALS, null)); return; } authInProgress.set(true); - AuthCompletionHandler completionHandler = createAuthCompletionHandler(result); + AuthCompletionHandler completionHandler = createAuthCompletionHandler(callback); boolean allowCredentials = !options.getBiometricOnly() && canAuthenticateWithDeviceCredential(); @@ -130,8 +130,8 @@ public void authenticate( @VisibleForTesting public @NonNull AuthCompletionHandler createAuthCompletionHandler( - @NonNull final Result result) { - return authResult -> onAuthenticationCompleted(result, authResult); + @NonNull Function1, @NotNull Unit> callback) { + return authResult -> onAuthenticationCompleted(callback, authResult); } @VisibleForTesting @@ -152,9 +152,11 @@ public void sendAuthenticationRequest( authHelper.authenticate(); } - void onAuthenticationCompleted(Result result, AuthResult value) { + void onAuthenticationCompleted( + @NonNull Function1, @NotNull Unit> callback, + AuthResult value) { if (authInProgress.compareAndSet(true, false)) { - result.success(value); + ResultUtilsKt.completeWithValue(callback, value); } } @@ -192,12 +194,12 @@ public boolean canAuthenticateWithDeviceCredential() { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - LocalAuthApi.setUp(binding.getBinaryMessenger(), this); + LocalAuthApi.Companion.setUp(binding.getBinaryMessenger(), this); } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - LocalAuthApi.setUp(binding.getBinaryMessenger(), null); + LocalAuthApi.Companion.setUp(binding.getBinaryMessenger(), null); } private void setServicesFromActivity(Activity activity) { diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java deleted file mode 100644 index fe20c622d4ac..000000000000 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java +++ /dev/null @@ -1,745 +0,0 @@ -// Copyright 2013 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -package io.flutter.plugins.localauth; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.CLASS; - -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.plugin.common.BasicMessageChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MessageCodec; -import io.flutter.plugin.common.StandardMessageCodec; -import java.io.ByteArrayOutputStream; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** Generated class from Pigeon. */ -@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) -public class Messages { - - /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ - public static class FlutterError extends RuntimeException { - - /** The error code. */ - public final String code; - - /** The error details. Must be a datatype supported by the api codec. */ - public final Object details; - - public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { - super(message); - this.code = code; - this.details = details; - } - } - - @NonNull - protected static ArrayList wrapError(@NonNull Throwable exception) { - ArrayList errorList = new ArrayList<>(3); - if (exception instanceof FlutterError) { - FlutterError error = (FlutterError) exception; - errorList.add(error.code); - errorList.add(error.getMessage()); - errorList.add(error.details); - } else { - errorList.add(exception.toString()); - errorList.add(exception.getClass().getSimpleName()); - errorList.add( - "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - } - return errorList; - } - - @Target(METHOD) - @Retention(CLASS) - @interface CanIgnoreReturnValue {} - - /** Possible outcomes of an authentication attempt. */ - public enum AuthResultCode { - /** The user authenticated successfully. */ - SUCCESS(0), - /** The user pressed the negative button, which corresponds to [AuthStrings.cancelButton]. */ - NEGATIVE_BUTTON(1), - /** - * The user canceled authentication without pressing the negative button. - * - *

This may be triggered by a swipe or a back button, for example. - */ - USER_CANCELED(2), - /** Authentication was caneceled by the system. */ - SYSTEM_CANCELED(3), - /** Authentication timed out. */ - TIMEOUT(4), - /** An authentication was already in progress. */ - ALREADY_IN_PROGRESS(5), - /** There is no foreground activity. */ - NO_ACTIVITY(6), - /** The foreground activity is not a FragmentActivity. */ - NOT_FRAGMENT_ACTIVITY(7), - /** The device does not have any credentials available. */ - NO_CREDENTIALS(8), - /** No biometric hardware is present. */ - NO_HARDWARE(9), - /** The biometric is temporarily unavailable. */ - HARDWARE_UNAVAILABLE(10), - /** No biometrics are enrolled. */ - NOT_ENROLLED(11), - /** The user is locked out temporarily due to too many failed attempts. */ - LOCKED_OUT_TEMPORARILY(12), - /** The user is locked out until they log in another way due to too many failed attempts. */ - LOCKED_OUT_PERMANENTLY(13), - /** The device does not have enough storage to complete authentication. */ - NO_SPACE(14), - /** The hardware is unavailable until a security update is performed. */ - SECURITY_UPDATE_REQUIRED(15), - /** Some unrecognized error case was encountered */ - UNKNOWN_ERROR(16); - - final int index; - - AuthResultCode(final int index) { - this.index = index; - } - } - - /** Pigeon equivalent of the subset of BiometricType used by Android. */ - public enum AuthClassification { - WEAK(0), - STRONG(1); - - final int index; - - AuthClassification(final int index) { - this.index = index; - } - } - - /** - * Pigeon version of AndroidAuthStrings, plus the authorization reason. - * - *

See auth_messages_android.dart for details. - * - *

Generated class from Pigeon that represents data sent in messages. - */ - public static final class AuthStrings { - private @NonNull String reason; - - public @NonNull String getReason() { - return reason; - } - - public void setReason(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"reason\" is null."); - } - this.reason = setterArg; - } - - private @NonNull String signInHint; - - public @NonNull String getSignInHint() { - return signInHint; - } - - public void setSignInHint(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"signInHint\" is null."); - } - this.signInHint = setterArg; - } - - private @NonNull String cancelButton; - - public @NonNull String getCancelButton() { - return cancelButton; - } - - public void setCancelButton(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"cancelButton\" is null."); - } - this.cancelButton = setterArg; - } - - private @NonNull String signInTitle; - - public @NonNull String getSignInTitle() { - return signInTitle; - } - - public void setSignInTitle(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"signInTitle\" is null."); - } - this.signInTitle = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - AuthStrings() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AuthStrings that = (AuthStrings) o; - return reason.equals(that.reason) - && signInHint.equals(that.signInHint) - && cancelButton.equals(that.cancelButton) - && signInTitle.equals(that.signInTitle); - } - - @Override - public int hashCode() { - return Objects.hash(reason, signInHint, cancelButton, signInTitle); - } - - public static final class Builder { - - private @Nullable String reason; - - @CanIgnoreReturnValue - public @NonNull Builder setReason(@NonNull String setterArg) { - this.reason = setterArg; - return this; - } - - private @Nullable String signInHint; - - @CanIgnoreReturnValue - public @NonNull Builder setSignInHint(@NonNull String setterArg) { - this.signInHint = setterArg; - return this; - } - - private @Nullable String cancelButton; - - @CanIgnoreReturnValue - public @NonNull Builder setCancelButton(@NonNull String setterArg) { - this.cancelButton = setterArg; - return this; - } - - private @Nullable String signInTitle; - - @CanIgnoreReturnValue - public @NonNull Builder setSignInTitle(@NonNull String setterArg) { - this.signInTitle = setterArg; - return this; - } - - public @NonNull AuthStrings build() { - AuthStrings pigeonReturn = new AuthStrings(); - pigeonReturn.setReason(reason); - pigeonReturn.setSignInHint(signInHint); - pigeonReturn.setCancelButton(cancelButton); - pigeonReturn.setSignInTitle(signInTitle); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(4); - toListResult.add(reason); - toListResult.add(signInHint); - toListResult.add(cancelButton); - toListResult.add(signInTitle); - return toListResult; - } - - static @NonNull AuthStrings fromList(@NonNull ArrayList pigeonVar_list) { - AuthStrings pigeonResult = new AuthStrings(); - Object reason = pigeonVar_list.get(0); - pigeonResult.setReason((String) reason); - Object signInHint = pigeonVar_list.get(1); - pigeonResult.setSignInHint((String) signInHint); - Object cancelButton = pigeonVar_list.get(2); - pigeonResult.setCancelButton((String) cancelButton); - Object signInTitle = pigeonVar_list.get(3); - pigeonResult.setSignInTitle((String) signInTitle); - return pigeonResult; - } - } - - /** - * The results of an authentication request. - * - *

Generated class from Pigeon that represents data sent in messages. - */ - public static final class AuthResult { - /** The specific result returned from the SDK. */ - private @NonNull AuthResultCode code; - - public @NonNull AuthResultCode getCode() { - return code; - } - - public void setCode(@NonNull AuthResultCode setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"code\" is null."); - } - this.code = setterArg; - } - - /** The error message associated with the result, if any. */ - private @Nullable String errorMessage; - - public @Nullable String getErrorMessage() { - return errorMessage; - } - - public void setErrorMessage(@Nullable String setterArg) { - this.errorMessage = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - AuthResult() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AuthResult that = (AuthResult) o; - return code.equals(that.code) && Objects.equals(errorMessage, that.errorMessage); - } - - @Override - public int hashCode() { - return Objects.hash(code, errorMessage); - } - - public static final class Builder { - - private @Nullable AuthResultCode code; - - @CanIgnoreReturnValue - public @NonNull Builder setCode(@NonNull AuthResultCode setterArg) { - this.code = setterArg; - return this; - } - - private @Nullable String errorMessage; - - @CanIgnoreReturnValue - public @NonNull Builder setErrorMessage(@Nullable String setterArg) { - this.errorMessage = setterArg; - return this; - } - - public @NonNull AuthResult build() { - AuthResult pigeonReturn = new AuthResult(); - pigeonReturn.setCode(code); - pigeonReturn.setErrorMessage(errorMessage); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(2); - toListResult.add(code); - toListResult.add(errorMessage); - return toListResult; - } - - static @NonNull AuthResult fromList(@NonNull ArrayList pigeonVar_list) { - AuthResult pigeonResult = new AuthResult(); - Object code = pigeonVar_list.get(0); - pigeonResult.setCode((AuthResultCode) code); - Object errorMessage = pigeonVar_list.get(1); - pigeonResult.setErrorMessage((String) errorMessage); - return pigeonResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static final class AuthOptions { - private @NonNull Boolean biometricOnly; - - public @NonNull Boolean getBiometricOnly() { - return biometricOnly; - } - - public void setBiometricOnly(@NonNull Boolean setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"biometricOnly\" is null."); - } - this.biometricOnly = setterArg; - } - - private @NonNull Boolean sensitiveTransaction; - - public @NonNull Boolean getSensitiveTransaction() { - return sensitiveTransaction; - } - - public void setSensitiveTransaction(@NonNull Boolean setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"sensitiveTransaction\" is null."); - } - this.sensitiveTransaction = setterArg; - } - - private @NonNull Boolean sticky; - - public @NonNull Boolean getSticky() { - return sticky; - } - - public void setSticky(@NonNull Boolean setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"sticky\" is null."); - } - this.sticky = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - AuthOptions() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AuthOptions that = (AuthOptions) o; - return biometricOnly.equals(that.biometricOnly) - && sensitiveTransaction.equals(that.sensitiveTransaction) - && sticky.equals(that.sticky); - } - - @Override - public int hashCode() { - return Objects.hash(biometricOnly, sensitiveTransaction, sticky); - } - - public static final class Builder { - - private @Nullable Boolean biometricOnly; - - @CanIgnoreReturnValue - public @NonNull Builder setBiometricOnly(@NonNull Boolean setterArg) { - this.biometricOnly = setterArg; - return this; - } - - private @Nullable Boolean sensitiveTransaction; - - @CanIgnoreReturnValue - public @NonNull Builder setSensitiveTransaction(@NonNull Boolean setterArg) { - this.sensitiveTransaction = setterArg; - return this; - } - - private @Nullable Boolean sticky; - - @CanIgnoreReturnValue - public @NonNull Builder setSticky(@NonNull Boolean setterArg) { - this.sticky = setterArg; - return this; - } - - public @NonNull AuthOptions build() { - AuthOptions pigeonReturn = new AuthOptions(); - pigeonReturn.setBiometricOnly(biometricOnly); - pigeonReturn.setSensitiveTransaction(sensitiveTransaction); - pigeonReturn.setSticky(sticky); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(3); - toListResult.add(biometricOnly); - toListResult.add(sensitiveTransaction); - toListResult.add(sticky); - return toListResult; - } - - static @NonNull AuthOptions fromList(@NonNull ArrayList pigeonVar_list) { - AuthOptions pigeonResult = new AuthOptions(); - Object biometricOnly = pigeonVar_list.get(0); - pigeonResult.setBiometricOnly((Boolean) biometricOnly); - Object sensitiveTransaction = pigeonVar_list.get(1); - pigeonResult.setSensitiveTransaction((Boolean) sensitiveTransaction); - Object sticky = pigeonVar_list.get(2); - pigeonResult.setSticky((Boolean) sticky); - return pigeonResult; - } - } - - private static class PigeonCodec extends StandardMessageCodec { - public static final PigeonCodec INSTANCE = new PigeonCodec(); - - private PigeonCodec() {} - - @Override - protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { - switch (type) { - case (byte) 129: - { - Object value = readValue(buffer); - return value == null ? null : AuthResultCode.values()[((Long) value).intValue()]; - } - case (byte) 130: - { - Object value = readValue(buffer); - return value == null ? null : AuthClassification.values()[((Long) value).intValue()]; - } - case (byte) 131: - return AuthStrings.fromList((ArrayList) readValue(buffer)); - case (byte) 132: - return AuthResult.fromList((ArrayList) readValue(buffer)); - case (byte) 133: - return AuthOptions.fromList((ArrayList) readValue(buffer)); - default: - return super.readValueOfType(type, buffer); - } - } - - @Override - protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof AuthResultCode) { - stream.write(129); - writeValue(stream, value == null ? null : ((AuthResultCode) value).index); - } else if (value instanceof AuthClassification) { - stream.write(130); - writeValue(stream, value == null ? null : ((AuthClassification) value).index); - } else if (value instanceof AuthStrings) { - stream.write(131); - writeValue(stream, ((AuthStrings) value).toList()); - } else if (value instanceof AuthResult) { - stream.write(132); - writeValue(stream, ((AuthResult) value).toList()); - } else if (value instanceof AuthOptions) { - stream.write(133); - writeValue(stream, ((AuthOptions) value).toList()); - } else { - super.writeValue(stream, value); - } - } - } - - /** Asynchronous error handling return type for non-nullable API method returns. */ - public interface Result { - /** Success case callback method for handling returns. */ - void success(@NonNull T result); - - /** Failure case callback method for handling errors. */ - void error(@NonNull Throwable error); - } - /** Asynchronous error handling return type for nullable API method returns. */ - public interface NullableResult { - /** Success case callback method for handling returns. */ - void success(@Nullable T result); - - /** Failure case callback method for handling errors. */ - void error(@NonNull Throwable error); - } - /** Asynchronous error handling return type for void API method returns. */ - public interface VoidResult { - /** Success case callback method for handling returns. */ - void success(); - - /** Failure case callback method for handling errors. */ - void error(@NonNull Throwable error); - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface LocalAuthApi { - /** Returns true if this device supports authentication. */ - @NonNull - Boolean isDeviceSupported(); - /** - * Returns true if this device can support biometric authentication, whether any biometrics are - * enrolled or not. - */ - @NonNull - Boolean deviceCanSupportBiometrics(); - /** - * Cancels any in-progress authentication. - * - *

Returns true only if authentication was in progress, and was successfully cancelled. - */ - @NonNull - Boolean stopAuthentication(); - /** - * Returns the biometric types that are enrolled, and can thus be used without additional setup. - * - *

Returns null if there is no activity, in which case the enrolled biometrics can't be - * determined. - */ - @Nullable - List getEnrolledBiometrics(); - /** - * Attempts to authenticate the user with the provided [options], and using [strings] for any - * UI. - */ - void authenticate( - @NonNull AuthOptions options, - @NonNull AuthStrings strings, - @NonNull Result result); - - /** The codec used by LocalAuthApi. */ - static @NonNull MessageCodec getCodec() { - return PigeonCodec.INSTANCE; - } - /** Sets up an instance of `LocalAuthApi` to handle messages through the `binaryMessenger`. */ - static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable LocalAuthApi api) { - setUp(binaryMessenger, "", api); - } - - static void setUp( - @NonNull BinaryMessenger binaryMessenger, - @NonNull String messageChannelSuffix, - @Nullable LocalAuthApi api) { - messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.local_auth_android.LocalAuthApi.isDeviceSupported" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - Boolean output = api.isDeviceSupported(); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.local_auth_android.LocalAuthApi.deviceCanSupportBiometrics" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - Boolean output = api.deviceCanSupportBiometrics(); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.local_auth_android.LocalAuthApi.stopAuthentication" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - Boolean output = api.stopAuthentication(); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.local_auth_android.LocalAuthApi.getEnrolledBiometrics" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - List output = api.getEnrolledBiometrics(); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.local_auth_android.LocalAuthApi.authenticate" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - AuthOptions optionsArg = (AuthOptions) args.get(0); - AuthStrings stringsArg = (AuthStrings) args.get(1); - Result resultCallback = - new Result() { - public void success(AuthResult result) { - wrapped.add(0, result); - reply.reply(wrapped); - } - - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; - - api.authenticate(optionsArg, stringsArg, resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } - } - } -} diff --git a/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt b/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt new file mode 100644 index 000000000000..5822b5b85b64 --- /dev/null +++ b/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt @@ -0,0 +1,454 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v26.2.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package io.flutter.plugins.localauth + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private object MessagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf(exception.code, exception.message, exception.details) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) + } + } + + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && + a.all { (b as Map).contains(it.key) && deepEquals(it.value, b[it.key]) } + } + return a == b + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Possible outcomes of an authentication attempt. */ +enum class AuthResultCode(val raw: Int) { + /** The user authenticated successfully. */ + SUCCESS(0), + /** The user pressed the negative button, which corresponds to [AuthStrings.cancelButton]. */ + NEGATIVE_BUTTON(1), + /** + * The user canceled authentication without pressing the negative button. + * + * This may be triggered by a swipe or a back button, for example. + */ + USER_CANCELED(2), + /** Authentication was caneceled by the system. */ + SYSTEM_CANCELED(3), + /** Authentication timed out. */ + TIMEOUT(4), + /** An authentication was already in progress. */ + ALREADY_IN_PROGRESS(5), + /** There is no foreground activity. */ + NO_ACTIVITY(6), + /** The foreground activity is not a FragmentActivity. */ + NOT_FRAGMENT_ACTIVITY(7), + /** The device does not have any credentials available. */ + NO_CREDENTIALS(8), + /** No biometric hardware is present. */ + NO_HARDWARE(9), + /** The biometric is temporarily unavailable. */ + HARDWARE_UNAVAILABLE(10), + /** No biometrics are enrolled. */ + NOT_ENROLLED(11), + /** The user is locked out temporarily due to too many failed attempts. */ + LOCKED_OUT_TEMPORARILY(12), + /** The user is locked out until they log in another way due to too many failed attempts. */ + LOCKED_OUT_PERMANENTLY(13), + /** The device does not have enough storage to complete authentication. */ + NO_SPACE(14), + /** The hardware is unavailable until a security update is performed. */ + SECURITY_UPDATE_REQUIRED(15), + /** Some unrecognized error case was encountered */ + UNKNOWN_ERROR(16); + + companion object { + fun ofRaw(raw: Int): AuthResultCode? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Pigeon equivalent of the subset of BiometricType used by Android. */ +enum class AuthClassification(val raw: Int) { + WEAK(0), + STRONG(1); + + companion object { + fun ofRaw(raw: Int): AuthClassification? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * Pigeon version of AndroidAuthStrings, plus the authorization reason. + * + * See auth_messages_android.dart for details. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class AuthStrings( + val reason: String, + val signInHint: String, + val cancelButton: String, + val signInTitle: String +) { + companion object { + fun fromList(pigeonVar_list: List): AuthStrings { + val reason = pigeonVar_list[0] as String + val signInHint = pigeonVar_list[1] as String + val cancelButton = pigeonVar_list[2] as String + val signInTitle = pigeonVar_list[3] as String + return AuthStrings(reason, signInHint, cancelButton, signInTitle) + } + } + + fun toList(): List { + return listOf( + reason, + signInHint, + cancelButton, + signInTitle, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is AuthStrings) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * The results of an authentication request. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class AuthResult( + /** The specific result returned from the SDK. */ + val code: AuthResultCode, + /** The error message associated with the result, if any. */ + val errorMessage: String? = null +) { + companion object { + fun fromList(pigeonVar_list: List): AuthResult { + val code = pigeonVar_list[0] as AuthResultCode + val errorMessage = pigeonVar_list[1] as String? + return AuthResult(code, errorMessage) + } + } + + fun toList(): List { + return listOf( + code, + errorMessage, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is AuthResult) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class AuthOptions( + val biometricOnly: Boolean, + val sensitiveTransaction: Boolean, + val sticky: Boolean +) { + companion object { + fun fromList(pigeonVar_list: List): AuthOptions { + val biometricOnly = pigeonVar_list[0] as Boolean + val sensitiveTransaction = pigeonVar_list[1] as Boolean + val sticky = pigeonVar_list[2] as Boolean + return AuthOptions(biometricOnly, sensitiveTransaction, sticky) + } + } + + fun toList(): List { + return listOf( + biometricOnly, + sensitiveTransaction, + sticky, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is AuthOptions) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { AuthResultCode.ofRaw(it.toInt()) } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { AuthClassification.ofRaw(it.toInt()) } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { AuthStrings.fromList(it) } + } + 132.toByte() -> { + return (readValue(buffer) as? List)?.let { AuthResult.fromList(it) } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { AuthOptions.fromList(it) } + } + else -> super.readValueOfType(type, buffer) + } + } + + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is AuthResultCode -> { + stream.write(129) + writeValue(stream, value.raw.toLong()) + } + is AuthClassification -> { + stream.write(130) + writeValue(stream, value.raw.toLong()) + } + is AuthStrings -> { + stream.write(131) + writeValue(stream, value.toList()) + } + is AuthResult -> { + stream.write(132) + writeValue(stream, value.toList()) + } + is AuthOptions -> { + stream.write(133) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface LocalAuthApi { + /** Returns true if this device supports authentication. */ + fun isDeviceSupported(): Boolean + /** + * Returns true if this device can support biometric authentication, whether any biometrics are + * enrolled or not. + */ + fun deviceCanSupportBiometrics(): Boolean + /** + * Cancels any in-progress authentication. + * + * Returns true only if authentication was in progress, and was successfully cancelled. + */ + fun stopAuthentication(): Boolean + /** + * Returns the biometric types that are enrolled, and can thus be used without additional setup. + * + * Returns null if there is no activity, in which case the enrolled biometrics can't be + * determined. + */ + fun getEnrolledBiometrics(): List? + /** + * Attempts to authenticate the user with the provided [options], and using [strings] for any UI. + */ + fun authenticate( + options: AuthOptions, + strings: AuthStrings, + callback: (Result) -> Unit + ) + + companion object { + /** The codec used by LocalAuthApi. */ + val codec: MessageCodec by lazy { MessagesPigeonCodec() } + /** Sets up an instance of `LocalAuthApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: LocalAuthApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.local_auth_android.LocalAuthApi.isDeviceSupported$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.isDeviceSupported()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.local_auth_android.LocalAuthApi.deviceCanSupportBiometrics$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.deviceCanSupportBiometrics()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.local_auth_android.LocalAuthApi.stopAuthentication$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.stopAuthentication()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.local_auth_android.LocalAuthApi.getEnrolledBiometrics$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getEnrolledBiometrics()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.local_auth_android.LocalAuthApi.authenticate$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val optionsArg = args[0] as AuthOptions + val stringsArg = args[1] as AuthStrings + api.authenticate(optionsArg, stringsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/ResultUtils.kt b/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/ResultUtils.kt new file mode 100644 index 000000000000..7e811c893bac --- /dev/null +++ b/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/ResultUtils.kt @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.localauth + +fun completeWithError(callback: (Result<@JvmSuppressWildcards T>) -> Unit, failure: Throwable) { + callback(Result.failure(failure)) +} + +fun completeWithValue(callback: (Result<@JvmSuppressWildcards T>) -> Unit, value: T) { + callback(Result.success(value)) +} diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java index d6020fa396fb..754838f996fc 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java @@ -13,10 +13,6 @@ import androidx.biometric.BiometricPrompt; import androidx.fragment.app.FragmentActivity; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; -import io.flutter.plugins.localauth.Messages.AuthOptions; -import io.flutter.plugins.localauth.Messages.AuthResult; -import io.flutter.plugins.localauth.Messages.AuthResultCode; -import io.flutter.plugins.localauth.Messages.AuthStrings; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -27,19 +23,9 @@ @RunWith(RobolectricTestRunner.class) public class AuthenticationHelperTest { static final AuthStrings dummyStrings = - new AuthStrings.Builder() - .setReason("a reason") - .setSignInHint("a hint") - .setCancelButton("cancel") - .setSignInTitle("sign in") - .build(); - - static final AuthOptions defaultOptions = - new AuthOptions.Builder() - .setBiometricOnly(false) - .setSensitiveTransaction(false) - .setSticky(false) - .build(); + new AuthStrings("a reason", "a hint", "cancel", "sign in"); + + static final AuthOptions defaultOptions = new AuthOptions(false, false, false); @Test public void onAuthenticationError_returnsUserCanceled() { @@ -55,12 +41,7 @@ public void onAuthenticationError_returnsUserCanceled() { helper.onAuthenticationError(BiometricPrompt.ERROR_USER_CANCELED, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.USER_CANCELED) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.USER_CANCELED, "")); } @Test @@ -77,12 +58,7 @@ public void onAuthenticationError_returnsNegativeButton() { helper.onAuthenticationError(BiometricPrompt.ERROR_NEGATIVE_BUTTON, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.NEGATIVE_BUTTON) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.NEGATIVE_BUTTON, "")); } @Test @@ -99,12 +75,7 @@ public void onAuthenticationError_withoutDialogs_returnsNoCredential() { helper.onAuthenticationError(BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.NO_CREDENTIALS) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.NO_CREDENTIALS, "")); } @Test @@ -121,12 +92,7 @@ public void onAuthenticationError_withoutDialogs_returnsNotEnrolledForNoBiometri helper.onAuthenticationError(BiometricPrompt.ERROR_NO_BIOMETRICS, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.NOT_ENROLLED) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.NOT_ENROLLED, "")); } @Test @@ -143,12 +109,7 @@ public void onAuthenticationError_returnsHardwareUnavailable() { helper.onAuthenticationError(BiometricPrompt.ERROR_HW_UNAVAILABLE, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.HARDWARE_UNAVAILABLE) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.HARDWARE_UNAVAILABLE, "")); } @Test @@ -165,12 +126,7 @@ public void onAuthenticationError_returnsHardwareNotPresent() { helper.onAuthenticationError(BiometricPrompt.ERROR_HW_NOT_PRESENT, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.NO_HARDWARE) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.NO_HARDWARE, "")); } @Test @@ -187,12 +143,7 @@ public void onAuthenticationError_returnsTemporaryLockoutForLockout() { helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.LOCKED_OUT_TEMPORARILY) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.LOCKED_OUT_TEMPORARILY, "")); } @Test @@ -209,12 +160,7 @@ public void onAuthenticationError_returnsPermanentLockoutForLockoutPermanent() { helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT_PERMANENT, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.LOCKED_OUT_PERMANENTLY) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.LOCKED_OUT_PERMANENTLY, "")); } @Test @@ -231,12 +177,7 @@ public void onAuthenticationError_withoutSticky_returnsSystemCanceled() { helper.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.SYSTEM_CANCELED) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.SYSTEM_CANCELED, "")); } @Test @@ -253,9 +194,7 @@ public void onAuthenticationError_returnsTimeout() { helper.onAuthenticationError(BiometricPrompt.ERROR_TIMEOUT, ""); - verify(handler) - .complete( - new AuthResult.Builder().setCode(AuthResultCode.TIMEOUT).setErrorMessage("").build()); + verify(handler).complete(new AuthResult(AuthResultCode.TIMEOUT, "")); } @Test @@ -272,9 +211,7 @@ public void onAuthenticationError_returnsNoSpace() { helper.onAuthenticationError(BiometricPrompt.ERROR_NO_SPACE, ""); - verify(handler) - .complete( - new AuthResult.Builder().setCode(AuthResultCode.NO_SPACE).setErrorMessage("").build()); + verify(handler).complete(new AuthResult(AuthResultCode.NO_SPACE, "")); } @Test @@ -291,12 +228,7 @@ public void onAuthenticationError_returnsSecurityUpdateRequired() { helper.onAuthenticationError(BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.SECURITY_UPDATE_REQUIRED) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.SECURITY_UPDATE_REQUIRED, "")); } @Test @@ -313,12 +245,7 @@ public void onAuthenticationError_returnsUnknownForOtherCases() { helper.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS, ""); - verify(handler) - .complete( - new AuthResult.Builder() - .setCode(AuthResultCode.UNKNOWN_ERROR) - .setErrorMessage("") - .build()); + verify(handler).complete(new AuthResult(AuthResultCode.UNKNOWN_ERROR, "")); } private FragmentActivity buildMockActivityWithContext(FragmentActivity mockActivity) { 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 5a58297360f5..765cde08f4dd 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 @@ -29,12 +29,6 @@ import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; -import io.flutter.plugins.localauth.Messages.AuthClassification; -import io.flutter.plugins.localauth.Messages.AuthOptions; -import io.flutter.plugins.localauth.Messages.AuthResult; -import io.flutter.plugins.localauth.Messages.AuthResultCode; -import io.flutter.plugins.localauth.Messages.AuthStrings; -import io.flutter.plugins.localauth.Messages.Result; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,67 +39,77 @@ @RunWith(RobolectricTestRunner.class) public class LocalAuthTest { static final AuthStrings dummyStrings = - new AuthStrings.Builder() - .setReason("a reason") - .setSignInHint("a hint") - .setCancelButton("cancel") - .setSignInTitle("sign in") - .build(); - - static final AuthOptions defaultOptions = - new AuthOptions.Builder() - .setBiometricOnly(false) - .setSensitiveTransaction(false) - .setSticky(false) - .build(); + new AuthStrings("a reason", "a hint", "cancel", "sign in"); + + static final AuthOptions defaultOptions = new AuthOptions(false, false, false); @Test public void authenticate_returnsErrorWhenAuthInProgress() { final LocalAuthPlugin plugin = new LocalAuthPlugin(); plugin.authInProgress.set(true); - @SuppressWarnings("unchecked") - final Result mockResult = mock(Result.class); - plugin.authenticate(defaultOptions, dummyStrings, mockResult); - ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); - verify(mockResult).success(captor.capture()); - assertEquals(AuthResultCode.ALREADY_IN_PROGRESS, captor.getValue().getCode()); + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authenticate( + defaultOptions, + dummyStrings, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + assertEquals(AuthResultCode.ALREADY_IN_PROGRESS, reply.getOrNull().getCode()); + return null; + })); + assertTrue(callbackCalled[0]); } @Test public void authenticate_returnsErrorWithNoForegroundActivity() { final LocalAuthPlugin plugin = new LocalAuthPlugin(); - @SuppressWarnings("unchecked") - final Result mockResult = mock(Result.class); - - plugin.authenticate(defaultOptions, dummyStrings, mockResult); - ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); - verify(mockResult).success(captor.capture()); - assertEquals(AuthResultCode.NO_ACTIVITY, captor.getValue().getCode()); + final Boolean[] callbackCalled = new Boolean[1]; + + plugin.authenticate( + defaultOptions, + dummyStrings, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + assertEquals(AuthResultCode.NO_ACTIVITY, reply.getOrNull().getCode()); + return null; + })); + assertTrue(callbackCalled[0]); } @Test public void authenticate_returnsErrorWhenActivityNotFragmentActivity() { final LocalAuthPlugin plugin = new LocalAuthPlugin(); setPluginActivity(plugin, buildMockActivityWithContext(mock(NativeActivity.class))); - @SuppressWarnings("unchecked") - final Result mockResult = mock(Result.class); - plugin.authenticate(defaultOptions, dummyStrings, mockResult); - ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); - verify(mockResult).success(captor.capture()); - assertEquals(AuthResultCode.NOT_FRAGMENT_ACTIVITY, captor.getValue().getCode()); + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authenticate( + defaultOptions, + dummyStrings, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + assertEquals(AuthResultCode.NOT_FRAGMENT_ACTIVITY, reply.getOrNull().getCode()); + return null; + })); + assertTrue(callbackCalled[0]); } @Test public void authenticate_returnsErrorWhenDeviceNotSupported() { final LocalAuthPlugin plugin = new LocalAuthPlugin(); setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class))); - @SuppressWarnings("unchecked") - final Result mockResult = mock(Result.class); - - plugin.authenticate(defaultOptions, dummyStrings, mockResult); - ArgumentCaptor captor = ArgumentCaptor.forClass(AuthResult.class); - verify(mockResult).success(captor.capture()); - assertEquals(AuthResultCode.NO_CREDENTIALS, captor.getValue().getCode()); + final Boolean[] callbackCalled = new Boolean[1]; + + plugin.authenticate( + defaultOptions, + dummyStrings, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + assertEquals(AuthResultCode.NO_CREDENTIALS, reply.getOrNull().getCode()); + return null; + })); + assertTrue(callbackCalled[0]); } @Test @@ -129,16 +133,14 @@ public void authenticate_properlyConfiguresBiometricOnlyAuthenticationRequest() any(AuthStrings.class), allowCredentialsCaptor.capture(), any(AuthCompletionHandler.class)); - @SuppressWarnings("unchecked") - final Result mockResult = mock(Result.class); - - final AuthOptions options = - new AuthOptions.Builder() - .setBiometricOnly(true) - .setSensitiveTransaction(false) - .setSticky(false) - .build(); - plugin.authenticate(options, dummyStrings, mockResult); + final AuthOptions options = new AuthOptions(true, false, false); + plugin.authenticate( + options, + dummyStrings, + ResultCompat.asCompatCallback( + reply -> { + return null; + })); assertFalse(allowCredentialsCaptor.getValue()); } @@ -162,10 +164,13 @@ public void authenticate_properlyConfiguresBiometricAndDeviceCredentialAuthentic any(AuthStrings.class), allowCredentialsCaptor.capture(), any(AuthCompletionHandler.class)); - @SuppressWarnings("unchecked") - final Result mockResult = mock(Result.class); - - plugin.authenticate(defaultOptions, dummyStrings, mockResult); + plugin.authenticate( + defaultOptions, + dummyStrings, + ResultCompat.asCompatCallback( + reply -> { + return null; + })); assertTrue(allowCredentialsCaptor.getValue()); } @@ -191,10 +196,13 @@ public void authenticate_properlyConfiguresDeviceCredentialOnlyAuthenticationReq any(AuthStrings.class), allowCredentialsCaptor.capture(), any(AuthCompletionHandler.class)); - @SuppressWarnings("unchecked") - final Result mockResult = mock(Result.class); - - plugin.authenticate(defaultOptions, dummyStrings, mockResult); + plugin.authenticate( + defaultOptions, + dummyStrings, + ResultCompat.asCompatCallback( + reply -> { + return null; + })); assertTrue(allowCredentialsCaptor.getValue()); } diff --git a/packages/local_auth/local_auth_android/android/src/test/kotlin/io/flutter/plugins/localauth/TestResultUtils.kt b/packages/local_auth/local_auth_android/android/src/test/kotlin/io/flutter/plugins/localauth/TestResultUtils.kt new file mode 100644 index 000000000000..822a704b192b --- /dev/null +++ b/packages/local_auth/local_auth_android/android/src/test/kotlin/io/flutter/plugins/localauth/TestResultUtils.kt @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.localauth + +/** Wraps Kotlin Result for use in Java unit tests. */ +@Suppress("UNCHECKED_CAST") +class ResultCompat(private val result: Result) { + private val value: T? = result.getOrNull() + private val exception = result.exceptionOrNull() + val isSuccess = result.isSuccess + val isFailure = result.isFailure + + companion object { + @JvmStatic + fun success(value: T, callback: Any) { + val castedCallback: (Result) -> Unit = callback as (Result) -> Unit + castedCallback(Result.success(value)) + } + + @JvmStatic + fun asCompatCallback(result: (ResultCompat) -> Unit): (Result) -> Unit { + return { result(ResultCompat(it)) } + } + } + + fun getOrNull(): T? { + return value + } + + fun exceptionOrNull(): Throwable? { + return exception + } +} diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index fae25d17019f..a0f627154aee 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/lib/src/messages.g.dart @@ -1,21 +1,40 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// Autogenerated from Pigeon (v26.2.3), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; - -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { @@ -272,10 +291,10 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - final int? value = readValue(buffer) as int?; + final value = readValue(buffer) as int?; return value == null ? null : AuthResultCode.values[value]; case 130: - final int? value = readValue(buffer) as int?; + final value = readValue(buffer) as int?; return value == null ? null : AuthClassification.values[value]; case 131: return AuthStrings.decode(readValue(buffer)!); @@ -308,65 +327,43 @@ class LocalAuthApi { /// Returns true if this device supports authentication. Future isDeviceSupported() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_android.LocalAuthApi.isDeviceSupported$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as bool?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as bool; } /// Returns true if this device can support biometric authentication, whether /// any biometrics are enrolled or not. Future deviceCanSupportBiometrics() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_android.LocalAuthApi.deviceCanSupportBiometrics$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as bool?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as bool; } /// Cancels any in-progress authentication. @@ -374,33 +371,22 @@ class LocalAuthApi { /// Returns true only if authentication was in progress, and was successfully /// cancelled. Future stopAuthentication() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_android.LocalAuthApi.stopAuthentication$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as bool?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as bool; } /// Returns the biometric types that are enrolled, and can thus be used @@ -409,29 +395,22 @@ class LocalAuthApi { /// Returns null if there is no activity, in which case the enrolled /// biometrics can't be determined. Future?> getEnrolledBiometrics() async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_android.LocalAuthApi.getEnrolledBiometrics$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return (pigeonVar_replyList[0] as List?) - ?.cast(); - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + return (pigeonVar_replyValue as List?)?.cast(); } /// Attempts to authenticate the user with the provided [options], and using @@ -440,34 +419,23 @@ class LocalAuthApi { AuthOptions options, AuthStrings strings, ) async { - final String pigeonVar_channelName = + final pigeonVar_channelName = 'dev.flutter.pigeon.local_auth_android.LocalAuthApi.authenticate$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send( [options, strings], ); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as AuthResult?)!; - } + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as AuthResult; } } diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index 698f012a3dcc..11c29b377e0e 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -7,8 +7,9 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( dartOut: 'lib/src/messages.g.dart', - javaOut: 'android/src/main/java/io/flutter/plugins/localauth/Messages.java', - javaOptions: JavaOptions(package: 'io.flutter.plugins.localauth'), + kotlinOut: + 'android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt', + kotlinOptions: KotlinOptions(package: 'io.flutter.plugins.localauth'), copyrightHeader: 'pigeons/copyright.txt', ), ) diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index a53ab31388ce..9854bdca01e8 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/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: 2.0.7 +version: 2.0.8 environment: sdk: ^3.9.0 @@ -23,6 +23,7 @@ dependencies: flutter_plugin_android_lifecycle: ^2.0.1 intl: ">=0.17.0 <0.21.0" local_auth_platform_interface: ^1.1.0 + meta: ^1.10.0 dev_dependencies: build_runner: ^2.3.3 From ca7fb6010c965a39e8c9cb28f13fe2d2ec03982b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 10 Apr 2026 12:16:46 -0400 Subject: [PATCH 2/3] Missed autoformat --- .../test/java/io/flutter/plugins/localauth/LocalAuthTest.java | 1 - 1 file changed, 1 deletion(-) 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 765cde08f4dd..a2ff2dbb51d4 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 @@ -14,7 +14,6 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; From 0a06c6f6765c3b369238e0f21ba4f9cea0350096 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 14 Apr 2026 11:08:23 -0400 Subject: [PATCH 3/3] Add param name comments --- .../plugins/localauth/AuthenticationHelperTest.java | 4 +++- .../java/io/flutter/plugins/localauth/LocalAuthTest.java | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java index 754838f996fc..4c2db213148c 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/AuthenticationHelperTest.java @@ -25,7 +25,9 @@ public class AuthenticationHelperTest { static final AuthStrings dummyStrings = new AuthStrings("a reason", "a hint", "cancel", "sign in"); - static final AuthOptions defaultOptions = new AuthOptions(false, false, false); + static final AuthOptions defaultOptions = + new AuthOptions( + /* biometricOnly */ false, /* sensitiveTransaction */ false, /* sticky */ false); @Test public void onAuthenticationError_returnsUserCanceled() { 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 a2ff2dbb51d4..5d580e1d3ed0 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 @@ -40,7 +40,9 @@ public class LocalAuthTest { static final AuthStrings dummyStrings = new AuthStrings("a reason", "a hint", "cancel", "sign in"); - static final AuthOptions defaultOptions = new AuthOptions(false, false, false); + static final AuthOptions defaultOptions = + new AuthOptions( + /* biometricOnly */ false, /* sensitiveTransaction */ false, /* sticky */ false); @Test public void authenticate_returnsErrorWhenAuthInProgress() { @@ -132,7 +134,10 @@ public void authenticate_properlyConfiguresBiometricOnlyAuthenticationRequest() any(AuthStrings.class), allowCredentialsCaptor.capture(), any(AuthCompletionHandler.class)); - final AuthOptions options = new AuthOptions(true, false, false); + final AuthOptions options = + new AuthOptions( + /* biometricOnly */ true, /* sensitiveTransaction */ false, /* sticky */ false); + ; plugin.authenticate( options, dummyStrings,