diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 943b04ca4c71a..9b7ee297e1094 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -435,6 +435,7 @@ action("robolectric_tests") { "test/io/flutter/plugin/common/StandardMessageCodecTest.java", "test/io/flutter/plugin/editing/TextInputPluginTest.java", "test/io/flutter/plugin/platform/SingleViewPresentationTest.java", + "test/io/flutter/plugins/GeneratedPluginRegistrant.java", "test/io/flutter/util/PreconditionsTest.java", ] diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 6e5ada1d8d77c..8a7a8475e71f8 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -33,6 +33,9 @@ import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.view.FlutterMain; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DART_ENTRYPOINT_META_DATA_KEY; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_BACKGROUND_MODE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; @@ -873,14 +876,18 @@ public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNul } /** - * Hook for subclasses to easily configure a {@code FlutterEngine}, e.g., register - * plugins. - * <p> - * This method is called after {@link #provideFlutterEngine(Context)}. + * Hook for subclasses to easily configure a {@code FlutterEngine}. + * + * <p>This method is called after {@link #provideFlutterEngine(Context)}. + * + * <p>All plugins listed in the app's pubspec are registered in the base implementation of this + * method. To avoid automatic plugin registration, override this method without invoking super(). + * To keep automatic plugin registration and further configure the flutterEngine, override this + * method, invoke super(), and then configure the flutterEngine as desired. */ @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - // No-op. Hook for subclasses. + registerPlugins(flutterEngine); } /** @@ -950,4 +957,28 @@ public void onFlutterUiNoLongerDisplayed() { // no-op } + /** + * Registers all plugins that an app lists in its pubspec.yaml. + * <p> + * 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. + * <p> + * 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 static void registerPlugins(@NonNull FlutterEngine flutterEngine) { + try { + Class<?> generatedPluginRegistrant = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant"); + Method registrationMethod = generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class); + registrationMethod.invoke(null, flutterEngine); + } catch (Exception e) { + Log.w(TAG, "Tried to automatically register plugins with FlutterEngine (" + + flutterEngine + ") but could not find and invoke the GeneratedPluginRegistrant."); + } + } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 17bd3c46d5632..5b36dfaafe735 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -27,6 +27,8 @@ import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.plugin.platform.PlatformPlugin; import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; @@ -223,7 +225,7 @@ void onAttach(@NonNull Context context) { // FlutterView. Log.v(TAG, "No preferred FlutterEngine was provided. Creating a new FlutterEngine for" + " this FlutterFragment."); - flutterEngine = new FlutterEngine(host.getContext(), host.getFlutterShellArgs().toArray()); + flutterEngine = new FlutterEngine(host.getContext(), host.getFlutterShellArgs().toArray(), /*automaticallyRegisterPlugins=*/false); isFlutterEngineFromHost = false; } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index cd8956a172961..1d142a0bf9b2c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -126,8 +126,7 @@ public void onPreEngineRestart() { * {@link RenderSurface} is registered. See * {@link #getRenderer()} and {@link FlutterRenderer#startRenderingToSurface(RenderSurface)}. * <p> - * A new {@code FlutterEngine} does not come with any Flutter plugins attached. To attach plugins, - * see {@link #getPlugins()}. + * A new {@code FlutterEngine} automatically attaches all plugins. See {@link #getPlugins()}. * <p> * A new {@code FlutterEngine} does come with all default system channels attached. * <p> @@ -152,6 +151,19 @@ public FlutterEngine(@NonNull Context context, @Nullable String[] dartVmArgs) { this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs, true); } + /** + * Same as {@link #FlutterEngine(Context)} with added support for passing Dart + * VM arguments and avoiding automatic plugin registration. + * <p> + * If the Dart VM has already started, the given arguments will have no effect. + */ + public FlutterEngine( + @NonNull Context context, + @Nullable String[] dartVmArgs, + boolean automaticallyRegisterPlugins) { + this(context, FlutterLoader.getInstance(), new FlutterJNI(), dartVmArgs, automaticallyRegisterPlugins); + } + /** * Same as {@link #FlutterEngine(Context, FlutterLoader, FlutterJNI, String[])} but with no Dart * VM flags. diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java index 64528bfd766cf..e4cfbfdda70d0 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -6,6 +6,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import io.flutter.plugins.GeneratedPluginRegistrant; +import java.util.List; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -30,6 +34,16 @@ @Config(manifest=Config.NONE) @RunWith(RobolectricTestRunner.class) public class FlutterActivityTest { + @Before + public void setUp() { + GeneratedPluginRegistrant.clearRegisteredEngines(); + } + + @After + public void tearDown() { + GeneratedPluginRegistrant.clearRegisteredEngines(); + } + @Test public void itCreatesDefaultIntentWithExpectedDefaults() { Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); @@ -122,6 +136,19 @@ public void itCreatesCachedEngineIntentThatDestroysTheEngine() { assertTrue(flutterActivity.shouldDestroyEngineWithHost()); } + @Test + public void itRegistersPluginsAtConfigurationTime() { + FlutterActivity activity = Robolectric.buildActivity(FlutterActivityWithProvidedEngine.class).get(); + activity.onCreate(null); + + assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); + activity.configureFlutterEngine(activity.getFlutterEngine()); + + List<FlutterEngine> registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines(); + assertEquals(1, registeredEngines.size()); + assertEquals(activity.getFlutterEngine(), registeredEngines.get(0)); + } + static class FlutterActivityWithProvidedEngine extends FlutterActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -140,7 +167,7 @@ public FlutterEngine provideFlutterEngine(@NonNull Context context) { mock(FlutterLoader.class), flutterJNI, new String[]{}, - true + false ); } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java index 80ec2c9455708..133beb6dd75fc 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java @@ -1,7 +1,13 @@ package test.io.flutter.embedding.engine; +import io.flutter.plugins.GeneratedPluginRegistrant; +import java.util.List; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -10,25 +16,52 @@ import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.loader.FlutterLoader; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; 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); + @Mock FlutterJNI flutterJNI; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); when(flutterJNI.isAttached()).thenReturn(true); + GeneratedPluginRegistrant.clearRegisteredEngines(); + } + @After + public void tearDown() { + GeneratedPluginRegistrant.clearRegisteredEngines(); + } + + @Test + public void itAutomaticallyRegistersPluginsByDefault() { + assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); FlutterEngine flutterEngine = new FlutterEngine( + RuntimeEnvironment.application, + mock(FlutterLoader.class), + flutterJNI + ); + + List<FlutterEngine> registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines(); + assertEquals(1, registeredEngines.size()); + assertEquals(flutterEngine, registeredEngines.get(0)); + } + + @Test + public void itCanBeConfiguredToNotAutomaticallyRegisterPlugins() { + new FlutterEngine( RuntimeEnvironment.application, mock(FlutterLoader.class), flutterJNI, - new String[] {}, - true + /*dartVmArgs=*/new String[] {}, + /*automaticallyRegisterPlugins=*/false ); - // The fact that the above constructor executed without error means that - // it dealt with a non-existent GeneratedPluginRegistrant. + + assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); } } diff --git a/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java b/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000000000..8cf817064bbc7 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,48 @@ +package io.flutter.plugins; + +import android.support.annotation.VisibleForTesting; +import io.flutter.embedding.engine.FlutterEngine; +import java.util.List; +import java.util.ArrayList; + +/** + * A fake of the {@code GeneratedPluginRegistrant} normally built by the tool into Flutter apps. + * + * <p>Used to test engine logic which interacts with the generated class. + */ +@VisibleForTesting +public class GeneratedPluginRegistrant { + private static final List<FlutterEngine> registeredEngines = new ArrayList<>(); + + /** + * The one and only method currently generated by the tool. + * + * <p>Normally it registers all plugins in an app with the given {@code engine}. This fake tracks + * all registered engines instead. + */ + public static void registerWith(FlutterEngine engine) { + registeredEngines.add(engine); + } + + /** + * Clears the mutable static state regrettably stored in this class. + * + * <p>{@link #registerWith} is a static call with no visible side effects. In order to verify when + * it's been called we also unfortunately need to store the state statically. This should be + * called before and after each test run accessing this class to make sure the state is clear both + * before and after the run. + */ + public static void clearRegisteredEngines() { + registeredEngines.clear(); + } + + /** + * Returns a list of all the engines registered so far. + * + * <p>CAUTION: This list is static and must be manually wiped in between test runs. See + * {@link #clearRegisteredEngines()}. + */ + public static List<FlutterEngine> getRegisteredEngines() { + return new ArrayList<>(registeredEngines); + } +}