DynamicFeatureChannel MethodChannel and Install state tracking (flutter/engine#22833)

This commit is contained in:
Gary Qian 2020-12-14 16:43:56 -07:00 committed by GitHub
parent 6d0fe946e2
commit d337acaa97
9 changed files with 435 additions and 45 deletions

View File

@ -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

View File

@ -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",

View File

@ -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() {

View File

@ -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(

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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");
}
}