diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 636d287928d58..0fdca85bb1df8 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -840,6 +840,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroupCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.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 fa1b3fb948a80..75c914b0a8bc8 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -187,6 +187,7 @@ android_java_sources = [ "io/flutter/embedding/engine/FlutterEngineCache.java", "io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java", "io/flutter/embedding/engine/FlutterEngineGroup.java", + "io/flutter/embedding/engine/FlutterEngineGroupCache.java", "io/flutter/embedding/engine/FlutterJNI.java", "io/flutter/embedding/engine/FlutterOverlaySurface.java", "io/flutter/embedding/engine/FlutterShellArgs.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 8ace803e5db0f..8ec8b419c2ba8 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -9,7 +9,9 @@ import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_GROUP_ID; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DART_ENTRYPOINT; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_ENABLE_STATE_RESTORATION; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE; @@ -269,7 +271,7 @@ public NewEngineIntentBuilder(@NonNull Class activity } /** - * The initial route that a Flutter app will render in this {@link FlutterFragment}, defaults to + * The initial route that a Flutter app will render in this {@link FlutterActivity}, defaults to * "/". * * @param initialRoute The route. @@ -421,6 +423,151 @@ public Intent build(@NonNull Context context) { } } + /** + * Creates a {@link NewEngineInGroupIntentBuilder}, which can be used to configure an {@link + * Intent} to launch a {@code FlutterActivity} by internally creating a FlutterEngine from an + * existing {@link io.flutter.embedding.engine.FlutterEngineGroup} cached in a specified {@link + * io.flutter.embedding.engine.FlutterEngineGroupCache}, and creates a new {@link + * io.flutter.embedding.engine.FlutterEngine} by FlutterEngineGroup#createAndRunEngine + * + *
{@code
+   * // Create a FlutterEngineGroup, usually we could create it in onCreate method of Application.
+   * FlutterEngineGroup engineGroup = new FlutterEngineGroup(this);
+   * FlutterEngineGroupCache.getInstance().put("my_cached_engine_group_id", engineGroup);
+   *
+   * // use the intent that build by withNewEngineInGroup to start FlutterActivity
+   * Intent intent = FlutterActivity.withNewEngineInGroup("my_cached_engine_group_id")
+   *     .dartEntrypoint("custom_entrypoint")
+   *     .initialRoute("/custom/route")
+   *     .backgroundMode(BackgroundMode.transparent)
+   *     .build(context);
+   * startActivity(intent);
+   * }
+ * + * @param engineGroupId A cached engine group ID. + * @return The builder. + */ + public static NewEngineInGroupIntentBuilder withNewEngineInGroup(@NonNull String engineGroupId) { + return new NewEngineInGroupIntentBuilder(FlutterActivity.class, engineGroupId); + } + + /** + * Builder to create an {@code Intent} that launches a {@code FlutterActivity} with a new {@link + * FlutterEngine} by FlutterEngineGroup#createAndRunEngine. + */ + public static class NewEngineInGroupIntentBuilder { + private final Class activityClass; + private final String cachedEngineGroupId; + private String dartEntrypoint = DEFAULT_DART_ENTRYPOINT; + private String initialRoute = DEFAULT_INITIAL_ROUTE; + private String backgroundMode = DEFAULT_BACKGROUND_MODE; + + /** + * Constructor that allows this {@code NewEngineInGroupIntentBuilder} to be used by subclasses + * of {@code FlutterActivity}. + * + *

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

{@code return new NewEngineInGroupIntentBuilder(MyFlutterActivity.class, + * cacheedEngineGroupId); } + * + *

{@code
+     * // Create a FlutterEngineGroup, usually we could create it in onCreate method of Application.
+     * FlutterEngineGroup engineGroup = new FlutterEngineGroup(this);
+     * FlutterEngineGroupCache.getInstance().put("my_cached_engine_group_id", engineGroup);
+     *
+     * // create NewEngineInGroupIntentBuilder, and start my custom FlutterActivity with the intent
+     * // that build by NewEngineInGroupIntentBuilder.
+     * FlutterActivity.NewEngineInGroupIntentBuilder intentBuilder =
+     *     new FlutterActivity.NewEngineInGroupIntentBuilder(
+     *           MyFlutterActivity.class,
+     *           app.engineGroupId);
+     * intentBuilder.dartEntrypoint("main")
+     *     .initialRoute("/custom/route")
+     *     .backgroundMode(BackgroundMode.transparent);
+     * startActivity(intentBuilder.build(context));
+     * }
+ * + * @param activityClass A subclass of {@code FlutterActivity}. + * @param engineGroupId The engine group id. + */ + public NewEngineInGroupIntentBuilder( + @NonNull Class activityClass, @NonNull String engineGroupId) { + this.activityClass = activityClass; + this.cachedEngineGroupId = engineGroupId; + } + + /** + * The Dart entrypoint that will be executed in the newly created FlutterEngine as soon as the + * Dart snapshot is loaded. Default to "main". + * + * @param dartEntrypoint The dart entrypoint's name + * @return The engine group intent builder + */ + @NonNull + public NewEngineInGroupIntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { + this.dartEntrypoint = dartEntrypoint; + return this; + } + + /** + * The initial route that a Flutter app will render in this {@link FlutterActivity}, defaults to + * "/". + * + * @param initialRoute The route. + * @return The engine group intent builder. + */ + @NonNull + public NewEngineInGroupIntentBuilder initialRoute(@NonNull String initialRoute) { + this.initialRoute = initialRoute; + 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}. + * + * @param backgroundMode The background mode. + * @return The engine group intent builder. + */ + @NonNull + public NewEngineInGroupIntentBuilder 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. + * + * @param context The context. e.g. An Activity. + * @return The intent. + */ + @NonNull + public Intent build(@NonNull Context context) { + return new Intent(context, activityClass) + .putExtra(EXTRA_DART_ENTRYPOINT, dartEntrypoint) + .putExtra(EXTRA_INITIAL_ROUTE, initialRoute) + .putExtra(EXTRA_CACHED_ENGINE_GROUP_ID, cachedEngineGroupId) + .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode) + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true); + } + } + // 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. @@ -775,6 +922,17 @@ public String getCachedEngineId() { return getIntent().getStringExtra(EXTRA_CACHED_ENGINE_ID); } + /** + * Returns the ID of a statically cached {@link io.flutter.embedding.engine.FlutterEngineGroup} to + * use within this {@code FlutterActivity}, or {@code null} if this {@code FlutterActivity} does + * not want to use a cached {@link io.flutter.embedding.engine.FlutterEngineGroup}. + */ + @Override + @Nullable + public String getCachedEngineGroupId() { + return getIntent().getStringExtra(EXTRA_CACHED_ENGINE_GROUP_ID); + } + /** * Returns false if the {@link io.flutter.embedding.engine.FlutterEngine} backing this {@code * FlutterActivity} should outlive this {@code FlutterActivity}, or true to be destroyed when the @@ -801,14 +959,26 @@ public boolean shouldDestroyEngineWithHost() { /** * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded. * - *

This preference can be controlled by setting a {@code } called {@link - * FlutterActivityLaunchConfigs#DART_ENTRYPOINT_META_DATA_KEY} within the Android manifest - * definition for this {@code FlutterActivity}. + *

This preference can be controlled with 2 methods: + * + *

    + *
  1. Pass a boolean as {@link FlutterActivityLaunchConfigs#EXTRA_DART_ENTRYPOINT} with the + * launching {@code Intent}, or + *
  2. Set a {@code } called {@link + * FlutterActivityLaunchConfigs#DART_ENTRYPOINT_META_DATA_KEY} within the Android manifest + * definition for this {@code FlutterActivity} + *
+ * + * If both preferences are set, the {@code Intent} preference takes priority. * *

Subclasses may override this method to directly control the Dart entrypoint. */ @NonNull public String getDartEntrypointFunctionName() { + if (getIntent().hasExtra(EXTRA_DART_ENTRYPOINT)) { + return getIntent().getStringExtra(EXTRA_DART_ENTRYPOINT); + } + try { Bundle metaData = getMetaData(); String desiredDartEntrypoint = diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index 79b2d69754d91..bcb23152d241a 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -24,6 +24,8 @@ import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterEngineGroup; +import io.flutter.embedding.engine.FlutterEngineGroupCache; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; @@ -213,6 +215,10 @@ void onAttach(@NonNull Context context) { *

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

Third, the {@code host} is given an FluttenEngineGroup Id, then used a cached {@link + * io.flutter.embedding.engine.FlutterEngineGroup} to create a new {@link FlutterEngine} by {@link + * FlutterEngineGroup#createAndRunEngine} + * *

If the {@code host} does not provide a {@link io.flutter.embedding.engine.FlutterEngine}, * then a new {@link FlutterEngine} is instantiated. */ @@ -241,6 +247,34 @@ void onAttach(@NonNull Context context) { return; } + // Third, check if the host wants to use a cached FlutterEngineGroup + // and create new FlutterEngine by FlutterEngineGroup#createAndRunEngine + String cachedEngineGroupId = host.getCachedEngineGroupId(); + if (cachedEngineGroupId != null) { + FlutterEngineGroup flutterEngineGroup = + FlutterEngineGroupCache.getInstance().get(cachedEngineGroupId); + if (flutterEngineGroup == null) { + throw new IllegalStateException( + "The requested cached FlutterEngineGroup did not exist in the FlutterEngineGroupCache: '" + + cachedEngineGroupId + + "'"); + } + + String appBundlePathOverride = host.getAppBundlePath(); + if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) { + appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath(); + } + + DartExecutor.DartEntrypoint dartEntrypoint = + new DartExecutor.DartEntrypoint( + appBundlePathOverride, host.getDartEntrypointFunctionName()); + flutterEngine = + flutterEngineGroup.createAndRunEngine( + host.getContext(), dartEntrypoint, host.getInitialRoute()); + isFlutterEngineFromHost = false; + return; + } + // Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our // FlutterView. Log.v( @@ -876,6 +910,9 @@ private void ensureAlive() { @Nullable String getCachedEngineId(); + @Nullable + String getCachedEngineGroupId(); + /** * Returns true if the {@link io.flutter.embedding.engine.FlutterEngine} used in this delegate * should be destroyed when the host/delegate are destroyed. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java index a92e342333765..7106b4bc75c7c 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java @@ -19,9 +19,11 @@ public class FlutterActivityLaunchConfigs { /* package */ static final String HANDLE_DEEPLINKING_META_DATA_KEY = "flutter_deeplinking_enabled"; // Intent extra arguments. + /* package */ static final String EXTRA_DART_ENTRYPOINT = "dart_entrypoint"; /* package */ static final String EXTRA_INITIAL_ROUTE = "route"; /* package */ static final String EXTRA_BACKGROUND_MODE = "background_mode"; /* package */ static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id"; + /* package */ static final String EXTRA_CACHED_ENGINE_GROUP_ID = "cached_engine_group_id"; /* package */ static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity"; /* package */ static final String EXTRA_ENABLE_STATE_RESTORATION = "enable_state_restoration"; diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index 365e6984f0b07..29dd1f0e511ec 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -136,6 +136,9 @@ public class FlutterFragment extends Fragment * FlutterFragment}. */ protected static final String ARG_CACHED_ENGINE_ID = "cached_engine_id"; + + protected static final String ARG_CACHED_ENGINE_GROUP_ID = "cached_engine_group_id"; + /** * True if the {@link io.flutter.embedding.engine.FlutterEngine} in the created {@code * FlutterFragment} should be destroyed when the {@code FlutterFragment} is destroyed, false if @@ -702,6 +705,263 @@ public T build() { } } + /** + * Returns a {@link NewEngineInGroupFragmentBuilder} to create a {@code FlutterFragment} with a + * cached {@link io.flutter.embedding.engine.FlutterEngineGroup} in {@link + * io.flutter.embedding.engine.FlutterEngineGroupCache}. + * + *

An {@code IllegalStateException} will be thrown during the lifecycle of the {@code + * FlutterFragment} if a cached {@link io.flutter.embedding.engine.FlutterEngineGroup} is + * requested but does not exist in the {@link + * io.flutter.embedding.engine.FlutterEngineGroupCache}. + * + *

To create a {@code FlutterFragment} that uses a new {@link + * io.flutter.embedding.engine.FlutterEngine} that created by {@link + * io.flutter.embedding.engine.FlutterEngineGroup#createAndRunEngine} + */ + @NonNull + public static NewEngineInGroupFragmentBuilder withNewEngineInGroup( + @NonNull String engineGroupId) { + return new NewEngineInGroupFragmentBuilder(engineGroupId); + } + + /** + * Builder that creates a new {@code FlutterFragment} that uses a cached {@link + * io.flutter.embedding.engine.FlutterEngineGroup} to create a new {@link + * io.flutter.embedding.engine.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.NewEngineInGroupFragmentBuilder(MyFlutterFragment.class, engineGroupId) + * .someProperty(...) .someOtherProperty(...) .build(); } + * + *

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

    + *
  1. Ensure the {@code FlutterFragment} subclass has a no-arg constructor. + *
  2. Subclass this {@code NewEngineInGroupFragmentBuilder}. + *
  3. Override the new {@code NewEngineInGroupFragmentBuilder}'s no-arg constructor and invoke + * the super constructor to set the {@code FlutterFragment} subclass: {@code public + * MyBuilder() { super(MyFlutterFragment.class); } } + *
  4. Add appropriate property methods for the new properties. + *
  5. Override {@link NewEngineInGroupFragmentBuilder#createArgs()}, call through to the super + * method, then add the new properties as arguments in the {@link Bundle}. + *
+ * + * Once a {@code NewEngineInGroupFragmentBuilder} subclass is defined, the {@code FlutterFragment} + * subclass can be instantiated as follows. {@code MyFlutterFragment f = new MyBuilder() + * .someExistingProperty(...) .someNewProperty(...) .build(); } + */ + public static class NewEngineInGroupFragmentBuilder { + private final Class fragmentClass; + private final String cachedEngineGroupId; + private @NonNull String dartEntrypoint = "main"; + private @NonNull String initialRoute = "/"; + private @NonNull boolean handleDeeplinking = false; + private @NonNull RenderMode renderMode = RenderMode.surface; + private @NonNull TransparencyMode transparencyMode = TransparencyMode.transparent; + private boolean shouldAttachEngineToActivity = true; + private boolean shouldAutomaticallyHandleOnBackPressed = false; + private boolean shouldDelayFirstAndroidViewDraw = false; + + public NewEngineInGroupFragmentBuilder(@NonNull String engineGroupId) { + this(FlutterFragment.class, engineGroupId); + } + + public NewEngineInGroupFragmentBuilder( + @NonNull Class fragmentClass, @NonNull String engineGroupId) { + this.fragmentClass = fragmentClass; + this.cachedEngineGroupId = engineGroupId; + } + + /** The name of the initial Dart method to invoke, defaults to "main". */ + @NonNull + public NewEngineInGroupFragmentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { + this.dartEntrypoint = dartEntrypoint; + return this; + } + + /** + * The initial route that a Flutter app will render in this {@link FlutterFragment}, defaults to + * "/". + */ + @NonNull + public NewEngineInGroupFragmentBuilder initialRoute(@NonNull String initialRoute) { + this.initialRoute = initialRoute; + return this; + } + + /** + * Whether to handle the deeplinking from the {@code Intent} automatically if the {@code + * getInitialRoute} returns null. + */ + @NonNull + public NewEngineInGroupFragmentBuilder handleDeeplinking(@NonNull boolean handleDeeplinking) { + this.handleDeeplinking = handleDeeplinking; + return this; + } + + /** + * Render Flutter either as a {@link RenderMode#surface} or a {@link 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 NewEngineInGroupFragmentBuilder renderMode(@NonNull RenderMode renderMode) { + this.renderMode = renderMode; + return this; + } + + /** + * Support a {@link TransparencyMode#transparent} background within {@link + * io.flutter.embedding.android.FlutterView}, or force an {@link TransparencyMode#opaque} + * background. + * + *

See {@link TransparencyMode} for implications of this selection. + */ + @NonNull + public NewEngineInGroupFragmentBuilder transparencyMode( + @NonNull 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 io.flutter.embedding.engine.FlutterEngine}. + * + *

Control surfaces are used to provide Android resources and lifecycle events to plugins + * that are attached to the {@link io.flutter.embedding.engine.FlutterEngine}. If {@code + * shouldAttachEngineToActivity} is true then this {@code FlutterFragment} will connect its + * {@link io.flutter.embedding.engine.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 io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}. The + * {@code Activity} will need to be manually connected to this {@code FlutterFragment}'s {@link + * io.flutter.embedding.engine.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 io.flutter.embedding.engine.FlutterEngine} is if the developer + * wants to move the {@link FlutterEngine} somewhere else. For example, a developer might want + * the {@link io.flutter.embedding.engine.FlutterEngine} to outlive the surrounding {@code + * Activity} so that it can be used later in a different {@code Activity}. To accomplish this, + * the {@link io.flutter.embedding.engine.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 + * io.flutter.embedding.engine.FlutterEngine} and the surrounding {@code Activity}. + * + *

Another reason that a developer might choose to manually manage the relationship between + * the {@code Activity} and {@link io.flutter.embedding.engine.FlutterEngine} is if the + * developer wants to prevent, or explicitly control when the {@link + * io.flutter.embedding.engine.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 io.flutter.embedding.engine.FlutterEngine} to have access to the {@code Activity}, + * which can be accomplished by setting {@code shouldAttachEngineToActivity} to {@code false}. + */ + @NonNull + public NewEngineInGroupFragmentBuilder shouldAttachEngineToActivity( + boolean shouldAttachEngineToActivity) { + this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; + return this; + } + + /** + * Whether or not this {@code FlutterFragment} should automatically receive {@link + * #onBackPressed()} events, rather than requiring an explicit activity call through. Disabled + * by default. + * + *

When enabled, the activity will automatically dispatch back-press events to the fragment's + * {@link OnBackPressedCallback}, instead of requiring the activity to manually call {@link + * #onBackPressed()} in client code. If enabled, do not invoke {@link #onBackPressed()} + * manually. + * + *

This behavior relies on the implementation of {@link #popSystemNavigator()}. It's not + * recommended to override that method when enabling this attribute, but if you do, you should + * always fall back to calling {@code super.popSystemNavigator()} when not relying on custom + * behavior. + */ + @NonNull + public NewEngineInGroupFragmentBuilder shouldAutomaticallyHandleOnBackPressed( + boolean shouldAutomaticallyHandleOnBackPressed) { + this.shouldAutomaticallyHandleOnBackPressed = shouldAutomaticallyHandleOnBackPressed; + return this; + } + + /** + * Whether to delay the Android drawing pass till after the Flutter UI has been displayed. + * + *

See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details. + */ + @NonNull + public NewEngineInGroupFragmentBuilder shouldDelayFirstAndroidViewDraw( + @NonNull boolean shouldDelayFirstAndroidViewDraw) { + this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw; + 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_GROUP_ID, cachedEngineGroupId); + args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint); + args.putString(ARG_INITIAL_ROUTE, initialRoute); + args.putBoolean(ARG_HANDLE_DEEPLINKING, handleDeeplinking); + args.putString( + ARG_FLUTTERVIEW_RENDER_MODE, + renderMode != null ? renderMode.name() : RenderMode.surface.name()); + args.putString( + ARG_FLUTTERVIEW_TRANSPARENCY_MODE, + transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name()); + args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); + args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true); + args.putBoolean( + ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed); + args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw); + return args; + } + + /** + * Constructs a new {@code FlutterFragment} (or a subclass) that is configured based on + * properties set on this {@code Builder}. + */ + @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. @@ -983,6 +1243,17 @@ public String getCachedEngineId() { return getArguments().getString(ARG_CACHED_ENGINE_ID, null); } + /** + * Returns the ID of a statically cached {@link io.flutter.embedding.engine.FlutterEngineGroup} to + * use within this {@code FlutterFragment}, or {@code null} if this {@code FlutterFragment} does + * not want to use a cached {@link io.flutter.embedding.engine.FlutterEngineGroup}. + */ + @Override + @Nullable + public String getCachedEngineGroupId() { + return getArguments().getString(ARG_CACHED_ENGINE_GROUP_ID, null); + } + /** * Returns true a {@code FlutterEngine} was explicitly created and injected into the {@code * FlutterFragment} rather than one that was created implicitly in the {@code FlutterFragment}. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 52c65ca855d45..dee2dac86bb77 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -9,7 +9,9 @@ import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_DART_ENTRYPOINT; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.DEFAULT_INITIAL_ROUTE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_BACKGROUND_MODE; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_GROUP_ID; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_CACHED_ENGINE_ID; +import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DART_ENTRYPOINT; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_DESTROY_ENGINE_WITH_ACTIVITY; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE; import static io.flutter.embedding.android.FlutterActivityLaunchConfigs.HANDLE_DEEPLINKING_META_DATA_KEY; @@ -239,6 +241,113 @@ public Intent build(@NonNull Context context) { } } + /** + * Creates a {@link NewEngineInGroupIntentBuilder}, which can be used to configure an {@link + * Intent} to launch a {@code FlutterFragmentActivity} that internally uses an existing {@link + * io.flutter.embedding.engine.FlutterEngineGroup} that is cached in {@link + * io.flutter.embedding.engine.FlutterEngineGroupCache}, and creates a new {@link + * io.flutter.embedding.engine.FlutterEngine} by FlutterEngineGroup#createAndRunEngine + * + * @param engineGroupId A cached engine group ID. + * @return The builder. + */ + public static NewEngineInGroupIntentBuilder withNewEngineInGroup(@NonNull String engineGroupId) { + return new NewEngineInGroupIntentBuilder(FlutterFragmentActivity.class, engineGroupId); + } + + /** + * Builder to create an {@code Intent} that launches a {@code FlutterFragmentActivity} with a new + * {@link FlutterEngine} by FlutterEngineGroup#createAndRunEngine. + */ + public static class NewEngineInGroupIntentBuilder { + private final Class activityClass; + private final String cachedEngineGroupId; + private String dartEntrypoint = DEFAULT_DART_ENTRYPOINT; + private String initialRoute = DEFAULT_INITIAL_ROUTE; + private String backgroundMode = DEFAULT_BACKGROUND_MODE; + + /** + * Constructor that allows this {@code NewEngineInGroupIntentBuilder} to be used by subclasses + * of {@code FlutterActivity}. + * + *

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

{@code return new NewEngineInGroupIntentBuilder(FlutterFragmentActivity.class, + * cacheedEngineGroupId); } + * + * @param activityClass A subclass of {@code FlutterFragmentActivity}. + * @param engineGroupId The engine group id. + */ + public NewEngineInGroupIntentBuilder( + @NonNull Class activityClass, + @NonNull String engineGroupId) { + this.activityClass = activityClass; + this.cachedEngineGroupId = engineGroupId; + } + + /** + * The Dart entrypoint that will be executed as soon as the Dart snapshot is loaded, default to + * "main". + * + * @param dartEntrypoint The dart entrypoint's name + * @return The engine group intent builder + */ + @NonNull + public NewEngineInGroupIntentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { + this.dartEntrypoint = dartEntrypoint; + return this; + } + + /** + * The initial route that a Flutter app will render in this {@code FlutterFragmentActivity}, + * defaults to "/". + */ + @NonNull + public NewEngineInGroupIntentBuilder initialRoute(@NonNull String initialRoute) { + this.initialRoute = initialRoute; + return this; + } + + /** + * The mode of {@code FlutterFragmentActivity}'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 FlutterFragmentActivity} 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 FlutterFragmentActivity} 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 NewEngineInGroupIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) { + this.backgroundMode = backgroundMode.name(); + return this; + } + + /** + * Creates and returns an {@link Intent} that will launch a {@code FlutterFragmentActivity} with + * the desired configuration. + */ + @NonNull + public Intent build(@NonNull Context context) { + return new Intent(context, activityClass) + .putExtra(EXTRA_DART_ENTRYPOINT, dartEntrypoint) + .putExtra(EXTRA_INITIAL_ROUTE, initialRoute) + .putExtra(EXTRA_CACHED_ENGINE_GROUP_ID, cachedEngineGroupId) + .putExtra(EXTRA_BACKGROUND_MODE, backgroundMode) + .putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, true); + } + } + @Nullable private FlutterFragment flutterFragment; @Override @@ -454,6 +563,9 @@ protected FlutterFragment createFlutterFragment() { Log.v( TAG, "Creating FlutterFragment with new engine:\n" + + "Cached engine group ID: " + + getCachedEngineGroupId() + + "\n" + "Background transparency mode: " + backgroundMode + "\n" @@ -469,6 +581,19 @@ protected FlutterFragment createFlutterFragment() { + "Will attach FlutterEngine to Activity: " + shouldAttachEngineToActivity()); + if (getCachedEngineGroupId() != null) { + return flutterFragment + .withNewEngineInGroup(getCachedEngineGroupId()) + .dartEntrypoint(getDartEntrypointFunctionName()) + .initialRoute(getInitialRoute()) + .handleDeeplinking(shouldHandleDeeplinking()) + .renderMode(renderMode) + .transparencyMode(transparencyMode) + .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) + .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw) + .build(); + } + return FlutterFragment.withNewEngine() .dartEntrypoint(getDartEntrypointFunctionName()) .initialRoute(getInitialRoute()) @@ -743,6 +868,11 @@ protected String getCachedEngineId() { return getIntent().getStringExtra(EXTRA_CACHED_ENGINE_ID); } + @Nullable + protected String getCachedEngineGroupId() { + return getIntent().getStringExtra(EXTRA_CACHED_ENGINE_GROUP_ID); + } + /** * The desired window background mode of this {@code Activity}, which defaults to {@link * BackgroundMode#opaque}. diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java index bb1a9ba2995cf..6bfad3a4f0616 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java @@ -178,7 +178,7 @@ public void onEngineWillDestroy() { } @VisibleForTesting - /* package */ FlutterEngine createEngine(Context context) { + public FlutterEngine createEngine(Context context) { return new FlutterEngine(context); } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroupCache.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroupCache.java new file mode 100644 index 0000000000000..7eac25a310474 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroupCache.java @@ -0,0 +1,96 @@ +// 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import java.util.HashMap; +import java.util.Map; + +/** + * Static singleton cache that holds {@link io.flutter.embedding.engine.FlutterEngineGroup} + * instances identified by {@code String}s. + * + *

The ID of a given {@link io.flutter.embedding.engine.FlutterEngineGroup} can be whatever + * {@code String} is desired. + * + *

{@link io.flutter.embedding.android.FlutterActivity} and {@link + * io.flutter.embedding.android.FlutterFragment} use the {@code FlutterEngineGroupCache} singleton + * internally when instructed to use a cached {@link io.flutter.embedding.engine.FlutterEngineGroup} + * based on a given ID. See {@link + * io.flutter.embedding.android.FlutterActivity.NewEngineInGroupIntentBuilder} and {@link + * io.flutter.embedding.android.FlutterFragment#withNewEngineInGroup(String)} for related APIs. + */ +public class FlutterEngineGroupCache { + private static FlutterEngineGroupCache instance; + + /** + * Returns the static singleton instance of {@code FlutterEngineGroupCache}. + * + *

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

If a {@link io.flutter.embedding.engine.FlutterEngineGroup} is null, that {@link + * io.flutter.embedding.engine.FlutterEngineGroup} is removed from this cache. + */ + public void put(@NonNull String engineGroupId, @Nullable FlutterEngineGroup engineGroup) { + if (engineGroup != null) { + cachedEngineGroups.put(engineGroupId, engineGroup); + } else { + cachedEngineGroups.remove(engineGroupId); + } + } + + /** + * Removes any {@link io.flutter.embedding.engine.FlutterEngineGroup} that is currently in the + * cache that is identified by the given {@code engineGroupId}. + */ + public void remove(@NonNull String engineGroupId) { + put(engineGroupId, null); + } + + /** + * Removes all {@link io.flutter.embedding.engine.FlutterEngineGroup}'s that are currently in the + * cache. + */ + public void clear() { + cachedEngineGroups.clear(); + } +} 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 a50de5a38ba2d..c5cec2d48135f 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -27,6 +27,8 @@ import io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.Host; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterEngineGroup; +import io.flutter.embedding.engine.FlutterEngineGroupCache; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.loader.FlutterLoader; @@ -205,6 +207,56 @@ public void itThrowsExceptionIfCachedEngineDoesNotExist() { // Expect IllegalStateException. } + @Test + public void itUsesNewEngineInGroupWhenProvided() { + // ---- Test setup ---- + FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); + when(mockFlutterLoader.findAppBundlePath()).thenReturn("default_flutter_assets/path"); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build()); + FlutterEngineGroup flutterEngineGroup = mock(FlutterEngineGroup.class); + FlutterEngineGroupCache.getInstance().put("my_flutter_engine_group", flutterEngineGroup); + + // Adjust fake host to request cached engine group. + when(mockHost.getCachedEngineGroupId()).thenReturn("my_flutter_engine_group"); + when(mockHost.provideFlutterEngine(any(Context.class))).thenReturn(null); + when(mockHost.shouldAttachEngineToActivity()).thenReturn(false); + + // 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); + + // If the engine in FlutterEngineGroup is being used, it should have sent a resumed lifecycle + // event. + DartExecutor.DartEntrypoint entrypoint = new DartExecutor.DartEntrypoint("/fake/path", "main"); + verify(flutterEngineGroup, times(1)) + .createAndRunEngine(mockHost.getContext(), entrypoint, mockHost.getInitialRoute()); + } + + @Test(expected = IllegalStateException.class) + public void itThrowsExceptionIfNewEngineInGroupNotExist() { + // ---- Test setup ---- + FlutterEngineGroupCache.getInstance().clear(); + + // Adjust fake host to request cached engine group that does not exist. + when(mockHost.getCachedEngineGroupId()).thenReturn("my_flutter_engine_group"); + when(mockHost.getCachedEngineId()).thenReturn(null); + when(mockHost.provideFlutterEngine(any(Context.class))).thenReturn(null); + when(mockHost.shouldAttachEngineToActivity()).thenReturn(false); + + // 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 ---- diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java index c0fccca8ddae7..8aeaeb99c5bf0 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -153,6 +153,31 @@ public void itCreatesNewEngineIntentWithRequestedSettings() { assertEquals(TransparencyMode.transparent, flutterActivity.getTransparencyMode()); } + @Test + public void itCreatesNewEngineInGroupIntentWithRequestedSettings() { + Intent intent = + FlutterActivity.withNewEngineInGroup("my_cached_engine_group") + .dartEntrypoint("custom_entrypoint") + .initialRoute("/custom/route") + .backgroundMode(BackgroundMode.transparent) + .build(RuntimeEnvironment.application); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity flutterActivity = activityController.get(); + flutterActivity.setDelegate(new FlutterActivityAndFragmentDelegate(flutterActivity)); + + assertEquals("my_cached_engine_group", flutterActivity.getCachedEngineGroupId()); + assertEquals("custom_entrypoint", flutterActivity.getDartEntrypointFunctionName()); + assertEquals("/custom/route", flutterActivity.getInitialRoute()); + assertArrayEquals(new String[] {}, flutterActivity.getFlutterShellArgs().toArray()); + assertTrue(flutterActivity.shouldAttachEngineToActivity()); + assertTrue(flutterActivity.shouldDestroyEngineWithHost()); + assertNull(flutterActivity.getCachedEngineId()); + assertEquals(BackgroundMode.transparent, flutterActivity.getBackgroundMode()); + assertEquals(RenderMode.texture, flutterActivity.getRenderMode()); + assertEquals(TransparencyMode.transparent, flutterActivity.getTransparencyMode()); + } + @Test public void itReturnsValueFromMetaDataWhenCallsShouldHandleDeepLinkingCase1() throws PackageManager.NameNotFoundException { diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java index 3f483bf6e9582..99d738ec31795 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -294,6 +294,12 @@ public String getCachedEngineId() { return "my_flutter_engine"; } + @Nullable + @Override + public String getCachedEngineGroupId() { + return "my_flutter_engine_group"; + } + @Override public boolean shouldDestroyEngineWithHost() { return shouldDestroyEngineWithHost; diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java index d47992afa6106..c719727b604f6 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java @@ -390,5 +390,11 @@ public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedE return new CachedEngineIntentBuilder( FlutterFragmentActivityWithIntentBuilders.class, cachedEngineId); } + + public static NewEngineInGroupIntentBuilder withNewEngineInGroup( + @NonNull String engineGroupId) { + return new NewEngineInGroupIntentBuilder( + FlutterFragmentActivityWithIntentBuilders.class, engineGroupId); + } } } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java index 1a901b7e40a8a..a4f7f7d1de04a 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java @@ -70,6 +70,31 @@ public void itCreatesNewEngineFragmentWithRequestedSettings() { assertEquals(TransparencyMode.opaque, fragment.getTransparencyMode()); } + @Test + public void itCreatesNewEngineInGroupFragmentWithRequestedSettings() { + FlutterFragment fragment = + FlutterFragment.withNewEngineInGroup("my_cached_engine_group") + .dartEntrypoint("custom_entrypoint") + .initialRoute("/custom/route") + .shouldAttachEngineToActivity(false) + .handleDeeplinking(true) + .renderMode(RenderMode.texture) + .transparencyMode(TransparencyMode.opaque) + .build(); + fragment.setDelegate(new FlutterActivityAndFragmentDelegate(fragment)); + + assertEquals("my_cached_engine_group", fragment.getCachedEngineGroupId()); + assertEquals("custom_entrypoint", fragment.getDartEntrypointFunctionName()); + assertEquals("/custom/route", fragment.getInitialRoute()); + assertArrayEquals(new String[] {}, fragment.getFlutterShellArgs().toArray()); + assertFalse(fragment.shouldAttachEngineToActivity()); + assertTrue(fragment.shouldHandleDeeplinking()); + assertNull(fragment.getCachedEngineId()); + assertTrue(fragment.shouldDestroyEngineWithHost()); + assertEquals(RenderMode.texture, fragment.getRenderMode()); + assertEquals(TransparencyMode.opaque, fragment.getTransparencyMode()); + } + @Test public void itCreatesNewEngineFragmentThatDelaysFirstDrawWhenRequested() { FlutterFragment fragment = diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupCacheTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupCacheTest.java new file mode 100644 index 0000000000000..55802f6cecd74 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupCacheTest.java @@ -0,0 +1,96 @@ +package io.flutter.embedding.engine; + +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; +import static org.mockito.Mockito.when; + +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.loader.FlutterLoader; +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 FlutterEngineGroupCacheTest { + + private FlutterEngineGroup flutterEngineGroup; + + @Before + public void setup() { + // Create a mocked FlutterEngineGroup that provided to run this test case + FlutterInjector.reset(); + FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); + when(mockFlutterLoader.findAppBundlePath()).thenReturn("default_flutter_assets/path"); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build()); + flutterEngineGroup = mock(FlutterEngineGroup.class); + } + + @Test + public void itHoldsFlutterEngineGroups() { + // --- Test Setup --- + FlutterEngineGroupCache cache = new FlutterEngineGroupCache(); + + // --- Execute Test --- + cache.put("my_flutter_engine_group", flutterEngineGroup); + + // --- Verify Results --- + assertEquals(flutterEngineGroup, cache.get("my_flutter_engine_group")); + } + + @Test + public void itQueriesFlutterEngineGroupExistence() { + // --- Test Setup --- + FlutterEngineGroupCache cache = new FlutterEngineGroupCache(); + + // --- Execute Test --- + assertFalse(cache.contains("my_flutter_engine_group")); + + cache.put("my_flutter_engine_group", flutterEngineGroup); + + // --- Verify Results --- + assertTrue(cache.contains("my_flutter_engine_group")); + } + + @Test + public void itRemovesFlutterEngineGroups() { + // --- Test Setup --- + FlutterEngineGroupCache cache = new FlutterEngineGroupCache(); + + // --- Execute Test --- + cache.put("my_flutter_engine_group", flutterEngineGroup); + cache.remove("my_flutter_engine_group"); + + // --- Verify Results --- + assertNull(cache.get("my_flutter_engine_group")); + } + + @Test + public void itRemovesAllFlutterEngineGroups() { + // --- Test Setup --- + FlutterEngineGroup flutterEngineGroup1 = new FlutterEngineGroup(RuntimeEnvironment.application); + FlutterEngineGroup flutterEngineGroup2 = new FlutterEngineGroup(RuntimeEnvironment.application); + FlutterEngineGroupCache cache = new FlutterEngineGroupCache(); + + // --- Execute Test --- + cache.put("my_flutter_engine_group", flutterEngineGroup1); + cache.put("my_flutter_engine_group_2", flutterEngineGroup2); + + // --- Verify Results --- + assertEquals(flutterEngineGroup1, cache.get("my_flutter_engine_group")); + assertEquals(flutterEngineGroup2, cache.get("my_flutter_engine_group_2")); + + cache.clear(); + + // --- Verify Results --- + assertNull(cache.get("my_flutter_engine_group")); + assertNull(cache.get("my_flutter_engine_group_2")); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java index aa6ffb00ab75e..9c8d92ec403fc 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java @@ -72,7 +72,7 @@ public void setUp() { engineGroupUnderTest = new FlutterEngineGroup(RuntimeEnvironment.application) { @Override - FlutterEngine createEngine(Context context) { + public FlutterEngine createEngine(Context context) { return firstEngineUnderTest; } }; diff --git a/tools/android_lint/project.xml b/tools/android_lint/project.xml index 916da6b37a313..deed33fdfa363 100644 --- a/tools/android_lint/project.xml +++ b/tools/android_lint/project.xml @@ -35,6 +35,7 @@ + @@ -145,6 +146,7 @@ +