Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>The loadAssets and loadDartLibrary methods are separated out because they may also be called
* manually via platform channel messages. A full installDeferredComponent implementation should
Expand Down Expand Up @@ -183,10 +182,14 @@ public interface DeferredComponentManager {
* Load the .so shared library file into the Dart VM.
*
* <p>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.
*
* <p>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.
*
* <p>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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<String> apkPaths = new ArrayList<>();
// If not found in APKs, we check in extracted native libs for the lib directly.
List<String> soPaths = new ArrayList<>();
Queue<File> 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<String> 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) {
Expand Down
18 changes: 11 additions & 7 deletions shell/platform/android/platform_view_android_jni_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<intptr_t>(jLoadingUnitId);
std::string sharedLibraryName =
fml::jni::JavaStringToString(env, jSharedLibraryName);
std::vector<std::string> 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<fml::NativeLibrary> native_lib =
Expand Down Expand Up @@ -777,7 +781,7 @@ bool RegisterApi(JNIEnv* env) {
},
{
.name = "nativeLoadDartDeferredLibrary",
.signature = "(JILjava/lang/String;)V",
.signature = "(JI[Ljava/lang/String;)V",
.fnPtr = reinterpret_cast<void*>(&LoadDartDeferredLibrary),
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,17 +36,17 @@ 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;

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;
}

Expand Down Expand Up @@ -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);
Expand All @@ -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");
}
Expand All @@ -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);
Expand All @@ -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();
Expand Down