diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index e87e78e4dbadd..827129219ee43 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -783,6 +783,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/render FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index e84c60754f61a..9aa06b754774b 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -188,6 +188,7 @@ android_java_sources = [ "io/flutter/embedding/engine/renderer/RenderSurface.java", "io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java", "io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java", + "io/flutter/embedding/engine/systemchannels/DynamicFeatureChannel.java", "io/flutter/embedding/engine/systemchannels/KeyEventChannel.java", "io/flutter/embedding/engine/systemchannels/LifecycleChannel.java", "io/flutter/embedding/engine/systemchannels/LocalizationChannel.java", @@ -477,6 +478,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java", "test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java", "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", + "test/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannelTest.java", "test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java", "test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java", "test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java", diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 556b4cd3af021..ec489f25ae667 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -12,6 +12,7 @@ import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.dynamicfeatures.DynamicFeatureManager; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.PluginRegistry; import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface; @@ -21,6 +22,7 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.embedding.engine.systemchannels.DynamicFeatureChannel; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; import io.flutter.embedding.engine.systemchannels.LocalizationChannel; @@ -81,6 +83,7 @@ public class FlutterEngine { // System channels. @NonNull private final AccessibilityChannel accessibilityChannel; + @NonNull private final DynamicFeatureChannel dynamicFeatureChannel; @NonNull private final KeyEventChannel keyEventChannel; @NonNull private final LifecycleChannel lifecycleChannel; @NonNull private final LocalizationChannel localizationChannel; @@ -275,7 +278,11 @@ public FlutterEngine( this.dartExecutor = new DartExecutor(flutterJNI, assetManager); this.dartExecutor.onAttachedToJNI(); + DynamicFeatureManager dynamicFeatureManager = + FlutterInjector.instance().dynamicFeatureManager(); + accessibilityChannel = new AccessibilityChannel(dartExecutor, flutterJNI); + dynamicFeatureChannel = new DynamicFeatureChannel(dartExecutor); keyEventChannel = new KeyEventChannel(dartExecutor); lifecycleChannel = new LifecycleChannel(dartExecutor); localizationChannel = new LocalizationChannel(dartExecutor); @@ -287,6 +294,10 @@ public FlutterEngine( systemChannel = new SystemChannel(dartExecutor); textInputChannel = new TextInputChannel(dartExecutor); + if (dynamicFeatureManager != null) { + dynamicFeatureManager.setDynamicFeatureChannel(dynamicFeatureChannel); + } + this.localizationPlugin = new LocalizationPlugin(context, localizationChannel); this.flutterJNI = flutterJNI; @@ -380,6 +391,7 @@ public void destroy() { flutterJNI.detachFromNativeAndReleaseResources(); if (FlutterInjector.instance().dynamicFeatureManager() != null) { FlutterInjector.instance().dynamicFeatureManager().destroy(); + dynamicFeatureChannel.setDynamicFeatureManager(null); } } @@ -484,6 +496,12 @@ public SettingsChannel getSettingsChannel() { return settingsChannel; } + /** System channel that allows manual installation and state querying of dynamic features. */ + @NonNull + public DynamicFeatureChannel getDynamicFeatureChannel() { + return dynamicFeatureChannel; + } + /** System channel that sends memory pressure warnings from Android to Flutter. */ @NonNull public SystemChannel getSystemChannel() { diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 94576fb302f10..85d49909cea9d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -1010,7 +1010,7 @@ public void setDynamicFeatureManager(@Nullable DynamicFeatureManager dynamicFeat @UiThread public void requestDartDeferredLibrary(int loadingUnitId) { if (dynamicFeatureManager != null) { - dynamicFeatureManager.downloadDynamicFeature(loadingUnitId, null); + dynamicFeatureManager.installDynamicFeature(loadingUnitId, null); } else { // TODO(garyq): Add link to setup/instructions guide wiki. Log.e( diff --git a/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java index 747f6df01d56f..5acdf59c06602 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java +++ b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java @@ -5,6 +5,7 @@ package io.flutter.embedding.engine.dynamicfeatures; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.systemchannels.DynamicFeatureChannel; // TODO: add links to external documentation on how to use split aot features. /** @@ -20,14 +21,14 @@ * deferred imported library. See https://dart.dev/guides/language/language-tour#deferred-loading * This call retrieves a unique identifier called the loading unit id, which is assigned by * gen_snapshot during compilation. The loading unit id is passed down through the engine and - * invokes downloadDynamicFeature. Once the feature module is downloaded, loadAssets and + * invokes installDynamicFeature. Once the feature module is downloaded, loadAssets and * loadDartLibrary should be invoked. loadDartLibrary should find shared library .so files for the * engine to open and pass the .so path to FlutterJNI.loadDartDeferredLibrary. loadAssets should * typically ensure the new assets are available to the engine's asset manager by passing an updated * Android AssetManager to the engine via FlutterJNI.updateAssetManager. * *
The loadAssets and loadDartLibrary methods are separated out because they may also be called - * manually via platform channel messages. A full downloadDynamicFeature implementation should call + * manually via platform channel messages. A full installDynamicFeature implementation should call * these two methods as needed. * *
A dynamic feature module is uniquely identified by a module name as defined in @@ -46,6 +47,27 @@ public interface DynamicFeatureManager { */ public abstract void setJNI(FlutterJNI flutterJNI); + /** + * Sets the DynamicFeatureChannel system channel to handle the framework API to directly call + * methods in DynamicFeatureManager. + * + *
A DynamicFeatureChannel is required to handle assets-only dynamic features and manually + * installed dynamic features. + * + *
Since this class may be instantiated for injection before the FlutterEngine and System + * Channels are initialized, this method should be called to provide the DynamicFeatureChannel. + * Similarly, the {@link DynamicFeatureChannel.setDynamicFeatureManager} method should also be + * called with this DynamicFeatureManager instance to properly forward method invocations. + * + *
The {@link DynamicFeatureChannel} passes manual invocations of {@link installDynamicFeature} + * and {@link getDynamicFeatureInstallState} from the method channel to this + * DynamicFeatureManager. Upon completion of the install process, sucessful installations should + * notify the DynamicFeatureChannel by calling {@link + * DynamicFeatureChannel.completeInstallSuccess} while errors and failures should call {@link + * DynamicFeatureChannel.completeInstallError}. + */ + public abstract void setDynamicFeatureChannel(DynamicFeatureChannel channel); + /** * Request that the feature module be downloaded and installed. * @@ -53,7 +75,10 @@ public interface DynamicFeatureManager { * example, the Play Store dynamic delivery implementation uses SplitInstallManager to request the * download of the module. Download is not complete when this method returns. The download process * should be listened for and upon completion of download, listeners should invoke loadAssets - * first and then loadDartLibrary to complete the dynamic feature load process. + * first and then loadDartLibrary to complete the dynamic feature load process. Assets-only + * dynamic features should also call {@link DynamicFeatureChannel.completeInstallSuccess} or + * {@link DynamicFeatureChannel.completeInstallError} to complete the method channel invocation's + * dart Future. * *
Both parameters are not always necessary to identify which module to install. Asset-only * modules do not have an associated loadingUnitId. Instead, an invalid ID like -1 may be passed @@ -62,12 +87,21 @@ public interface DynamicFeatureManager { * one of loadingUnitId or moduleName must be valid or non-null. * *
Flutter will typically call this method in two ways. When invoked as part of a dart - * loadLibrary() call, a valid loadingUnitId is passed in while the moduleName is null. In this + * `loadLibrary()` call, a valid loadingUnitId is passed in while the moduleName is null. In this * case, this method is responsible for figuring out what module the loadingUnitId corresponds to. * *
When invoked manually as part of loading an assets-only module, loadingUnitId is -1 * (invalid) and moduleName is supplied. Without a loadingUnitId, this method just downloads the - * module by name and attempts to load assets via loadAssets. + * module by name and attempts to load assets via loadAssets while loadDartLibrary is skipped, + * even if the dynamic feature module includes valid dart libs. To load dart libs, call + * `loadLibrary()` using the first way described in the previous paragraph as the method channel + * invocation will not load dart shared libraries. + * + *
While the Future retuned by either `loadLibary` or the method channel invocation will + * indicate when the code and assets are ready to be used, informational querying of the install + * process' state can be done with {@link getDynamicFeatureInstallState}, though the results of + * this query should not be used to decide if the dynamic feature is ready to use. Only the Future + * completion should be used to do this. * * @param loadingUnitId The unique identifier associated with a Dart deferred library. This id is * assigned by the compiler and can be seen for reference in bundle_config.yaml. This ID is @@ -85,7 +119,41 @@ public interface DynamicFeatureManager { * associated Dart deferred library, loading unit id should a negative value and moduleName * must be non-null. */ - public abstract void downloadDynamicFeature(int loadingUnitId, String moduleName); + public abstract void installDynamicFeature(int loadingUnitId, String moduleName); + + /** + * Gets the current state of the installation session corresponding to the specified loadingUnitId + * and/or moduleName. + * + *
Invocations of {@link installDynamicFeature} typically result in asynchronous downloading + * and other tasks. This method enables querying of the state of the installation. Querying the + * installation state is purely informational and does not impact the installation process. The + * results of this query should not be used to decide if the dynamic feature is ready to use. Upon + * completion of installation, the Future returned by the installation request will complete. Only + * after dart Future completion is it safe to use code and assets from the dynamic feature. + * + *
If no dynamic feature has been installed or requested to be installed by the provided + * loadingUnitId or moduleName, then this method will return null. + * + *
Depending on the implementation, the returned String may vary. The Play store default + * implementation begins in the "requested" state before transitioning to the "downloading" and + * "installed" states. + * + *
Only sucessfully requested modules have state. Modules that are invalid or have not been + * requested with {@link installDynamicFeature} will not have a state. Due to the asynchronous + * nature of the download process, modules may not immediately have a valid state upon return of + * {@link installDynamicFeature}, though valid modules will eventually obtain a state. + * + *
Both parameters are not always necessary to identify which module to install. Asset-only + * modules do not have an associated loadingUnitId. Instead, an invalid ID like -1 may be passed + * to query only with moduleName. On the other hand, it can be possible to resolve the moduleName + * based on the loadingUnitId. This resolution is done if moduleName is null. At least one of + * loadingUnitId or moduleName must be valid or non-null. + * + * @param loadingUnitId The unique identifier associated with a Dart deferred library. + * @param moduleName The dynamic feature module name as defined in bundle_config.yaml. + */ + public abstract String getDynamicFeatureInstallState(int loadingUnitId, String moduleName); /** * Extract and load any assets and resources from the module for use by Flutter. @@ -102,7 +170,7 @@ public interface DynamicFeatureManager { * *
Assets shoud be loaded before the Dart deferred library is loaded, as successful loading of
* the Dart loading unit indicates the dynamic feature is fully loaded. Implementations of
- * downloadDynamicFeature should invoke this after successful download.
+ * installDynamicFeature should invoke this after successful download.
*
* @param loadingUnitId The unique identifier associated with a Dart deferred library.
* @param moduleName The dynamic feature module name as defined in bundle_config.yaml.
diff --git a/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java
index 406392cd28b53..2e1b8592a2dd4 100644
--- a/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java
+++ b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java
@@ -4,6 +4,7 @@
package io.flutter.embedding.engine.dynamicfeatures;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
@@ -22,10 +23,13 @@
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterJNI;
+import io.flutter.embedding.engine.systemchannels.DynamicFeatureChannel;
import java.io.File;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Queue;
/**
@@ -37,18 +41,22 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
private @NonNull SplitInstallManager splitInstallManager;
private @Nullable FlutterJNI flutterJNI;
+ private @Nullable DynamicFeatureChannel channel;
private @NonNull Context context;
// Each request to install a feature module gets a session ID. These maps associate
// the session ID with the loading unit and module name that was requested.
private @NonNull SparseArray This channel is able to handle multiple simultaneous installation requests
+ */
+public class DynamicFeatureChannel {
+ private static final String TAG = "DynamicFeatureChannel";
+
+ @NonNull private final MethodChannel channel;
+ @Nullable private DynamicFeatureManager dynamicFeatureManager;
+ // Track the Result objects to be able to handle multiple install requests of
+ // the same module at a time. When installation enters a terminal state, either
+ // completeInstallSuccess or completeInstallError can be called.
+ @NonNull private Map The given {@code dartExecutor} is permitted to be idle or executing code.
+ *
+ * See {@link DartExecutor}.
+ */
+ public DynamicFeatureChannel(@NonNull DartExecutor dartExecutor) {
+ this.channel =
+ new MethodChannel(dartExecutor, "flutter/dynamicfeature", StandardMethodCodec.INSTANCE);
+ channel.setMethodCallHandler(parsingMethodHandler);
+ dynamicFeatureManager = FlutterInjector.instance().dynamicFeatureManager();
+ moduleNameToResults = new HashMap<>();
+ }
+
+ /**
+ * Sets the DynamicFeatureManager to exectue method channel calls with.
+ *
+ * @param dynamicFeatureManager the DynamicFeatureManager to use.
+ */
+ @VisibleForTesting
+ public void setDynamicFeatureManager(@Nullable DynamicFeatureManager dynamicFeatureManager) {
+ this.dynamicFeatureManager = dynamicFeatureManager;
+ }
+
+ /**
+ * Finishes the `installDynamicFeature` method channel call for the specified moduleName with a
+ * success.
+ *
+ * @param moduleName The name of the android dynamic feature module install request to complete.
+ */
+ public void completeInstallSuccess(String moduleName) {
+ if (moduleNameToResults.containsKey(moduleName)) {
+ for (MethodChannel.Result result : moduleNameToResults.get(moduleName)) {
+ result.success(null);
+ }
+ moduleNameToResults.get(moduleName).clear();
+ }
+ return;
+ }
+
+ /**
+ * Finishes the `installDynamicFeature` method channel call for the specified moduleName with an
+ * error/failure.
+ *
+ * @param moduleName The name of the android dynamic feature module install request to complete.
+ * @param errorMessage The error message to display to complete the future with.
+ */
+ public void completeInstallError(String moduleName, String errorMessage) {
+ if (moduleNameToResults.containsKey(moduleName)) {
+ for (MethodChannel.Result result : moduleNameToResults.get(moduleName)) {
+ result.error("DynamicFeature Install failure", errorMessage, null);
+ }
+ moduleNameToResults.get(moduleName).clear();
+ }
+ return;
+ }
+}
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManagerTest.java b/shell/platform/android/test/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManagerTest.java
index 1fafbb3437890..d6c7bea3c160f 100644
--- a/shell/platform/android/test/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManagerTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManagerTest.java
@@ -65,7 +65,7 @@ public TestPlayStoreDynamicFeatureManager(Context context, FlutterJNI jni) {
}
@Override
- public void downloadDynamicFeature(int loadingUnitId, String moduleName) {
+ public void installDynamicFeature(int loadingUnitId, String moduleName) {
// Override this to skip the online SplitInstallManager portion.
loadAssets(loadingUnitId, moduleName);
loadDartLibrary(loadingUnitId, moduleName);
@@ -85,7 +85,7 @@ public void downloadCallsJNIFunctions() throws NameNotFoundException {
jni.setDynamicFeatureManager(playStoreManager);
assertEquals(jni.loadingUnitId, 0);
- playStoreManager.downloadDynamicFeature(123, "TestModuleName");
+ playStoreManager.installDynamicFeature(123, "TestModuleName");
assertEquals(jni.loadDartDeferredLibraryCalled, 1);
assertEquals(jni.updateAssetManagerCalled, 1);
assertEquals(jni.dynamicFeatureInstallFailureCalled, 0);
@@ -109,7 +109,7 @@ public void searchPathsAddsApks() throws NameNotFoundException {
assertEquals(jni.loadingUnitId, 0);
- playStoreManager.downloadDynamicFeature(123, "TestModuleName");
+ playStoreManager.installDynamicFeature(123, "TestModuleName");
assertEquals(jni.loadDartDeferredLibraryCalled, 1);
assertEquals(jni.updateAssetManagerCalled, 1);
assertEquals(jni.dynamicFeatureInstallFailureCalled, 0);
@@ -133,7 +133,7 @@ public void invalidSearchPathsAreIgnored() throws NameNotFoundException {
assertEquals(jni.loadingUnitId, 0);
- playStoreManager.downloadDynamicFeature(123, "TestModuleName");
+ playStoreManager.installDynamicFeature(123, "TestModuleName");
assertEquals(jni.loadDartDeferredLibraryCalled, 1);
assertEquals(jni.updateAssetManagerCalled, 1);
assertEquals(jni.dynamicFeatureInstallFailureCalled, 0);
@@ -156,7 +156,7 @@ public void assetManagerUpdateInvoked() throws NameNotFoundException {
assertEquals(jni.loadingUnitId, 0);
- playStoreManager.downloadDynamicFeature(123, "TestModuleName");
+ playStoreManager.installDynamicFeature(123, "TestModuleName");
assertEquals(jni.loadDartDeferredLibraryCalled, 1);
assertEquals(jni.updateAssetManagerCalled, 1);
assertEquals(jni.dynamicFeatureInstallFailureCalled, 0);
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannelTest.java
new file mode 100644
index 0000000000000..591d063b2a8e3
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannelTest.java
@@ -0,0 +1,115 @@
+package io.flutter.embedding.engine.systemchannels;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.AssetManager;
+import io.flutter.embedding.engine.FlutterJNI;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.embedding.engine.dynamicfeatures.DynamicFeatureManager;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+class TestDynamicFeatureManager implements DynamicFeatureManager {
+ DynamicFeatureChannel channel;
+ String moduleName;
+
+ public void setJNI(FlutterJNI flutterJNI) {}
+
+ public void setDynamicFeatureChannel(DynamicFeatureChannel channel) {
+ this.channel = channel;
+ }
+
+ public void installDynamicFeature(int loadingUnitId, String moduleName) {
+ this.moduleName = moduleName;
+ }
+
+ public void completeInstall() {
+ channel.completeInstallSuccess(moduleName);
+ }
+
+ public String getDynamicFeatureInstallState(int loadingUnitId, String moduleName) {
+ return "installed";
+ }
+
+ public void loadAssets(int loadingUnitId, String moduleName) {}
+
+ public void loadDartLibrary(int loadingUnitId, String moduleName) {}
+
+ public void uninstallFeature(int loadingUnitId, String moduleName) {}
+
+ public void destroy() {}
+}
+
+@Config(manifest = Config.NONE)
+@RunWith(RobolectricTestRunner.class)
+public class DynamicFeatureChannelTest {
+ @Test
+ public void dynamicFeatureChannel_installCompletesResults() {
+ MethodChannel rawChannel = mock(MethodChannel.class);
+ FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
+ DartExecutor dartExecutor = new DartExecutor(mockFlutterJNI, mock(AssetManager.class));
+ TestDynamicFeatureManager testDynamicFeatureManager = new TestDynamicFeatureManager();
+ DynamicFeatureChannel fakeDynamicFeatureChannel = new DynamicFeatureChannel(dartExecutor);
+ fakeDynamicFeatureChannel.setDynamicFeatureManager(testDynamicFeatureManager);
+
+ Map