diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 3d7f197dcfeeb..389d3af06afdb 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; @@ -342,7 +342,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 +391,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/plugins/util/GeneratedPluginRegister.java b/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java index 5bb0ff5a6f37b..fab7eb8c7ee65 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,15 @@ 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."); + Log.e( + TAG, + // getCause here because the first layer of the exception would be from reflect. + "Received exception while registering: " + e.getCause()); } } } 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..1af79de943259 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,38 @@ 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")); + assertTrue( + ShadowLog.getLogsForTag("GeneratedPluginsRegister") + .get(1) + .msg + .contains("I'm a bug in the plugin")); + + GeneratedPluginRegistrant.pluginRegistrationException = null; + } + @Test public void itDoesNotAutomaticallyRegistersPluginsWhenFlutterLoaderDisablesIt() { assertTrue(GeneratedPluginRegistrant.getRegisteredEngines().isEmpty()); @@ -105,18 +141,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. 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); }