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