diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 473995a76f6..cf31e38d02e 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -420,6 +420,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/android/FlutterFragmentTest.java", "test/io/flutter/embedding/android/FlutterViewTest.java", "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", + "test/io/flutter/embedding/engine/FlutterEngineTest.java", "test/io/flutter/embedding/engine/FlutterJNITest.java", "test/io/flutter/embedding/engine/RenderingComponentTest.java", "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 515b15070ce..f91214974ad 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -8,6 +8,8 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; @@ -150,31 +152,35 @@ public class FlutterEngine { * If the Dart VM has already started, the given arguments will have no effect. */ public FlutterEngine(@NonNull Context context, @Nullable String[] dartVmArgs) { - this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs); + this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs, true); } /** * Same as {@link #FlutterEngine(Context, FlutterLoader, FlutterJNI, String[])} but with no Dart * VM flags. + *

+ * {@code flutterJNI} should be a new instance that has never been attached to an engine before. */ public FlutterEngine( @NonNull Context context, @NonNull FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI ) { - this(context, flutterLoader, flutterJNI, null); + this(context, flutterLoader, flutterJNI, null, true); } /** - * Constructs a new {@code FlutterEngine}. See {@link #FlutterEngine(Context)}. - * - * {@code flutterJNI} should be a new instance that has never been attached to an engine before. + * Same as {@link #FlutterEngine(Context, FlutterLoader, FlutterJNI)}, plus Dart VM flags in + * {@code dartVmArgs}, and control over whether plugins are automatically registered with this + * {@code FlutterEngine} in {@code automaticallyRegisterPlugins}. If plugins are automatically + * registered, then they are registered during the execution of this constructor. */ public FlutterEngine( @NonNull Context context, @NonNull FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI, - @Nullable String[] dartVmArgs + @Nullable String[] dartVmArgs, + boolean automaticallyRegisterPlugins ) { this.flutterJNI = flutterJNI; flutterLoader.startInitialization(context); @@ -205,6 +211,10 @@ public class FlutterEngine { context.getApplicationContext(), this ); + + if (automaticallyRegisterPlugins) { + registerPlugins(); + } } private void attachToJni() { @@ -222,6 +232,31 @@ public class FlutterEngine { return flutterJNI.isAttached(); } + /** + * Registers all plugins that an app lists in its pubspec.yaml. + *

+ * The Flutter tool generates a class called GeneratedPluginRegistrant, which includes the code + * necessary to register every plugin in the pubspec.yaml with a given {@code FlutterEngine}. + * The GeneratedPluginRegistrant must be generated per app, because each app uses different sets + * of plugins. Therefore, the Android embedding cannot place a compile-time dependency on this + * generated class. This method uses reflection to attempt to locate the generated file and then + * use it at runtime. + *

+ * This method fizzles if the GeneratedPluginRegistrant cannot be found or invoked. This situation + * should never occur, but if any eventuality comes up that prevents an app from using this + * behavior, that app can still write code that explicitly registers plugins. + */ + private void registerPlugins() { + try { + Class generatedPluginRegistrant = Class.forName("io.plugins.GeneratedPluginRegistrant"); + Method registrationMethod = generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class); + registrationMethod.invoke(null, this); + } catch (Exception e) { + Log.w(TAG, "Tried to automatically register plugins with FlutterEngine (" + + this + ") but could not find and invoke the GeneratedPluginRegistrant."); + } + } + /** * Cleans up all components within this {@code FlutterEngine} and destroys the associated Dart * Isolate. All state held by the Dart Isolate, such as the Flutter Elements tree, is lost. diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 645c8c9b0ea..224676f1a59 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -21,6 +21,7 @@ import io.flutter.plugin.common.StandardMessageCodecTest; import io.flutter.plugin.editing.TextInputPluginTest; import io.flutter.plugin.platform.SingleViewPresentationTest; import io.flutter.util.PreconditionsTest; +import test.io.flutter.embedding.engine.FlutterEngineTest; import test.io.flutter.embedding.engine.dart.DartExecutorTest; @RunWith(Suite.class) @@ -30,6 +31,7 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest; FlutterActivityTest.class, FlutterAndroidComponentTest.class, FlutterEngineCacheTest.class, + FlutterEngineTest.class, FlutterFragmentTest.class, FlutterJNITest.class, FlutterRendererTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java new file mode 100644 index 00000000000..80ec2c94557 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java @@ -0,0 +1,34 @@ +package test.io.flutter.embedding.engine; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterLoader; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterEngineTest { + @Test + public void itDoesNotCrashIfGeneratedPluginRegistrantIsUnavailable() { + FlutterJNI flutterJNI = mock(FlutterJNI.class); + when(flutterJNI.isAttached()).thenReturn(true); + + FlutterEngine flutterEngine = new FlutterEngine( + RuntimeEnvironment.application, + mock(FlutterLoader.class), + flutterJNI, + new String[] {}, + true + ); + // The fact that the above constructor executed without error means that + // it dealt with a non-existent GeneratedPluginRegistrant. + } +}