From 443630542d6d97528fc0d84dc4a0212dad0cc7c2 Mon Sep 17 00:00:00 2001 From: Mehmet Fidanboylu Date: Tue, 21 May 2019 15:10:44 -0700 Subject: [PATCH 1/3] Change LocalAuth to use Biometric API --- packages/local_auth/CHANGELOG.md | 5 + packages/local_auth/android/build.gradle | 4 +- .../localauth/AuthenticationHelper.java | 176 ++++++------------ .../plugins/localauth/LocalAuthPlugin.java | 8 +- .../localauthexample/MainActivity.java | 4 +- packages/local_auth/pubspec.yaml | 2 +- 6 files changed, 70 insertions(+), 129 deletions(-) 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..8dfb580bf23b 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,53 @@ 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 { + public void onAuthenticationError(int errorCode, CharSequence errString) { + if (!activityPaused || !isAuthSticky) { + // Either the authentication got cancelled by user or we are not interested + // in sticky auth, so return failure. stop(false); } } @Override - public void onActivityResumed(Activity activity) { - if (call.argument("stickyAuth")) { - resume(); - } - } - - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - updateFingerprintDialog(DialogState.FAILURE, errString.toString()); - } - - @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 +199,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..0daba0647b42 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 @@ -4,8 +4,8 @@ package io.flutter.plugins.localauth; -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,7 +42,11 @@ public void onMethodCall(MethodCall call, final Result result) { result.error("auth_in_progress", "Authentication in progress", null); return; } - Activity activity = registrar.activity(); + if (!(registrar.activity() instanceof FragmentActivity)) { + throw new RuntimeException( + "Authentication plugin requires activity to be FragmentActivity."); + } + FragmentActivity activity = (FragmentActivity) registrar.activity(); if (activity == null || activity.isFinishing()) { result.error("no_activity", "local_auth plugin requires a foreground activity", null); return; 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: From 2fd9cec63a9b8daa2275d3a4cd82e256082da55b Mon Sep 17 00:00:00 2001 From: Mehmet Fidanboylu Date: Wed, 22 May 2019 20:37:31 -0700 Subject: [PATCH 2/3] Apply review changes. --- .../plugins/localauth/AuthenticationHelper.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 8dfb580bf23b..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 @@ -125,11 +125,13 @@ private void stop(boolean success) { @Override public void onAuthenticationError(int errorCode, CharSequence errString) { - if (!activityPaused || !isAuthSticky) { - // Either the authentication got cancelled by user or we are not interested - // in sticky auth, so return failure. - stop(false); + if (activityPaused && isAuthSticky) { + return; } + + // Either the authentication got cancelled by user or we are not interested + // in sticky auth, so return failure. + stop(false); } @Override From 136940f2ad1cccc4c02c0ef023e3ab4f5cb45f31 Mon Sep 17 00:00:00 2001 From: Mehmet Fidanboylu Date: Wed, 22 May 2019 20:46:13 -0700 Subject: [PATCH 3/3] Bug fixes --- .../plugins/localauth/LocalAuthPlugin.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 0daba0647b42..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 @@ -4,6 +4,7 @@ package io.flutter.plugins.localauth; +import android.app.Activity; import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.fragment.app.FragmentActivity; import io.flutter.plugin.common.MethodCall; @@ -42,18 +43,23 @@ public void onMethodCall(MethodCall call, final Result result) { result.error("auth_in_progress", "Authentication in progress", null); return; } - if (!(registrar.activity() instanceof FragmentActivity)) { - throw new RuntimeException( - "Authentication plugin requires activity to be FragmentActivity."); - } - FragmentActivity activity = (FragmentActivity) registrar.activity(); + + 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