How do I embed a native code library in an Android app (SDK 29+)?

1.9k Views Asked by At

I have an app that uses a native code library -- a custom build of ffmpeg as it happens, though that isn't strictly relevant.

Up until now I have added the executable to my res/raw directory and then at runtime extracted it to a data folder before calling it with exec(). However, as of compileSdkVersion=29 this has stopped working as a result of new security for a W^X vulnerability. Calling exec() now causes an exception:

java.io.IOException: Cannot run program "..." (in directory "..."): error=13, Permission denied

I have read these pages:
permission is denied using Android Q ffmpeg": error=13, Permission denied
https://issuetracker.google.com/issues/152645643

So, as I understand it the correct way to do this is:

  1. Put my library in the /libs directory of the project
  2. Add android:extractNativeLibs = "true" to the AndroidManifest.xml
  3. Ensure the filename matches the pattern "lib*.so"

then the installation should unpack the library into an executable location and I should be able to read it at runtime.

It's just not working. I must be missing a vital step.

My code

app/build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.1"

    defaultConfig {
        applicationId "com.example.nativelibrarytest"
        minSdkVersion 29
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.1'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.nativelibrarytest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:extractNativeLibs="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Activity code (based on the Basic Activity template):

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onResume() {
        super.onResume()

        // Test code: see if we can find the /libs library
        val libName = "libffmpeg.so"
        val nativeLibsDir = applicationInfo.nativeLibraryDir
        val libPath = "$nativeLibsDir/$libName"
        val libFile = File(libPath)
        val result = if (libFile.exists()) "exists" else "does not exist"

        val label = findViewById<TextView>(R.id.label)
        label.text = "$libPath $result"

        Log.d("MainActivity", label.text.toString())
    }
}

Project tree enter image description here

Result (on emulator)

/data/app/~~Q0jHO-XwEvieuwr-92_Fyg==/com.example.nativelibrarytest-byqCoqnbYsgtmCOVg361NA==/lib/x86/libffmpeg.so does not exist

Why can't my runtime code find the library?

I have tried:

  • Adding implementation files('libs/libffmpeg.so') inside the build.gradle > dependencies { ... }
  • Adding implementation fileTree(dir: "libs", include: ["lib*.so"])
  • Creating libs/x86 and libs/arm64 subdirectories and putting libffmpeg.so inside those

I have expanded the .apk file and unzipped it and there is no lib directory in it. It's as though the build process is not aware that the native library exists and is not copying it in.

1

There are 1 best solutions below

2
On

Update: After much Googling I think I have found an answer. I was missing ndk abiFilters and sourceSets from my build.gradle file.

So my final project setup is:

  1. Create a libs folder at the root of the module, with sub-folders for each architecture. Each binary must be named lib<something>.so even it it isn't strictly a shared library.
|app/
|-- build/
|-- libs/
|---- arm64-v8a/
|------ libffmpeg.so (compiled for 64-bit ARM)
|---- armeabi-v7a/
|------ libffmpeg.so (compiled for 32-bit ARM)
|---- x86/
|------ libffmpeg.so (compiled for 32-bit Intel)
|---- x86_64/
|------ libffmpeg.so (compiled for 64-bit Intel)
|-- src/

Final project layout

  1. build.gradle for the app should include entries for
  • android > defaultConfig > ndk > abiFilters
  • android > sourceSets
  • dependencies > implementation fileTree

This is the complete build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.1"

    defaultConfig {
        applicationId "com.example.nativelibrarytest"
        minSdkVersion 29
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    sourceSets {
        main {
            // let gradle pack the shared library into apk
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["lib*.so"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.1'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

The AndroidManifest.xml and Activity code are unchanged from my original post.

And the Logcat result is:

2020-09-29 19:32:50.911 9866-9866/com.example.nativelibrarytest D/MainActivity: /data/app/~~zwvTGRwVjL_Uv5d4joi4jw==/com.example.nativelibrarytest-9G5w819CD5uenr5N1jiAhg==/lib/x86/libffmpeg.so exists