Native Library working correctly for install-time module but doesn't work as expected for on-demand module

186 Views Asked by At

I'm trying to localize an app that uses Native C++. Most of the codes written in C++ and all strings/text was in certain build flag. E.g LANG_JP // For japanese

I've created an on-demand dynamic module to my project namely "module_jp". The difference between the base module and dynamic module would be that in module_jp's build.gradle it will has an extra cpp flag.

**module_jp's build.gradle**
plugins {
   id 'com.android.dynamic-feature'
}
android {
   ...

   defaultConfig {
      ...
      externalNativeBuild {
         cmake {
            cppFlags "-fno-rtti -DLUTF8 -DLANG_JP" // Added extra flag here LANG_JP
         }
      }
   }

In addition to this, I also created separate CMakeLists.txt to create new library namely (libnative-activity-jp.so)

**module_jp's CMakeLists.txt**
...
add_library( # Sets the name of the library.
        native-activity-jp

        # Sets the library as a shared library.
        SHARED
        ...
...
target_include_directories(
        native-activity-jp PRIVATE
        ${PLAYCORE_LOCATION}/include
        )
...
target_link_libraries( # Specifies the target library.
        native-activity-jp
...

My app starts by asking the user what language it prefers. After that on main Activity, it will load what library the user prefers. For example, if the user prefer JP, it will call System.LoadLibrary("native-activity-jp"); If English, it will just load the base library. System.LoadLibrary("native-activity");

   // LanguageSelectionActivity
   // This function open the native activity to start the normal process
   private void StartMainActivity(String LanguageCode) {

      if (LanguageCode.equals("en")) System.loadLibrary("native-activity");
      else System.loadLibrary("native-activity-" + szLanguageCode);

      // Initialize intent to start the normal activity
      Intent intent = new Intent(getApplicationContext(), NativeActivity.class);
      startActivity(intent);
      finish();
   }

The Problem All seems fine when testing locally. When user select jp, it will just show the exact app with translated JP text taken from C++ source files. However when uploaded in Google's Closed Testing(Alpha) and Internal App Sharing, logs shows that module_jp was indeed installed and it is loading "libnative-activity-jp.so" but it still showing EN translation which tells me that "libnative-activity.so" was loaded first.

I've tried to investigate the issue by

  • First I've checked the list of splitcompat installed at the device by running adb backup command and extracted the backup. I confirmed that libnative-activity-jp.so was indeed extracted correctly and was saved in the device directory (data/data//splitcompat/##/native-libraries)
  • I also used that same "libnative-activity-jp.so" and tested it locally and it shows the JP translation
  • I also changed the module delivery type from "On-Demand" to "Install-Time" and it works perfectly when testing in Closed Testing(Alpha) and Internal App Sharing.

Questions

  1. Does the base native library always loaded first even if System.LoadLibrary("native-activity") wasn't called?
  2. Why does it works completely correct when setting that module to install-time but not on-demand?
2

There are 2 best solutions below

0
pleasebugmenot.mrg On

Found the problem. This was caused by System.loadLibrary call inside the static initializer block from other Java activities that was included as part of the source sets.

static {
   System.loadLibrary("native-activity");
}

Basically code statements inside static initializer block were called before any object initialization which loads this unwanted library(libnative-activity.so). Fixed the issue by removing this static initializer block and load the needed library at the onCreate() function of the MainActivity.

0
Isfa On

A few weeks ago, I encountered a similar issue when using the FFMPEG library in my app. The install-time looks fine, but when users attempt to navigate into a feature module using a dynamic feature at on-demand time, the app got crashes due to an invalid load of the native dependencies in FFMPEG, which we couldn't control.

If you encounter an issue like this:

Caused by: java.lang.ClassNotFoundException: Didn't find class "com.arthenica.mobileffmpeg.FFmpeg" on path: DexPathList[[zip file "/system/framework/org.apache.http.legacy.boot.jar", zip file "/data/app/com.app.app-b8ihOG8aQ5WoY-L3gRHD_g==/base.apk"],nativeLibraryDirectories=[/data/app/com.app.app-b8ihOG8aQ5WoY-L3gRHD_g==/lib/arm, /data/app/com.app.app-b8ihOG8aQ5WoY-L3gRHD_g==/base.apk!/lib/armeabi-v7a, /system/lib]]

Then, you have to load all native libraries. (you could also use SplitInstallHelper.loadLibrary instead of System..loadLibrary).

...
SplitInstallHelper.loadLibrary(context, "avfilter")
...

But if you're facing an issue even though you've already loaded it:

Caused by: java.lang.UnsatisfiedLinkError: dlopen failed: library "libavfilter.so" not found: needed by /data/app/~~EmR9-p8P0BbXpzWWUu5oHg==/com.app.app-TNVAQY0NbrsnsVQsGHdYGw==/lib/arm64/libmobileffmpeg.so in namespace classloader-namespace
    at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
    at java.lang.Runtime.loadLibrary0(Runtime.java:998)
    at java.lang.System.loadLibrary(System.java:1656)
    at com.arthenica.mobileffmpeg.Config.<clinit>(Config.java:33)
    at com.arthenica.mobileffmpeg.Config.ffmpegExecute(Config.java:1)
    at com.arthenica.mobileffmpeg.AsyncFFmpegExecuteTask.doInBackground(AsyncFFmpegExecuteTask.java:2)
    at com.arthenica.mobileffmpeg.AsyncFFmpegExecuteTask.doInBackground(AsyncFFmpegExecuteTask.java:1)
    at android.os.AsyncTask$3.call(AsyncTask.java:394)

You have to load the native libraries in the attachBaseContext method. First, call SplitCompat.installActivity() and then load your native libraries with SplitInstallHelper.loadLibrary using a new context that you receive from attachBaseContext.

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(newBase)
    if (newBase != null) {
        SplitCompat.installActivity(newBase)

        SplitInstallHelper.loadLibrary(newBase, "avfilter")
    }
}

I hope this helps!