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