diff --git a/DEPS b/DEPS index b742d90c4356a..35fd3fca0c60e 100644 --- a/DEPS +++ b/DEPS @@ -481,7 +481,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/robolectric_bundle', - 'version': 'last_updated:2019-07-29T15:27:42-0700' + 'version': 'last_updated:2019-08-02T16:01:27-0700' } ], 'condition': 'download_android_deps', diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 59dffca2ddf4b..f55a8eaf27f5d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -563,6 +563,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Splas FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 5c151e05db8cc..3aae6e3d2d5eb 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -139,6 +139,7 @@ action("flutter_shell_java") { "io/flutter/embedding/android/SplashScreenProvider.java", "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java", + "io/flutter/embedding/engine/FlutterEngineCache.java", "io/flutter/embedding/engine/FlutterEnginePluginRegistry.java", "io/flutter/embedding/engine/FlutterJNI.java", "io/flutter/embedding/engine/FlutterShellArgs.java", @@ -330,6 +331,9 @@ action("robolectric_tests") { "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.java", "test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java", + "test/io/flutter/embedding/android/FlutterActivityTest.java", + "test/io/flutter/embedding/android/FlutterFragmentTest.java", + "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", "test/io/flutter/util/PreconditionsTest.java", ] @@ -351,6 +355,7 @@ action("robolectric_tests") { "//third_party/robolectric/lib/common-1.1.1.jar", "//third_party/robolectric/lib/common-java8-1.1.1.jar", "//third_party/robolectric/lib/support-annotations-28.0.0.jar", + "//third_party/robolectric/lib/support-fragment-25.2.0.jar", "//third_party/robolectric/lib/mockito-all-1.10.19.jar", ] diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 2fa4488b9f458..390124afb2e2a 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -47,11 +47,11 @@ * route may be specified explicitly by passing the name of the route as a {@code String} in * {@link #EXTRA_INITIAL_ROUTE}, e.g., "my/deep/link". *

- * The Dart entrypoint and initial route can each be controlled using a {@link IntentBuilder} + * The Dart entrypoint and initial route can each be controlled using a {@link NewEngineIntentBuilder} * via the following methods: *

*

* The app bundle path, Dart entrypoint, and initial route can also be controlled in a subclass of @@ -61,6 +61,37 @@ *

  • {@link #getDartEntrypointFunctionName()}
  • *
  • {@link #getInitialRoute()}
  • * + *

    + * {@code FlutterActivity} can be used with a cached {@link FlutterEngine} instead of creating a new + * one. Use {@link #withCachedEngine(String)} to build a {@code FlutterActivity} {@code Intent} that + * is configured to use an existing, cached {@link FlutterEngine}. {@link FlutterEngineCache} is the + * cache that is used to obtain a given cached {@link FlutterEngine}. An + * {@code IllegalStateException} will be thrown if a cached engine is requested but does not exist + * in the cache. + *

    + * It is generally recommended to use a cached {@link FlutterEngine} to avoid a momentary delay + * when initializing a new {@link FlutterEngine}. The two exceptions to using a cached + * {@link FlutterEngine} are: + *

    + *

    + *

    + * The following illustrates how to pre-warm and cache a {@link FlutterEngine}: + *

    + * {@code + * // Create and pre-warm a FlutterEngine. + * FlutterEngine flutterEngine = new FlutterEngine(context); + * flutterEngine + * .getDartExecutor() + * .executeDartEntrypoint(DartEntrypoint.createDefault()); + * + * // Cache the pre-warmed FlutterEngine in the FlutterEngineCache. + * FlutterEngineCache.getInstance().put("my_engine", flutterEngine); + * } + *

    * If Flutter is needed in a location that cannot use an {@code Activity}, consider using * a {@link FlutterFragment}. Using a {@link FlutterFragment} requires forwarding some calls from * an {@code Activity} to the {@link FlutterFragment}. @@ -149,6 +180,8 @@ public class FlutterActivity extends Activity protected static final String EXTRA_DART_ENTRYPOINT = "dart_entrypoint"; protected static final String EXTRA_INITIAL_ROUTE = "initial_route"; protected static final String EXTRA_BACKGROUND_MODE = "background_mode"; + protected static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; + protected static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity"; // Default configuration. protected static final String DEFAULT_DART_ENTRYPOINT = "main"; @@ -161,42 +194,43 @@ public class FlutterActivity extends Activity */ @NonNull public static Intent createDefaultIntent(@NonNull Context launchContext) { - return createBuilder().build(launchContext); + return withNewEngine().build(launchContext); } /** - * Creates an {@link IntentBuilder}, which can be used to configure an {@link Intent} to - * launch a {@code FlutterActivity}. + * Creates an {@link NewEngineIntentBuilder}, which can be used to configure an {@link Intent} to + * launch a {@code FlutterActivity} that internally creates a new {@link FlutterEngine} using + * the desired Dart entrypoint, initial route, etc. */ @NonNull - public static IntentBuilder createBuilder() { - return new IntentBuilder(FlutterActivity.class); + public static NewEngineIntentBuilder withNewEngine() { + return new NewEngineIntentBuilder(FlutterActivity.class); } /** - * Builder to create an {@code Intent} that launches a {@code FlutterActivity} with the - * desired configuration. + * Builder to create an {@code Intent} that launches a {@code FlutterActivity} with a new + * {@link FlutterEngine} and the desired configuration. */ - public static class IntentBuilder { + public static class NewEngineIntentBuilder { private final Class activityClass; private String dartEntrypoint = DEFAULT_DART_ENTRYPOINT; private String initialRoute = DEFAULT_INITIAL_ROUTE; private String backgroundMode = DEFAULT_BACKGROUND_MODE; /** - * Constructor that allows this {@code IntentBuilder} to be used by subclasses of + * Constructor that allows this {@code NewEngineIntentBuilder} to be used by subclasses of * {@code FlutterActivity}. *

    * Subclasses of {@code FlutterActivity} should provide their own static version of - * {@link #createBuilder()}, which returns an instance of {@code IntentBuilder} + * {@link #withNewEngine()}, which returns an instance of {@code NewEngineIntentBuilder} * constructed with a {@code Class} reference to the {@code FlutterActivity} subclass, * e.g.: *

    * {@code - * return new IntentBuilder(MyFlutterActivity.class); + * return new NewEngineIntentBuilder(MyFlutterActivity.class); * } */ - protected IntentBuilder(@NonNull Class activityClass) { + protected NewEngineIntentBuilder(@NonNull Class activityClass) { this.activityClass = activityClass; } @@ -204,7 +238,7 @@ protected IntentBuilder(@NonNull Class activityClass) * The name of the initial Dart method to invoke, defaults to "main". */ @NonNull - public IntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { + public NewEngineIntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { this.dartEntrypoint = dartEntrypoint; return this; } @@ -214,7 +248,7 @@ public IntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { * defaults to "/". */ @NonNull - public IntentBuilder initialRoute(@NonNull String initialRoute) { + public NewEngineIntentBuilder initialRoute(@NonNull String initialRoute) { this.initialRoute = initialRoute; return this; } @@ -236,7 +270,7 @@ public IntentBuilder initialRoute(@NonNull String initialRoute) { * following property: {@code true}. */ @NonNull - public IntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) { + public NewEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) { this.backgroundMode = backgroundMode.name(); return this; } @@ -250,6 +284,93 @@ public Intent build(@NonNull Context context) { return new Intent(context, activityClass) .putExtra(EXTRA_DART_ENTRYPOINT, dartEntrypoint) .putExtra(EXTRA_INITIAL_ROUTE, initialRoute) + .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode) + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true); + } + } + + /** + * Creates a {@link CachedEngineIntentBuilder}, which can be used to configure an {@link Intent} + * to launch a {@code FlutterActivity} that internally uses an existing {@link FlutterEngine} that + * is cached in {@link FlutterEngineCache}. + */ + public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedEngineId) { + return new CachedEngineIntentBuilder(FlutterActivity.class, cachedEngineId); + } + + /** + * Builder to create an {@code Intent} that launches a {@code FlutterActivity} with an existing + * {@link FlutterEngine} that is cached in {@link FlutterEngineCache}. + */ + public static class CachedEngineIntentBuilder { + private final Class activityClass; + private final String cachedEngineId; + private boolean destroyEngineWithActivity = false; + private String backgroundMode = DEFAULT_BACKGROUND_MODE; + + /** + * Constructor that allows this {@code CachedEngineIntentBuilder} to be used by subclasses of + * {@code FlutterActivity}. + *

    + * Subclasses of {@code FlutterActivity} should provide their own static version of + * {@link #withNewEngine()}, which returns an instance of {@code CachedEngineIntentBuilder} + * constructed with a {@code Class} reference to the {@code FlutterActivity} subclass, + * e.g.: + *

    + * {@code + * return new CachedEngineIntentBuilder(MyFlutterActivity.class, engineId); + * } + */ + protected CachedEngineIntentBuilder( + @NonNull Class activityClass, + @NonNull String engineId + ) { + this.activityClass = activityClass; + this.cachedEngineId = engineId; + } + + /** + * Returns true if the cached {@link FlutterEngine} should be destroyed and removed from the + * cache when this {@code FlutterActivity} is destroyed. + *

    + * The default value is {@code false}. + */ + public CachedEngineIntentBuilder destroyEngineWithActivity(boolean destroyEngineWithActivity) { + this.destroyEngineWithActivity = destroyEngineWithActivity; + return this; + } + + /** + * The mode of {@code FlutterActivity}'s background, either {@link BackgroundMode#opaque} or + * {@link BackgroundMode#transparent}. + *

    + * The default background mode is {@link BackgroundMode#opaque}. + *

    + * Choosing a background mode of {@link BackgroundMode#transparent} will configure the inner + * {@link FlutterView} of this {@code FlutterActivity} to be configured with a + * {@link FlutterTextureView} to support transparency. This choice has a non-trivial performance + * impact. A transparent background should only be used if it is necessary for the app design + * being implemented. + *

    + * A {@code FlutterActivity} that is configured with a background mode of + * {@link BackgroundMode#transparent} must have a theme applied to it that includes the + * following property: {@code true}. + */ + @NonNull + public CachedEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) { + this.backgroundMode = backgroundMode.name(); + return this; + } + + /** + * Creates and returns an {@link Intent} that will launch a {@code FlutterActivity} with + * the desired configuration. + */ + @NonNull + public Intent build(@NonNull Context context) { + return new Intent(context, activityClass) + .putExtra(EXTRA_CACHED_ENGINE_ID, cachedEngineId) + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, destroyEngineWithActivity) .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode); } } @@ -521,6 +642,31 @@ public FlutterShellArgs getFlutterShellArgs() { return FlutterShellArgs.fromIntent(getIntent()); } + /** + * Returns the ID of a statically cached {@link FlutterEngine} to use within this + * {@code FlutterActivity}, or {@code null} if this {@code FlutterActivity} does not want to + * use a cached {@link FlutterEngine}. + */ + @Override + @Nullable + public String getCachedEngineId() { + return getIntent().getStringExtra(EXTRA_CACHED_ENGINE_ID); + } + + /** + * Returns false if the {@link FlutterEngine} backing this {@code FlutterActivity} should + * outlive this {@code FlutterActivity}, or true to be destroyed when the {@code FlutterActivity} + * is destroyed. + *

    + * The default value is {@code true} in cases where {@code FlutterActivity} created its own + * {@link FlutterEngine}, and {@code false} in cases where a cached {@link FlutterEngine} was + * provided. + */ + @Override + public boolean shouldDestroyEngineWithHost() { + return getIntent().getBooleanExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false); + } + /** * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded. *

    @@ -617,7 +763,7 @@ public String getAppBundlePath() { // Return the default app bundle path. // TODO(mattcarroll): move app bundle resolution into an appropriately named class. - return FlutterMain.findAppBundlePath(getApplicationContext()); + return FlutterMain.findAppBundlePath(); } /** @@ -752,16 +898,6 @@ public boolean shouldAttachEngineToActivity() { return true; } - /** - * Returns true if the {@link FlutterEngine} backing this {@code FlutterActivity} should - * outlive this {@code FlutterActivity}, or be destroyed when the {@code FlutterActivity} - * is destroyed. - */ - @Override - public boolean retainFlutterEngineAfterHostDestruction() { - return false; - } - @Override public void onFirstFrameRendered() {} diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 37db2b1a6ce29..863925e6ae9ca 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -22,6 +22,7 @@ import io.flutter.Log; import io.flutter.app.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; @@ -182,7 +183,11 @@ private void initializeFlutter(@NonNull Context context) { /** * Obtains a reference to a FlutterEngine to back this delegate and its {@code host}. *

    - * First, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via + *

    + * First, the {@code host} is asked if it would like to use a cached {@link FlutterEngine}, and + * if so, the cached {@link FlutterEngine} is retrieved. + *

    + * Second, the {@code host} is given an opportunity to provide a {@link FlutterEngine} via * {@link Host#provideFlutterEngine(Context)}. *

    * If the {@code host} does not provide a {@link FlutterEngine}, then a new {@link FlutterEngine} @@ -191,9 +196,21 @@ private void initializeFlutter(@NonNull Context context) { private void setupFlutterEngine() { Log.d(TAG, "Setting up FlutterEngine."); - // First, defer to subclasses for a custom FlutterEngine. + // First, check if the host wants to use a cached FlutterEngine. + String cachedEngineId = host.getCachedEngineId(); + if (cachedEngineId != null) { + flutterEngine = FlutterEngineCache.getInstance().get(cachedEngineId); + isFlutterEngineFromHost = true; + if (flutterEngine == null) { + throw new IllegalStateException("The requested cached FlutterEngine did not exist in the FlutterEngineCache: '" + cachedEngineId + "'"); + } + return; + } + + // Second, defer to subclasses for a custom FlutterEngine. flutterEngine = host.provideFlutterEngine(host.getContext()); if (flutterEngine != null) { + isFlutterEngineFromHost = true; return; } @@ -275,6 +292,11 @@ public void run() { * {@code flutterEngine} must be non-null when invoking this method. */ private void doInitialFlutterViewRun() { + // Don't attempt to start a FlutterEngine if we're using a cached FlutterEngine. + if (host.getCachedEngineId() != null) { + return; + } + if (flutterEngine.getDartExecutor().isExecutingDart()) { // No warning is logged because this situation will happen on every config // change if the developer does not choose to retain the Fragment instance. @@ -387,7 +409,7 @@ void onDestroyView() { * if it was previously attached. *

  • Destroys this delegate's {@link PlatformPlugin}.
  • *
  • Destroys this delegate's {@link FlutterEngine} if - * {@link Host#retainFlutterEngineAfterHostDestruction()} returns false.
  • + * {@link Host#shouldDestroyEngineWithHost()} ()} returns true. * */ void onDetach() { @@ -412,8 +434,13 @@ void onDetach() { } // Destroy our FlutterEngine if we're not set to retain it. - if (!host.retainFlutterEngineAfterHostDestruction() && !isFlutterEngineFromHost) { + if (host.shouldDestroyEngineWithHost()) { flutterEngine.destroy(); + + if (host.getCachedEngineId() != null) { + FlutterEngineCache.getInstance().remove(host.getCachedEngineId()); + } + flutterEngine = null; } } @@ -587,6 +614,24 @@ private void ensureAlive() { @NonNull FlutterShellArgs getFlutterShellArgs(); + /** + * Returns the ID of a statically cached {@link FlutterEngine} to use within this + * delegate's host, or {@code null} if this delegate's host does not want to + * use a cached {@link FlutterEngine}. + */ + @Nullable + String getCachedEngineId(); + + /** + * Returns true if the {@link FlutterEngine} used in this delegate should be destroyed + * when the host/delegate are destroyed. + *

    + * The default value is {@code true} in cases where {@code FlutterFragment} created its own + * {@link FlutterEngine}, and {@code false} in cases where a cached {@link FlutterEngine} was + * provided. + */ + boolean shouldDestroyEngineWithHost(); + /** * Returns the Dart entrypoint that should run when a new {@link FlutterEngine} is * created. @@ -649,15 +694,6 @@ private void ensureAlive() { */ boolean shouldAttachEngineToActivity(); - /** - * Returns true if the {@link FlutterEngine} used in this delegate should outlive the - * delegate. - *

    - * If {@code false} is returned, the {@link FlutterEngine} used in this delegate will be - * destroyed when the delegate is destroyed. - */ - boolean retainFlutterEngineAfterHostDestruction(); - /** * Invoked by this delegate when its {@link FlutterView} has rendered its first Flutter * frame. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index c004d7c8c4a6e..6b42858184e85 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -47,6 +47,34 @@ * If convenient, consider using a {@link FlutterActivity} instead of a {@code FlutterFragment} to * avoid the work of forwarding calls. *

    + * {@code FlutterFragment} supports the use of an existing, cached {@link FlutterEngine}. To use a + * cached {@link FlutterEngine}, ensure that the {@link FlutterEngine} is stored in + * {@link FlutterEngineCache} and then use {@link #withCachedEngine(String)} to build a + * {@code FlutterFragment} with the cached {@link FlutterEngine}'s ID. + *

    + * It is generally recommended to use a cached {@link FlutterEngine} to avoid a momentary delay + * when initializing a new {@link FlutterEngine}. The two exceptions to using a cached + * {@link FlutterEngine} are: + *

    + *

    + *

    + * The following illustrates how to pre-warm and cache a {@link FlutterEngine}: + *

    + * {@code + * // Create and pre-warm a FlutterEngine. + * FlutterEngine flutterEngine = new FlutterEngine(context); + * flutterEngine + * .getDartExecutor() + * .executeDartEntrypoint(DartEntrypoint.createDefault()); + * + * // Cache the pre-warmed FlutterEngine in the FlutterEngineCache. + * FlutterEngineCache.getInstance().put("my_engine", flutterEngine); + * } + *

    * If Flutter is needed in a location that can only use a {@code View}, consider using a * {@link FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an * {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a @@ -85,45 +113,83 @@ public class FlutterFragment extends Fragment implements FlutterActivityAndFragm * See {@link #shouldAttachEngineToActivity()}. */ protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY = "should_attach_engine_to_activity"; + /** + * The ID of a {@link FlutterEngine} cached in {@link FlutterEngineCache} that will be used within + * the created {@code FlutterFragment}. + */ + protected static final String ARG_CACHED_ENGINE_ID = "cached_engine_id"; + /** + * True if the {@link FlutterEngine} in the created {@code FlutterFragment} should be destroyed + * when the {@code FlutterFragment} is destroyed, false if the {@link FlutterEngine} should + * outlive the {@code FlutterFragment}. + */ + protected static final String ARG_DESTROY_ENGINE_WITH_FRAGMENT = "destroy_engine_with_fragment"; + + /** + * Creates a {@code FlutterFragment} with a default configuration. + *

    + * {@code FlutterFragment}'s default configuration creates a new {@link FlutterEngine} within + * the {@code FlutterFragment} and uses the following settings: + *

    + *

    + * To use a new {@link FlutterEngine} with different settings, use {@link #withNewEngine()}. + *

    + * To use a cached {@link FlutterEngine} instead of creating a new one, use + * {@link #withCachedEngine(String)}. + */ + @NonNull + public static FlutterFragment createDefault() { + return new NewEngineFragmentBuilder().build(); + } + /** + * Returns a {@link NewEngineFragmentBuilder} to create a {@code FlutterFragment} with a new + * {@link FlutterEngine} and a desired engine configuration. + */ @NonNull - public static FlutterFragment createDefaultFlutterFragment() { - return new FlutterFragment.Builder().build(); + public static NewEngineFragmentBuilder withNewEngine() { + return new NewEngineFragmentBuilder(); } /** * Builder that creates a new {@code FlutterFragment} with {@code arguments} that correspond - * to the values set on this {@code Builder}. + * to the values set on this {@code NewEngineFragmentBuilder}. *

    * To create a {@code FlutterFragment} with default {@code arguments}, invoke - * {@link #createDefaultFlutterFragment()}. + * {@link #createDefault()}. *

    * Subclasses of {@code FlutterFragment} that do not introduce any new arguments can use this - * {@code Builder} to construct instances of the subclass without subclassing this {@code Builder}. + * {@code NewEngineFragmentBuilder} to construct instances of the subclass without subclassing + * this {@code NewEngineFragmentBuilder}. * {@code - * MyFlutterFragment f = new FlutterFragment.Builder(MyFlutterFragment.class) + * MyFlutterFragment f = new FlutterFragment.NewEngineFragmentBuilder(MyFlutterFragment.class) * .someProperty(...) * .someOtherProperty(...) * .build(); * } *

    * Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this - * {@code Builder} to add the new properties: + * {@code NewEngineFragmentBuilder} to add the new properties: *

      *
    1. Ensure the {@code FlutterFragment} subclass has a no-arg constructor.
    2. - *
    3. Subclass this {@code Builder}.
    4. - *
    5. Override the new {@code Builder}'s no-arg constructor and invoke the super constructor - * to set the {@code FlutterFragment} subclass: {@code + *
    6. Subclass this {@code NewEngineFragmentBuilder}.
    7. + *
    8. Override the new {@code NewEngineFragmentBuilder}'s no-arg constructor and invoke the + * super constructor to set the {@code FlutterFragment} subclass: {@code * public MyBuilder() { * super(MyFlutterFragment.class); * } * }
    9. *
    10. Add appropriate property methods for the new properties.
    11. - *
    12. Override {@link Builder#createArgs()}, call through to the super method, then add - * the new properties as arguments in the {@link Bundle}.
    13. + *
    14. Override {@link NewEngineFragmentBuilder#createArgs()}, call through to the super method, + * then add the new properties as arguments in the {@link Bundle}.
    15. *
    - * Once a {@code Builder} subclass is defined, the {@code FlutterFragment} subclass can be - * instantiated as follows. + * Once a {@code NewEngineFragmentBuilder} subclass is defined, the {@code FlutterFragment} + * subclass can be instantiated as follows. * {@code * MyFlutterFragment f = new MyBuilder() * .someExistingProperty(...) @@ -131,7 +197,7 @@ public static FlutterFragment createDefaultFlutterFragment() { * .build(); * } */ - public static class Builder { + public static class NewEngineFragmentBuilder { private final Class fragmentClass; private String dartEntrypoint = "main"; private String initialRoute = "/"; @@ -142,18 +208,18 @@ public static class Builder { private boolean shouldAttachEngineToActivity = true; /** - * Constructs a {@code Builder} that is configured to construct an instance of + * Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of * {@code FlutterFragment}. */ - public Builder() { + public NewEngineFragmentBuilder() { fragmentClass = FlutterFragment.class; } /** - * Constructs a {@code Builder} that is configured to construct an instance of + * Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of * {@code subclass}, which extends {@code FlutterFragment}. */ - public Builder(@NonNull Class subclass) { + public NewEngineFragmentBuilder(@NonNull Class subclass) { fragmentClass = subclass; } @@ -161,7 +227,7 @@ public Builder(@NonNull Class subclass) { * The name of the initial Dart method to invoke, defaults to "main". */ @NonNull - public Builder dartEntrypoint(@NonNull String dartEntrypoint) { + public NewEngineFragmentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { this.dartEntrypoint = dartEntrypoint; return this; } @@ -171,7 +237,7 @@ public Builder dartEntrypoint(@NonNull String dartEntrypoint) { * defaults to "/". */ @NonNull - public Builder initialRoute(@NonNull String initialRoute) { + public NewEngineFragmentBuilder initialRoute(@NonNull String initialRoute) { this.initialRoute = initialRoute; return this; } @@ -181,7 +247,7 @@ public Builder initialRoute(@NonNull String initialRoute) { * to {@link FlutterMain#findAppBundlePath(Context)} */ @NonNull - public Builder appBundlePath(@NonNull String appBundlePath) { + public NewEngineFragmentBuilder appBundlePath(@NonNull String appBundlePath) { this.appBundlePath = appBundlePath; return this; } @@ -190,7 +256,7 @@ public Builder appBundlePath(@NonNull String appBundlePath) { * Any special configuration arguments for the Flutter engine */ @NonNull - public Builder flutterShellArgs(@NonNull FlutterShellArgs shellArgs) { + public NewEngineFragmentBuilder flutterShellArgs(@NonNull FlutterShellArgs shellArgs) { this.shellArgs = shellArgs; return this; } @@ -204,7 +270,7 @@ public Builder flutterShellArgs(@NonNull FlutterShellArgs shellArgs) { * cannot. */ @NonNull - public Builder renderMode(@NonNull FlutterView.RenderMode renderMode) { + public NewEngineFragmentBuilder renderMode(@NonNull FlutterView.RenderMode renderMode) { this.renderMode = renderMode; return this; } @@ -216,7 +282,7 @@ public Builder renderMode(@NonNull FlutterView.RenderMode renderMode) { * See {@link FlutterView.TransparencyMode} for implications of this selection. */ @NonNull - public Builder transparencyMode(@NonNull FlutterView.TransparencyMode transparencyMode) { + public NewEngineFragmentBuilder transparencyMode(@NonNull FlutterView.TransparencyMode transparencyMode) { this.transparencyMode = transparencyMode; return this; } @@ -256,7 +322,7 @@ public Builder transparencyMode(@NonNull FlutterView.TransparencyMode transparen * setting {@code shouldAttachEngineToActivity} to {@code false}. */ @NonNull - public Builder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) { + public NewEngineFragmentBuilder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) { this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; return this; } @@ -280,6 +346,7 @@ protected Bundle createArgs() { args.putString(ARG_FLUTTERVIEW_RENDER_MODE, renderMode != null ? renderMode.name() : FlutterView.RenderMode.surface.name()); args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, transparencyMode != null ? transparencyMode.name() : FlutterView.TransparencyMode.transparent.name()); args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); + args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true); return args; } @@ -307,6 +374,194 @@ public T build() { } } + /** + * Returns a {@link CachedEngineFragmentBuilder} to create a {@code FlutterFragment} with a cached + * {@link FlutterEngine} in {@link FlutterEngineCache}. + *

    + * An {@code IllegalStateException} will be thrown during the lifecycle of the + * {@code FlutterFragment} if a cached {@link FlutterEngine} is requested but does not exist in + * the cache. + *

    + * To create a {@code FlutterFragment} that uses a new {@link FlutterEngine}, use + * {@link #createDefault()} or {@link #withNewEngine()}. + */ + @NonNull + public static CachedEngineFragmentBuilder withCachedEngine(@NonNull String engineId) { + return new CachedEngineFragmentBuilder(engineId); + } + + /** + * Builder that creates a new {@code FlutterFragment} that uses a cached {@link FlutterEngine} + * with {@code arguments} that correspond to the values set on this {@code Builder}. + *

    + * Subclasses of {@code FlutterFragment} that do not introduce any new arguments can use this + * {@code Builder} to construct instances of the subclass without subclassing this {@code Builder}. + * {@code + * MyFlutterFragment f = new FlutterFragment.CachedEngineFragmentBuilder(MyFlutterFragment.class) + * .someProperty(...) + * .someOtherProperty(...) + * .build(); + * } + *

    + * Subclasses of {@code FlutterFragment} that introduce new arguments should subclass this + * {@code CachedEngineFragmentBuilder} to add the new properties: + *

      + *
    1. Ensure the {@code FlutterFragment} subclass has a no-arg constructor.
    2. + *
    3. Subclass this {@code CachedEngineFragmentBuilder}.
    4. + *
    5. Override the new {@code CachedEngineFragmentBuilder}'s no-arg constructor and invoke the + * super constructor to set the {@code FlutterFragment} subclass: {@code + * public MyBuilder() { + * super(MyFlutterFragment.class); + * } + * }
    6. + *
    7. Add appropriate property methods for the new properties.
    8. + *
    9. Override {@link CachedEngineFragmentBuilder#createArgs()}, call through to the super + * method, then add the new properties as arguments in the {@link Bundle}.
    10. + *
    + * Once a {@code CachedEngineFragmentBuilder} subclass is defined, the {@code FlutterFragment} + * subclass can be instantiated as follows. + * {@code + * MyFlutterFragment f = new MyBuilder() + * .someExistingProperty(...) + * .someNewProperty(...) + * .build(); + * } + */ + public static class CachedEngineFragmentBuilder { + private final Class fragmentClass; + private final String engineId; + private boolean destroyEngineWithFragment = false; + private FlutterView.RenderMode renderMode = FlutterView.RenderMode.surface; + private FlutterView.TransparencyMode transparencyMode = FlutterView.TransparencyMode.transparent; + private boolean shouldAttachEngineToActivity = true; + + private CachedEngineFragmentBuilder(@NonNull String engineId) { + this(FlutterFragment.class, engineId); + } + + protected CachedEngineFragmentBuilder(@NonNull Class subclass, @NonNull String engineId) { + this.fragmentClass = subclass; + this.engineId = engineId; + } + + /** + * Pass {@code true} to destroy the cached {@link FlutterEngine} when this + * {@code FlutterFragment} is destroyed, or {@code false} for the cached {@link FlutterEngine} + * to outlive this {@code FlutterFragment}. + */ + @NonNull + public CachedEngineFragmentBuilder destroyEngineWithFragment(boolean destroyEngineWithFragment) { + this.destroyEngineWithFragment = destroyEngineWithFragment; + return this; + } + + /** + * Render Flutter either as a {@link FlutterView.RenderMode#surface} or a + * {@link FlutterView.RenderMode#texture}. You should use {@code surface} unless + * you have a specific reason to use {@code texture}. {@code texture} comes with + * a significant performance impact, but {@code texture} can be displayed + * beneath other Android {@code View}s and animated, whereas {@code surface} + * cannot. + */ + @NonNull + public CachedEngineFragmentBuilder renderMode(@NonNull FlutterView.RenderMode renderMode) { + this.renderMode = renderMode; + return this; + } + + /** + * Support a {@link FlutterView.TransparencyMode#transparent} background within {@link FlutterView}, + * or force an {@link FlutterView.TransparencyMode#opaque} background. + *

    + * See {@link FlutterView.TransparencyMode} for implications of this selection. + */ + @NonNull + public CachedEngineFragmentBuilder transparencyMode(@NonNull FlutterView.TransparencyMode transparencyMode) { + this.transparencyMode = transparencyMode; + return this; + } + + /** + * Whether or not this {@code FlutterFragment} should automatically attach its + * {@code Activity} as a control surface for its {@link FlutterEngine}. + *

    + * Control surfaces are used to provide Android resources and lifecycle events to + * plugins that are attached to the {@link FlutterEngine}. If {@code shouldAttachEngineToActivity} + * is true then this {@code FlutterFragment} will connect its {@link FlutterEngine} to the + * surrounding {@code Activity}, along with any plugins that are registered with that + * {@link FlutterEngine}. This allows plugins to access the {@code Activity}, as well as + * receive {@code Activity}-specific calls, e.g., {@link android.app.Activity#onNewIntent(Intent)}. + * If {@code shouldAttachEngineToActivity} is false, then this {@code FlutterFragment} will not + * automatically manage the connection between its {@link FlutterEngine} and the surrounding + * {@code Activity}. The {@code Activity} will need to be manually connected to this + * {@code FlutterFragment}'s {@link FlutterEngine} by the app developer. See + * {@link FlutterEngine#getActivityControlSurface()}. + *

    + * One reason that a developer might choose to manually manage the relationship between the + * {@code Activity} and {@link FlutterEngine} is if the developer wants to move the + * {@link FlutterEngine} somewhere else. For example, a developer might want the + * {@link FlutterEngine} to outlive the surrounding {@code Activity} so that it can be used + * later in a different {@code Activity}. To accomplish this, the {@link FlutterEngine} will + * need to be disconnected from the surrounding {@code Activity} at an unusual time, preventing + * this {@code FlutterFragment} from correctly managing the relationship between the + * {@link FlutterEngine} and the surrounding {@code Activity}. + *

    + * Another reason that a developer might choose to manually manage the relationship between the + * {@code Activity} and {@link FlutterEngine} is if the developer wants to prevent, or explicitly + * control when the {@link FlutterEngine}'s plugins have access to the surrounding {@code Activity}. + * For example, imagine that this {@code FlutterFragment} only takes up part of the screen and + * the app developer wants to ensure that none of the Flutter plugins are able to manipulate + * the surrounding {@code Activity}. In this case, the developer would not want the + * {@link FlutterEngine} to have access to the {@code Activity}, which can be accomplished by + * setting {@code shouldAttachEngineToActivity} to {@code false}. + */ + @NonNull + public CachedEngineFragmentBuilder shouldAttachEngineToActivity(boolean shouldAttachEngineToActivity) { + this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; + return this; + } + + /** + * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}. + *

    + * Subclasses should override this method to add new properties to the {@link Bundle}. Subclasses + * must call through to the super method to collect all existing property values. + */ + @NonNull + protected Bundle createArgs() { + Bundle args = new Bundle(); + args.putString(ARG_CACHED_ENGINE_ID, engineId); + args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, destroyEngineWithFragment); + args.putString(ARG_FLUTTERVIEW_RENDER_MODE, renderMode != null ? renderMode.name() : FlutterView.RenderMode.surface.name()); + args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, transparencyMode != null ? transparencyMode.name() : FlutterView.TransparencyMode.transparent.name()); + args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); + return args; + } + + /** + * Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on + * properties set on this {@code CachedEngineFragmentBuilder}. + */ + @NonNull + public T build() { + try { + @SuppressWarnings("unchecked") + T frag = (T) fragmentClass.getDeclaredConstructor().newInstance(); + if (frag == null) { + throw new RuntimeException("The FlutterFragment subclass sent in the constructor (" + + fragmentClass.getCanonicalName() + ") does not match the expected return type."); + } + + Bundle args = createArgs(); + frag.setArguments(args); + + return frag; + } catch (Exception e) { + throw new RuntimeException("Could not instantiate FlutterFragment subclass (" + fragmentClass.getName() + ")", e); + } + } + } + // Delegate that runs all lifecycle and OS hook logic that is common between // FlutterActivity and FlutterFragment. See the FlutterActivityAndFragmentDelegate // implementation for details about why it exists. @@ -494,6 +749,29 @@ public FlutterShellArgs getFlutterShellArgs() { ); } + /** + * Returns the ID of a statically cached {@link FlutterEngine} to use within this + * {@code FlutterFragment}, or {@code null} if this {@code FlutterFragment} does not want to + * use a cached {@link FlutterEngine}. + */ + @Nullable + @Override + public String getCachedEngineId() { + return getArguments().getString(ARG_CACHED_ENGINE_ID, null); + } + + /** + * Returns false if the {@link FlutterEngine} within this {@code FlutterFragment} should outlive + * the {@code FlutterFragment}, itself. + *

    + * Defaults to true if no custom {@link FlutterEngine is provided}, false if a custom + * {@link FlutterEngine} is provided. + */ + @Override + public boolean shouldDestroyEngineWithHost() { + return getArguments().getBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, false); + } + /** * Returns the name of the Dart method that this {@code FlutterFragment} should execute to * start a Flutter app. @@ -662,32 +940,16 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { } /** - * See {@link Builder#shouldAttachEngineToActivity()}. + * See {@link NewEngineFragmentBuilder#shouldAttachEngineToActivity()} and + * {@link CachedEngineFragmentBuilder#shouldAttachEngineToActivity()}. *

    - * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate} */ @Override public boolean shouldAttachEngineToActivity() { return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY); } - /** - * Returns true if the {@link FlutterEngine} within this {@code FlutterFragment} should outlive - * the {@code FlutterFragment}, itself. - *

    - * Defaults to false. This method can be overridden in subclasses to retain the - * {@link FlutterEngine}. - *

    - * Used by this {@code FlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} - */ - // TODO(mattcarroll): consider a dynamic determination of this preference based on whether the - // engine was created automatically, or if the engine was provided manually. - // Manually provided engines should probably not be destroyed. - @Override - public boolean retainFlutterEngineAfterHostDestruction() { - return false; - } - /** * Invoked after the {@link FlutterView} within this {@code FlutterFragment} renders its first * frame. diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java index 600d952cd4525..0a33aca1de717 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.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; import android.arch.lifecycle.DefaultLifecycleObserver; diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java new file mode 100644 index 0000000000000..99011fc17eb56 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java @@ -0,0 +1,86 @@ +// 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; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import java.util.HashMap; +import java.util.Map; + +/** + * Static singleton cache that holds {@link FlutterEngine} instances identified by {@code String}s. + *

    + * The ID of a given {@link FlutterEngine} can be whatever {@code String} is desired. + *

    + * {@code FlutterEngineCache} is useful for storing pre-warmed {@link FlutterEngine} instances. + * {@link io.flutter.embedding.android.FlutterActivity} and + * {@link io.flutter.embedding.android.FlutterFragment} use the {@code FlutterEngineCache} singleton + * internally when instructed to use a cached {@link FlutterEngine} based on a given ID. See + * {@link io.flutter.embedding.android.FlutterActivity.CachedEngineIntentBuilder} and + * {@link io.flutter.embedding.android.FlutterFragment#withCachedEngine(String)} for related APIs. + */ +public class FlutterEngineCache { + private static FlutterEngineCache instance; + + /** + * Returns the static singleton instance of {@code FlutterEngineCache}. + *

    + * Creates a new instance if one does not yet exist. + */ + @NonNull + public static FlutterEngineCache getInstance() { + if (instance == null) { + instance = new FlutterEngineCache(); + } + return instance; + } + + private final Map cachedEngines = new HashMap<>(); + + @VisibleForTesting + /* package */ FlutterEngineCache() {} + + /** + * Returns {@code true} if a {@link FlutterEngine} in this cache is associated with the + * given {@code engineId}. + */ + public boolean contains(@NonNull String engineId) { + return cachedEngines.containsKey(engineId); + } + + /** + * Returns the {@link FlutterEngine} in this cache that is associated with the given + * {@code engineId}, or {@code null} is no such {@link FlutterEngine} exists. + */ + @Nullable + public FlutterEngine get(@NonNull String engineId) { + return cachedEngines.get(engineId); + } + + /** + * Places the given {@link FlutterEngine} in this cache and associates it with the given + * {@code engineId}. + *

    + * If a {@link FlutterEngine} already exists in this cache for the given {@code engineId}, that + * {@link FlutterEngine} is removed from this cache. + */ + public void put(@NonNull String engineId, @Nullable FlutterEngine engine) { + if (engine != null) { + cachedEngines.put(engineId, engine); + } else { + cachedEngines.remove(engineId); + } + } + + /** + * Removes any {@link FlutterEngine} that is currently in the cache that is identified by + * the given {@code engineId}. + */ + public void remove(@NonNull String engineId) { + put(engineId, null); + } +} diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 23b9a6010fffd..e6e8ee38e1d3b 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -4,19 +4,24 @@ package io.flutter; -import io.flutter.SmokeTest; -import io.flutter.util.PreconditionsTest; -import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest; - import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; +import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest; +import io.flutter.embedding.android.FlutterActivityTest; +import io.flutter.embedding.android.FlutterFragmentTest; +import io.flutter.embedding.engine.FlutterEngineCacheTest; +import io.flutter.util.PreconditionsTest; + @RunWith(Suite.class) @SuiteClasses({ PreconditionsTest.class, SmokeTest.class, + FlutterActivityTest.class, + FlutterFragmentTest.class, FlutterActivityAndFragmentDelegateTest.class, + FlutterEngineCacheTest.class }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ public class FlutterTestSuite {} diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 9350e83789cf3..9adab51588954 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -5,7 +5,6 @@ import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import org.junit.After; import org.junit.Before; @@ -17,6 +16,7 @@ import org.robolectric.annotation.Config; import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface; @@ -27,17 +27,16 @@ import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; -import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.plugin.platform.PlatformViewsController; import io.flutter.view.FlutterMain; import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -46,8 +45,7 @@ @RunWith(RobolectricTestRunner.class) public class FlutterActivityAndFragmentDelegateTest { private FlutterEngine mockFlutterEngine; - private FakeHost fakeHost; - private FakeHost spyHost; + private FlutterActivityAndFragmentDelegate.Host mockHost; @Before public void setup() { @@ -59,12 +57,20 @@ public void setup() { // being tested. mockFlutterEngine = mockFlutterEngine(); - // Create a fake Host, which is required by the delegate being tested. - fakeHost = new FakeHost(); - fakeHost.flutterEngine = mockFlutterEngine; - - // Create a spy around the FakeHost so that we can verify method invocations. - spyHost = spy(fakeHost); + // Create a mocked Host, which is required by the delegate being tested. + mockHost = mock(FlutterActivityAndFragmentDelegate.Host.class); + when(mockHost.getContext()).thenReturn(RuntimeEnvironment.application); + when(mockHost.getActivity()).thenReturn(Robolectric.setupActivity(Activity.class)); + when(mockHost.getLifecycle()).thenReturn(mock(Lifecycle.class)); + when(mockHost.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[]{})); + when(mockHost.getDartEntrypointFunctionName()).thenReturn("main"); + when(mockHost.getAppBundlePath()).thenReturn("/fake/path"); + when(mockHost.getInitialRoute()).thenReturn("/"); + when(mockHost.getRenderMode()).thenReturn(FlutterView.RenderMode.surface); + when(mockHost.getTransparencyMode()).thenReturn(FlutterView.TransparencyMode.transparent); + when(mockHost.provideFlutterEngine(any(Context.class))).thenReturn(mockFlutterEngine); + when(mockHost.shouldAttachEngineToActivity()).thenReturn(true); + when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true); } @After @@ -77,7 +83,7 @@ public void teardown() { public void itSendsLifecycleEventsToFlutter() { // ---- Test setup ---- // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // We're testing lifecycle behaviors, which require/expect that certain methods have already // been executed by the time they run. Therefore, we run those expected methods first. @@ -117,41 +123,88 @@ public void itSendsLifecycleEventsToFlutter() { public void itDefersToTheHostToProvideFlutterEngine() { // ---- Test setup ---- // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is created in onAttach(). delegate.onAttach(RuntimeEnvironment.application); // Verify that the host was asked to provide a FlutterEngine. - verify(spyHost, times(1)).provideFlutterEngine(any(Context.class)); + verify(mockHost, times(1)).provideFlutterEngine(any(Context.class)); // Verify that the delegate's FlutterEngine is our mock FlutterEngine. assertEquals("The delegate failed to use the host's FlutterEngine.", mockFlutterEngine, delegate.getFlutterEngine()); } + @Test + public void itUsesCachedEngineWhenProvided() { + // ---- Test setup ---- + // Place a FlutterEngine in the static cache. + FlutterEngine cachedEngine = mockFlutterEngine(); + FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine); + + // Adjust fake host to request cached engine. + when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine"); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // The FlutterEngine is obtained in onAttach(). + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + delegate.onStart(); + delegate.onResume(); + + // --- Verify that the cached engine was used --- + // Verify that the non-cached engine was not used. + verify(mockFlutterEngine.getDartExecutor(), never()).executeDartEntrypoint(any(DartExecutor.DartEntrypoint.class)); + + // We should never instruct a cached engine to execute Dart code - it should already be executing it. + verify(cachedEngine.getDartExecutor(), never()).executeDartEntrypoint(any(DartExecutor.DartEntrypoint.class)); + + // If the cached engine is being used, it should have sent a resumed lifecycle event. + verify(cachedEngine.getLifecycleChannel(), times(1)).appIsResumed(); + } + + @Test(expected = IllegalStateException.class) + public void itThrowsExceptionIfCachedEngineDoesNotExist() { + // ---- Test setup ---- + // Adjust fake host to request cached engine that does not exist. + when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine"); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // The FlutterEngine existence is verified in onAttach() + delegate.onAttach(RuntimeEnvironment.application); + + // Expect IllegalStateException. + } + @Test public void itGivesHostAnOpportunityToConfigureFlutterEngine() { // ---- Test setup ---- // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is created in onAttach(). delegate.onAttach(RuntimeEnvironment.application); // Verify that the host was asked to configure our FlutterEngine. - verify(spyHost, times(1)).configureFlutterEngine(mockFlutterEngine); + verify(mockHost, times(1)).configureFlutterEngine(mockFlutterEngine); } @Test public void itSendsInitialRouteToFlutter() { // ---- Test setup ---- // Set initial route on our fake Host. - spyHost.initialRoute = "/my/route"; + when(mockHost.getInitialRoute()).thenReturn("/my/route"); // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The initial route is sent in onStart(). @@ -167,8 +220,8 @@ public void itSendsInitialRouteToFlutter() { public void itExecutesDartEntrypointProvidedByHost() { // ---- Test setup ---- // Set Dart entrypoint parameters on fake host. - spyHost.appBundlePath = "/my/bundle/path"; - spyHost.dartEntrypointFunctionName = "myEntrypoint"; + when(mockHost.getAppBundlePath()).thenReturn("/my/bundle/path"); + when(mockHost.getDartEntrypointFunctionName()).thenReturn("myEntrypoint"); // Create the DartEntrypoint that we expect to be executed. DartExecutor.DartEntrypoint dartEntrypoint = new DartExecutor.DartEntrypoint( @@ -177,7 +230,7 @@ public void itExecutesDartEntrypointProvidedByHost() { ); // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // Dart is executed in onStart(). @@ -196,10 +249,10 @@ public void itExecutesDartEntrypointProvidedByHost() { public void itAttachesFlutterToTheActivityIfDesired() { // ---- Test setup ---- // Declare that the host wants Flutter to attach to the surrounding Activity. - spyHost.shouldAttachToActivity = true; + when(mockHost.shouldAttachEngineToActivity()).thenReturn(true); // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // Flutter is attached to the surrounding Activity in onAttach. @@ -222,10 +275,10 @@ public void itAttachesFlutterToTheActivityIfDesired() { public void itDoesNotAttachFlutterToTheActivityIfNotDesired() { // ---- Test setup ---- // Declare that the host does NOT want Flutter to attach to the surrounding Activity. - spyHost.shouldAttachToActivity = false; + when(mockHost.shouldAttachEngineToActivity()).thenReturn(false); // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // Flutter is attached to the surrounding Activity in onAttach. @@ -244,7 +297,7 @@ public void itDoesNotAttachFlutterToTheActivityIfNotDesired() { @Test public void itSendsPopRouteMessageToFlutterWhenHardwareBackButtonIsPressed() { // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is setup in onAttach(). @@ -260,7 +313,7 @@ public void itSendsPopRouteMessageToFlutterWhenHardwareBackButtonIsPressed() { @Test public void itForwardsOnRequestPermissionsResultToFlutterEngine() { // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is setup in onAttach(). @@ -276,7 +329,7 @@ public void itForwardsOnRequestPermissionsResultToFlutterEngine() { @Test public void itForwardsOnNewIntentToFlutterEngine() { // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is setup in onAttach(). @@ -292,7 +345,7 @@ public void itForwardsOnNewIntentToFlutterEngine() { @Test public void itForwardsOnActivityResultToFlutterEngine() { // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is setup in onAttach(). @@ -308,7 +361,7 @@ public void itForwardsOnActivityResultToFlutterEngine() { @Test public void itForwardsOnUserLeaveHintToFlutterEngine() { // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is setup in onAttach(). @@ -324,7 +377,7 @@ public void itForwardsOnUserLeaveHintToFlutterEngine() { @Test public void itSendsMessageOverSystemChannelWhenToldToTrimMemory() { // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is setup in onAttach(). @@ -340,7 +393,7 @@ public void itSendsMessageOverSystemChannelWhenToldToTrimMemory() { @Test public void itSendsMessageOverSystemChannelWhenInformedOfLowMemory() { // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(spyHost); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); // --- Execute the behavior under test --- // The FlutterEngine is setup in onAttach(). @@ -353,6 +406,117 @@ public void itSendsMessageOverSystemChannelWhenInformedOfLowMemory() { verify(mockFlutterEngine.getSystemChannel(), times(1)).sendMemoryPressureWarning(); } + @Test + public void itDestroysItsOwnEngineIfHostRequestsIt() { + // ---- Test setup ---- + // Adjust fake host to request engine destruction. + when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); + + // --- Verify that the cached engine was destroyed --- + verify(mockFlutterEngine, times(1)).destroy(); + } + + @Test + public void itDoesNotDestroyItsOwnEngineWhenHostSaysNotTo() { + // ---- Test setup ---- + // Adjust fake host to request engine destruction. + when(mockHost.shouldDestroyEngineWithHost()).thenReturn(false); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); + + // --- Verify that the cached engine was destroyed --- + verify(mockFlutterEngine, never()).destroy(); + } + + @Test + public void itDestroysCachedEngineWhenHostRequestsIt() { + // ---- Test setup ---- + // Place a FlutterEngine in the static cache. + FlutterEngine cachedEngine = mockFlutterEngine(); + FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine); + + // Adjust fake host to request cached engine. + when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine"); + + // Adjust fake host to request engine destruction. + when(mockHost.shouldDestroyEngineWithHost()).thenReturn(true); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); + + // --- Verify that the cached engine was destroyed --- + verify(cachedEngine, times(1)).destroy(); + assertNull(FlutterEngineCache.getInstance().get("my_flutter_engine")); + } + + @Test + public void itDoesNotDestroyCachedEngineWhenHostSaysNotTo() { + // ---- Test setup ---- + // Place a FlutterEngine in the static cache. + FlutterEngine cachedEngine = mockFlutterEngine(); + FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine); + + // Adjust fake host to request cached engine. + when(mockHost.getCachedEngineId()).thenReturn("my_flutter_engine"); + + // Adjust fake host to request engine retention. + when(mockHost.shouldDestroyEngineWithHost()).thenReturn(false); + + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(RuntimeEnvironment.application); + delegate.onCreateView(null, null, null); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); + + // --- Verify that the cached engine was NOT destroyed --- + verify(cachedEngine, never()).destroy(); + } + /** * Creates a mock {@link FlutterEngine}. *

    @@ -387,115 +551,4 @@ private FlutterEngine mockFlutterEngine() { return engine; } - - /** - * A {@link FlutterActivityAndFragmentDelegate.Host} that returns values desired by this - * test suite. - *

    - * Sane defaults are set for all properties. Tests in this suite can alter {@code FakeHost} - * properties as needed for each test. - */ - private static class FakeHost implements FlutterActivityAndFragmentDelegate.Host { - private FlutterEngine flutterEngine; - private String initialRoute = null; - private String appBundlePath = "fake/path/"; - private String dartEntrypointFunctionName = "main"; - private Activity activity; - private boolean shouldAttachToActivity = false; - private boolean retainFlutterEngine = false; - - @NonNull - @Override - public Context getContext() { - return RuntimeEnvironment.application; - } - - @Nullable - @Override - public Activity getActivity() { - if (activity == null) { - // We must provide a real (or close to real) Activity because it is passed to - // the FlutterView that the delegate instantiates. - activity = Robolectric.setupActivity(Activity.class); - } - - return activity; - } - - @NonNull - @Override - public Lifecycle getLifecycle() { - return mock(Lifecycle.class); - } - - @NonNull - @Override - public FlutterShellArgs getFlutterShellArgs() { - return new FlutterShellArgs(new String[]{}); - } - - @NonNull - @Override - public String getDartEntrypointFunctionName() { - return dartEntrypointFunctionName; - } - - @NonNull - @Override - public String getAppBundlePath() { - return appBundlePath; - } - - @Nullable - @Override - public String getInitialRoute() { - return initialRoute; - } - - @NonNull - @Override - public FlutterView.RenderMode getRenderMode() { - return FlutterView.RenderMode.surface; - } - - @NonNull - @Override - public FlutterView.TransparencyMode getTransparencyMode() { - return FlutterView.TransparencyMode.opaque; - } - - @Nullable - @Override - public SplashScreen provideSplashScreen() { - return null; - } - - @Nullable - @Override - public FlutterEngine provideFlutterEngine(@NonNull Context context) { - return flutterEngine; - } - - @Nullable - @Override - public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) { - return null; - } - - @Override - public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {} - - @Override - public boolean shouldAttachEngineToActivity() { - return shouldAttachToActivity; - } - - @Override - public boolean retainFlutterEngineAfterHostDestruction() { - return retainFlutterEngine; - } - - @Override - public void onFirstFrameRendered() {} - } } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java new file mode 100644 index 0000000000000..694b349a8aa2f --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -0,0 +1,87 @@ +package io.flutter.embedding.android; + +import android.content.Intent; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.android.controller.ActivityController; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterActivityTest { + @Test + public void itCreatesDefaultIntentWithExpectedDefaults() { + Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); + ActivityController activityController = Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity flutterActivity = activityController.get(); + + assertEquals("main", flutterActivity.getDartEntrypointFunctionName()); + assertEquals("/", flutterActivity.getInitialRoute()); + assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray()); + assertTrue(flutterActivity.shouldAttachEngineToActivity()); + assertNull(flutterActivity.getCachedEngineId()); + assertTrue(flutterActivity.shouldDestroyEngineWithHost()); + assertEquals(FlutterActivity.BackgroundMode.opaque, flutterActivity.getBackgroundMode()); + assertEquals(FlutterView.RenderMode.surface, flutterActivity.getRenderMode()); + assertEquals(FlutterView.TransparencyMode.opaque, flutterActivity.getTransparencyMode()); + } + + @Test + public void itCreatesNewEngineIntentWithRequestedSettings() { + Intent intent = FlutterActivity.withNewEngine() + .dartEntrypoint("custom_entrypoint") + .initialRoute("/custom/route") + .backgroundMode(FlutterActivity.BackgroundMode.transparent) + .build(RuntimeEnvironment.application); + ActivityController activityController = Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity flutterActivity = activityController.get(); + + assertEquals("custom_entrypoint", flutterActivity.getDartEntrypointFunctionName()); + assertEquals("/custom/route", flutterActivity.getInitialRoute()); + assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray()); + assertTrue(flutterActivity.shouldAttachEngineToActivity()); + assertNull(flutterActivity.getCachedEngineId()); + assertTrue(flutterActivity.shouldDestroyEngineWithHost()); + assertEquals(FlutterActivity.BackgroundMode.transparent, flutterActivity.getBackgroundMode()); + assertEquals(FlutterView.RenderMode.texture, flutterActivity.getRenderMode()); + assertEquals(FlutterView.TransparencyMode.transparent, flutterActivity.getTransparencyMode()); + } + + @Test + public void itCreatesCachedEngineIntentThatDoesNotDestroyTheEngine() { + Intent intent = FlutterActivity.withCachedEngine("my_cached_engine") + .destroyEngineWithActivity(false) + .build(RuntimeEnvironment.application); + ActivityController activityController = Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity flutterActivity = activityController.get(); + + assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray()); + assertTrue(flutterActivity.shouldAttachEngineToActivity()); + assertEquals("my_cached_engine", flutterActivity.getCachedEngineId()); + assertFalse(flutterActivity.shouldDestroyEngineWithHost()); + } + + @Test + public void itCreatesCachedEngineIntentThatDestroysTheEngine() { + Intent intent = FlutterActivity.withCachedEngine("my_cached_engine") + .destroyEngineWithActivity(true) + .build(RuntimeEnvironment.application); + ActivityController activityController = Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity flutterActivity = activityController.get(); + + assertArrayEquals(new String[]{}, flutterActivity.getFlutterShellArgs().toArray()); + assertTrue(flutterActivity.shouldAttachEngineToActivity()); + assertEquals("my_cached_engine", flutterActivity.getCachedEngineId()); + assertTrue(flutterActivity.shouldDestroyEngineWithHost()); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java new file mode 100644 index 0000000000000..31a377e7de19c --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java @@ -0,0 +1,73 @@ +package io.flutter.embedding.android; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterFragmentTest { + @Test + public void itCreatesDefaultFragmentWithExpectedDefaults() { + FlutterFragment fragment = FlutterFragment.createDefault(); + + assertEquals("main", fragment.getDartEntrypointFunctionName()); + assertEquals("/", fragment.getInitialRoute()); + assertArrayEquals(new String[]{}, fragment.getFlutterShellArgs().toArray()); + assertTrue(fragment.shouldAttachEngineToActivity()); + assertNull(fragment.getCachedEngineId()); + assertTrue(fragment.shouldDestroyEngineWithHost()); + assertEquals(FlutterView.RenderMode.surface, fragment.getRenderMode()); + assertEquals(FlutterView.TransparencyMode.transparent, fragment.getTransparencyMode()); + } + + @Test + public void itCreatesNewEngineFragmentWithRequestedSettings() { + FlutterFragment fragment = FlutterFragment.withNewEngine() + .dartEntrypoint("custom_entrypoint") + .initialRoute("/custom/route") + .shouldAttachEngineToActivity(false) + .renderMode(FlutterView.RenderMode.texture) + .transparencyMode(FlutterView.TransparencyMode.opaque) + .build(); + + assertEquals("custom_entrypoint", fragment.getDartEntrypointFunctionName()); + assertEquals("/custom/route", fragment.getInitialRoute()); + assertArrayEquals(new String[]{}, fragment.getFlutterShellArgs().toArray()); + assertFalse(fragment.shouldAttachEngineToActivity()); + assertNull(fragment.getCachedEngineId()); + assertTrue(fragment.shouldDestroyEngineWithHost()); + assertEquals(FlutterView.RenderMode.texture, fragment.getRenderMode()); + assertEquals(FlutterView.TransparencyMode.opaque, fragment.getTransparencyMode()); + } + + @Test + public void itCreatesCachedEngineFragmentThatDoesNotDestroyTheEngine() { + FlutterFragment fragment = FlutterFragment + .withCachedEngine("my_cached_engine") + .build(); + + assertTrue(fragment.shouldAttachEngineToActivity()); + assertEquals("my_cached_engine", fragment.getCachedEngineId()); + assertFalse(fragment.shouldDestroyEngineWithHost()); + } + + @Test + public void itCreatesCachedEngineFragmentThatDestroysTheEngine() { + FlutterFragment fragment = FlutterFragment + .withCachedEngine("my_cached_engine") + .destroyEngineWithFragment(true) + .build(); + + assertTrue(fragment.shouldAttachEngineToActivity()); + assertEquals("my_cached_engine", fragment.getCachedEngineId()); + assertTrue(fragment.shouldDestroyEngineWithHost()); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineCacheTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineCacheTest.java new file mode 100644 index 0000000000000..35cfc5d82e003 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineCacheTest.java @@ -0,0 +1,58 @@ +package io.flutter.embedding.engine; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterEngineCacheTest { + @Test + public void itHoldsFlutterEngines() { + // --- Test Setup --- + FlutterEngine flutterEngine = mock(FlutterEngine.class); + FlutterEngineCache cache = new FlutterEngineCache(); + + // --- Execute Test --- + cache.put("my_flutter_engine", flutterEngine); + + // --- Verify Results --- + assertEquals(flutterEngine, cache.get("my_flutter_engine")); + } + + @Test + public void itQueriesFlutterEngineExistence() { + // --- Test Setup --- + FlutterEngine flutterEngine = mock(FlutterEngine.class); + FlutterEngineCache cache = new FlutterEngineCache(); + + // --- Execute Test --- + assertFalse(cache.contains("my_flutter_engine")); + + cache.put("my_flutter_engine", flutterEngine); + + // --- Verify Results --- + assertTrue(cache.contains("my_flutter_engine")); + } + + @Test + public void itRemovesFlutterEngines() { + // --- Test Setup --- + FlutterEngine flutterEngine = mock(FlutterEngine.class); + FlutterEngineCache cache = new FlutterEngineCache(); + + // --- Execute Test --- + cache.put("my_flutter_engine", flutterEngine); + cache.remove("my_flutter_engine"); + + // --- Verify Results --- + assertNull(cache.get("my_flutter_engine")); + } +}