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 rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java new file mode 100644 index 000000000000..9114170aab6f --- /dev/null +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java @@ -0,0 +1,12 @@ +package io.flutter.plugins.localauth; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.e2e.FlutterRunner; +import io.flutter.plugins.localauthexample.MainActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterRunner.class) +public class MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} diff --git a/packages/local_auth/example/android/app/src/main/AndroidManifest.xml b/packages/local_auth/example/android/app/src/main/AndroidManifest.xml index 752c8e7e2c59..25d749d29713 100644 --- a/packages/local_auth/example/android/app/src/main/AndroidManifest.xml +++ b/packages/local_auth/example/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ @@ -16,5 +16,11 @@ + + diff --git a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..8191bebc3f1b --- /dev/null +++ b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java @@ -0,0 +1,13 @@ +package io.flutter.plugins.localauthexample; + +import android.os.Bundle; +import io.flutter.app.FlutterFragmentActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterFragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} 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 8001c601eabb..87172b03454b 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 @@ -4,14 +4,14 @@ package io.flutter.plugins.localauthexample; -import android.os.Bundle; -import io.flutter.app.FlutterFragmentActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.localauth.LocalAuthPlugin; public class MainActivity extends FlutterFragmentActivity { + // TODO(bparrishMines): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694 @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new LocalAuthPlugin()); } } diff --git a/packages/local_auth/example/android/gradle.properties b/packages/local_auth/example/android/gradle.properties index 94adc3a3f97a..a6738207fd15 100644 --- a/packages/local_auth/example/android/gradle.properties +++ b/packages/local_auth/example/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.enableR8=true diff --git a/packages/local_auth/example/pubspec.yaml b/packages/local_auth/example/pubspec.yaml index 0bdaee674e34..36484d9feda7 100644 --- a/packages/local_auth/example/pubspec.yaml +++ b/packages/local_auth/example/pubspec.yaml @@ -7,5 +7,10 @@ dependencies: local_auth: path: ../ +dev_dependencies: + e2e: ^0.2.1 + flutter_driver: + sdk: flutter + flutter: uses-material-design: true diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index fb9077f65168..d73c0295d554 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth description: Flutter plugin for Android and iOS device authentication sensors such as Fingerprint Reader and Touch ID. homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth -version: 0.6.1+1 +version: 0.6.1+2 flutter: plugin: @@ -19,8 +19,12 @@ dependencies: meta: ^1.0.5 intl: ">=0.15.1 <0.17.0" platform: ^2.0.0 + flutter_plugin_android_lifecycle: ^1.0.2 dev_dependencies: + e2e: ^0.2.1 + flutter_driver: + sdk: flutter flutter_test: sdk: flutter diff --git a/packages/local_auth/test/local_auth_e2e.dart b/packages/local_auth/test/local_auth_e2e.dart new file mode 100644 index 000000000000..5e9dc57b73f6 --- /dev/null +++ b/packages/local_auth/test/local_auth_e2e.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:e2e/e2e.dart'; + +import 'package:local_auth/local_auth.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canCheckBiometrics', (WidgetTester tester) async { + expect(LocalAuthentication().getAvailableBiometrics(), completion(isList)); + }); +} diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index 94c3f6b2cb09..d658f6fece91 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/common.sh" check_changed_packages > /dev/null readonly EXCLUDED_PLUGINS_LIST=( + "flutter_plugin_android_lifecycle" "google_sign_in_platform_interface" "google_sign_in_web" "instrumentation_adapter"