Skip to content

Commit 835dccb

Browse files
[local_auth] Adopt structured errors and remove useErrorDialogs - platform implementations (#10147)
Platform implementation portion of #9981 - Adds the new `LocalAuthException` class for structured errors. - Deprecates `AuthenticationOptions.useErrorDialogs` Part of: - flutter/flutter#113687 - flutter/flutter#175125 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 8588f1b commit 835dccb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2421
-2043
lines changed

packages/local_auth/local_auth_android/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2.0.0
2+
3+
* **BREAKING CHANGES:**
4+
* Switches to `LocalAuthException` for error reporting.
5+
* Removes support for `useErrorDialogs`.
6+
* Renames `biometricHint` to `signInHint` to reflect its usage.
7+
18
## 1.0.56
29

310
* Updates Java compatibility version to 17 and minimum supported SDK version to Flutter 3.35/Dart 3.9.

packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java

Lines changed: 37 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,19 @@
55

66
import android.annotation.SuppressLint;
77
import android.app.Activity;
8-
import android.app.AlertDialog;
98
import android.app.Application;
10-
import android.content.Context;
11-
import android.content.DialogInterface.OnClickListener;
12-
import android.content.Intent;
139
import android.os.Bundle;
1410
import android.os.Handler;
1511
import android.os.Looper;
16-
import android.provider.Settings;
17-
import android.view.ContextThemeWrapper;
18-
import android.view.LayoutInflater;
19-
import android.view.View;
20-
import android.widget.TextView;
2112
import androidx.annotation.NonNull;
2213
import androidx.biometric.BiometricManager;
2314
import androidx.biometric.BiometricPrompt;
2415
import androidx.fragment.app.FragmentActivity;
2516
import androidx.lifecycle.DefaultLifecycleObserver;
2617
import androidx.lifecycle.Lifecycle;
2718
import androidx.lifecycle.LifecycleOwner;
19+
import io.flutter.plugins.localauth.Messages.AuthResult;
20+
import io.flutter.plugins.localauth.Messages.AuthResultCode;
2821
import java.util.concurrent.Executor;
2922

3023
/**
@@ -38,14 +31,12 @@ class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback
3831
/** The callback that handles the result of this authentication process. */
3932
interface AuthCompletionHandler {
4033
/** Called when authentication attempt is complete. */
41-
void complete(Messages.AuthResult authResult);
34+
void complete(AuthResult authResult);
4235
}
4336

44-
// This is null when not using v2 embedding;
4537
private final Lifecycle lifecycle;
4638
private final FragmentActivity activity;
4739
private final AuthCompletionHandler completionHandler;
48-
private final boolean useErrorDialogs;
4940
private final Messages.AuthStrings strings;
5041
private final BiometricPrompt.PromptInfo promptInfo;
5142
private final boolean isAuthSticky;
@@ -65,14 +56,13 @@ interface AuthCompletionHandler {
6556
this.completionHandler = completionHandler;
6657
this.strings = strings;
6758
this.isAuthSticky = options.getSticky();
68-
this.useErrorDialogs = options.getUseErrorDialgs();
6959
this.uiThreadExecutor = new UiThreadExecutor();
7060

7161
BiometricPrompt.PromptInfo.Builder promptBuilder =
7262
new BiometricPrompt.PromptInfo.Builder()
7363
.setDescription(strings.getReason())
7464
.setTitle(strings.getSignInTitle())
75-
.setSubtitle(strings.getBiometricHint())
65+
.setSubtitle(strings.getSignInHint())
7666
.setConfirmationRequired(options.getSensitiveTransaction());
7767

7868
int allowedAuthenticators =
@@ -120,58 +110,69 @@ private void stop() {
120110
@SuppressLint("SwitchIntDef")
121111
@Override
122112
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
113+
AuthResultCode code;
123114
switch (errorCode) {
115+
case BiometricPrompt.ERROR_USER_CANCELED:
116+
code = AuthResultCode.USER_CANCELED;
117+
break;
118+
case BiometricPrompt.ERROR_NEGATIVE_BUTTON:
119+
code = AuthResultCode.NEGATIVE_BUTTON;
120+
break;
124121
case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL:
125-
if (useErrorDialogs) {
126-
showGoToSettingsDialog(
127-
strings.getDeviceCredentialsRequiredTitle(),
128-
strings.getDeviceCredentialsSetupDescription());
129-
return;
130-
}
131-
completionHandler.complete(Messages.AuthResult.ERROR_NOT_AVAILABLE);
122+
code = AuthResultCode.NO_CREDENTIALS;
132123
break;
133-
case BiometricPrompt.ERROR_NO_SPACE:
134124
case BiometricPrompt.ERROR_NO_BIOMETRICS:
135-
if (useErrorDialogs) {
136-
showGoToSettingsDialog(
137-
strings.getBiometricRequiredTitle(), strings.getGoToSettingsDescription());
138-
return;
139-
}
140-
completionHandler.complete(Messages.AuthResult.ERROR_NOT_ENROLLED);
125+
code = AuthResultCode.NOT_ENROLLED;
141126
break;
142127
case BiometricPrompt.ERROR_HW_UNAVAILABLE:
128+
code = AuthResultCode.HARDWARE_UNAVAILABLE;
129+
break;
143130
case BiometricPrompt.ERROR_HW_NOT_PRESENT:
144-
completionHandler.complete(Messages.AuthResult.ERROR_NOT_AVAILABLE);
131+
code = AuthResultCode.NO_HARDWARE;
145132
break;
146133
case BiometricPrompt.ERROR_LOCKOUT:
147-
completionHandler.complete(Messages.AuthResult.ERROR_LOCKED_OUT_TEMPORARILY);
134+
code = AuthResultCode.LOCKED_OUT_TEMPORARILY;
148135
break;
149136
case BiometricPrompt.ERROR_LOCKOUT_PERMANENT:
150-
completionHandler.complete(Messages.AuthResult.ERROR_LOCKED_OUT_PERMANENTLY);
137+
code = AuthResultCode.LOCKED_OUT_PERMANENTLY;
151138
break;
152139
case BiometricPrompt.ERROR_CANCELED:
153140
// If we are doing sticky auth and the activity has been paused,
154141
// ignore this error. We will start listening again when resumed.
155142
if (activityPaused && isAuthSticky) {
156143
return;
157-
} else {
158-
completionHandler.complete(Messages.AuthResult.FAILURE);
159144
}
145+
code = AuthResultCode.SYSTEM_CANCELED;
146+
break;
147+
case BiometricPrompt.ERROR_TIMEOUT:
148+
code = AuthResultCode.TIMEOUT;
149+
break;
150+
case BiometricPrompt.ERROR_NO_SPACE:
151+
code = AuthResultCode.NO_SPACE;
152+
break;
153+
case BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED:
154+
code = AuthResultCode.SECURITY_UPDATE_REQUIRED;
160155
break;
161156
default:
162-
completionHandler.complete(Messages.AuthResult.FAILURE);
157+
code = AuthResultCode.UNKNOWN_ERROR;
158+
break;
163159
}
160+
completionHandler.complete(
161+
new AuthResult.Builder().setCode(code).setErrorMessage(errString.toString()).build());
164162
stop();
165163
}
166164

167165
@Override
168166
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
169-
completionHandler.complete(Messages.AuthResult.SUCCESS);
167+
completionHandler.complete(new AuthResult.Builder().setCode(AuthResultCode.SUCCESS).build());
170168
stop();
171169
}
172170

173171
@Override
174-
public void onAuthenticationFailed() {}
172+
public void onAuthenticationFailed() {
173+
// No-op; this is called for incremental failures. Wait for a final
174+
// resolution via the success or error callbacks.
175+
}
175176

176177
/**
177178
* If the activity is paused, we keep track because biometric dialog simply returns "User
@@ -205,34 +206,6 @@ public void onResume(@NonNull LifecycleOwner owner) {
205206
onActivityResumed(null);
206207
}
207208

208-
// Suppress inflateParams lint because dialogs do not need to attach to a parent view.
209-
@SuppressLint("InflateParams")
210-
private void showGoToSettingsDialog(String title, String descriptionText) {
211-
View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false);
212-
TextView message = view.findViewById(R.id.fingerprint_required);
213-
TextView description = view.findViewById(R.id.go_to_setting_description);
214-
message.setText(title);
215-
description.setText(descriptionText);
216-
Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom);
217-
OnClickListener goToSettingHandler =
218-
(dialog, which) -> {
219-
completionHandler.complete(Messages.AuthResult.FAILURE);
220-
stop();
221-
activity.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS));
222-
};
223-
OnClickListener cancelHandler =
224-
(dialog, which) -> {
225-
completionHandler.complete(Messages.AuthResult.FAILURE);
226-
stop();
227-
};
228-
new AlertDialog.Builder(context)
229-
.setView(view)
230-
.setPositiveButton(strings.getGoToSettingsButton(), goToSettingHandler)
231-
.setNegativeButton(strings.getCancelButton(), cancelHandler)
232-
.setCancelable(false)
233-
.show();
234-
}
235-
236209
// Unused methods for activity lifecycle.
237210

238211
@Override

packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44

55
package io.flutter.plugins.localauth;
66

7-
import static android.app.Activity.RESULT_OK;
87
import static android.content.Context.KEYGUARD_SERVICE;
98

109
import android.app.Activity;
1110
import android.app.KeyguardManager;
1211
import android.content.Context;
13-
import android.content.Intent;
1412
import android.os.Build;
1513
import androidx.annotation.NonNull;
1614
import androidx.annotation.VisibleForTesting;
@@ -21,11 +19,11 @@
2119
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
2220
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
2321
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
24-
import io.flutter.plugin.common.PluginRegistry;
2522
import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler;
2623
import io.flutter.plugins.localauth.Messages.AuthClassification;
2724
import io.flutter.plugins.localauth.Messages.AuthOptions;
2825
import io.flutter.plugins.localauth.Messages.AuthResult;
26+
import io.flutter.plugins.localauth.Messages.AuthResultCode;
2927
import io.flutter.plugins.localauth.Messages.AuthStrings;
3028
import io.flutter.plugins.localauth.Messages.LocalAuthApi;
3129
import io.flutter.plugins.localauth.Messages.Result;
@@ -39,32 +37,14 @@
3937
* <p>Instantiate this in an add to app scenario to gracefully handle activity and context changes.
4038
*/
4139
public class LocalAuthPlugin implements FlutterPlugin, ActivityAware, LocalAuthApi {
42-
private static final int LOCK_REQUEST_CODE = 221;
4340
private Activity activity;
4441
private AuthenticationHelper authHelper;
4542

4643
@VisibleForTesting final AtomicBoolean authInProgress = new AtomicBoolean(false);
4744

48-
// These are null when not using v2 embedding.
4945
private Lifecycle lifecycle;
5046
private BiometricManager biometricManager;
5147
private KeyguardManager keyguardManager;
52-
Result<AuthResult> lockRequestResult;
53-
private final PluginRegistry.ActivityResultListener resultListener =
54-
new PluginRegistry.ActivityResultListener() {
55-
@Override
56-
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
57-
if (requestCode == LOCK_REQUEST_CODE) {
58-
if (resultCode == RESULT_OK && lockRequestResult != null) {
59-
onAuthenticationCompleted(lockRequestResult, AuthResult.SUCCESS);
60-
} else {
61-
onAuthenticationCompleted(lockRequestResult, AuthResult.FAILURE);
62-
}
63-
lockRequestResult = null;
64-
}
65-
return false;
66-
}
67-
};
6848

6949
/**
7050
* Default constructor for LocalAuthPlugin.
@@ -73,15 +53,21 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
7353
*/
7454
public LocalAuthPlugin() {}
7555

56+
@Override
7657
public @NonNull Boolean isDeviceSupported() {
7758
return isDeviceSecure() || canAuthenticateWithBiometrics();
7859
}
7960

61+
@Override
8062
public @NonNull Boolean deviceCanSupportBiometrics() {
8163
return hasBiometricHardware();
8264
}
8365

66+
@Override
8467
public @NonNull List<AuthClassification> getEnrolledBiometrics() {
68+
if (biometricManager == null) {
69+
return null;
70+
}
8571
ArrayList<AuthClassification> biometrics = new ArrayList<>();
8672
if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
8773
== BiometricManager.BIOMETRIC_SUCCESS) {
@@ -94,6 +80,7 @@ public LocalAuthPlugin() {}
9480
return biometrics;
9581
}
9682

83+
@Override
9784
public @NonNull Boolean stopAuthentication() {
9885
try {
9986
if (authHelper != null && authInProgress.get()) {
@@ -107,27 +94,29 @@ public LocalAuthPlugin() {}
10794
}
10895
}
10996

97+
@Override
11098
public void authenticate(
11199
@NonNull AuthOptions options,
112100
@NonNull AuthStrings strings,
113101
@NonNull Result<AuthResult> result) {
114102
if (authInProgress.get()) {
115-
result.success(AuthResult.ERROR_ALREADY_IN_PROGRESS);
103+
result.success(new AuthResult.Builder().setCode(AuthResultCode.ALREADY_IN_PROGRESS).build());
116104
return;
117105
}
118106

119107
if (activity == null || activity.isFinishing()) {
120-
result.success(AuthResult.ERROR_NO_ACTIVITY);
108+
result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_ACTIVITY).build());
121109
return;
122110
}
123111

124112
if (!(activity instanceof FragmentActivity)) {
125-
result.success(AuthResult.ERROR_NOT_FRAGMENT_ACTIVITY);
113+
result.success(
114+
new AuthResult.Builder().setCode(AuthResultCode.NOT_FRAGMENT_ACTIVITY).build());
126115
return;
127116
}
128117

129118
if (!isDeviceSupported()) {
130-
result.success(AuthResult.ERROR_NOT_AVAILABLE);
119+
result.success(new AuthResult.Builder().setCode(AuthResultCode.NO_CREDENTIALS).build());
131120
return;
132121
}
133122

@@ -221,7 +210,6 @@ private void setServicesFromActivity(Activity activity) {
221210

222211
@Override
223212
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
224-
binding.addActivityResultListener(resultListener);
225213
setServicesFromActivity(binding.getActivity());
226214
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
227215
}
@@ -234,7 +222,6 @@ public void onDetachedFromActivityForConfigChanges() {
234222

235223
@Override
236224
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
237-
binding.addActivityResultListener(resultListener);
238225
setServicesFromActivity(binding.getActivity());
239226
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
240227
}

0 commit comments

Comments
 (0)