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 sessionIdToName; private @NonNull SparseIntArray sessionIdToLoadingUnitId; + private @NonNull SparseArray sessionIdToState; + private @NonNull Map 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 void onStateUpdate(SplitInstallSessionState state) { 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 PlayStoreDynamicFeatureManager(@NonNull Context context, @Nullable Flutte 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 @@ private boolean verifyJNI() { 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 @@ private String loadingUnitIdToModuleName(int loadingUnitId) { 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 void downloadDynamicFeature(int loadingUnitId, String moduleName) { // 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 void downloadDynamicFeature(int loadingUnitId, String moduleName) { }); } + 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 void uninstallFeature(int loadingUnitId, String moduleName) { public void destroy() { splitInstallManager.unregisterListener(listener); + channel = null; flutterJNI = null; } } diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannel.java new file mode 100644 index 0000000000000..4535fff8685ef --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/DynamicFeatureChannel.java @@ -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. + * + *

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> 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 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}. + * + *

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