diff --git a/DEPS b/DEPS index 679383dd9f702..96a25ad91cc4c 100644 --- a/DEPS +++ b/DEPS @@ -482,7 +482,7 @@ deps = { 'packages': [ { 'package': 'flutter/android/embedding_bundle', - 'version': 'last_updated:2020-05-20T01:36:16-0700' + 'version': 'last_updated:2020-09-11T17:57:41-0700' } ], 'condition': 'download_android_deps', diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index b2a91424b1af5..d7eb1dac4dc2a 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -747,6 +747,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 6c9304f1ad945..1b9c02413138b 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -507,4 +507,21 @@ const std::string& Engine::GetLastEntrypointLibrary() const { return last_entry_point_library_; } +// The Following commented out code connects into part 2 of the split AOT +// feature. Left commented out until it lands: + +// // |RuntimeDelegate| +// void Engine::RequestDartDeferredLibrary(intptr_t loading_unit_id) { +// return delegate_.RequestDartDeferredLibrary(loading_unit_id); +// } + +void Engine::LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) { + if (runtime_controller_->IsRootIsolateRunning()) { + // runtime_controller_->LoadDartDeferredLibrary(loading_unit_id, + // snapshot_data, snapshot_instructions); + } +} + } // namespace flutter diff --git a/shell/common/engine.h b/shell/common/engine.h index 1eb2cf36ecc72..93a5f2367dbc3 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -260,6 +260,21 @@ class Engine final : public RuntimeDelegate, virtual std::unique_ptr> ComputePlatformResolvedLocale( const std::vector& supported_locale_data) = 0; + + //-------------------------------------------------------------------------- + /// @brief Invoked when the Dart VM requests that a deferred library + /// be loaded. Notifies the engine that the deferred library + /// identified by the specified loading unit id should be + /// downloaded and loaded into the Dart VM via + /// `LoadDartDeferredLibrary` + /// + /// @param[in] loading_unit_id The unique id of the deferred library's + /// loading unit. This id is to be passed + /// back into LoadDartDeferredLibrary + /// in order to identify which deferred + /// library to load. + /// + virtual void RequestDartDeferredLibrary(intptr_t loading_unit_id) = 0; }; //---------------------------------------------------------------------------- @@ -767,6 +782,38 @@ class Engine final : public RuntimeDelegate, /// const std::string& InitialRoute() const { return initial_route_; } + //-------------------------------------------------------------------------- + /// @brief Loads the Dart shared library into the Dart VM. When the + /// Dart library is loaded successfully, the Dart future + /// returned by the originating loadLibrary() call completes. + /// + /// The Dart compiler may generate separate shared libraries + /// files called 'loading units' when libraries are imported + /// as deferred. Each of these shared libraries are identified + /// by a unique loading unit id. Callers should dlopen the + /// shared library file and use dlsym to resolve the dart + /// symbols. These symbols can then be passed to this method to + /// be dynamically loaded into the VM. + /// + /// This method is paired with a RequestDartDeferredLibrary + /// invocation that provides the embedder with the loading unit id + /// of the deferred library to load. + /// + /// + /// @param[in] loading_unit_id The unique id of the deferred library's + /// loading unit, as passed in by + /// RequestDartDeferredLibrary. + /// + /// @param[in] snapshot_data Dart snapshot data of the loading unit's + /// shared library. + /// + /// @param[in] snapshot_data Dart snapshot instructions of the loading + /// unit's shared library. + /// + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions); + private: Engine::Delegate& delegate_; const Settings settings_; @@ -815,6 +862,12 @@ class Engine final : public RuntimeDelegate, std::unique_ptr> ComputePlatformResolvedLocale( const std::vector& supported_locale_data) override; + // The Following commented out code connects into part 2 of the split AOT + // feature. Left commented out until it lands: + + // // |RuntimeDelegate| + // void RequestDartDeferredLibrary(intptr_t loading_unit_id) override; + void SetNeedsReportTimings(bool value) override; void StopAnimator(); diff --git a/shell/common/engine_unittests.cc b/shell/common/engine_unittests.cc index 7405511513a9b..e563a92a454a4 100644 --- a/shell/common/engine_unittests.cc +++ b/shell/common/engine_unittests.cc @@ -32,6 +32,7 @@ class MockDelegate : public Engine::Delegate { MOCK_METHOD1(ComputePlatformResolvedLocale, std::unique_ptr>( const std::vector&)); + MOCK_METHOD1(RequestDartDeferredLibrary, void(intptr_t)); }; class MockResponse : public PlatformMessageResponse { @@ -55,6 +56,7 @@ class MockRuntimeDelegate : public RuntimeDelegate { MOCK_METHOD1(ComputePlatformResolvedLocale, std::unique_ptr>( const std::vector&)); + MOCK_METHOD1(RequestDartDeferredLibrary, void(intptr_t)); }; class MockRuntimeController : public RuntimeController { diff --git a/shell/common/platform_view.cc b/shell/common/platform_view.cc index 3e2b1daa476a6..55d3ebbcc7147 100644 --- a/shell/common/platform_view.cc +++ b/shell/common/platform_view.cc @@ -159,4 +159,14 @@ PlatformView::ComputePlatformResolvedLocales( return out; } +void PlatformView::RequestDartDeferredLibrary(intptr_t loading_unit_id) {} + +void PlatformView::LoadDartDeferredLibrary( + intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) {} + +void PlatformView::UpdateAssetManager( + std::shared_ptr asset_manager) {} + } // namespace flutter diff --git a/shell/common/platform_view.h b/shell/common/platform_view.h index 07ba3a1bb6d93..dd200b5948fdb 100644 --- a/shell/common/platform_view.h +++ b/shell/common/platform_view.h @@ -210,6 +210,43 @@ class PlatformView { /// virtual void OnPlatformViewMarkTextureFrameAvailable( int64_t texture_id) = 0; + + //-------------------------------------------------------------------------- + /// @brief Loads the dart shared library into the dart VM. When the + /// dart library is loaded successfully, the dart future + /// returned by the originating loadLibrary() call completes. + /// + /// The Dart compiler may generate separate shared library .so + /// files called 'loading units' when libraries are imported + /// as deferred. Each of these shared libraries are identified + /// by a unique loading unit id and can be dynamically loaded + /// into the VM by dlopen-ing and resolving the data and + /// instructions symbols. + /// + /// + /// @param[in] loading_unit_id The unique id of the deferred library's + /// loading unit. + /// + /// @param[in] snapshot_data Dart snapshot data of the loading unit's + /// shared library. + /// + /// @param[in] snapshot_data Dart snapshot instructions of the loading + /// unit's shared library. + /// + virtual void LoadDartDeferredLibrary( + intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) = 0; + + // TODO(garyq): Implement a proper asset_resolver replacement instead of + // overwriting the entire asset manager. + //-------------------------------------------------------------------------- + /// @brief Sets the asset manager of the engine to asset_manager + /// + /// @param[in] asset_manager The asset manager to use. + /// + virtual void UpdateAssetManager( + std::shared_ptr asset_manager) = 0; }; //---------------------------------------------------------------------------- @@ -565,6 +602,62 @@ class PlatformView { virtual std::shared_ptr CreateExternalViewEmbedder(); + //-------------------------------------------------------------------------- + /// @brief Invoked when the dart VM requests that a deferred library + /// be loaded. Notifies the engine that the deferred library + /// identified by the specified loading unit id should be + /// downloaded and loaded into the Dart VM via + /// `LoadDartDeferredLibrary` + /// + /// @param[in] loading_unit_id The unique id of the deferred library's + /// loading unit. This id is to be passed + /// back into LoadDartDeferredLibrary + /// in order to identify which deferred + /// library to load. + /// + virtual void RequestDartDeferredLibrary(intptr_t loading_unit_id); + + //-------------------------------------------------------------------------- + /// @brief Loads the Dart shared library into the Dart VM. When the + /// Dart library is loaded successfully, the Dart future + /// returned by the originating loadLibrary() call completes. + /// + /// The Dart compiler may generate separate shared libraries + /// files called 'loading units' when libraries are imported + /// as deferred. Each of these shared libraries are identified + /// by a unique loading unit id. Callers should dlopen the + /// shared library file and use dlsym to resolve the dart + /// symbols. These symbols can then be passed to this method to + /// be dynamically loaded into the VM. + /// + /// This method is paired with a RequestDartDeferredLibrary + /// invocation that provides the embedder with the loading unit id + /// of the deferred library to load. + /// + /// + /// @param[in] loading_unit_id The unique id of the deferred library's + /// loading unit, as passed in by + /// RequestDartDeferredLibrary. + /// + /// @param[in] snapshot_data Dart snapshot data of the loading unit's + /// shared library. + /// + /// @param[in] snapshot_data Dart snapshot instructions of the loading + /// unit's shared library. + /// + virtual void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions); + + // TODO(garyq): Implement a proper asset_resolver replacement instead of + // overwriting the entire asset manager. + //-------------------------------------------------------------------------- + /// @brief Sets the asset manager of the engine to asset_manager + /// + /// @param[in] asset_manager The asset manager to use. + /// + virtual void UpdateAssetManager(std::shared_ptr asset_manager); + protected: PlatformView::Delegate& delegate_; const TaskRunners task_runners_; diff --git a/shell/common/shell.cc b/shell/common/shell.cc index a8a8b884e4c0f..aaf3c3c2242ae 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -1185,6 +1185,22 @@ std::unique_ptr> Shell::ComputePlatformResolvedLocale( return platform_view_->ComputePlatformResolvedLocales(supported_locale_data); } +void Shell::LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) { + engine_->LoadDartDeferredLibrary(loading_unit_id, snapshot_data, + snapshot_instructions); +} + +void Shell::UpdateAssetManager(std::shared_ptr asset_manager) { + engine_->UpdateAssetManager(std::move(asset_manager)); +} + +// |Engine::Delegate| +void Shell::RequestDartDeferredLibrary(intptr_t loading_unit_id) { + platform_view_->RequestDartDeferredLibrary(loading_unit_id); +} + void Shell::ReportTimings() { FML_DCHECK(is_setup_); FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); diff --git a/shell/common/shell.h b/shell/common/shell.h index ffb07b877d493..7ac297e5d07c6 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -507,6 +507,14 @@ class Shell final : public PlatformView::Delegate, // |PlatformView::Delegate| void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override; + // |PlatformView::Delegate| + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) override; + + // |PlatformView::Delegate| + void UpdateAssetManager(std::shared_ptr asset_manager) override; + // |Animator::Delegate| void OnAnimatorBeginFrame(fml::TimePoint frame_target_time) override; @@ -548,6 +556,9 @@ class Shell final : public PlatformView::Delegate, std::unique_ptr> ComputePlatformResolvedLocale( const std::vector& supported_locale_data) override; + // |Engine::Delegate| + void RequestDartDeferredLibrary(intptr_t loading_unit_id) override; + // |Rasterizer::Delegate| void OnFrameRasterized(const FrameTiming&) override; diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index a7ebd146db06e..29f204fd82dcc 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -156,6 +156,8 @@ android_java_sources = [ "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", + "io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java", + "io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java", "io/flutter/embedding/engine/loader/ApplicationInfoLoader.java", "io/flutter/embedding/engine/loader/FlutterApplicationInfo.java", "io/flutter/embedding/engine/loader/FlutterLoader.java", @@ -462,6 +464,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/RenderingComponentTest.java", "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", "test/io/flutter/embedding/engine/dart/DartMessengerTest.java", + "test/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManagerTest.java", "test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java", "test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java", "test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java", diff --git a/shell/platform/android/embedding_bundle/build.gradle b/shell/platform/android/embedding_bundle/build.gradle index 7a60d4c16a24c..c3eea2b22fbf4 100644 --- a/shell/platform/android/embedding_bundle/build.gradle +++ b/shell/platform/android/embedding_bundle/build.gradle @@ -48,6 +48,12 @@ android { embedding "androidx.lifecycle:lifecycle-common:$lifecycle_version" embedding "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + // This dependency is here to allow linking to Play core in tests, but + // is not used in a default Flutter app. This dependency should be manually + // added to the user's app gradle in order to opt into using split AOT + // dynamic features. + embedding "com.google.android.play:core:1.8.0" + // Testing // TODO(xster): remove these android-all compile time dependencies. // Use https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/plugins/LegacyDependencyResolver.java#L24 diff --git a/shell/platform/android/io/flutter/FlutterInjector.java b/shell/platform/android/io/flutter/FlutterInjector.java index 23324c7b7fe42..d703007ff968e 100644 --- a/shell/platform/android/io/flutter/FlutterInjector.java +++ b/shell/platform/android/io/flutter/FlutterInjector.java @@ -5,7 +5,9 @@ package io.flutter; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.dynamicfeatures.DynamicFeatureManager; import io.flutter.embedding.engine.loader.FlutterLoader; /** @@ -62,11 +64,14 @@ public static void reset() { instance = null; } - private FlutterInjector(@NonNull FlutterLoader flutterLoader) { + private FlutterInjector( + @NonNull FlutterLoader flutterLoader, DynamicFeatureManager dynamicFeatureManager) { this.flutterLoader = flutterLoader; + this.dynamicFeatureManager = dynamicFeatureManager; } private FlutterLoader flutterLoader; + private DynamicFeatureManager dynamicFeatureManager; /** Returns the {@link FlutterLoader} instance to use for the Flutter Android engine embedding. */ @NonNull @@ -74,6 +79,15 @@ public FlutterLoader flutterLoader() { return flutterLoader; } + /** + * Returns the {@link DynamicFeatureManager} instance to use for the Flutter Android engine + * embedding. + */ + @Nullable + public DynamicFeatureManager dynamicFeatureManager() { + return dynamicFeatureManager; + } + /** * Builder used to supply a custom FlutterInjector instance to {@link * FlutterInjector#setInstance(FlutterInjector)}. @@ -82,6 +96,7 @@ public FlutterLoader flutterLoader() { */ public static final class Builder { private FlutterLoader flutterLoader; + private DynamicFeatureManager dynamicFeatureManager; /** * Sets a {@link FlutterLoader} override. * @@ -92,10 +107,16 @@ public Builder setFlutterLoader(@NonNull FlutterLoader flutterLoader) { return this; } + public Builder setDynamicFeatureManager(@Nullable DynamicFeatureManager dynamicFeatureManager) { + this.dynamicFeatureManager = dynamicFeatureManager; + return this; + } + private void fillDefaults() { if (flutterLoader == null) { flutterLoader = new FlutterLoader(); } + // DynamicFeatureManager's intended default is null. } /** @@ -105,7 +126,7 @@ private void fillDefaults() { public FlutterInjector build() { fillDefaults(); - return new FlutterInjector(flutterLoader); + return new FlutterInjector(flutterLoader, dynamicFeatureManager); } } } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 2e001e7a73965..556b4cd3af021 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -299,6 +299,8 @@ public FlutterEngine( flutterJNI.addEngineLifecycleListener(engineLifecycleListener); flutterJNI.setPlatformViewsController(platformViewsController); flutterJNI.setLocalizationPlugin(localizationPlugin); + flutterJNI.setDynamicFeatureManager(FlutterInjector.instance().dynamicFeatureManager()); + attachToJni(); // TODO(mattcarroll): FlutterRenderer is temporally coupled to attach(). Remove that coupling if @@ -374,7 +376,11 @@ public void destroy() { platformViewsController.onDetachedFromJNI(); dartExecutor.onDetachedFromJNI(); flutterJNI.removeEngineLifecycleListener(engineLifecycleListener); + flutterJNI.setDynamicFeatureManager(null); flutterJNI.detachFromNativeAndReleaseResources(); + if (FlutterInjector.instance().dynamicFeatureManager() != null) { + FlutterInjector.instance().dynamicFeatureManager().destroy(); + } } /** diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 604bb0ebc4e2d..94576fb302f10 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -20,6 +20,7 @@ import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener; import io.flutter.embedding.engine.dart.PlatformMessageHandler; +import io.flutter.embedding.engine.dynamicfeatures.DynamicFeatureManager; import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.embedding.engine.renderer.RenderSurface; @@ -225,6 +226,8 @@ public static native void nativeOnVsync( @Nullable private LocalizationPlugin localizationPlugin; @Nullable private PlatformViewsController platformViewsController; + @Nullable private DynamicFeatureManager dynamicFeatureManager; + @NonNull private final Set engineLifecycleListeners = new CopyOnWriteArraySet<>(); @@ -981,6 +984,117 @@ String[] computePlatformResolvedLocale(@NonNull String[] strings) { // ----- End Localization Support ---- + // ----- Start Dynamic Features Support ---- + + /** Sets the dynamic feature manager that is used to download and install split features. */ + @UiThread + public void setDynamicFeatureManager(@Nullable DynamicFeatureManager dynamicFeatureManager) { + ensureRunningOnMainThread(); + this.dynamicFeatureManager = dynamicFeatureManager; + if (dynamicFeatureManager != null) { + dynamicFeatureManager.setJNI(this); + } + } + + /** + * Called by dart to request that a Dart deferred library corresponding to loadingUnitId be + * downloaded (if necessary) and loaded into the dart vm. + * + *

This method delegates the task to DynamicFeatureManager, which handles the download and + * loading of the dart library and any assets. + * + * @param loadingUnitId The loadingUnitId is assigned during compile time by gen_snapshot and is + * automatically retrieved when loadLibrary() is called on a dart deferred library. + */ + @SuppressWarnings("unused") + @UiThread + public void requestDartDeferredLibrary(int loadingUnitId) { + if (dynamicFeatureManager != null) { + dynamicFeatureManager.downloadDynamicFeature(loadingUnitId, null); + } else { + // TODO(garyq): Add link to setup/instructions guide wiki. + Log.e( + TAG, + "No DynamicFeatureManager found. Android setup must be completed before using split AOT dynamic features."); + } + } + + /** + * Searches each of the provided paths for a valid Dart shared library .so file and resolves + * symbols to load into the dart VM. + * + *

Successful loading of the dart library completes the future returned by loadLibrary() that + * triggered the install/load process. + * + * @param loadingUnitId The loadingUnitId is assigned during compile time by gen_snapshot and is + * automatically retrieved when loadLibrary() is called on a dart deferred library. This is + * used to identify which Dart deferred library the resolved correspond to. + * @param searchPaths An array of paths in which to look for valid dart shared libraries. This + * supports paths within zipped apks as long as the apks are not compressed using the + * `path/to/apk.apk!path/inside/apk/lib.so` format. Paths will be tried first to last and ends + * when a library is sucessfully found. When the found library is invalid, no additional paths + * will be attempted. + */ + @UiThread + public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) { + ensureRunningOnMainThread(); + ensureAttachedToNative(); + nativeLoadDartDeferredLibrary(nativePlatformViewId, loadingUnitId, searchPaths); + } + + private native void nativeLoadDartDeferredLibrary( + long nativePlatformViewId, int loadingUnitId, @NonNull String[] searchPaths); + + /** + * Adds the specified AssetManager as an APKAssetResolver in the Flutter Engine's AssetManager. + * + *

This may be used to update the engine AssetManager when a new dynamic feature is installed + * and a new Android AssetManager is created with access to new assets. + * + * @param assetManager An android AssetManager that is able to access the newly downloaded assets. + * @param assetBundlePath The subdirectory that the flutter assets are stored in. The typical + * value is `flutter_assets`. + */ + @UiThread + public void updateAssetManager( + @NonNull AssetManager assetManager, @NonNull String assetBundlePath) { + ensureRunningOnMainThread(); + ensureAttachedToNative(); + nativeUpdateAssetManager(nativePlatformViewId, assetManager, assetBundlePath); + } + + private native void nativeUpdateAssetManager( + long nativePlatformViewId, + @NonNull AssetManager assetManager, + @NonNull String assetBundlePath); + + /** + * Indicates that a failure was encountered during the Android portion of downloading a dynamic + * feature module and loading a dart deferred library, which is typically done by + * DynamicFeatureManager. + * + *

This will inform dart that the future returned by loadLibrary() should complete with an + * error. + * + * @param loadingUnitId The loadingUnitId that corresponds to the dart deferred library that + * failed to install. + * @param error The error message to display. + * @param isTransient When isTransient is false, new attempts to install will automatically result + * in same error in Dart before the request is passed to Android. + */ + @SuppressWarnings("unused") + @UiThread + public void dynamicFeatureInstallFailure( + int loadingUnitId, @NonNull String error, boolean isTransient) { + ensureRunningOnMainThread(); + nativeDynamicFeatureInstallFailure(loadingUnitId, error, isTransient); + } + + private native void nativeDynamicFeatureInstallFailure( + int loadingUnitId, @NonNull String error, boolean isTransient); + + // ----- End Dynamic Features Support ---- + // @SuppressWarnings("unused") @UiThread public void onDisplayPlatformView( diff --git a/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java new file mode 100644 index 0000000000000..747f6df01d56f --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/DynamicFeatureManager.java @@ -0,0 +1,154 @@ +// 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.dynamicfeatures; + +import io.flutter.embedding.engine.FlutterJNI; + +// TODO: add links to external documentation on how to use split aot features. +/** + * Basic interface that handles downloading and loading of dynamic features. + * + *

Flutter dynamic feature support is still in early developer preview and should not be used in + * production apps yet. + * + *

The Flutter default implementation is PlayStoreDynamicFeatureManager. + * + *

DynamicFeatureManager handles the embedder/Android level tasks of downloading, installing, and + * loading Dart deferred libraries. A typical code-flow begins with a Dart call to loadLibrary() on + * 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 + * 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 + * these two methods as needed. + * + *

A dynamic feature module is uniquely identified by a module name as defined in + * bundle_config.yaml. Each feature module may contain one or more loading units, uniquely + * identified by the loading unit ID and assets. + */ +public interface DynamicFeatureManager { + /** + * Sets the FlutterJNI to be used to communication with the Flutter native engine. + * + *

A FlutterJNI is required in order to properly execute loadAssets and loadDartLibrary. + * + *

Since this class may be instantiated for injection before the FlutterEngine and FlutterJNI + * is fully initialized, this method should be called to provide the FlutterJNI instance to use + * for use in loadDartLibrary and loadAssets. + */ + public abstract void setJNI(FlutterJNI flutterJNI); + + /** + * Request that the feature module be downloaded and installed. + * + *

This method begins the download and installation of the specified feature module. For + * 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. + * + *

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

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 + * 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. + * + * @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 + * primarily used in loadDartLibrary to indicate to Dart which Dart library is being loaded. + * Loading unit ids range from 0 to the number existing loading units. Passing a negative + * loading unit id indicates that no Dart deferred library should be loaded after download + * completes. This is the case when the dynamic feature module is an assets-only module. If a + * negative loadingUnitId is passed, then moduleName must not be null. Passing a loadingUnitId + * larger than the highest valid loading unit's id will cause the Dart loadLibrary() to + * complete with a failure. + * @param moduleName The dynamic feature module name as defined in bundle_config.yaml. This may be + * null if the dynamic feature to be loaded is associated with a loading unit/deferred dart + * library. In this case, it is this method's responsibility to map the loadingUnitId to its + * corresponding moduleName. When loading asset-only or other dynamic features without an + * 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); + + /** + * Extract and load any assets and resources from the module for use by Flutter. + * + *

This method should provide a refreshed AssetManager to FlutterJNI.updateAssetManager that + * can access the new assets. If no assets are included as part of the dynamic feature, then + * nothing needs to be done. + * + *

If using the Play Store dynamic feature delivery, refresh the context via: {@code + * context.createPackageContext(context.getPackageName(), 0);} This returns a new context, from + * which an updated asset manager may be obtained and passed to updateAssetManager in FlutterJNI. + * This process does not require loadingUnitId or moduleName, however, the two parameters are + * still present for custom implementations that store assets outside of Android's native system. + * + *

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. + * + * @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 void loadAssets(int loadingUnitId, String moduleName); + + /** + * Load the .so shared library file into the Dart VM. + * + *

When the download of a dynamic feature module completes, this method should be called to + * find the path .so library file. The path(s) should then be passed to + * FlutterJNI.loadDartDeferredLibrary to be dlopen-ed and loaded into the Dart VM. + * + *

Specifically, APKs distributed by Android's app bundle format may vary by device and API + * number, so FlutterJNI's loadDartDeferredLibrary accepts a list of search paths with can include + * paths within APKs that have not been unpacked using the + * `path/to/apk.apk!path/inside/apk/lib.so` format. Each search path will be attempted in order + * until a shared library is found. This allows for the developer to avoid unpacking the apk zip. + * + *

Upon successful load of the Dart library, the Dart future from the originating loadLibary() + * call completes and developers are able to use symbols and assets from the feature module. + * + * @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 + * primarily used in loadDartLibrary to indicate to Dart which Dart library is being loaded. + * Loading unit ids range from 0 to the number existing loading units. Negative loading unit + * ids are considered invalid and this method will result in a no-op. + * @param moduleName The dynamic feature module name as defined in bundle_config.yaml. If using + * Play Store dynamic feature delivery, this name corresponds to the root name on the + * installed APKs in which to search for the desired shared library .so file. + */ + public abstract void loadDartLibrary(int loadingUnitId, String moduleName); + + /** + * Uninstall the specified feature module. + * + *

Both parameters are not always necessary to identify which module to uninstall. Asset-only + * modules do not have an associated loadingUnitId. Instead, an invalid ID like -1 may be passed + * to download 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. + */ + public abstract void uninstallFeature(int loadingUnitId, String moduleName); + + /** + * Cleans up and releases resources. This object is no longer usable after calling this method. + */ + public abstract void destroy(); +} diff --git a/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java new file mode 100644 index 0000000000000..de613cacbcf2f --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManager.java @@ -0,0 +1,339 @@ +// 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.dynamicfeatures; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetManager; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.play.core.splitinstall.SplitInstallException; +import com.google.android.play.core.splitinstall.SplitInstallManager; +import com.google.android.play.core.splitinstall.SplitInstallManagerFactory; +import com.google.android.play.core.splitinstall.SplitInstallRequest; +import com.google.android.play.core.splitinstall.SplitInstallSessionState; +import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener; +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 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; + +/** + * Flutter default implementation of DynamicFeatureManager that downloads dynamic feature modules + * from the Google Play store. + */ +public class PlayStoreDynamicFeatureManager implements DynamicFeatureManager { + private static final String TAG = "PlayStoreDynamicFeatureManager"; + + private @NonNull SplitInstallManager splitInstallManager; + private @Nullable FlutterJNI flutterJNI; + 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 Map sessionIdToName; + private @NonNull Map sessionIdToLoadingUnitId; + + private FeatureInstallStateUpdatedListener listener; + + private class FeatureInstallStateUpdatedListener implements SplitInstallStateUpdatedListener { + public void onStateUpdate(SplitInstallSessionState state) { + if (sessionIdToName.containsKey(state.sessionId())) { + // TODO(garyq): Add system channel for split aot messages. + switch (state.status()) { + case SplitInstallSessionStatus.FAILED: + { + Log.e( + TAG, + String.format( + "Module \"%s\" (sessionId %d) install failed with: %s", + sessionIdToName.get(state.sessionId()), + state.sessionId(), + state.errorCode())); + flutterJNI.dynamicFeatureInstallFailure( + sessionIdToLoadingUnitId.get(state.sessionId()), + "Module install failed with " + state.errorCode(), + true); + sessionIdToName.remove(state.sessionId()); + sessionIdToLoadingUnitId.remove(state.sessionId()); + break; + } + case SplitInstallSessionStatus.INSTALLED: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) install successfully.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + loadAssets( + sessionIdToLoadingUnitId.get(state.sessionId()), + sessionIdToName.get(state.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.remove(state.sessionId()); + sessionIdToLoadingUnitId.remove(state.sessionId()); + break; + } + case SplitInstallSessionStatus.CANCELED: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) install canceled.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + sessionIdToName.remove(state.sessionId()); + break; + } + case SplitInstallSessionStatus.CANCELING: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) install canceling.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + break; + } + case SplitInstallSessionStatus.PENDING: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) install pending.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + break; + } + case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) install requires user confirmation.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + break; + } + case SplitInstallSessionStatus.DOWNLOADING: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) downloading.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + break; + } + case SplitInstallSessionStatus.DOWNLOADED: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) downloaded.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + break; + } + case SplitInstallSessionStatus.INSTALLING: + { + Log.d( + TAG, + String.format( + "Module \"%s\" (sessionId %d) installing.", + sessionIdToName.get(state.sessionId()), state.sessionId())); + break; + } + default: + Log.d(TAG, "Status: " + state.status()); + } + } + } + } + + public PlayStoreDynamicFeatureManager(@NonNull Context context, @Nullable FlutterJNI flutterJNI) { + this.context = context; + this.flutterJNI = flutterJNI; + splitInstallManager = SplitInstallManagerFactory.create(context); + listener = new FeatureInstallStateUpdatedListener(); + splitInstallManager.registerListener(listener); + sessionIdToName = new HashMap(); + sessionIdToLoadingUnitId = new HashMap(); + } + + public void setJNI(@NonNull FlutterJNI flutterJNI) { + this.flutterJNI = flutterJNI; + } + + private boolean verifyJNI() { + if (flutterJNI == null) { + Log.e( + TAG, + "No FlutterJNI provided. `setJNI` must be called on the DynamicFeatureManager before attempting to load dart libraries or invoking with platform channels."); + return false; + } + return true; + } + + private String loadingUnitIdToModuleName(int loadingUnitId) { + // Loading unit id to module name mapping stored in android Strings + // resources. + int moduleNameIdentifier = + context + .getResources() + .getIdentifier("loadingUnit" + loadingUnitId, "string", context.getPackageName()); + return context.getResources().getString(moduleNameIdentifier); + } + + public void downloadDynamicFeature(int loadingUnitId, String moduleName) { + String resolvedModuleName = + moduleName == null ? moduleName : loadingUnitIdToModuleName(loadingUnitId); + if (resolvedModuleName == null) { + Log.d(TAG, "Dynamic feature module name was null."); + return; + } + + SplitInstallRequest request = SplitInstallRequest.newBuilder().addModule(moduleName).build(); + + splitInstallManager + // Submits the request to install the module through the + // asynchronous startInstall() task. Your app needs to be + // in the foreground to submit the request. + .startInstall(request) + // Called when the install request is sent successfully. This is different than a successful + // install which is handled in FeatureInstallStateUpdatedListener. + .addOnSuccessListener( + sessionId -> { + this.sessionIdToName.put(sessionId, moduleName); + this.sessionIdToLoadingUnitId.put(sessionId, loadingUnitId); + }) + .addOnFailureListener( + exception -> { + switch (((SplitInstallException) exception).getErrorCode()) { + case SplitInstallErrorCode.NETWORK_ERROR: + flutterJNI.dynamicFeatureInstallFailure( + loadingUnitId, + String.format( + "Install of dynamic feature module \"%s\" failed with a network error", + moduleName), + true); + break; + case SplitInstallErrorCode.MODULE_UNAVAILABLE: + flutterJNI.dynamicFeatureInstallFailure( + loadingUnitId, + String.format( + "Install of dynamic feature module \"%s\" failed as it is unavailable", + moduleName), + false); + break; + default: + flutterJNI.dynamicFeatureInstallFailure( + loadingUnitId, + String.format( + "Install of dynamic feature module \"%s\" failed with error %d: %s", + moduleName, + ((SplitInstallException) exception).getErrorCode(), + ((SplitInstallException) exception).getMessage()), + false); + break; + } + }); + } + + public void loadAssets(int loadingUnitId, String moduleName) { + if (!verifyJNI()) { + return; + } + // Since android dynamic feature asset manager is handled through + // context, neither parameter is used here. Assets are stored in + // the apk's `assets` directory allowing them to be accessed by + // Android's AssetManager directly. + try { + context = context.createPackageContext(context.getPackageName(), 0); + + AssetManager assetManager = context.getAssets(); + flutterJNI.updateAssetManager( + assetManager, + // TODO(garyq): Made the "flutter_assets" directory dynamic based off of DartEntryPoint. + "flutter_assets"); + } catch (NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + public void loadDartLibrary(int loadingUnitId, String moduleName) { + if (!verifyJNI()) { + return; + } + // Loading unit must be specified and valid to load a dart library. + if (loadingUnitId < 0) { + return; + } + + // This matches/depends on dart's loading unit naming convention, which we use unchanged. + String aotSharedLibraryName = "app.so-" + loadingUnitId + ".part.so"; + + // Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64 + String abi; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + abi = Build.SUPPORTED_ABIS[0]; + } else { + abi = Build.CPU_ABI; + } + String pathAbi = abi.replace("-", "_"); // abis are represented with underscores in paths. + + // TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more + // performant and robust. + + // Search directly in APKs first + List apkPaths = new ArrayList(); + // If not found in APKs, we check in extracted native libs for the lib directly. + List soPaths = new ArrayList(); + Queue searchFiles = new LinkedList(); + searchFiles.add(context.getFilesDir()); + while (!searchFiles.isEmpty()) { + File file = searchFiles.remove(); + if (file != null && file.isDirectory()) { + for (File f : file.listFiles()) { + searchFiles.add(f); + } + continue; + } + String name = file.getName(); + if (name.endsWith(".apk") && name.startsWith(moduleName) && name.contains(pathAbi)) { + apkPaths.add(file.getAbsolutePath()); + continue; + } + if (name.equals(aotSharedLibraryName)) { + soPaths.add(file.getAbsolutePath()); + } + } + + List searchPaths = new ArrayList(); + for (String path : apkPaths) { + searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName); + } + for (String path : soPaths) { + searchPaths.add(path); + } + + flutterJNI.loadDartDeferredLibrary( + loadingUnitId, searchPaths.toArray(new String[apkPaths.size()])); + } + + public void uninstallFeature(int loadingUnitId, String moduleName) { + // TODO(garyq): support uninstalling. + } + + public void destroy() { + splitInstallManager.unregisterListener(listener); + flutterJNI = null; + } +} diff --git a/shell/platform/android/jni/jni_mock.h b/shell/platform/android/jni/jni_mock.h index 65f790d972ee8..52b38e1e84308 100644 --- a/shell/platform/android/jni/jni_mock.h +++ b/shell/platform/android/jni/jni_mock.h @@ -95,6 +95,11 @@ class JNIMock final : public PlatformViewAndroidJNI { (override)); MOCK_METHOD(double, GetDisplayRefreshRate, (), (override)); + + MOCK_METHOD(bool, + RequestDartDeferredLibrary, + (int loading_unit_id), + (override)); }; } // namespace flutter diff --git a/shell/platform/android/jni/platform_view_android_jni.h b/shell/platform/android/jni/platform_view_android_jni.h index e06079c1f5c4f..81323605de68c 100644 --- a/shell/platform/android/jni/platform_view_android_jni.h +++ b/shell/platform/android/jni/platform_view_android_jni.h @@ -195,6 +195,8 @@ class PlatformViewAndroidJNI { std::vector supported_locales_data) = 0; virtual double GetDisplayRefreshRate() = 0; + + virtual bool RequestDartDeferredLibrary(int loading_unit_id) = 0; }; } // namespace flutter diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 8b6b27658b621..f5049b23223d6 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -336,6 +336,29 @@ PlatformViewAndroid::ComputePlatformResolvedLocales( supported_locale_data); } +// |PlatformView| +void PlatformViewAndroid::RequestDartDeferredLibrary(intptr_t loading_unit_id) { + if (jni_facade_->RequestDartDeferredLibrary(loading_unit_id)) { + return; + } + return; // TODO(garyq): Call LoadDartDeferredLibraryFailure() +} + +// |PlatformView| +void PlatformViewAndroid::LoadDartDeferredLibrary( + intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) { + delegate_.LoadDartDeferredLibrary(loading_unit_id, snapshot_data, + snapshot_instructions); +} + +// |PlatformView| +void PlatformViewAndroid::UpdateAssetManager( + std::shared_ptr asset_manager) { + delegate_.UpdateAssetManager(std::move(asset_manager)); +} + void PlatformViewAndroid::InstallFirstFrameCallback() { // On Platform Task Runner. SetNextFrameCallback( diff --git a/shell/platform/android/platform_view_android.h b/shell/platform/android/platform_view_android.h index 3b424f2920a6f..ff2ac1353f2b3 100644 --- a/shell/platform/android/platform_view_android.h +++ b/shell/platform/android/platform_view_android.h @@ -93,6 +93,14 @@ class PlatformViewAndroid final : public PlatformView { int64_t texture_id, const fml::jni::JavaObjectWeakGlobalRef& surface_texture); + // |PlatformView| + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) override; + + // |PlatformView| + void UpdateAssetManager(std::shared_ptr asset_manager) override; + private: const std::shared_ptr jni_facade_; std::unique_ptr android_context_; @@ -137,6 +145,9 @@ class PlatformViewAndroid final : public PlatformView { std::unique_ptr> ComputePlatformResolvedLocales( const std::vector& supported_locale_data) override; + // |PlatformView| + void RequestDartDeferredLibrary(intptr_t loading_unit_id) override; + void InstallFirstFrameCallback(); void FireFirstFrameCallback(); diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 084872a023313..a252d167b07c3 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -5,7 +5,9 @@ #include "flutter/shell/platform/android/platform_view_android_jni_impl.h" #include +#include #include +#include #include #include "unicode/uchar.h" @@ -100,6 +102,8 @@ static jmethodID g_detach_from_gl_context_method = nullptr; static jmethodID g_compute_platform_resolved_locale_method = nullptr; +static jmethodID g_request_dart_deferred_library_method = nullptr; + // Called By Java static jmethodID g_on_display_platform_view_method = nullptr; @@ -508,6 +512,108 @@ static jboolean FlutterTextUtilsIsRegionalIndicator(JNIEnv* env, jint codePoint) { return u_hasBinaryProperty(codePoint, UProperty::UCHAR_REGIONAL_INDICATOR); } + +static void LoadLoadingUnitFailure(intptr_t loading_unit_id, + std::string message, + bool transient) { + // TODO(garyq): Implement +} + +static void DynamicFeatureInstallFailure(JNIEnv* env, + jobject obj, + jint jLoadingUnitId, + jstring jError, + jboolean jTransient) { + LoadLoadingUnitFailure(static_cast(jLoadingUnitId), + fml::jni::JavaStringToString(env, jError), + static_cast(jTransient)); +} + +static void LoadDartDeferredLibrary(JNIEnv* env, + jobject obj, + jlong shell_holder, + jint jLoadingUnitId, + jobjectArray jSearchPaths) { + // Convert java->c++ + intptr_t loading_unit_id = static_cast(jLoadingUnitId); + std::vector search_paths = + fml::jni::StringArrayToVector(env, jSearchPaths); + + // TODO: Switch to using the NativeLibrary class, eg: + // + // fml::RefPtr native_lib = + // fml::NativeLibrary::Create(lib_name.c_str()); + // + // Find and open the shared library. + void* handle = nullptr; + while (handle == nullptr && !search_paths.empty()) { + std::string path = search_paths.back(); + handle = ::dlopen(path.c_str(), RTLD_NOW); + search_paths.pop_back(); + } + if (handle == nullptr) { + LoadLoadingUnitFailure(loading_unit_id, + "No lib .so found for provided search paths.", true); + return; + } + + // Resolve symbols. + uint8_t* isolate_data = + static_cast(::dlsym(handle, DartSnapshot::kIsolateDataSymbol)); + if (isolate_data == nullptr) { + // Mac sometimes requires an underscore prefix. + std::stringstream underscore_symbol_name; + underscore_symbol_name << "_" << DartSnapshot::kIsolateDataSymbol; + isolate_data = static_cast( + ::dlsym(handle, underscore_symbol_name.str().c_str())); + if (isolate_data == nullptr) { + LoadLoadingUnitFailure(loading_unit_id, + "Could not resolve data symbol in library", true); + return; + } + } + uint8_t* isolate_instructions = static_cast( + ::dlsym(handle, DartSnapshot::kIsolateInstructionsSymbol)); + if (isolate_instructions == nullptr) { + // Mac sometimes requires an underscore prefix. + std::stringstream underscore_symbol_name; + underscore_symbol_name << "_" << DartSnapshot::kIsolateInstructionsSymbol; + isolate_instructions = static_cast( + ::dlsym(handle, underscore_symbol_name.str().c_str())); + if (isolate_data == nullptr) { + LoadLoadingUnitFailure(loading_unit_id, + "Could not resolve instructions symbol in library", + true); + return; + } + } + + ANDROID_SHELL_HOLDER->GetPlatformView()->LoadDartDeferredLibrary( + loading_unit_id, isolate_data, isolate_instructions); + + // TODO(garyq): fallback on soPath. +} + +// TODO(garyq): persist additional asset resolvers by updating instead of +// replacing with newly created asset_manager +static void UpdateAssetManager(JNIEnv* env, + jobject obj, + jlong shell_holder, + jobject jAssetManager, + jstring jAssetBundlePath) { + auto asset_manager = std::make_shared(); + asset_manager->PushBack(std::make_unique( + env, // jni environment + jAssetManager, // asset manager + fml::jni::JavaStringToString(env, jAssetBundlePath)) // apk asset dir + ); + // Create config to set persistent cache asset manager + RunConfiguration config(nullptr, std::move(asset_manager)); + + ANDROID_SHELL_HOLDER->GetPlatformView()->UpdateAssetManager( + config.GetAssetManager()); +} + bool RegisterApi(JNIEnv* env) { static const JNINativeMethod flutter_jni_methods[] = { // Start of methods from FlutterJNI @@ -664,6 +770,22 @@ bool RegisterApi(JNIEnv* env) { .fnPtr = reinterpret_cast(&FlutterTextUtilsIsRegionalIndicator), }, + { + .name = "nativeLoadDartDeferredLibrary", + .signature = "(JI[Ljava/lang/String;)V", + .fnPtr = reinterpret_cast(&LoadDartDeferredLibrary), + }, + { + .name = "nativeUpdateAssetManager", + .signature = + "(JLandroid/content/res/AssetManager;Ljava/lang/String;)V", + .fnPtr = reinterpret_cast(&UpdateAssetManager), + }, + { + .name = "nativeDynamicFeatureInstallFailure", + .signature = "(ILjava/lang/String;Z)V", + .fnPtr = reinterpret_cast(&DynamicFeatureInstallFailure), + }, }; if (env->RegisterNatives(g_flutter_jni_class->obj(), flutter_jni_methods, @@ -907,6 +1029,14 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { return false; } + g_request_dart_deferred_library_method = env->GetMethodID( + g_flutter_jni_class->obj(), "requestDartDeferredLibrary", "(I)V"); + + if (g_request_dart_deferred_library_method == nullptr) { + FML_LOG(ERROR) << "Could not locate requestDartDeferredLibrary method"; + return false; + } + return RegisterApi(env); } @@ -1334,4 +1464,21 @@ double PlatformViewAndroidJNIImpl::GetDisplayRefreshRate() { return static_cast(env->GetStaticFloatField(clazz, fid)); } +bool PlatformViewAndroidJNIImpl::RequestDartDeferredLibrary( + int loading_unit_id) { + JNIEnv* env = fml::jni::AttachCurrentThread(); + + auto java_object = java_object_.get(env); + if (java_object.is_null()) { + return true; + } + + env->CallObjectMethod(java_object.obj(), + g_request_dart_deferred_library_method, + loading_unit_id); + + FML_CHECK(CheckException(env)); + return true; +} + } // namespace flutter diff --git a/shell/platform/android/platform_view_android_jni_impl.h b/shell/platform/android/platform_view_android_jni_impl.h index 313a9ee921e6c..8fe0618929e95 100644 --- a/shell/platform/android/platform_view_android_jni_impl.h +++ b/shell/platform/android/platform_view_android_jni_impl.h @@ -80,6 +80,8 @@ class PlatformViewAndroidJNIImpl final : public PlatformViewAndroidJNI { double GetDisplayRefreshRate() override; + bool RequestDartDeferredLibrary(int loading_unit_id) override; + private: // Reference to FlutterJNI object. const fml::jni::JavaObjectWeakGlobalRef java_object_; diff --git a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java index 9809095a0746d..66384fcb7c27c 100644 --- a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java +++ b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java @@ -6,8 +6,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; +import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDynamicFeatureManager; import io.flutter.embedding.engine.loader.FlutterLoader; import org.junit.Before; import org.junit.Test; @@ -21,6 +23,7 @@ @RunWith(RobolectricTestRunner.class) public class FlutterInjectorTest { @Mock FlutterLoader mockFlutterLoader; + @Mock PlayStoreDynamicFeatureManager mockDynamicFeatureManager; @Before public void setUp() { @@ -34,6 +37,7 @@ public void itHasSomeReasonableDefaults() { // Implicitly builds when first accessed. FlutterInjector injector = FlutterInjector.instance(); assertNotNull(injector.flutterLoader()); + assertNull(injector.dynamicFeatureManager()); } @Test @@ -44,6 +48,14 @@ public void canPartiallyOverride() { assertEquals(injector.flutterLoader(), mockFlutterLoader); } + @Test + public void canInjectDynamicFeatureManager() { + FlutterInjector.setInstance( + new FlutterInjector.Builder().setDynamicFeatureManager(mockDynamicFeatureManager).build()); + FlutterInjector injector = FlutterInjector.instance(); + assertEquals(injector.dynamicFeatureManager(), mockDynamicFeatureManager); + } + @Test() public void cannotBeChangedOnceRead() { FlutterInjector.instance(); diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 635b6680475d0..8a7332d9a0ad0 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -18,6 +18,7 @@ import io.flutter.embedding.engine.RenderingComponentTest; import io.flutter.embedding.engine.dart.DartExecutorTest; import io.flutter.embedding.engine.dart.DartMessengerTest; +import io.flutter.embedding.engine.dynamicfeatures.PlayStoreDynamicFeatureManagerTest; import io.flutter.embedding.engine.loader.ApplicationInfoLoaderTest; import io.flutter.embedding.engine.loader.FlutterLoaderTest; import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorViewTest; @@ -77,6 +78,7 @@ PlatformChannelTest.class, PlatformPluginTest.class, PlatformViewsControllerTest.class, + PlayStoreDynamicFeatureManagerTest.class, PluginComponentTest.class, PreconditionsTest.class, RenderingComponentTest.class, 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 new file mode 100644 index 0000000000000..1fafbb3437890 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/dynamicfeatures/PlayStoreDynamicFeatureManagerTest.java @@ -0,0 +1,166 @@ +// 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.dynamicfeatures; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetManager; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.FlutterJNI; +import java.io.File; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class PlayStoreDynamicFeatureManagerTest { + private class TestFlutterJNI extends FlutterJNI { + public int loadDartDeferredLibraryCalled = 0; + public int updateAssetManagerCalled = 0; + public int dynamicFeatureInstallFailureCalled = 0; + public String[] searchPaths; + public int loadingUnitId; + public AssetManager assetManager; + + public TestFlutterJNI() {} + + @Override + public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) { + loadDartDeferredLibraryCalled++; + this.searchPaths = searchPaths; + this.loadingUnitId = loadingUnitId; + } + + @Override + public void updateAssetManager( + @NonNull AssetManager assetManager, @NonNull String assetBundlePath) { + updateAssetManagerCalled++; + this.loadingUnitId = loadingUnitId; + this.assetManager = assetManager; + } + + @Override + public void dynamicFeatureInstallFailure( + int loadingUnitId, @NonNull String error, boolean isTransient) { + dynamicFeatureInstallFailureCalled++; + } + } + + // Skips the download process to directly call the loadAssets and loadDartLibrary methods. + private class TestPlayStoreDynamicFeatureManager extends PlayStoreDynamicFeatureManager { + public TestPlayStoreDynamicFeatureManager(Context context, FlutterJNI jni) { + super(context, jni); + } + + @Override + public void downloadDynamicFeature(int loadingUnitId, String moduleName) { + // Override this to skip the online SplitInstallManager portion. + loadAssets(loadingUnitId, moduleName); + loadDartLibrary(loadingUnitId, moduleName); + } + } + + @Test + public void downloadCallsJNIFunctions() throws NameNotFoundException { + TestFlutterJNI jni = new TestFlutterJNI(); + Context spyContext = spy(RuntimeEnvironment.systemContext); + doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt()); + doReturn(null).when(spyContext).getAssets(); + String soTestPath = "test/path/app.so-123.part.so"; + doReturn(new File(soTestPath)).when(spyContext).getFilesDir(); + TestPlayStoreDynamicFeatureManager playStoreManager = + new TestPlayStoreDynamicFeatureManager(spyContext, jni); + jni.setDynamicFeatureManager(playStoreManager); + assertEquals(jni.loadingUnitId, 0); + + playStoreManager.downloadDynamicFeature(123, "TestModuleName"); + assertEquals(jni.loadDartDeferredLibraryCalled, 1); + assertEquals(jni.updateAssetManagerCalled, 1); + assertEquals(jni.dynamicFeatureInstallFailureCalled, 0); + + assertTrue(jni.searchPaths[0].endsWith(soTestPath)); + assertEquals(jni.searchPaths.length, 1); + assertEquals(jni.loadingUnitId, 123); + } + + @Test + public void searchPathsAddsApks() throws NameNotFoundException { + TestFlutterJNI jni = new TestFlutterJNI(); + Context spyContext = spy(RuntimeEnvironment.systemContext); + doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt()); + doReturn(null).when(spyContext).getAssets(); + String apkTestPath = "test/path/TestModuleName_armeabi_v7a.apk"; + doReturn(new File(apkTestPath)).when(spyContext).getFilesDir(); + TestPlayStoreDynamicFeatureManager playStoreManager = + new TestPlayStoreDynamicFeatureManager(spyContext, jni); + jni.setDynamicFeatureManager(playStoreManager); + + assertEquals(jni.loadingUnitId, 0); + + playStoreManager.downloadDynamicFeature(123, "TestModuleName"); + assertEquals(jni.loadDartDeferredLibraryCalled, 1); + assertEquals(jni.updateAssetManagerCalled, 1); + assertEquals(jni.dynamicFeatureInstallFailureCalled, 0); + + assertTrue(jni.searchPaths[0].endsWith(apkTestPath + "!lib/armeabi-v7a/app.so-123.part.so")); + assertEquals(jni.searchPaths.length, 1); + assertEquals(jni.loadingUnitId, 123); + } + + @Test + public void invalidSearchPathsAreIgnored() throws NameNotFoundException { + TestFlutterJNI jni = new TestFlutterJNI(); + Context spyContext = spy(RuntimeEnvironment.systemContext); + doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt()); + doReturn(null).when(spyContext).getAssets(); + String apkTestPath = "test/path/invalidpath.apk"; + doReturn(new File(apkTestPath)).when(spyContext).getFilesDir(); + TestPlayStoreDynamicFeatureManager playStoreManager = + new TestPlayStoreDynamicFeatureManager(spyContext, jni); + jni.setDynamicFeatureManager(playStoreManager); + + assertEquals(jni.loadingUnitId, 0); + + playStoreManager.downloadDynamicFeature(123, "TestModuleName"); + assertEquals(jni.loadDartDeferredLibraryCalled, 1); + assertEquals(jni.updateAssetManagerCalled, 1); + assertEquals(jni.dynamicFeatureInstallFailureCalled, 0); + + assertEquals(jni.searchPaths.length, 0); + assertEquals(jni.loadingUnitId, 123); + } + + @Test + public void assetManagerUpdateInvoked() throws NameNotFoundException { + TestFlutterJNI jni = new TestFlutterJNI(); + Context spyContext = spy(RuntimeEnvironment.systemContext); + doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt()); + AssetManager assetManager = spyContext.getAssets(); + String apkTestPath = "blah doesn't matter here"; + doReturn(new File(apkTestPath)).when(spyContext).getFilesDir(); + TestPlayStoreDynamicFeatureManager playStoreManager = + new TestPlayStoreDynamicFeatureManager(spyContext, jni); + jni.setDynamicFeatureManager(playStoreManager); + + assertEquals(jni.loadingUnitId, 0); + + playStoreManager.downloadDynamicFeature(123, "TestModuleName"); + assertEquals(jni.loadDartDeferredLibraryCalled, 1); + assertEquals(jni.updateAssetManagerCalled, 1); + assertEquals(jni.dynamicFeatureInstallFailureCalled, 0); + + assertEquals(jni.assetManager, assetManager); + } +} diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm index 125551a841c42..fbccf6677b02f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm @@ -33,6 +33,11 @@ void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {} void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} + + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) override {} + void UpdateAssetManager(std::shared_ptr asset_manager) override {} }; } // namespace diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 0b0aa2b80da11..d9abef5a093ef 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -103,6 +103,11 @@ void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {} void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} + + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) override {} + void UpdateAssetManager(std::shared_ptr asset_manager) override {} }; } // namespace diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index fbb2404b7a3e2..b027b20e915b4 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -86,6 +86,11 @@ void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {} void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} + + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) override {} + void UpdateAssetManager(std::shared_ptr asset_manager) override {} }; class MockIosDelegate : public AccessibilityBridge::IosDelegate { diff --git a/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/shell/platform/fuchsia/flutter/platform_view_unittest.cc index 559d2b8f7cbfb..a54504a92ea74 100644 --- a/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -104,6 +104,13 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { const std::vector& supported_locale_data) { return nullptr; } + // |flutter::PlatformView::Delegate| + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + const uint8_t* snapshot_data, + const uint8_t* snapshot_instructions) {} + // |flutter::PlatformView::Delegate| + void UpdateAssetManager( + std::shared_ptr asset_manager) {} flutter::Surface* surface() const { return surface_.get(); } flutter::PlatformMessage* message() const { return message_.get(); }