Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/local_auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/local_auth/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -33,20 +32,9 @@
* <p>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 {

Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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.
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/local_auth/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS device authentication sensors
such as Fingerprint Reader and Touch ID.
author: Flutter Team <[email protected]>
homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth
version: 0.4.0+1
version: 0.5.0

flutter:
plugin:
Expand Down