mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
DynamicFeatureChannel MethodChannel and Install state tracking (flutter/engine#22833)
This commit is contained in:
parent
6d0fe946e2
commit
d337acaa97
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
|
||||
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.plugins.service.ServiceControlSurface;
|
||||
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 class 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 class 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 class FlutterEngine {
|
||||
flutterJNI.detachFromNativeAndReleaseResources();
|
||||
if (FlutterInjector.instance().dynamicFeatureManager() != null) {
|
||||
FlutterInjector.instance().dynamicFeatureManager().destroy();
|
||||
dynamicFeatureChannel.setDynamicFeatureManager(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,6 +496,12 @@ public class FlutterEngine {
|
||||
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() {
|
||||
|
||||
@ -1010,7 +1010,7 @@ public class FlutterJNI {
|
||||
@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(
|
||||
|
||||
@ -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 @@ import io.flutter.embedding.engine.FlutterJNI;
|
||||
* 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>A DynamicFeatureChannel is required to handle assets-only dynamic features and manually
|
||||
* installed dynamic features.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>If no dynamic feature has been installed or requested to be installed by the provided
|
||||
* loadingUnitId or moduleName, then this method will return null.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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 {
|
||||
*
|
||||
* <p>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.
|
||||
|
||||
@ -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.SplitInstallErrorCode;
|
||||
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<String> sessionIdToName;
|
||||
private @NonNull SparseIntArray sessionIdToLoadingUnitId;
|
||||
private @NonNull SparseArray<String> sessionIdToState;
|
||||
private @NonNull Map<String, Integer> nameToSessionId;
|
||||
|
||||
private FeatureInstallStateUpdatedListener listener;
|
||||
|
||||
private class FeatureInstallStateUpdatedListener implements SplitInstallStateUpdatedListener {
|
||||
@SuppressLint("DefaultLocale")
|
||||
public void onStateUpdate(SplitInstallSessionState state) {
|
||||
if (sessionIdToName.get(state.sessionId()) != null) {
|
||||
// TODO(garyq): Add system channel for split aot messages.
|
||||
int sessionId = state.sessionId();
|
||||
if (sessionIdToName.get(sessionId) != null) {
|
||||
switch (state.status()) {
|
||||
case SplitInstallSessionStatus.FAILED:
|
||||
{
|
||||
@ -56,15 +64,18 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) install failed with: %s",
|
||||
sessionIdToName.get(state.sessionId()),
|
||||
state.sessionId(),
|
||||
state.errorCode()));
|
||||
sessionIdToName.get(sessionId), sessionId, state.errorCode()));
|
||||
flutterJNI.dynamicFeatureInstallFailure(
|
||||
sessionIdToLoadingUnitId.get(state.sessionId()),
|
||||
sessionIdToLoadingUnitId.get(sessionId),
|
||||
"Module install failed with " + state.errorCode(),
|
||||
true);
|
||||
sessionIdToName.delete(state.sessionId());
|
||||
sessionIdToLoadingUnitId.delete(state.sessionId());
|
||||
if (channel != null) {
|
||||
channel.completeInstallError(
|
||||
sessionIdToName.get(sessionId), "Android Dynamic Feature failed to install.");
|
||||
}
|
||||
sessionIdToName.delete(sessionId);
|
||||
sessionIdToLoadingUnitId.delete(sessionId);
|
||||
sessionIdToState.put(sessionId, "failed");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.INSTALLED:
|
||||
@ -73,18 +84,22 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) install successfully.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
loadAssets(
|
||||
sessionIdToLoadingUnitId.get(state.sessionId()),
|
||||
sessionIdToName.get(state.sessionId()));
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
loadAssets(sessionIdToLoadingUnitId.get(sessionId), sessionIdToName.get(sessionId));
|
||||
// We only load Dart shared lib for the loading unit id requested. Other loading units
|
||||
// (if present) in the dynamic feature module are not loaded, but can be loaded by
|
||||
// calling again with their loading unit id.
|
||||
loadDartLibrary(
|
||||
sessionIdToLoadingUnitId.get(state.sessionId()),
|
||||
sessionIdToName.get(state.sessionId()));
|
||||
sessionIdToName.delete(state.sessionId());
|
||||
sessionIdToLoadingUnitId.delete(state.sessionId());
|
||||
// calling again with their loading unit id. If no valid loadingUnitId was included in
|
||||
// the installation request such as for an asset only feature, then we can skip this.
|
||||
if (sessionIdToLoadingUnitId.get(sessionId) > 0) {
|
||||
loadDartLibrary(
|
||||
sessionIdToLoadingUnitId.get(sessionId), sessionIdToName.get(sessionId));
|
||||
}
|
||||
if (channel != null) {
|
||||
channel.completeInstallSuccess(sessionIdToName.get(sessionId));
|
||||
}
|
||||
sessionIdToName.delete(sessionId);
|
||||
sessionIdToLoadingUnitId.delete(sessionId);
|
||||
sessionIdToState.put(sessionId, "installed");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.CANCELED:
|
||||
@ -93,9 +108,15 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) install canceled.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
sessionIdToName.delete(state.sessionId());
|
||||
sessionIdToLoadingUnitId.delete(state.sessionId());
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
if (channel != null) {
|
||||
channel.completeInstallError(
|
||||
sessionIdToName.get(sessionId),
|
||||
"Android Dynamic Feature installation canceled.");
|
||||
}
|
||||
sessionIdToName.delete(sessionId);
|
||||
sessionIdToLoadingUnitId.delete(sessionId);
|
||||
sessionIdToState.put(sessionId, "cancelled");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.CANCELING:
|
||||
@ -104,7 +125,8 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) install canceling.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
sessionIdToState.put(sessionId, "canceling");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.PENDING:
|
||||
@ -113,7 +135,8 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) install pending.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
sessionIdToState.put(sessionId, "pending");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION:
|
||||
@ -122,7 +145,8 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) install requires user confirmation.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
sessionIdToState.put(sessionId, "requires_user_confirmation");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.DOWNLOADING:
|
||||
@ -131,7 +155,8 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) downloading.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
sessionIdToState.put(sessionId, "downloading");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.DOWNLOADED:
|
||||
@ -140,7 +165,8 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) downloaded.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
sessionIdToState.put(sessionId, "downloaded");
|
||||
break;
|
||||
}
|
||||
case SplitInstallSessionStatus.INSTALLING:
|
||||
@ -149,11 +175,12 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
TAG,
|
||||
String.format(
|
||||
"Module \"%s\" (sessionId %d) installing.",
|
||||
sessionIdToName.get(state.sessionId()), state.sessionId()));
|
||||
sessionIdToName.get(sessionId), sessionId));
|
||||
sessionIdToState.put(sessionId, "installing");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Log.d(TAG, "Status: " + state.status());
|
||||
Log.d(TAG, "Unknown status: " + state.status());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -167,6 +194,8 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
splitInstallManager.registerListener(listener);
|
||||
sessionIdToName = new SparseArray<>();
|
||||
sessionIdToLoadingUnitId = new SparseIntArray();
|
||||
sessionIdToState = new SparseArray<>();
|
||||
nameToSessionId = new HashMap<>();
|
||||
}
|
||||
|
||||
public void setJNI(@NonNull FlutterJNI flutterJNI) {
|
||||
@ -183,6 +212,10 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setDynamicFeatureChannel(DynamicFeatureChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
private String loadingUnitIdToModuleName(int loadingUnitId) {
|
||||
// Loading unit id to module name mapping stored in android Strings
|
||||
// resources.
|
||||
@ -193,11 +226,13 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
return context.getResources().getString(moduleNameIdentifier);
|
||||
}
|
||||
|
||||
public void downloadDynamicFeature(int loadingUnitId, String moduleName) {
|
||||
public void installDynamicFeature(int loadingUnitId, String moduleName) {
|
||||
String resolvedModuleName =
|
||||
moduleName != null ? moduleName : loadingUnitIdToModuleName(loadingUnitId);
|
||||
if (resolvedModuleName == null) {
|
||||
Log.d(TAG, "Dynamic feature module name was null.");
|
||||
Log.e(
|
||||
TAG,
|
||||
"Dynamic feature module name was null and could not be resolved from loading unit id.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -213,8 +248,13 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
// install which is handled in FeatureInstallStateUpdatedListener.
|
||||
.addOnSuccessListener(
|
||||
sessionId -> {
|
||||
this.sessionIdToName.put(sessionId, resolvedModuleName);
|
||||
this.sessionIdToLoadingUnitId.put(sessionId, loadingUnitId);
|
||||
sessionIdToName.put(sessionId, resolvedModuleName);
|
||||
sessionIdToLoadingUnitId.put(sessionId, loadingUnitId);
|
||||
if (nameToSessionId.containsKey(resolvedModuleName)) {
|
||||
sessionIdToState.remove(nameToSessionId.get(resolvedModuleName));
|
||||
}
|
||||
nameToSessionId.put(resolvedModuleName, sessionId);
|
||||
sessionIdToState.put(sessionId, "Requested");
|
||||
})
|
||||
.addOnFailureListener(
|
||||
exception -> {
|
||||
@ -249,6 +289,22 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
});
|
||||
}
|
||||
|
||||
public String getDynamicFeatureInstallState(int loadingUnitId, String moduleName) {
|
||||
String resolvedModuleName =
|
||||
moduleName != null ? moduleName : loadingUnitIdToModuleName(loadingUnitId);
|
||||
if (resolvedModuleName == null) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Dynamic feature module name was null and could not be resolved from loading unit id.");
|
||||
return null;
|
||||
}
|
||||
if (!nameToSessionId.containsKey(resolvedModuleName)) {
|
||||
return null;
|
||||
}
|
||||
int sessionId = nameToSessionId.get(resolvedModuleName);
|
||||
return sessionIdToState.get(sessionId);
|
||||
}
|
||||
|
||||
public void loadAssets(int loadingUnitId, String moduleName) {
|
||||
if (!verifyJNI()) {
|
||||
return;
|
||||
@ -336,6 +392,7 @@ public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager {
|
||||
|
||||
public void destroy() {
|
||||
splitInstallManager.unregisterListener(listener);
|
||||
channel = null;
|
||||
flutterJNI = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,129 @@
|
||||
// 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.systemchannels;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
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.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.StandardMethodCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Method channel that handles manual installation requests and queries for installation state for
|
||||
* dynamic feature modules.
|
||||
*
|
||||
* <p>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<String, List<MethodChannel.Result>> moduleNameToResults;
|
||||
|
||||
@NonNull @VisibleForTesting
|
||||
final MethodChannel.MethodCallHandler parsingMethodHandler =
|
||||
new MethodChannel.MethodCallHandler() {
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
if (dynamicFeatureManager == null) {
|
||||
// If no DynamicFeatureManager has been injected, then this channel is a no-op.
|
||||
return;
|
||||
}
|
||||
String method = call.method;
|
||||
Map<String, Object> args = call.arguments();
|
||||
Log.v(TAG, "Received '" + method + "' message.");
|
||||
final int loadingUnitId = (int) args.get("loadingUnitId");
|
||||
final String moduleName = (String) args.get("moduleName");
|
||||
switch (method) {
|
||||
case "installDynamicFeature":
|
||||
dynamicFeatureManager.installDynamicFeature(loadingUnitId, moduleName);
|
||||
if (!moduleNameToResults.containsKey(moduleName)) {
|
||||
moduleNameToResults.put(moduleName, new ArrayList<>());
|
||||
}
|
||||
moduleNameToResults.get(moduleName).add(result);
|
||||
break;
|
||||
case "getDynamicFeatureInstallState":
|
||||
result.success(
|
||||
dynamicFeatureManager.getDynamicFeatureInstallState(loadingUnitId, moduleName));
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs a {@code DynamicFeatureChannel} that connects Android to the Dart code running in
|
||||
* {@code dartExecutor}.
|
||||
*
|
||||
* <p>The given {@code dartExecutor} is permitted to be idle or executing code.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@ public class PlayStoreDynamicFeatureManagerTest {
|
||||
}
|
||||
|
||||
@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 class PlayStoreDynamicFeatureManagerTest {
|
||||
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 class PlayStoreDynamicFeatureManagerTest {
|
||||
|
||||
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 class PlayStoreDynamicFeatureManagerTest {
|
||||
|
||||
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 class PlayStoreDynamicFeatureManagerTest {
|
||||
|
||||
assertEquals(jni.loadingUnitId, 0);
|
||||
|
||||
playStoreManager.downloadDynamicFeature(123, "TestModuleName");
|
||||
playStoreManager.installDynamicFeature(123, "TestModuleName");
|
||||
assertEquals(jni.loadDartDeferredLibraryCalled, 1);
|
||||
assertEquals(jni.updateAssetManagerCalled, 1);
|
||||
assertEquals(jni.dynamicFeatureInstallFailureCalled, 0);
|
||||
|
||||
@ -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<String, Object> args = new HashMap<>();
|
||||
args.put("loadingUnitId", -1);
|
||||
args.put("moduleName", "hello");
|
||||
MethodCall methodCall = new MethodCall("installDynamicFeature", args);
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
fakeDynamicFeatureChannel.parsingMethodHandler.onMethodCall(methodCall, mockResult);
|
||||
|
||||
testDynamicFeatureManager.completeInstall();
|
||||
verify(mockResult).success(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicFeatureChannel_installCompletesMultipleResults() {
|
||||
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<String, Object> args = new HashMap<>();
|
||||
args.put("loadingUnitId", -1);
|
||||
args.put("moduleName", "hello");
|
||||
MethodCall methodCall = new MethodCall("installDynamicFeature", args);
|
||||
MethodChannel.Result mockResult1 = mock(MethodChannel.Result.class);
|
||||
MethodChannel.Result mockResult2 = mock(MethodChannel.Result.class);
|
||||
fakeDynamicFeatureChannel.parsingMethodHandler.onMethodCall(methodCall, mockResult1);
|
||||
fakeDynamicFeatureChannel.parsingMethodHandler.onMethodCall(methodCall, mockResult2);
|
||||
|
||||
testDynamicFeatureManager.completeInstall();
|
||||
verify(mockResult1).success(null);
|
||||
verify(mockResult2).success(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicFeatureChannel_getInstallState() {
|
||||
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<String, Object> args = new HashMap<>();
|
||||
args.put("loadingUnitId", -1);
|
||||
args.put("moduleName", "hello");
|
||||
MethodCall methodCall = new MethodCall("getDynamicFeatureInstallState", args);
|
||||
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
|
||||
fakeDynamicFeatureChannel.parsingMethodHandler.onMethodCall(methodCall, mockResult);
|
||||
|
||||
testDynamicFeatureManager.completeInstall();
|
||||
verify(mockResult).success("installed");
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user