From 15f5696c4139a21e1fc54014ce17d01f6ad1737c Mon Sep 17 00:00:00 2001 From: xster Date: Sat, 29 Aug 2020 22:29:23 -0700 Subject: [PATCH] Add a java injector for testing (#20789) --- ci/licenses_golden/licenses_flutter | 1 + shell/platform/android/BUILD.gn | 3 + .../android/io/flutter/FlutterInjector.java | 136 ++++++++++++++++++ .../flutter/app/FlutterActivityDelegate.java | 7 +- .../io/flutter/app/FlutterApplication.java | 4 +- .../embedding/engine/FlutterEngine.java | 21 +-- .../embedding/engine/FlutterShellArgs.java | 4 +- .../embedding/engine/dart/DartExecutor.java | 15 +- .../engine/loader/FlutterLoader.java | 36 +++-- .../engine/plugins/shim/ShimRegistrar.java | 6 +- .../android/io/flutter/view/FlutterMain.java | 50 ++----- .../test/io/flutter/FlutterInjectorTest.java | 61 ++++++++ .../test/io/flutter/FlutterTestSuite.java | 6 + .../embedding/engine/FlutterEngineTest.java | 16 +++ .../embedding/engine/PluginComponentTest.java | 55 ++----- .../engine/dart/DartExecutorTest.java | 36 +++++ .../loader/ApplicationInfoLoaderTest.java | 4 + .../engine/loader/FlutterLoaderTest.java | 43 ++++++ 18 files changed, 393 insertions(+), 111 deletions(-) create mode 100644 shell/platform/android/io/flutter/FlutterInjector.java create mode 100644 shell/platform/android/test/io/flutter/FlutterInjectorTest.java create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 09bc2ba6555c4..611cdc4dba0bf 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -694,6 +694,7 @@ FILE: ../../../flutter/shell/platform/android/external_view_embedder/surface_poo FILE: ../../../flutter/shell/platform/android/external_view_embedder/surface_pool_unittests.cc FILE: ../../../flutter/shell/platform/android/flutter_main.cc FILE: ../../../flutter/shell/platform/android/flutter_main.h +FILE: ../../../flutter/shell/platform/android/io/flutter/FlutterInjector.java FILE: ../../../flutter/shell/platform/android/io/flutter/Log.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivity.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 3fe51be12a214..23300cf01a4d3 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -117,6 +117,7 @@ embedding_sources_jar_filename = "$embedding_artifact_id-sources.jar" embedding_source_jar_path = "$root_out_dir/$embedding_sources_jar_filename" android_java_sources = [ + "io/flutter/FlutterInjector.java", "io/flutter/Log.java", "io/flutter/app/FlutterActivity.java", "io/flutter/app/FlutterActivityDelegate.java", @@ -415,6 +416,7 @@ action("robolectric_tests") { jar_path = "$root_out_dir/robolectric_tests.jar" sources = [ + "test/io/flutter/FlutterInjectorTest.java", "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.java", "test/io/flutter/embedding/android/AndroidKeyProcessorTest.java", @@ -434,6 +436,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/RenderingComponentTest.java", "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", "test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java", + "test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java", "test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java", "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", "test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java", diff --git a/shell/platform/android/io/flutter/FlutterInjector.java b/shell/platform/android/io/flutter/FlutterInjector.java new file mode 100644 index 0000000000000..864dfffb526e3 --- /dev/null +++ b/shell/platform/android/io/flutter/FlutterInjector.java @@ -0,0 +1,136 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter; + +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import io.flutter.embedding.engine.loader.FlutterLoader; + +/** + * This class is a simple dependency injector for the relatively thin Android part of the Flutter + * engine. + * + *

This simple solution is used facilitate testability without bringing in heavier + * app-development centric dependency injection frameworks such as Guice or Dagger2 or spreading + * construction injection everywhere. + */ +public final class FlutterInjector { + + private static FlutterInjector instance; + private static boolean accessed; + + /** + * Use {@link FlutterInjector.Builder} to specify members to be injected via the static {@code + * FlutterInjector}. + * + *

This can only be called at the beginning of the program before the {@link #instance()} is + * accessed. + */ + public static void setInstance(@NonNull FlutterInjector injector) { + if (accessed) { + throw new IllegalStateException( + "Cannot change the FlutterInjector instance once it's been " + + "read. If you're trying to dependency inject, be sure to do so at the beginning of " + + "the program"); + } + instance = injector; + } + + /** + * Retrieve the static instance of the {@code FlutterInjector} to use in your program. + * + *

Once you access it, you can no longer change the values injected. + * + *

If no override is provided for the injector, reasonable defaults are provided. + */ + public static FlutterInjector instance() { + accessed = true; + if (instance == null) { + instance = new Builder().build(); + } + return instance; + } + + // This whole class is here to enable testing so to test the thing that lets you test, some degree + // of hack is needed. + @VisibleForTesting + public static void reset() { + accessed = false; + instance = null; + } + + private FlutterInjector(boolean shouldLoadNative, @NonNull FlutterLoader flutterLoader) { + this.shouldLoadNative = shouldLoadNative; + this.flutterLoader = flutterLoader; + } + + private boolean shouldLoadNative; + private FlutterLoader flutterLoader; + + /** + * Returns whether the Flutter Android engine embedding should load the native C++ engine. + * + *

Useful for testing since JVM tests via Robolectric can't load native libraries. + */ + public boolean shouldLoadNative() { + return shouldLoadNative; + } + + /** Returns the {@link FlutterLoader} instance to use for the Flutter Android engine embedding. */ + @NonNull + public FlutterLoader flutterLoader() { + return flutterLoader; + } + + /** + * Builder used to supply a custom FlutterInjector instance to {@link + * FlutterInjector#setInstance(FlutterInjector)}. + * + *

Non-overriden values have reasonable defaults. + */ + public static final class Builder { + + private boolean shouldLoadNative = true; + /** + * Sets whether the Flutter Android engine embedding should load the native C++ engine. + * + *

Useful for testing since JVM tests via Robolectric can't load native libraries. + * + *

Defaults to true. + */ + public Builder setShouldLoadNative(boolean shouldLoadNative) { + this.shouldLoadNative = shouldLoadNative; + return this; + } + + private FlutterLoader flutterLoader; + /** + * Sets a {@link FlutterLoader} override. + * + *

A reasonable default will be used if unspecified. + */ + public Builder setFlutterLoader(@NonNull FlutterLoader flutterLoader) { + this.flutterLoader = flutterLoader; + return this; + } + + private void fillDefaults() { + if (flutterLoader == null) { + flutterLoader = new FlutterLoader(); + } + } + + /** + * Builds a {@link FlutterInjector} from the builder. Unspecified properties will have + * reasonable defaults. + */ + public FlutterInjector build() { + fillDefaults(); + + System.out.println("should load native is " + shouldLoadNative); + return new FlutterInjector(shouldLoadNative, flutterLoader); + } + } +} diff --git a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java index b1619baebf82b..ee96cd072443b 100644 --- a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java +++ b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java @@ -35,7 +35,8 @@ import java.util.ArrayList; /** - * Class that performs the actual work of tying Android {@link Activity} instances to Flutter. + * Deprecated class that performs the actual work of tying Android {@link Activity} instances to + * Flutter. * *

This exists as a dedicated class (as opposed to being integrated directly into {@link * FlutterActivity}) to facilitate applications that don't wish to subclass {@code FlutterActivity}. @@ -48,6 +49,10 @@ * FlutterActivityEvents} from your activity to an instance of this class. Optionally, you can make * your activity implement {@link PluginRegistry} and/or {@link * io.flutter.view.FlutterView.Provider} and forward those methods to this class as well. + * + *

Deprecation: {@link io.flutter.embedding.android.FlutterActivity} is the new API that now + * replaces this class and {@link io.flutter.app.FlutterActivity}. See + * https://flutter.dev/go/android-project-migration for more migration details. */ public final class FlutterActivityDelegate implements FlutterActivityEvents, FlutterView.Provider, PluginRegistry { diff --git a/shell/platform/android/io/flutter/app/FlutterApplication.java b/shell/platform/android/io/flutter/app/FlutterApplication.java index cd55217176ef7..a211c268548cd 100644 --- a/shell/platform/android/io/flutter/app/FlutterApplication.java +++ b/shell/platform/android/io/flutter/app/FlutterApplication.java @@ -7,7 +7,7 @@ import android.app.Activity; import android.app.Application; import androidx.annotation.CallSuper; -import io.flutter.view.FlutterMain; +import io.flutter.FlutterInjector; /** * Flutter implementation of {@link android.app.Application}, managing application-level global @@ -21,7 +21,7 @@ public class FlutterApplication extends Application { @CallSuper public void onCreate() { super.onCreate(); - FlutterMain.startInitialization(this); + FlutterInjector.instance().flutterLoader().startInitialization(this); } private Activity mCurrentActivity = null; diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 1408156ebaf24..9ec50d7ef98eb 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -7,6 +7,7 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.loader.FlutterLoader; @@ -131,7 +132,8 @@ public void onPreEngineRestart() { *

In order to pass Dart VM initialization arguments (see {@link * io.flutter.embedding.engine.FlutterShellArgs}) when creating the VM, manually set the * initialization arguments by calling {@link FlutterLoader#startInitialization(Context)} and - * {@link FlutterLoader#ensureInitializationComplete(Context, String[])}. + * {@link FlutterLoader#ensureInitializationComplete(Context, String[])} before constructing the + * engine. */ public FlutterEngine(@NonNull Context context) { this(context, null); @@ -143,7 +145,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.getInstance(), new FlutterJNI(), dartVmArgs, true); + this(context, /* flutterLoader */ null, new FlutterJNI(), dartVmArgs, true); } /** @@ -158,7 +160,7 @@ public FlutterEngine( boolean automaticallyRegisterPlugins) { this( context, - FlutterLoader.getInstance(), + /* flutterLoader */ null, new FlutterJNI(), dartVmArgs, automaticallyRegisterPlugins); @@ -189,7 +191,7 @@ public FlutterEngine( boolean waitForRestorationData) { this( context, - FlutterLoader.getInstance(), + /* flutterLoader */ null, new FlutterJNI(), new PlatformViewsController(), dartVmArgs, @@ -206,7 +208,7 @@ public FlutterEngine( */ public FlutterEngine( @NonNull Context context, - @NonNull FlutterLoader flutterLoader, + @Nullable FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI) { this(context, flutterLoader, flutterJNI, null, true); } @@ -219,7 +221,7 @@ public FlutterEngine( */ public FlutterEngine( @NonNull Context context, - @NonNull FlutterLoader flutterLoader, + @Nullable FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI, @Nullable String[] dartVmArgs, boolean automaticallyRegisterPlugins) { @@ -238,7 +240,7 @@ public FlutterEngine( */ public FlutterEngine( @NonNull Context context, - @NonNull FlutterLoader flutterLoader, + @Nullable FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI, @NonNull PlatformViewsController platformViewsController, @Nullable String[] dartVmArgs, @@ -256,7 +258,7 @@ public FlutterEngine( /** Fully configurable {@code FlutterEngine} constructor. */ public FlutterEngine( @NonNull Context context, - @NonNull FlutterLoader flutterLoader, + @Nullable FlutterLoader flutterLoader, @NonNull FlutterJNI flutterJNI, @NonNull PlatformViewsController platformViewsController, @Nullable String[] dartVmArgs, @@ -280,6 +282,9 @@ public FlutterEngine( this.localizationPlugin = new LocalizationPlugin(context, localizationChannel); this.flutterJNI = flutterJNI; + if (flutterLoader == null) { + flutterLoader = FlutterInjector.instance().flutterLoader(); + } flutterLoader.startInitialization(context.getApplicationContext()); flutterLoader.ensureInitializationComplete(context, dartVmArgs); diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java b/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java index 9f0c4eef07856..b064ef8f17eaf 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java @@ -15,8 +15,8 @@ *

The term "shell" refers to the native code that adapts Flutter to different platforms. * Flutter's Android Java code initializes a native "shell" and passes these arguments to that * native shell when it is initialized. See {@link - * io.flutter.view.FlutterMain#ensureInitializationComplete(Context, String[])} for more - * information. + * io.flutter.embedding.engine.loader.FlutterLoader#ensureInitializationComplete(Context, String[])} + * for more information. */ @SuppressWarnings({"WeakerAccess", "unused"}) public class FlutterShellArgs { diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java index 13b3ff05a62ea..3d6cb8416e21d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java @@ -8,12 +8,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.StringCodec; import io.flutter.view.FlutterCallbackInformation; -import io.flutter.view.FlutterMain; import java.nio.ByteBuffer; /** @@ -250,9 +251,19 @@ public void notifyLowMemoryWarning() { * that entrypoint and other assets required for Dart execution. */ public static class DartEntrypoint { + /** + * Create a DartEntrypoint pointing to the default Flutter assets location with a default Dart + * entrypoint. + */ @NonNull public static DartEntrypoint createDefault() { - return new DartEntrypoint(FlutterMain.findAppBundlePath(), "main"); + FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader(); + + if (!flutterLoader.initialized()) { + throw new AssertionError( + "DartEntrypoints can only be created once a FlutterEngine is created."); + } + return new DartEntrypoint(flutterLoader.findAppBundlePath(), "main"); } /** The path within the AssetManager where the app will look for assets. */ 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 3df6fe097f81f..8af9539c94ea8 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -15,6 +15,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.BuildConfig; +import io.flutter.FlutterInjector; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.util.PathUtils; import io.flutter.view.VsyncWaiter; @@ -47,7 +48,10 @@ public class FlutterLoader { *

The returned instance loads Flutter native libraries in the standard way. A singleton object * is used instead of static methods to facilitate testing without actually running native library * linking. + * + * @deprecated Use the {@link io.flutter.FlutterInjector} instead. */ + @Deprecated @NonNull public static FlutterLoader getInstance() { if (instance == null) { @@ -56,13 +60,6 @@ public static FlutterLoader getInstance() { return instance; } - @NonNull - public static FlutterLoader getInstanceForTest(FlutterApplicationInfo flutterApplicationInfo) { - FlutterLoader loader = new FlutterLoader(); - loader.flutterApplicationInfo = flutterApplicationInfo; - return loader; - } - private boolean initialized = false; @Nullable private Settings settings; private long initStartTimestampMillis; @@ -128,7 +125,9 @@ public void startInitialization(@NonNull Context applicationContext, @NonNull Se public InitResult call() { ResourceExtractor resourceExtractor = initResources(appContext); - System.loadLibrary("flutter"); + if (FlutterInjector.instance().shouldLoadNative()) { + System.loadLibrary("flutter"); + } // Prefetch the default font manager as soon as possible on a background thread. // It helps to reduce time cost of engine setup that blocks the platform thread. @@ -231,13 +230,15 @@ public void ensureInitializationComplete( long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; - FlutterJNI.nativeInit( - applicationContext, - shellArgs.toArray(new String[0]), - kernelPath, - result.appStoragePath, - result.engineCachesPath, - initTimeMillis); + if (FlutterInjector.instance().shouldLoadNative()) { + FlutterJNI.nativeInit( + applicationContext, + shellArgs.toArray(new String[0]), + kernelPath, + result.appStoragePath, + result.engineCachesPath, + initTimeMillis); + } initialized = true; } catch (Exception e) { @@ -293,6 +294,11 @@ public void run() { }); } + /** Returns whether the FlutterLoader has finished loading the native library. */ + public boolean initialized() { + return initialized; + } + /** Extract assets out of the APK that need to be cached as uncompressed files on disk. */ private ResourceExtractor initResources(@NonNull Context applicationContext) { ResourceExtractor resourceExtractor = null; diff --git a/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java index 9ec464e4f5736..9c40d0a016f21 100644 --- a/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java +++ b/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java @@ -7,6 +7,7 @@ import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; +import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -14,7 +15,6 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.platform.PlatformViewRegistry; -import io.flutter.view.FlutterMain; import io.flutter.view.FlutterView; import io.flutter.view.TextureRegistry; import java.util.HashSet; @@ -85,12 +85,12 @@ public FlutterView view() { @Override public String lookupKeyForAsset(String asset) { - return FlutterMain.getLookupKeyForAsset(asset); + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset); } @Override public String lookupKeyForAsset(String asset, String packageName) { - return FlutterMain.getLookupKeyForAsset(asset, packageName); + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset, packageName); } @Override diff --git a/shell/platform/android/io/flutter/view/FlutterMain.java b/shell/platform/android/io/flutter/view/FlutterMain.java index 8cc80ec8dc96f..1b13b45aee208 100644 --- a/shell/platform/android/io/flutter/view/FlutterMain.java +++ b/shell/platform/android/io/flutter/view/FlutterMain.java @@ -8,7 +8,7 @@ import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; +import io.flutter.FlutterInjector; import io.flutter.embedding.engine.loader.FlutterLoader; /** @@ -42,10 +42,7 @@ public void setLogTag(String tag) { * @param applicationContext The Android application context. */ public static void startInitialization(@NonNull Context applicationContext) { - if (isRunningInRobolectricTest) { - return; - } - FlutterLoader.getInstance().startInitialization(applicationContext); + FlutterInjector.instance().flutterLoader().startInitialization(applicationContext); } /** @@ -61,12 +58,9 @@ public static void startInitialization(@NonNull Context applicationContext) { */ public static void startInitialization( @NonNull Context applicationContext, @NonNull Settings settings) { - if (isRunningInRobolectricTest) { - return; - } FlutterLoader.Settings newSettings = new FlutterLoader.Settings(); newSettings.setLogTag(settings.getLogTag()); - FlutterLoader.getInstance().startInitialization(applicationContext, newSettings); + FlutterInjector.instance().flutterLoader().startInitialization(applicationContext, newSettings); } /** @@ -79,10 +73,9 @@ public static void startInitialization( */ public static void ensureInitializationComplete( @NonNull Context applicationContext, @Nullable String[] args) { - if (isRunningInRobolectricTest) { - return; - } - FlutterLoader.getInstance().ensureInitializationComplete(applicationContext, args); + FlutterInjector.instance() + .flutterLoader() + .ensureInitializationComplete(applicationContext, args); } /** @@ -94,22 +87,20 @@ public static void ensureInitializationCompleteAsync( @Nullable String[] args, @NonNull Handler callbackHandler, @NonNull Runnable callback) { - if (isRunningInRobolectricTest) { - return; - } - FlutterLoader.getInstance() + FlutterInjector.instance() + .flutterLoader() .ensureInitializationCompleteAsync(applicationContext, args, callbackHandler, callback); } @NonNull public static String findAppBundlePath() { - return FlutterLoader.getInstance().findAppBundlePath(); + return FlutterInjector.instance().flutterLoader().findAppBundlePath(); } @Deprecated @Nullable public static String findAppBundlePath(@NonNull Context applicationContext) { - return FlutterLoader.getInstance().findAppBundlePath(); + return FlutterInjector.instance().flutterLoader().findAppBundlePath(); } /** @@ -121,7 +112,7 @@ public static String findAppBundlePath(@NonNull Context applicationContext) { */ @NonNull public static String getLookupKeyForAsset(@NonNull String asset) { - return FlutterLoader.getInstance().getLookupKeyForAsset(asset); + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset); } /** @@ -135,23 +126,6 @@ public static String getLookupKeyForAsset(@NonNull String asset) { */ @NonNull public static String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { - return FlutterLoader.getInstance().getLookupKeyForAsset(asset, packageName); - } - - private static boolean isRunningInRobolectricTest = false; - - /* - * Indicates whether we are currently running in a Robolectric Test. - * - *

Flutter cannot be initialized inside a Robolectric environment since it cannot load - * native libraries. - * - * @deprecated Use the new embedding (io.flutter.embedding) instead which provides better - * modularity for testing. - */ - @Deprecated - @VisibleForTesting - public static void setIsRunningInRobolectricTest(boolean isRunningInRobolectricTest) { - FlutterMain.isRunningInRobolectricTest = isRunningInRobolectricTest; + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(asset, packageName); } } diff --git a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java new file mode 100644 index 0000000000000..225c24959e136 --- /dev/null +++ b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import io.flutter.embedding.engine.loader.FlutterLoader; +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.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterInjectorTest { + @Mock FlutterLoader mockFlutterLoader; + + @Before + public void setUp() { + // Since the intent is to have a convenient static class to use for production. + FlutterInjector.reset(); + MockitoAnnotations.initMocks(this); + } + + @Test + public void itHasSomeReasonableDefaults() { + // Implicitly builds when first accessed. + FlutterInjector injector = FlutterInjector.instance(); + assertNotNull(injector.flutterLoader()); + assertTrue(injector.shouldLoadNative()); + } + + @Test + public void canPartiallyOverride() { + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build()); + FlutterInjector injector = FlutterInjector.instance(); + assertEquals(injector.flutterLoader(), mockFlutterLoader); + assertTrue(injector.shouldLoadNative()); + } + + @Test() + public void cannotBeChangedOnceRead() { + FlutterInjector.instance(); + + assertThrows( + IllegalStateException.class, + () -> { + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build()); + }); + } +} diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 67611eac687b8..bb5c44c6568df 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -16,6 +16,8 @@ import io.flutter.embedding.engine.FlutterJNITest; import io.flutter.embedding.engine.LocalizationPluginTest; import io.flutter.embedding.engine.RenderingComponentTest; +import io.flutter.embedding.engine.loader.ApplicationInfoLoaderTest; +import io.flutter.embedding.engine.loader.FlutterLoaderTest; import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest; import io.flutter.embedding.engine.renderer.FlutterRendererTest; import io.flutter.embedding.engine.systemchannels.KeyEventChannelTest; @@ -44,6 +46,7 @@ @SuiteClasses({ AccessibilityBridgeTest.class, AndroidKeyProcessorTest.class, + ApplicationInfoLoaderTest.class, DartExecutorTest.class, FlutterActivityAndFragmentDelegateTest.class, FlutterActivityTest.class, @@ -53,8 +56,11 @@ FlutterEngineTest.class, FlutterFragmentActivityTest.class, FlutterFragmentTest.class, + FlutterInjectorTest.class, FlutterJNITest.class, FlutterLaunchTests.class, + FlutterLoaderTest.class, + FlutterShellArgsTest.class, FlutterRendererTest.class, FlutterShellArgsTest.class, FlutterViewTest.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 index 9a15364373b7a..6f141fa1aa192 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -10,6 +11,7 @@ import static org.mockito.Mockito.when; import android.content.Context; +import io.flutter.FlutterInjector; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.loader.FlutterLoader; @@ -137,4 +139,18 @@ public void itUsesApplicationContext() { verify(context, atLeast(1)).getApplicationContext(); } + + @Test + public void itCanUseFlutterLoaderInjectionViaFlutterInjector() { + FlutterInjector.reset(); + FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build()); + Context mockContext = mock(Context.class); + + new FlutterEngine(mockContext, null, flutterJNI); + + verify(mockFlutterLoader, times(1)).startInitialization(any()); + verify(mockFlutterLoader, times(1)).ensureInitializationComplete(any(), any()); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java index 01d012c38b83a..71f47890245bc 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java @@ -1,20 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package test.io.flutter.embedding.engine; import static junit.framework.TestCase.assertEquals; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import androidx.annotation.NonNull; +import io.flutter.FlutterInjector; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; -import io.flutter.embedding.engine.loader.FlutterApplicationInfo; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -22,47 +24,19 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class PluginComponentTest { + @Before + public void setUp() { + FlutterInjector.reset(); + } + @Test public void pluginsCanAccessFlutterAssetPaths() { // Setup test. + FlutterInjector.setInstance(new FlutterInjector.Builder().setShouldLoadNative(false).build()); FlutterJNI flutterJNI = mock(FlutterJNI.class); when(flutterJNI.isAttached()).thenReturn(true); - FlutterApplicationInfo emptyInfo = - new FlutterApplicationInfo(null, null, null, null, null, null, false, false); - - // FlutterLoader is the object to which the PluginRegistry defers for obtaining - // the path to a Flutter asset. Ideally in this component test we would use a - // real FlutterLoader and directly verify the relationship between FlutterAssets - // and FlutterLoader. However, a real FlutterLoader cannot be used in a JVM test - // because it would attempt to load native libraries. Therefore, we create a fake - // FlutterLoader, but then we defer the corresponding asset lookup methods to the - // real FlutterLoader singleton. This test ends up verifying that when FlutterAssets - // is queried for an asset path, it returns the real expected path based on real - // FlutterLoader behavior. - FlutterLoader flutterLoader = mock(FlutterLoader.class); - when(flutterLoader.getLookupKeyForAsset(any(String.class))) - .thenAnswer( - new Answer() { - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - // Defer to a real FlutterLoader to return the asset path. - String fileNameOrSubpath = (String) invocation.getArguments()[0]; - return FlutterLoader.getInstanceForTest(emptyInfo) - .getLookupKeyForAsset(fileNameOrSubpath); - } - }); - when(flutterLoader.getLookupKeyForAsset(any(String.class), any(String.class))) - .thenAnswer( - new Answer() { - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - // Defer to a real FlutterLoader to return the asset path. - String fileNameOrSubpath = (String) invocation.getArguments()[0]; - String packageName = (String) invocation.getArguments()[1]; - return FlutterLoader.getInstanceForTest(emptyInfo) - .getLookupKeyForAsset(fileNameOrSubpath, packageName); - } - }); + + FlutterLoader flutterLoader = new FlutterLoader(); // Execute behavior under test. FlutterEngine flutterEngine = @@ -82,6 +56,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { assertEquals( "flutter_assets/packages/fakepackage/some/path/fake_asset.jpg", plugin.getAssetPathBasedOnSubpathAndPackage()); + FlutterInjector.reset(); } private static class PluginThatAccessesAssets implements FlutterPlugin { diff --git a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java index 6470651db3ed9..6c8e766bd557b 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java @@ -1,6 +1,8 @@ package test.io.flutter.embedding.engine.dart; import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; @@ -9,17 +11,31 @@ import static org.mockito.Mockito.when; import android.content.res.AssetManager; +import io.flutter.FlutterInjector; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint; +import io.flutter.embedding.engine.loader.FlutterLoader; import java.nio.ByteBuffer; +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.annotation.Config; @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class DartExecutorTest { + @Mock FlutterLoader mockFlutterLoader; + + @Before + public void setUp() { + FlutterInjector.reset(); + MockitoAnnotations.initMocks(this); + } + @Test public void itSendsBinaryMessages() { // Setup test. @@ -49,4 +65,24 @@ public void itNotifiesLowMemoryWarning() { dartExecutor.notifyLowMemoryWarning(); verify(mockFlutterJNI, times(1)).notifyLowMemoryWarning(); } + + @Test + public void itThrowsWhenCreatingADefaultDartEntrypointWithAnUninitializedFlutterLoader() { + assertThrows( + AssertionError.class, + () -> { + DartEntrypoint.createDefault(); + }); + } + + @Test + public void itHasReasonableDefaultsWhenFlutterLoaderIsInitialized() { + when(mockFlutterLoader.initialized()).thenReturn(true); + when(mockFlutterLoader.findAppBundlePath()).thenReturn("my/custom/path"); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build()); + DartEntrypoint entrypoint = DartEntrypoint.createDefault(); + assertEquals(entrypoint.pathToBundle, "my/custom/path"); + assertEquals(entrypoint.dartEntrypointFunctionName, "main"); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java index 769525f7db3c9..69ead8182be4b 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.embedding.engine.loader; import static org.junit.Assert.assertEquals; diff --git a/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java b/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java new file mode 100644 index 0000000000000..4ffd848d855b7 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.loader; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +import io.flutter.FlutterInjector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterLoaderTest { + @Before + public void setUp() { + FlutterInjector.reset(); + } + + @Test + public void itReportsUninitializedAfterCreating() { + FlutterLoader flutterLoader = new FlutterLoader(); + assertFalse(flutterLoader.initialized()); + } + + @Test + public void itReportsInitializedAfterInitializing() { + FlutterInjector.setInstance(new FlutterInjector.Builder().setShouldLoadNative(false).build()); + FlutterLoader flutterLoader = new FlutterLoader(); + + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(RuntimeEnvironment.application); + flutterLoader.ensureInitializationComplete(RuntimeEnvironment.application, null); + assertTrue(flutterLoader.initialized()); + FlutterInjector.reset(); + } +}