diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index f1e64da1c16e..3921db7fa343 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.0 + * **Breaking change**. Update the Android API to use androidx Biometric package. This gives + the prompt the updated Material look. However, it also requires the activity to be a + FragmentActivity. Users can switch to FlutterFragmentActivity in their main app to migrate. + ## 0.4.0+1 * Log a more detailed warning at build time about the previous AndroidX diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle index fd9893f94e29..4da3597692c1 100644 --- a/packages/local_auth/android/build.gradle +++ b/packages/local_auth/android/build.gradle @@ -47,5 +47,7 @@ android { } dependencies { - api "androidx.core:core:1.1.0-alpha03" + api "androidx.core:core:1.1.0-beta01" + api "androidx.biometric:biometric:1.0.0-alpha04" + api "androidx.fragment:fragment:1.1.0-alpha06" } diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index 9c2ed5474e75..de7574143b33 100644 --- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -20,11 +20,10 @@ import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; -import android.widget.ImageView; import android.widget.TextView; -import androidx.core.content.ContextCompat; +import androidx.biometric.BiometricPrompt; import androidx.core.hardware.fingerprint.FingerprintManagerCompat; -import androidx.core.os.CancellationSignal; +import androidx.fragment.app.FragmentActivity; import io.flutter.plugin.common.MethodCall; /** @@ -33,20 +32,9 @@ *

One instance per call is generated to ensure readable separation of executable paths across * method calls. */ -class AuthenticationHelper extends FingerprintManagerCompat.AuthenticationCallback +class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback implements Application.ActivityLifecycleCallbacks { - /** How long will the fp dialog be delayed to dismiss. */ - private static final long DISMISS_AFTER_MS = 300; - - private static final String CANCEL_BUTTON = "cancelButton"; - - /** Captures the state of the fingerprint dialog. */ - private enum DialogState { - SUCCESS, - FAILURE - } - /** The callback that handles the result of this authentication process. */ interface AuthCompletionHandler { @@ -69,30 +57,33 @@ interface AuthCompletionHandler { void onError(String code, String error); } - private final Activity activity; + private final FragmentActivity activity; private final AuthCompletionHandler completionHandler; private final KeyguardManager keyguardManager; private final FingerprintManagerCompat fingerprintManager; private final MethodCall call; + private final BiometricPrompt.PromptInfo promptInfo; + private final boolean isAuthSticky; + private boolean activityPaused = false; - /** - * The prominent UI element during this transaction. It is used to communicate the state of - * authentication to the user. - */ - private AlertDialog fingerprintDialog; - - private CancellationSignal cancellationSignal; - - AuthenticationHelper( - Activity activity, MethodCall call, AuthCompletionHandler completionHandler) { + public AuthenticationHelper( + FragmentActivity activity, MethodCall call, AuthCompletionHandler completionHandler) { this.activity = activity; this.completionHandler = completionHandler; this.call = call; this.keyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); this.fingerprintManager = FingerprintManagerCompat.from(activity); + this.isAuthSticky = call.argument("stickyAuth"); + this.promptInfo = + new BiometricPrompt.PromptInfo.Builder() + .setDescription((String) call.argument("localizedReason")) + .setTitle((String) call.argument("signInTitle")) + .setSubtitle((String) call.argument("fingerprintHint")) + .setNegativeButtonText((String) call.argument("cancelButton")) + .build(); } - void authenticate() { + public void authenticate() { if (fingerprintManager.isHardwareDetected()) { if (keyguardManager.isKeyguardSecure() && fingerprintManager.hasEnrolledFingerprints()) { start(); @@ -112,33 +103,18 @@ void authenticate() { } } + /** Start the fingerprint listener. */ private void start() { activity.getApplication().registerActivityLifecycleCallbacks(this); - resume(); - } - - private void resume() { - cancellationSignal = new CancellationSignal(); - showFingerprintDialog(); - fingerprintManager.authenticate(null, 0, cancellationSignal, this, null); - } - - private void pause() { - if (cancellationSignal != null) { - cancellationSignal.cancel(); - } - if (fingerprintDialog != null && fingerprintDialog.isShowing()) { - fingerprintDialog.dismiss(); - } + new BiometricPrompt(activity, activity.getMainExecutor(), this).authenticate(promptInfo); } /** - * Stops the fingerprint listener and dismisses the fingerprint dialog. + * Stops the fingerprint listener. * * @param success If the authentication was successful. */ private void stop(boolean success) { - pause(); activity.getApplication().unregisterActivityLifecycleCallbacks(this); if (success) { completionHandler.onSuccess(); @@ -147,99 +123,55 @@ private void stop(boolean success) { } } - /** - * If the activity is paused or stopped, we have to stop listening for fingerprint. Otherwise, - * user can still interact with fp reader in the background.. Sigh.. - */ - @Override - public void onActivityPaused(Activity activity) { - if (call.argument("stickyAuth")) { - pause(); - } else { - stop(false); - } - } - @Override - public void onActivityResumed(Activity activity) { - if (call.argument("stickyAuth")) { - resume(); + public void onAuthenticationError(int errorCode, CharSequence errString) { + if (activityPaused && isAuthSticky) { + return; } - } - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - updateFingerprintDialog(DialogState.FAILURE, errString.toString()); + // Either the authentication got cancelled by user or we are not interested + // in sticky auth, so return failure. + stop(false); } @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - updateFingerprintDialog(DialogState.FAILURE, helpString.toString()); + public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { + stop(true); } @Override public void onAuthenticationFailed() { - updateFingerprintDialog( - DialogState.FAILURE, (String) call.argument("fingerprintNotRecognized")); + stop(false); } + /** + * If the activity is paused, we keep track because fingerprint dialog simply returns "User + * cancelled" when the activity is paused. + */ @Override - public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { - updateFingerprintDialog(DialogState.SUCCESS, (String) call.argument("fingerprintSuccess")); - new Handler(Looper.myLooper()) - .postDelayed( - new Runnable() { - @Override - public void run() { - stop(true); - } - }, - DISMISS_AFTER_MS); - } - - private void updateFingerprintDialog(DialogState state, String message) { - if (cancellationSignal.isCanceled() || !fingerprintDialog.isShowing()) { - return; - } - TextView resultInfo = (TextView) fingerprintDialog.findViewById(R.id.fingerprint_status); - ImageView icon = (ImageView) fingerprintDialog.findViewById(R.id.fingerprint_icon); - switch (state) { - case FAILURE: - icon.setImageResource(R.drawable.fingerprint_warning_icon); - resultInfo.setTextColor(ContextCompat.getColor(activity, R.color.warning_color)); - break; - case SUCCESS: - icon.setImageResource(R.drawable.fingerprint_success_icon); - resultInfo.setTextColor(ContextCompat.getColor(activity, R.color.success_color)); - break; + public void onActivityPaused(Activity ignored) { + if (isAuthSticky) { + activityPaused = true; } - resultInfo.setText(message); } - // Suppress inflateParams lint because dialogs do not need to attach to a parent view. - @SuppressLint("InflateParams") - private void showFingerprintDialog() { - View view = LayoutInflater.from(activity).inflate(R.layout.scan_fp, null, false); - TextView fpDescription = (TextView) view.findViewById(R.id.fingerprint_description); - TextView title = (TextView) view.findViewById(R.id.fingerprint_signin); - TextView status = (TextView) view.findViewById(R.id.fingerprint_status); - fpDescription.setText((String) call.argument("localizedReason")); - title.setText((String) call.argument("signInTitle")); - status.setText((String) call.argument("fingerprintHint")); - Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom); - OnClickListener cancelHandler = - new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - stop(false); - } - }; - fingerprintDialog = - new AlertDialog.Builder(context) - .setView(view) - .setNegativeButton((String) call.argument(CANCEL_BUTTON), cancelHandler) - .setCancelable(false) - .show(); + @Override + public void onActivityResumed(Activity ignored) { + if (isAuthSticky) { + activityPaused = false; + final BiometricPrompt prompt = + new BiometricPrompt(activity, activity.getMainExecutor(), this); + // When activity is resuming, we cannot show the prompt right away. We need to post it to the UI queue. + new Handler(Looper.myLooper()) + .postDelayed( + new Runnable() { + @Override + public void run() { + prompt.authenticate(promptInfo); + } + }, + 100); + } } // Suppress inflateParams lint because dialogs do not need to attach to a parent view. @@ -269,7 +201,7 @@ public void onClick(DialogInterface dialog, int which) { new AlertDialog.Builder(context) .setView(view) .setPositiveButton((String) call.argument("goToSetting"), goToSettingHandler) - .setNegativeButton((String) call.argument(CANCEL_BUTTON), cancelHandler) + .setNegativeButton((String) call.argument("cancelButton"), cancelHandler) .setCancelable(false) .show(); } diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index f2d14e7a67ef..06dc7a808f9c 100644 --- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -6,6 +6,7 @@ import android.app.Activity; import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import androidx.fragment.app.FragmentActivity; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -42,14 +43,23 @@ public void onMethodCall(MethodCall call, final Result result) { result.error("auth_in_progress", "Authentication in progress", null); return; } + Activity activity = registrar.activity(); if (activity == null || activity.isFinishing()) { result.error("no_activity", "local_auth plugin requires a foreground activity", null); return; } + + if (!(activity instanceof FragmentActivity)) { + result.error( + "no_fragment_activity", + "local_auth plugin requires activity to be a FragmentActivity.", + null); + return; + } AuthenticationHelper authenticationHelper = new AuthenticationHelper( - activity, + (FragmentActivity) activity, call, new AuthCompletionHandler() { @Override diff --git a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/MainActivity.java b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/MainActivity.java index b9a2e1162a0e..8001c601eabb 100644 --- a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/MainActivity.java +++ b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/MainActivity.java @@ -5,10 +5,10 @@ package io.flutter.plugins.localauthexample; import android.os.Bundle; -import io.flutter.app.FlutterActivity; +import io.flutter.app.FlutterFragmentActivity; import io.flutter.plugins.GeneratedPluginRegistrant; -public class MainActivity extends FlutterActivity { +public class MainActivity extends FlutterFragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index 93afb6299fc4..4e420a945873 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS device authentication sensors such as Fingerprint Reader and Touch ID. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth -version: 0.4.0+1 +version: 0.5.0 flutter: plugin: