I’ve done this work to help out with the open source programming course at Seneca (DPS911).

The goal: see if it’s possible (and realistic) to use XMP in an Android app.

I’ve spent about 20 hours working on it, mostly going round in circles. The XMP library is shit developed by idiots and Android Studio is a pain in the neck because if Apple can be, so can Google.

I can’t possibly log everything I’ve done in this post. The problem itself is complex to begin with: cross-compile a major library, and make it usable (via JNI) in an Android app. I was expecting that figuring out cross-compilers, toochains, ABIs, and APIs (which is hard) but the what made it nearly impossible was Android Studio with it’s Gradle nonsense.

Every time I thought I solved a major problem another one took its place. But enough whining, here’s what I found:

janrueegg’s Android port of the library works. I’ve forked it on github just so I know which version that was. Compiling it is simple, from the build directory:

export ANDROID_NDK=~/programs/android-ndk-r10d/
make StaticReleaseAndroid_armeabi-v7a

Note the armeabi-v7a part, you’ll need to build several versions if you’re actually planning to release an app using it or if you want to run it in an x86 emulator. The result is two .ar files which I need to turn into one .a file (for a long time because of retarded Android documentation I was trying to make a .so file from it but that turned out to be unnecessary):

ar xv staticXMPCore.ar
ar xv staticXMPFiles.ar
ar cr libXMP.a *.o
cp libXMP.a ~/temp/MyApplication/app/src/main/prebuilt/

The following steps were so many and confusing I’ve simply posted the resulting sample application on github. Pull it to better understand what I’m talking about.

I added my libXMP.a to app/src/main/prebuilt so I can build it in.

I needed a .cpp file to do the JNI (app/src/main/jni/xmp-jni.cpp). That file needs to include the headers for the library. I’ve dumped all the contents of the directory xmp/public/include from the library into app/src/main/jni.

XMPlib.java is the glue between the JNI code and whatever wants to use it in the rest of my java.

The best for last, the build part. After spending many hours trying to get it to work with Gradle (first tried using a static version of the library, then dynamic) I figured out that it’s just not going to work, I found snippets of google employees saying online they won’t even try to fix it. Instead I configured gradle to call ndk-build manually, which uses my own Android.mk and Application.mk

I’ll just post the files here with a couple of notes, explaining it all would take too long.

build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"

    defaultConfig {
        applicationId "com.example.andrew.myapplication"
        minSdkVersion 15
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"

        ndk {
            moduleName "xmp-jni"
            /**
             *  Need this so that I get STL support, needed by XMP.incl_cpp
             * Sadly I think this makes the apk much bigger because
             * of the included libgnustl_shared.so.
            */
            stl "gnustl_shared"
            /**
             * gnustl_shared has exceptions turned off by default, but I need for XMP.
            */
            cFlags "-fexceptions"

        }
    }

    sourceSets.main {
        // Uncomment this line to use Android.mk instead
        //jni.srcDirs = ["src/main/jni"]
        jni.srcDirs = []
        jniLibs.srcDir "src/main/jniLibs"
    }

    // call regular ndk-build(.cmd) script from app directory
    task ndkBuild(type: Exec, description: 'Build XMP JNI object files') {
        commandLine '/home/andrew/programs/android-ndk-r10d/ndk-build',
                '-C', file('src/main/jni').absolutePath,
                'NDK_OUT=../../../build/intermediates/ndk/debug/obj',
                'NDK_LIBS_OUT=../../../build/intermediates/ndk/debug/lib'
    }
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkBuild
    }

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

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
}

Note in particular the ndkBuild with its hard-coded paths.

Android.mk:

# This makefile is only used if the magic string is uncommented in build.gradle
#

LOCAL_PATH := $(call my-dir)

# static library info
LOCAL_MODULE := libXMP
LOCAL_SRC_FILES := ../prebuilt/libXMP.a
include $(PREBUILT_STATIC_LIBRARY)

# wrapper info
include $(CLEAR_VARS)
LOCAL_MODULE    := xmp-jni
LOCAL_SRC_FILES := xmp-jni.cpp
LOCAL_CFLAGS    += -fexceptions
LOCAL_STATIC_LIBRARIES := libXMP
LOCAL_LDLIBS += -lz
include $(BUILD_SHARED_LIBRARY)

Application.mk:

APP_STL := gnustl_static
APP_PLATFORM := android-21

local.properties:

sdk.dir=/home/andrew/programs/android-sdk-linux
ndk.dir=/home/andrew/programs/android-ndk-r10

Note that here also the paths need to be changed to work on your machine.

I wish I could say the exercise was a huge success. I got to work everything I planned to, but it was really, really hard and even though I managed to call one XMP function from Java, that was the end of my patience. See the comment in the xmp-jni.cpp below:

#include <jni.h>

#define UNIX_ENV 1 // for XMP_Environment.h

#include <string>
#define XMP_INCLUDE_XMPFILES 1 //if using XMPFiles
#define TXMP_STRING_TYPE std::string
/**
 * Clients must compile XMP.incl_cpp to ensure that all client-side glue code is generated. Do this by
 * including it in exactly one of your source files.
 * This has to happen before the #include "XMP.hpp" below
 */
#include "XMP.incl_cpp"

#include "XMP.hpp"

const char* doSomeXMPStuffInCPP()
{
    SXMPMeta::Initialize();
}

extern "C" {

    jstring
    Java_com_example_andrew_myapplication_XMPlib_doSomeXMPStuff( JNIEnv* env,
                                                                 jobject thiz )
    {
        if(!SXMPMeta::Initialize())
        {
            return env->NewStringUTF("Could not initialize SXMPMeta");
        }
        // This returns true but causes the app to crash like this:
        // 02-25 18:27:12.852    2960-2960/com.example.andrew.myapplication A/libc﹕ Fatal signal 6 (SIGABRT), code -6 in tid 2960 (w.myapplication)
        /*if (!SXMPFiles::Initialize())
        {
            return env->NewStringUTF("Could not initialize SXMPFiles.");
        }*/

        // Do XMP stuff here

    //SXMPFiles::Terminate();
    //SXMPMeta::Terminate();

        return env->NewStringUTF("XMP toolkit seems to be working!");
    }
}

I hope for your sake you don’t have to work with this crap, but if you do, here again are the URLs for the two projects: