From 259b13e2c5c916564008435b2a651b07d3a20d6a Mon Sep 17 00:00:00 2001 From: Chingjun Lau Date: Thu, 21 Jan 2021 22:35:18 -0800 Subject: [PATCH] Search multiple paths when loading deferred component .so files. This is a partial revert of 7c19824c6d08bb8e3fc4217058178d42d451e26b On some devices we still need to the original search paths approach because dlopen with just the base file name doesn't work. We're combining both approaches now, adding the base filename as the first entry in the searchPaths. --- .../flutter/embedding/engine/FlutterJNI.java | 14 ++-- .../DeferredComponentManager.java | 21 +++--- .../PlayStoreDeferredComponentManager.java | 56 ++++++++++++++- .../android/platform_view_android_jni_impl.cc | 18 +++-- ...PlayStoreDeferredComponentManagerTest.java | 72 +++++++++++++++++-- 5 files changed, 151 insertions(+), 30 deletions(-) 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();