diff --git a/shell/platform/android/io/flutter/FlutterInjector.java b/shell/platform/android/io/flutter/FlutterInjector.java index 20c46e6418990..405dab5adf770 100644 --- a/shell/platform/android/io/flutter/FlutterInjector.java +++ b/shell/platform/android/io/flutter/FlutterInjector.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager; import io.flutter.embedding.engine.loader.FlutterLoader; @@ -65,13 +66,17 @@ public static void reset() { } private FlutterInjector( - @NonNull FlutterLoader flutterLoader, DeferredComponentManager deferredComponentManager) { + @NonNull FlutterLoader flutterLoader, + @Nullable DeferredComponentManager deferredComponentManager, + @NonNull FlutterJNI.Factory flutterJniFactory) { this.flutterLoader = flutterLoader; this.deferredComponentManager = deferredComponentManager; + this.flutterJniFactory = flutterJniFactory; } private FlutterLoader flutterLoader; private DeferredComponentManager deferredComponentManager; + private FlutterJNI.Factory flutterJniFactory; /** Returns the {@link FlutterLoader} instance to use for the Flutter Android engine embedding. */ @NonNull @@ -88,6 +93,11 @@ public DeferredComponentManager deferredComponentManager() { return deferredComponentManager; } + @NonNull + public FlutterJNI.Factory getFlutterJNIFactory() { + return flutterJniFactory; + } + /** * Builder used to supply a custom FlutterInjector instance to {@link * FlutterInjector#setInstance(FlutterInjector)}. @@ -97,6 +107,7 @@ public DeferredComponentManager deferredComponentManager() { public static final class Builder { private FlutterLoader flutterLoader; private DeferredComponentManager deferredComponentManager; + private FlutterJNI.Factory flutterJniFactory; /** * Sets a {@link FlutterLoader} override. * @@ -113,9 +124,18 @@ public Builder setDeferredComponentManager( return this; } + public Builder setFlutterJNIFactory(@NonNull FlutterJNI.Factory factory) { + this.flutterJniFactory = factory; + return this; + } + private void fillDefaults() { + if (flutterJniFactory == null) { + flutterJniFactory = new FlutterJNI.Factory(); + } + if (flutterLoader == null) { - flutterLoader = new FlutterLoader(); + flutterLoader = new FlutterLoader(flutterJniFactory.provideFlutterJNI()); } // DeferredComponentManager's intended default is null. } @@ -127,7 +147,7 @@ private void fillDefaults() { public FlutterInjector build() { fillDefaults(); - return new FlutterInjector(flutterLoader, deferredComponentManager); + return new FlutterInjector(flutterLoader, deferredComponentManager, flutterJniFactory); } } } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index f4c308910e0b2..4bdbc9e73546a 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -919,12 +919,21 @@ public PlatformPlugin providePlatformPlugin( *

This method is called after {@link #provideFlutterEngine(Context)}. * *

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. + * method unless the FlutterEngine for this activity was externally created. To avoid the + * automatic plugin registration for implicitly created FlutterEngines, 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) { + if (delegate.isFlutterEngineFromHost()) { + // If the FlutterEngine was explicitly built and injected into this FlutterActivity, the + // builder should explicitly decide whether to automatically register plugins via the + // FlutterEngine's construction parameter or via the AndroidManifest metadata. + return; + } + GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine); } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index db64d5ab231f8..9fb31dec4a82b 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -835,6 +835,14 @@ public String getCachedEngineId() { return getArguments().getString(ARG_CACHED_ENGINE_ID, null); } + /** + * Returns true a {@code FlutterEngine} was explicitly created and injected into the {@code + * FlutterFragment} rather than one that was created implicitly in the {@code FlutterFragment}. + */ + /* package */ boolean isFlutterEngineInjected() { + return delegate.isFlutterEngineFromHost(); + } + /** * Returns false if the {@link FlutterEngine} within this {@code FlutterFragment} should outlive * the {@code FlutterFragment}, itself. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 16763a23ffa06..f6682abbb58d3 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -580,12 +580,21 @@ public FlutterEngine provideFlutterEngine(@NonNull Context context) { *

This method is called after {@link #provideFlutterEngine(Context)}. * *

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. + * method unless the FlutterEngine for this activity was externally created. To avoid the + * automatic plugin registration for implicitly created FlutterEngines, 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) { + if (flutterFragment.isFlutterEngineInjected()) { + // If the FlutterEngine was explicitly built and injected into this FlutterActivity, the + // builder should explicitly decide whether to automatically register plugins via the + // FlutterEngine's construction parameter or via the AndroidManifest metadata. + return; + } + GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine); } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 3d7f197dcfeeb..d12c03cb77be0 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -20,6 +20,7 @@ import io.flutter.embedding.engine.plugins.broadcastreceiver.BroadcastReceiverControlSurface; import io.flutter.embedding.engine.plugins.contentprovider.ContentProviderControlSurface; import io.flutter.embedding.engine.plugins.service.ServiceControlSurface; +import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister; import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; @@ -36,7 +37,6 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; -import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; @@ -157,7 +157,7 @@ public FlutterEngine(@NonNull Context context) { *

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 */ null, new FlutterJNI(), dartVmArgs, true); + this(context, /* flutterLoader */ null, /* flutterJNI */ null, dartVmArgs, true); } /** @@ -173,7 +173,7 @@ public FlutterEngine( this( context, /* flutterLoader */ null, - new FlutterJNI(), + /* flutterJNI */ null, dartVmArgs, automaticallyRegisterPlugins); } @@ -204,7 +204,7 @@ public FlutterEngine( this( context, /* flutterLoader */ null, - new FlutterJNI(), + /* flutterJNI */ null, new PlatformViewsController(), dartVmArgs, automaticallyRegisterPlugins, @@ -282,6 +282,14 @@ public FlutterEngine( } catch (NameNotFoundException e) { assetManager = context.getAssets(); } + + FlutterInjector injector = FlutterInjector.instance(); + + if (flutterJNI == null) { + flutterJNI = injector.getFlutterJNIFactory().provideFlutterJNI(); + } + this.flutterJNI = flutterJNI; + this.dartExecutor = new DartExecutor(flutterJNI, assetManager); this.dartExecutor.onAttachedToJNI(); @@ -307,9 +315,8 @@ public FlutterEngine( this.localizationPlugin = new LocalizationPlugin(context, localizationChannel); - this.flutterJNI = flutterJNI; if (flutterLoader == null) { - flutterLoader = FlutterInjector.instance().flutterLoader(); + flutterLoader = injector.flutterLoader(); } if (!flutterJNI.isAttached()) { @@ -320,7 +327,7 @@ public FlutterEngine( flutterJNI.addEngineLifecycleListener(engineLifecycleListener); flutterJNI.setPlatformViewsController(platformViewsController); flutterJNI.setLocalizationPlugin(localizationPlugin); - flutterJNI.setDeferredComponentManager(FlutterInjector.instance().deferredComponentManager()); + flutterJNI.setDeferredComponentManager(injector.deferredComponentManager()); // It should typically be a fresh, unattached JNI. But on a spawned engine, the JNI instance // is already attached to a native shell. In that case, the Java FlutterEngine is created around @@ -342,7 +349,7 @@ public FlutterEngine( // Only automatically register plugins if both constructor parameter and // loaded AndroidManifest config turn this feature on. if (automaticallyRegisterPlugins && flutterLoader.automaticallyRegisterPlugins()) { - registerPlugins(); + GeneratedPluginRegister.registerGeneratedPlugins(this); } } @@ -391,36 +398,6 @@ private boolean isAttachedToJni() { newFlutterJNI); // FlutterJNI. } - /** - * 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.flutter.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/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 2778aa2489bd5..b4aaff8bcde33 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -282,6 +282,7 @@ public static native void nativeOnVsync( @NonNull private final Looper mainLooper; // cached to avoid synchronization on repeat access. + // Prefer using the FlutterJNI.Factory so it's easier to test. public FlutterJNI() { // We cache the main looper so that we can ensure calls are made on the main thread // without consistently paying the synchronization cost of getMainLooper(). @@ -1258,4 +1259,15 @@ public interface AccessibilityDelegate { public interface AsyncWaitForVsyncDelegate { void asyncWaitForVsync(final long cookie); } + + /** + * A factory for creating {@code FlutterJNI} instances. Useful for FlutterJNI injections during + * tests. + */ + public static class Factory { + /** @return a {@link FlutterJNI} instance. */ + public FlutterJNI provideFlutterJNI() { + return new FlutterJNI(); + } + } } diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index ea6b24b66f0e5..49016119479bb 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -17,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.BuildConfig; +import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.util.PathUtils; @@ -70,7 +71,7 @@ public static FlutterLoader getInstance() { /** Creates a {@code FlutterLoader} that uses a default constructed {@link FlutterJNI}. */ public FlutterLoader() { - this(new FlutterJNI()); + this(FlutterInjector.instance().getFlutterJNIFactory().provideFlutterJNI()); } /** diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java b/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java index 5bb0ff5a6f37b..4c0f320735b23 100644 --- a/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java @@ -33,11 +33,12 @@ public static void registerGeneratedPlugins(@NonNull FlutterEngine flutterEngine generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class); registrationMethod.invoke(null, flutterEngine); } catch (Exception e) { - Log.w( + Log.e( TAG, "Tried to automatically register plugins with FlutterEngine (" + flutterEngine - + ") but could not find and invoke the GeneratedPluginRegistrant."); + + ") but could not find or invoke the GeneratedPluginRegistrant."); + Log.e(TAG, "Received exception while registering", e); } } } 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 d0daf990bfdc3..423f3a0f93a8e 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; +import io.flutter.FlutterInjector; import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; @@ -48,11 +49,18 @@ public class FlutterActivityTest { @Before public void setUp() { GeneratedPluginRegistrant.clearRegisteredEngines(); + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + when(mockFlutterJNI.isAttached()).thenReturn(true); + FlutterJNI.Factory mockFlutterJNIFactory = mock(FlutterJNI.Factory.class); + when(mockFlutterJNIFactory.provideFlutterJNI()).thenReturn(mockFlutterJNI); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterJNIFactory(mockFlutterJNIFactory).build()); } @After public void tearDown() { GeneratedPluginRegistrant.clearRegisteredEngines(); + FlutterInjector.reset(); } @Test @@ -211,12 +219,14 @@ public void itCreatesCachedEngineIntentThatDestroysTheEngine() { @Test public void itRegistersPluginsAtConfigurationTime() { - FlutterActivity activity = - Robolectric.buildActivity(FlutterActivityWithProvidedEngine.class).get(); - activity.onCreate(null); + Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity activity = activityController.get(); - assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); - activity.configureFlutterEngine(activity.getFlutterEngine()); + // This calls onAttach on FlutterActivityAndFragmentDelegate and subsequently + // configureFlutterEngine which registers the plugins. + activity.onCreate(null); List registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines(); assertEquals(1, registeredEngines.size()); @@ -282,6 +292,23 @@ public void itRestoresPluginStateBeforePluginOnCreate() { "Expected FakeFlutterPlugin onCreateCalled to be true", fakeFlutterPlugin.onCreateCalled); } + @Test + public void itDoesNotRegisterPluginsTwiceWhenUsingACachedEngine() { + Intent intent = + new Intent(RuntimeEnvironment.application, FlutterActivityWithProvidedEngine.class); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivityWithProvidedEngine.class, intent); + activityController.create(); + FlutterActivityWithProvidedEngine flutterActivity = activityController.get(); + flutterActivity.configureFlutterEngine(flutterActivity.getFlutterEngine()); + + List registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines(); + // This might cause the plugins to be registered twice, once by the FlutterEngine constructor, + // and once by the default FlutterActivity.configureFlutterEngine implementation. + // Test that it doesn't happen. + assertEquals(1, registeredEngines.size()); + } + static class FlutterActivityWithProvidedEngine extends FlutterActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -293,10 +320,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { @Override public FlutterEngine provideFlutterEngine(@NonNull Context context) { FlutterJNI flutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = mock(FlutterLoader.class); when(flutterJNI.isAttached()).thenReturn(true); + when(flutterLoader.automaticallyRegisterPlugins()).thenReturn(true); - return new FlutterEngine( - context, mock(FlutterLoader.class), flutterJNI, new String[] {}, false); + return new FlutterEngine(context, flutterLoader, flutterJNI, new String[] {}, true); } } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java index dc4b6013b711f..0872c56196ff2 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java @@ -16,6 +16,7 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.FlutterInjector; import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; @@ -35,12 +36,20 @@ public class FlutterFragmentActivityTest { @Before public void setUp() { + FlutterInjector.reset(); GeneratedPluginRegistrant.clearRegisteredEngines(); + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + when(mockFlutterJNI.isAttached()).thenReturn(true); + FlutterJNI.Factory mockFlutterJNIFactory = mock(FlutterJNI.Factory.class); + when(mockFlutterJNIFactory.provideFlutterJNI()).thenReturn(mockFlutterJNI); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterJNIFactory(mockFlutterJNIFactory).build()); } @After public void tearDown() { GeneratedPluginRegistrant.clearRegisteredEngines(); + FlutterInjector.reset(); } @Test @@ -76,7 +85,7 @@ protected RenderMode getRenderMode() { @Test public void itRegistersPluginsAtConfigurationTime() { FlutterFragmentActivity activity = - Robolectric.buildActivity(FlutterFragmentActivityWithProvidedEngine.class).get(); + Robolectric.buildActivity(FlutterFragmentActivity.class).get(); assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); // Calling onCreate on the FlutterFragmentActivity will create a FlutterFragment and @@ -89,6 +98,20 @@ public void itRegistersPluginsAtConfigurationTime() { assertEquals(activity.getFlutterEngine(), registeredEngines.get(0)); } + @Test + public void itDoesNotRegisterPluginsTwiceWhenUsingACachedEngine() { + FlutterFragmentActivity activity = + Robolectric.buildActivity(FlutterFragmentActivityWithProvidedEngine.class).get(); + activity.onCreate(null); + activity.configureFlutterEngine(activity.getFlutterEngine()); + + List registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines(); + // This might cause the plugins to be registered twice, once by the FlutterEngine constructor, + // and once by the default FlutterFragmentActivity.configureFlutterEngine implementation. + // Test that it doesn't happen. + assertEquals(1, registeredEngines.size()); + } + @Test public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase1() throws PackageManager.NameNotFoundException { @@ -157,10 +180,11 @@ protected FlutterFragment createFlutterFragment() { @Override public FlutterEngine provideFlutterEngine(@NonNull Context context) { FlutterJNI flutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = mock(FlutterLoader.class); when(flutterJNI.isAttached()).thenReturn(true); + when(flutterLoader.automaticallyRegisterPlugins()).thenReturn(true); - return new FlutterEngine( - context, mock(FlutterLoader.class), flutterJNI, new String[] {}, false); + return new FlutterEngine(context, flutterLoader, flutterJNI, new String[] {}, true); } } 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 bcfabf863dafe..8066946c62e85 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java @@ -35,6 +35,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) @@ -63,6 +64,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @After public void tearDown() { GeneratedPluginRegistrant.clearRegisteredEngines(); + // Make sure to not forget to remove the mock exception in the generated plugin registration + // mock, or everything subsequent will break. + GeneratedPluginRegistrant.pluginRegistrationException = null; } @Test @@ -78,6 +82,36 @@ public void itAutomaticallyRegistersPluginsByDefault() { assertEquals(flutterEngine, registeredEngines.get(0)); } + // Helps show the root cause of MissingPluginException type errors like + // https://github.com/flutter/flutter/issues/78625. + @Test + public void itCatchesAndDisplaysRegistrationExceptions() { + assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); + GeneratedPluginRegistrant.pluginRegistrationException = + new RuntimeException("I'm a bug in the plugin"); + FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); + when(mockFlutterLoader.automaticallyRegisterPlugins()).thenReturn(true); + FlutterEngine flutterEngine = + new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, flutterJNI); + + List registeredEngines = GeneratedPluginRegistrant.getRegisteredEngines(); + // When it crashes, it doesn't end up registering anything. + assertEquals(0, registeredEngines.size()); + + // Check the logs actually says registration failed, so a subsequent MissingPluginException + // isn't mysterious. + assertTrue( + ShadowLog.getLogsForTag("GeneratedPluginsRegister") + .get(0) + .msg + .contains("Tried to automatically register plugins")); + assertEquals( + GeneratedPluginRegistrant.pluginRegistrationException, + ShadowLog.getLogsForTag("GeneratedPluginsRegister").get(1).throwable.getCause()); + + GeneratedPluginRegistrant.pluginRegistrationException = null; + } + @Test public void itDoesNotAutomaticallyRegistersPluginsWhenFlutterLoaderDisablesIt() { assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); @@ -105,18 +139,6 @@ public void itDoesNotAutomaticallyRegistersPluginsWhenFlutterEngineDisablesIt() assertTrue(registeredEngines.isEmpty()); } - @Test - public void itCanBeConfiguredToNotAutomaticallyRegisterPlugins() { - new FlutterEngine( - RuntimeEnvironment.application, - mock(FlutterLoader.class), - flutterJNI, - /*dartVmArgs=*/ new String[] {}, - /*automaticallyRegisterPlugins=*/ false); - - assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); - } - @Test public void itNotifiesPlatformViewsControllerWhenDevHotRestart() { // Setup test. @@ -220,6 +242,7 @@ public void itCanUseFlutterLoaderInjectionViaFlutterInjector() throws NameNotFou verify(mockFlutterLoader, times(1)).startInitialization(any()); verify(mockFlutterLoader, times(1)).ensureInitializationComplete(any(), any()); + FlutterInjector.reset(); } @Test diff --git a/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java b/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java index 9b93bd6399464..c2528725b89b3 100644 --- a/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -13,6 +13,7 @@ @VisibleForTesting public class GeneratedPluginRegistrant { private static final List registeredEngines = new ArrayList<>(); + public static RuntimeException pluginRegistrationException; /** * The one and only method currently generated by the tool. @@ -21,6 +22,9 @@ public class GeneratedPluginRegistrant { * all registered engines instead. */ public static void registerWith(FlutterEngine engine) { + if (pluginRegistrationException != null) { + throw pluginRegistrationException; + } registeredEngines.add(engine); }