diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
index cf81640759ec0..01797d6768fb7 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
@@ -1110,19 +1110,21 @@ public void requestDartDeferredLibrary(int loadingUnitId) {
* @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 sharedLibraryName File name of the .so file to be loaded, or if the file is not already
- * in LD_LIBRARY_PATH, the full path to the file. The .so files in the lib/[abi] directory are
- * already in LD_LIBRARY_PATH and in this case you only need to pass the file name.
+ * @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 sharedLibraryName) {
+ public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) {
ensureRunningOnMainThread();
ensureAttachedToNative();
- nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, sharedLibraryName);
+ nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, searchPaths);
}
private native void nativeLoadDartDeferredLibrary(
- long nativeShellHolderId, int loadingUnitId, @NonNull String sharedLibraryName);
+ long nativeShellHolderId, int loadingUnitId, @NonNull String[] searchPaths);
/**
* Adds the specified AssetManager as an APKAssetResolver in the Flutter Engine's AssetManager.
diff --git a/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java b/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java
index 467dd8896a522..f516cf38f56cd 100644
--- a/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java
+++ b/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java
@@ -22,11 +22,10 @@
* 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 installDeferredComponent. Once the feature module is downloaded, loadAssets and
- * loadDartLibrary should be invoked. loadDartLibrary should pass the file name of the shared
- * library .so file to FlutterJNI.loadDartDeferredLibrary for the engine to dlopen, or if the file
- * is not in LD_LIBRARY_PATH, it should find the shared library .so file and pass the full path.
- * 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.
+ * 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 installDeferredComponent implementation should
@@ -183,10 +182,14 @@ public interface DeferredComponentManager {
* Load the .so shared library file into the Dart VM.
*
*
When the download of a deferred component module completes, this method should be called to
- * find the .so library file. The filenames, or path if it's not in LD_LIBRARY_PATH, should then
- * be passed to FlutterJNI.loadDartDeferredLibrary to be dlopen-ed and loaded into the Dart VM.
- * The .so files in the lib/[abi] directory are already in LD_LIBRARY_PATH and in this case you
- * only need to pass the file name.
+ * 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.
diff --git a/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java b/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java
index ab5e6afc0efe5..22b25b34b3add 100644
--- a/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java
+++ b/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java
@@ -8,6 +8,7 @@
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
+import android.os.Build;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
@@ -25,10 +26,13 @@
import io.flutter.embedding.engine.loader.ApplicationInfoLoader;
import io.flutter.embedding.engine.loader.FlutterApplicationInfo;
import io.flutter.embedding.engine.systemchannels.DeferredComponentChannel;
+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 DeferredComponentManager that downloads deferred component
@@ -341,7 +345,57 @@ public void loadDartLibrary(int loadingUnitId, String moduleName) {
String aotSharedLibraryName =
flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so";
- flutterJNI.loadDartDeferredLibrary(loadingUnitId, aotSharedLibraryName);
+ // 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<>();
+
+ // Add the bare filename as the first search path. In some devices, the so
+ // file can be dlopen-ed with just the file name.
+ searchPaths.add(aotSharedLibraryName);
+
+ 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 boolean uninstallDeferredComponent(int loadingUnitId, String moduleName) {
diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc
index 444a761411f50..4f957bbf62f8f 100644
--- a/shell/platform/android/platform_view_android_jni_impl.cc
+++ b/shell/platform/android/platform_view_android_jni_impl.cc
@@ -567,19 +567,23 @@ static void LoadDartDeferredLibrary(JNIEnv* env,
jobject obj,
jlong shell_holder,
jint jLoadingUnitId,
- jstring jSharedLibraryName) {
+ jobjectArray jSearchPaths) {
// Convert java->c++
intptr_t loading_unit_id = static_cast(jLoadingUnitId);
- std::string sharedLibraryName =
- fml::jni::JavaStringToString(env, jSharedLibraryName);
+ std::vector search_paths =
+ fml::jni::StringArrayToVector(env, jSearchPaths);
// Use dlopen here to directly check if handle is nullptr before creating a
// NativeLibrary.
- void* handle = ::dlopen(sharedLibraryName.c_str(), RTLD_NOW);
+ 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,
- "Shared library not found for the provided name.",
- true);
+ "No lib .so found for provided search paths.", true);
return;
}
fml::RefPtr native_lib =
@@ -777,7 +781,7 @@ bool RegisterApi(JNIEnv* env) {
},
{
.name = "nativeLoadDartDeferredLibrary",
- .signature = "(JILjava/lang/String;)V",
+ .signature = "(JI[Ljava/lang/String;)V",
.fnPtr = reinterpret_cast(&LoadDartDeferredLibrary),
},
{
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java b/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java
index 77f04cf29f619..b5e98b27e5281 100644
--- a/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java
@@ -5,6 +5,7 @@
package io.flutter.embedding.engine.deferredcomponents;
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;
@@ -35,7 +36,7 @@ private class TestFlutterJNI extends FlutterJNI {
public int loadDartDeferredLibraryCalled = 0;
public int updateAssetManagerCalled = 0;
public int deferredComponentInstallFailureCalled = 0;
- public String sharedLibraryName;
+ public String[] searchPaths;
public int loadingUnitId;
public AssetManager assetManager;
public String assetBundlePath;
@@ -43,9 +44,9 @@ private class TestFlutterJNI extends FlutterJNI {
public TestFlutterJNI() {}
@Override
- public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String sharedLibraryName) {
+ public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) {
loadDartDeferredLibraryCalled++;
- this.sharedLibraryName = sharedLibraryName;
+ this.searchPaths = searchPaths;
this.loadingUnitId = loadingUnitId;
}
@@ -85,7 +86,9 @@ public void downloadCallsJNIFunctions() throws NameNotFoundException {
Context spyContext = spy(RuntimeEnvironment.application);
doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt());
doReturn(null).when(spyContext).getAssets();
- String soTestPath = "libapp.so-123.part.so";
+ String soTestFilename = "libapp.so-123.part.so";
+ String soTestPath = "test/path/" + soTestFilename;
+ doReturn(new File(soTestPath)).when(spyContext).getFilesDir();
TestPlayStoreDeferredComponentManager playStoreManager =
new TestPlayStoreDeferredComponentManager(spyContext, jni);
jni.setDeferredComponentManager(playStoreManager);
@@ -96,7 +99,9 @@ public void downloadCallsJNIFunctions() throws NameNotFoundException {
assertEquals(jni.updateAssetManagerCalled, 1);
assertEquals(jni.deferredComponentInstallFailureCalled, 0);
- assertEquals(jni.sharedLibraryName, soTestPath);
+ assertEquals(jni.searchPaths[0], soTestFilename);
+ assertTrue(jni.searchPaths[1].endsWith(soTestPath));
+ assertEquals(jni.searchPaths.length, 2);
assertEquals(jni.loadingUnitId, 123);
assertEquals(jni.assetBundlePath, "flutter_assets");
}
@@ -118,7 +123,9 @@ public void downloadCallsJNIFunctionsWithFilenameFromManifest() throws NameNotFo
.thenReturn(applicationInfo);
doReturn(packageManager).when(spyContext).getPackageManager();
- String soTestPath = "custom_name.so-123.part.so";
+ String soTestFilename = "custom_name.so-123.part.so";
+ String soTestPath = "test/path/" + soTestFilename;
+ doReturn(new File(soTestPath)).when(spyContext).getFilesDir();
TestPlayStoreDeferredComponentManager playStoreManager =
new TestPlayStoreDeferredComponentManager(spyContext, jni);
jni.setDeferredComponentManager(playStoreManager);
@@ -129,11 +136,62 @@ public void downloadCallsJNIFunctionsWithFilenameFromManifest() throws NameNotFo
assertEquals(jni.updateAssetManagerCalled, 1);
assertEquals(jni.deferredComponentInstallFailureCalled, 0);
- assertEquals(jni.sharedLibraryName, soTestPath);
+ assertEquals(jni.searchPaths[0], soTestFilename);
+ assertTrue(jni.searchPaths[1].endsWith(soTestPath));
+ assertEquals(jni.searchPaths.length, 2);
assertEquals(jni.loadingUnitId, 123);
assertEquals(jni.assetBundlePath, "custom_assets");
}
+ @Test
+ public void searchPathsAddsApks() throws NameNotFoundException {
+ TestFlutterJNI jni = new TestFlutterJNI();
+ Context spyContext = spy(RuntimeEnvironment.application);
+ 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();
+ TestPlayStoreDeferredComponentManager playStoreManager =
+ new TestPlayStoreDeferredComponentManager(spyContext, jni);
+ jni.setDeferredComponentManager(playStoreManager);
+
+ assertEquals(jni.loadingUnitId, 0);
+
+ playStoreManager.installDeferredComponent(123, "TestModuleName");
+ assertEquals(jni.loadDartDeferredLibraryCalled, 1);
+ assertEquals(jni.updateAssetManagerCalled, 1);
+ assertEquals(jni.deferredComponentInstallFailureCalled, 0);
+
+ assertEquals(jni.searchPaths[0], "libapp.so-123.part.so");
+ assertTrue(jni.searchPaths[1].endsWith(apkTestPath + "!lib/armeabi-v7a/libapp.so-123.part.so"));
+ assertEquals(jni.searchPaths.length, 2);
+ assertEquals(jni.loadingUnitId, 123);
+ }
+
+ @Test
+ public void invalidSearchPathsAreIgnored() throws NameNotFoundException {
+ TestFlutterJNI jni = new TestFlutterJNI();
+ Context spyContext = spy(RuntimeEnvironment.application);
+ 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();
+ TestPlayStoreDeferredComponentManager playStoreManager =
+ new TestPlayStoreDeferredComponentManager(spyContext, jni);
+ jni.setDeferredComponentManager(playStoreManager);
+
+ assertEquals(jni.loadingUnitId, 0);
+
+ playStoreManager.installDeferredComponent(123, "TestModuleName");
+ assertEquals(jni.loadDartDeferredLibraryCalled, 1);
+ assertEquals(jni.updateAssetManagerCalled, 1);
+ assertEquals(jni.deferredComponentInstallFailureCalled, 0);
+
+ assertEquals(jni.searchPaths[0], "libapp.so-123.part.so");
+ assertEquals(jni.searchPaths.length, 1);
+ assertEquals(jni.loadingUnitId, 123);
+ }
+
@Test
public void assetManagerUpdateInvoked() throws NameNotFoundException {
TestFlutterJNI jni = new TestFlutterJNI();