diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index 2babfecf92d7..5812a533f68b 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.1+2 + +* Support v2 embedding. + ## 0.6.1+1 * Remove the deprecated `author:` field from pubspec.yaml diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle index 92b9c173612d..e00e45e3ab9a 100644 --- a/packages/local_auth/android/build.gradle +++ b/packages/local_auth/android/build.gradle @@ -37,4 +37,33 @@ dependencies { api "androidx.core:core:1.1.0-beta01" api "androidx.biometric:biometric:1.0.0-beta01" api "androidx.fragment:fragment:1.1.0-alpha06" + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} + +// TODO(bparrishMines): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } } 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 99dab6de2975..e907cc43f2b4 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 @@ -19,8 +19,12 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.biometric.BiometricPrompt; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; import io.flutter.plugin.common.MethodCall; import java.util.concurrent.Executor; @@ -32,7 +36,7 @@ */ @SuppressWarnings("deprecation") class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback - implements Application.ActivityLifecycleCallbacks { + implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { /** The callback that handles the result of this authentication process. */ interface AuthCompletionHandler { @@ -56,6 +60,8 @@ interface AuthCompletionHandler { void onError(String code, String error); } + // This is null when not using v2 embedding; + private final Lifecycle lifecycle; private final FragmentActivity activity; private final AuthCompletionHandler completionHandler; private final MethodCall call; @@ -65,8 +71,12 @@ interface AuthCompletionHandler { private boolean activityPaused = false; private BiometricPrompt biometricPrompt; - public AuthenticationHelper( - FragmentActivity activity, MethodCall call, AuthCompletionHandler completionHandler) { + AuthenticationHelper( + Lifecycle lifecycle, + FragmentActivity activity, + MethodCall call, + AuthCompletionHandler completionHandler) { + this.lifecycle = lifecycle; this.activity = activity; this.completionHandler = completionHandler; this.call = call; @@ -83,14 +93,18 @@ public AuthenticationHelper( } /** Start the fingerprint listener. */ - public void authenticate() { - activity.getApplication().registerActivityLifecycleCallbacks(this); + void authenticate() { + if (lifecycle != null) { + lifecycle.addObserver(this); + } else { + activity.getApplication().registerActivityLifecycleCallbacks(this); + } biometricPrompt = new BiometricPrompt(activity, uiThreadExecutor, this); biometricPrompt.authenticate(promptInfo); } /** Cancels the fingerprint authentication. */ - public void stopAuthentication() { + void stopAuthentication() { if (biometricPrompt != null) { biometricPrompt.cancelAuthentication(); biometricPrompt = null; @@ -99,6 +113,10 @@ public void stopAuthentication() { /** Stops the fingerprint listener. */ private void stop() { + if (lifecycle != null) { + lifecycle.removeObserver(this); + return; + } activity.getApplication().unregisterActivityLifecycleCallbacks(this); } @@ -185,6 +203,16 @@ public void run() { } } + @Override + public void onPause(@NonNull LifecycleOwner owner) { + onActivityPaused(null); + } + + @Override + public void onResume(@NonNull LifecycleOwner owner) { + onActivityResumed(null); + } + // Suppress inflateParams lint because dialogs do not need to attach to a parent view. @SuppressLint("InflateParams") private void showGoToSettingsDialog() { @@ -236,8 +264,20 @@ public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {} @Override public void onActivityDestroyed(Activity activity) {} + @Override + public void onDestroy(@NonNull LifecycleOwner owner) {} + + @Override + public void onStop(@NonNull LifecycleOwner owner) {} + + @Override + public void onStart(@NonNull LifecycleOwner owner) {} + + @Override + public void onCreate(@NonNull LifecycleOwner owner) {} + private static class UiThreadExecutor implements Executor { - public final Handler handler = new Handler(Looper.getMainLooper()); + final Handler handler = new Handler(Looper.getMainLooper()); @Override public void execute(Runnable command) { 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 6a8bd9e20e53..1f3d35f44bfa 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 @@ -8,6 +8,11 @@ import android.content.pm.PackageManager; import android.os.Build; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.Lifecycle; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -17,24 +22,49 @@ import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; -/** LocalAuthPlugin */ +/** + * Flutter plugin providing access to local authentication. + * + *
Instantiate this in an add to app scenario to gracefully handle activity and context changes. + */ @SuppressWarnings("deprecation") -public class LocalAuthPlugin implements MethodCallHandler { - private final Registrar registrar; +public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { + private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth"; + + private Activity activity; private final AtomicBoolean authInProgress = new AtomicBoolean(false); private AuthenticationHelper authenticationHelper; - /** Plugin registration. */ + // These are null when not using v2 embedding. + private MethodChannel channel; + private Lifecycle lifecycle; + + /** + * Registers a plugin with the v1 embedding api {@code io.flutter.plugin.common}. + * + *
Calling this will register the plugin with the passed registrar. However, plugins + * initialized this way won't react to changes in activity or context. + * + * @param registrar attaches this plugin's {@link + * io.flutter.plugin.common.MethodChannel.MethodCallHandler} to the registrar's {@link + * io.flutter.plugin.common.BinaryMessenger}. + */ public static void registerWith(Registrar registrar) { - final MethodChannel channel = - new MethodChannel(registrar.messenger(), "plugins.flutter.io/local_auth"); - channel.setMethodCallHandler(new LocalAuthPlugin(registrar)); + final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); + channel.setMethodCallHandler(new LocalAuthPlugin(registrar.activity())); } - private LocalAuthPlugin(Registrar registrar) { - this.registrar = registrar; + private LocalAuthPlugin(Activity activity) { + this.activity = activity; } + /** + * Default constructor for LocalAuthPlugin. + * + *
Use this constructor when adding this plugin to an app with v2 embedding.
+ */
+ public LocalAuthPlugin() {}
+
@Override
public void onMethodCall(MethodCall call, final Result result) {
if (call.method.equals("authenticateWithBiometrics")) {
@@ -47,7 +77,6 @@ public void onMethodCall(MethodCall call, final Result result) {
return;
}
- Activity activity = registrar.activity();
if (activity == null || activity.isFinishing()) {
result.error("no_activity", "local_auth plugin requires a foreground activity", null);
return;
@@ -63,6 +92,7 @@ public void onMethodCall(MethodCall call, final Result result) {
authInProgress.set(true);
authenticationHelper =
new AuthenticationHelper(
+ lifecycle,
(FragmentActivity) activity,
call,
new AuthCompletionHandler() {
@@ -90,7 +120,6 @@ public void onError(String code, String error) {
authenticationHelper.authenticate();
} else if (call.method.equals("getAvailableBiometrics")) {
try {
- Activity activity = registrar.activity();
if (activity == null || activity.isFinishing()) {
result.error("no_activity", "local_auth plugin requires a foreground activity", null);
return;
@@ -137,4 +166,38 @@ private void stopAuthentication(Result result) {
result.success(false);
}
}
+
+ @Override
+ public void onAttachedToEngine(FlutterPluginBinding binding) {
+ channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME);
+ }
+
+ @Override
+ public void onDetachedFromEngine(FlutterPluginBinding binding) {}
+
+ @Override
+ public void onAttachedToActivity(ActivityPluginBinding binding) {
+ activity = binding.getActivity();
+ lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
+ channel.setMethodCallHandler(this);
+ }
+
+ @Override
+ public void onDetachedFromActivityForConfigChanges() {
+ lifecycle = null;
+ activity = null;
+ }
+
+ @Override
+ public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
+ activity = binding.getActivity();
+ lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
+ }
+
+ @Override
+ public void onDetachedFromActivity() {
+ activity = null;
+ lifecycle = null;
+ channel.setMethodCallHandler(null);
+ }
}
diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java
new file mode 100644
index 000000000000..b2cea8dc57a2
--- /dev/null
+++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java
@@ -0,0 +1,14 @@
+package io.flutter.plugins.localauth;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.e2e.FlutterRunner;
+import io.flutter.plugins.localauthexample.EmbeddingV1Activity;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterRunner.class)
+public class EmbeddingV1ActivityTest {
+ @Rule
+ public ActivityTestRule