diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 01797d6768fb..cf81640759ec 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -1110,21 +1110,19 @@ 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 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. + * @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. */ @UiThread - public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) { + public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String sharedLibraryName) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, searchPaths); + nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, sharedLibraryName); } private native void nativeLoadDartDeferredLibrary( - long nativeShellHolderId, int loadingUnitId, @NonNull String[] searchPaths); + long nativeShellHolderId, int loadingUnitId, @NonNull String sharedLibraryName); /** * 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 f516cf38f56c..467dd8896a52 100644 --- a/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java +++ b/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java @@ -22,10 +22,11 @@ * 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 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. + * 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. * *

The loadAssets and loadDartLibrary methods are separated out because they may also be called * manually via platform channel messages. A full installDeferredComponent implementation should @@ -182,14 +183,10 @@ 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 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. + * 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. * *

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 8ce4b4eba16b..ab5e6afc0efe 100644 --- a/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java +++ b/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java @@ -8,7 +8,6 @@ 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; @@ -23,14 +22,13 @@ import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.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 @@ -43,6 +41,7 @@ public class PlayStoreDeferredComponentManager implements DeferredComponentManag private @Nullable FlutterJNI flutterJNI; private @Nullable DeferredComponentChannel channel; private @NonNull Context context; + private @NonNull FlutterApplicationInfo flutterApplicationInfo; // Each request to install a feature module gets a session ID. These maps associate // the session ID with the loading unit and module name that was requested. private @NonNull SparseArray sessionIdToName; @@ -191,6 +190,7 @@ public PlayStoreDeferredComponentManager( @NonNull Context context, @Nullable FlutterJNI flutterJNI) { this.context = context; this.flutterJNI = flutterJNI; + this.flutterApplicationInfo = ApplicationInfoLoader.load(context); splitInstallManager = SplitInstallManagerFactory.create(context); listener = new FeatureInstallStateUpdatedListener(); splitInstallManager.registerListener(listener); @@ -322,10 +322,7 @@ public void loadAssets(int loadingUnitId, String moduleName) { context = context.createPackageContext(context.getPackageName(), 0); AssetManager assetManager = context.getAssets(); - flutterJNI.updateJavaAssetManager( - assetManager, - // TODO(garyq): Made the "flutter_assets" directory dynamic based off of DartEntryPoint. - "flutter_assets"); + flutterJNI.updateJavaAssetManager(assetManager, flutterApplicationInfo.flutterAssetsDir); } catch (NameNotFoundException e) { throw new RuntimeException(e); } @@ -341,54 +338,10 @@ public void loadDartLibrary(int loadingUnitId, String moduleName) { } // This matches/depends on dart's loading unit naming convention, which we use unchanged. - String aotSharedLibraryName = "app.so-" + loadingUnitId + ".part.so"; + String aotSharedLibraryName = + flutterApplicationInfo.aotSharedLibraryName + "-" + 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()])); + flutterJNI.loadDartDeferredLibrary(loadingUnitId, aotSharedLibraryName); } public boolean uninstallDeferredComponent(int loadingUnitId, String moduleName) { diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java index bc1f71af2b3f..4383d0db051a 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java @@ -16,17 +16,17 @@ import org.xmlpull.v1.XmlPullParserException; /** Loads application information given a Context. */ -final class ApplicationInfoLoader { +public final class ApplicationInfoLoader { // XML Attribute keys supported in AndroidManifest.xml - static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = + public static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = FlutterLoader.class.getName() + '.' + FlutterLoader.AOT_SHARED_LIBRARY_NAME; - static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = + public static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = FlutterLoader.class.getName() + '.' + FlutterLoader.VM_SNAPSHOT_DATA_KEY; - static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = + public static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = FlutterLoader.class.getName() + '.' + FlutterLoader.ISOLATE_SNAPSHOT_DATA_KEY; - static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = + public static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = FlutterLoader.class.getName() + '.' + FlutterLoader.FLUTTER_ASSETS_DIR_KEY; - static final String NETWORK_POLICY_METADATA_KEY = "io.flutter.network-policy"; + public static final String NETWORK_POLICY_METADATA_KEY = "io.flutter.network-policy"; @NonNull private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java index 33a9e4488f18..052235821389 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java @@ -11,13 +11,13 @@ public final class FlutterApplicationInfo { private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; - final String aotSharedLibraryName; - final String vmSnapshotData; - final String isolateSnapshotData; - final String flutterAssetsDir; - final String domainNetworkPolicy; - final String nativeLibraryDir; - final boolean clearTextPermitted; + public final String aotSharedLibraryName; + public final String vmSnapshotData; + public final String isolateSnapshotData; + public final String flutterAssetsDir; + public final String domainNetworkPolicy; + public final String nativeLibraryDir; + public final boolean clearTextPermitted; public FlutterApplicationInfo( String aotSharedLibraryName, diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 4f957bbf62f8..444a761411f5 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -567,23 +567,19 @@ static void LoadDartDeferredLibrary(JNIEnv* env, jobject obj, jlong shell_holder, jint jLoadingUnitId, - jobjectArray jSearchPaths) { + jstring jSharedLibraryName) { // Convert java->c++ intptr_t loading_unit_id = static_cast(jLoadingUnitId); - std::vector search_paths = - fml::jni::StringArrayToVector(env, jSearchPaths); + std::string sharedLibraryName = + fml::jni::JavaStringToString(env, jSharedLibraryName); // Use dlopen here to directly check if handle is nullptr before creating a // NativeLibrary. - 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(); - } + void* handle = ::dlopen(sharedLibraryName.c_str(), RTLD_NOW); if (handle == nullptr) { LoadLoadingUnitFailure(loading_unit_id, - "No lib .so found for provided search paths.", true); + "Shared library not found for the provided name.", + true); return; } fml::RefPtr native_lib = @@ -781,7 +777,7 @@ bool RegisterApi(JNIEnv* env) { }, { .name = "nativeLoadDartDeferredLibrary", - .signature = "(JI[Ljava/lang/String;)V", + .signature = "(JILjava/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 3c80596684dd..77f04cf29f61 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,17 +5,22 @@ 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; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; +import android.os.Bundle; import androidx.annotation.NonNull; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.ApplicationInfoLoader; import java.io.File; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,16 +35,17 @@ private class TestFlutterJNI extends FlutterJNI { public int loadDartDeferredLibraryCalled = 0; public int updateAssetManagerCalled = 0; public int deferredComponentInstallFailureCalled = 0; - public String[] searchPaths; + public String sharedLibraryName; public int loadingUnitId; public AssetManager assetManager; + public String assetBundlePath; public TestFlutterJNI() {} @Override - public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) { + public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String sharedLibraryName) { loadDartDeferredLibraryCalled++; - this.searchPaths = searchPaths; + this.sharedLibraryName = sharedLibraryName; this.loadingUnitId = loadingUnitId; } @@ -49,6 +55,7 @@ public void updateJavaAssetManager( updateAssetManagerCalled++; this.loadingUnitId = loadingUnitId; this.assetManager = assetManager; + this.assetBundlePath = assetBundlePath; } @Override @@ -75,11 +82,10 @@ public void installDeferredComponent(int loadingUnitId, String moduleName) { @Test public void downloadCallsJNIFunctions() throws NameNotFoundException { TestFlutterJNI jni = new TestFlutterJNI(); - Context spyContext = spy(RuntimeEnvironment.systemContext); + Context spyContext = spy(RuntimeEnvironment.application); 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(); + String soTestPath = "libapp.so-123.part.so"; TestPlayStoreDeferredComponentManager playStoreManager = new TestPlayStoreDeferredComponentManager(spyContext, jni); jni.setDeferredComponentManager(playStoreManager); @@ -90,47 +96,32 @@ public void downloadCallsJNIFunctions() throws NameNotFoundException { assertEquals(jni.updateAssetManagerCalled, 1); assertEquals(jni.deferredComponentInstallFailureCalled, 0); - assertTrue(jni.searchPaths[0].endsWith(soTestPath)); - assertEquals(jni.searchPaths.length, 1); + assertEquals(jni.sharedLibraryName, soTestPath); assertEquals(jni.loadingUnitId, 123); + assertEquals(jni.assetBundlePath, "flutter_assets"); } @Test - public void searchPathsAddsApks() throws NameNotFoundException { + public void downloadCallsJNIFunctionsWithFilenameFromManifest() throws NameNotFoundException { TestFlutterJNI jni = new TestFlutterJNI(); - Context spyContext = spy(RuntimeEnvironment.systemContext); + 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); - 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(); + Bundle bundle = new Bundle(); + bundle.putString(ApplicationInfoLoader.PUBLIC_AOT_SHARED_LIBRARY_NAME, "custom_name.so"); + bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "custom_assets"); + PackageManager packageManager = mock(PackageManager.class); + ApplicationInfo applicationInfo = mock(ApplicationInfo.class); + applicationInfo.metaData = bundle; + when(packageManager.getApplicationInfo(any(String.class), any(int.class))) + .thenReturn(applicationInfo); + doReturn(packageManager).when(spyContext).getPackageManager(); + + String soTestPath = "custom_name.so-123.part.so"; TestPlayStoreDeferredComponentManager playStoreManager = new TestPlayStoreDeferredComponentManager(spyContext, jni); jni.setDeferredComponentManager(playStoreManager); - assertEquals(jni.loadingUnitId, 0); playStoreManager.installDeferredComponent(123, "TestModuleName"); @@ -138,14 +129,15 @@ public void invalidSearchPathsAreIgnored() throws NameNotFoundException { assertEquals(jni.updateAssetManagerCalled, 1); assertEquals(jni.deferredComponentInstallFailureCalled, 0); - assertEquals(jni.searchPaths.length, 0); + assertEquals(jni.sharedLibraryName, soTestPath); assertEquals(jni.loadingUnitId, 123); + assertEquals(jni.assetBundlePath, "custom_assets"); } @Test public void assetManagerUpdateInvoked() throws NameNotFoundException { TestFlutterJNI jni = new TestFlutterJNI(); - Context spyContext = spy(RuntimeEnvironment.systemContext); + Context spyContext = spy(RuntimeEnvironment.application); doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt()); AssetManager assetManager = spyContext.getAssets(); String apkTestPath = "blah doesn't matter here"; @@ -167,7 +159,7 @@ public void assetManagerUpdateInvoked() throws NameNotFoundException { @Test public void stateGetterReturnsUnknowByDefault() throws NameNotFoundException { TestFlutterJNI jni = new TestFlutterJNI(); - Context spyContext = spy(RuntimeEnvironment.systemContext); + Context spyContext = spy(RuntimeEnvironment.application); doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt()); TestPlayStoreDeferredComponentManager playStoreManager = new TestPlayStoreDeferredComponentManager(spyContext, jni);