Friday, May 09, 2014

Run native binaries on Android: or run cross-compiled NDK application on an Android device.

What if you would like to run a program you cross compile with the NDK on your android device?

As requirements / assumptions:
  • You have a nice standalone application (e.g. a prefered GNU application) written e.g. in C or C++
  • Usualy, you run it on Linux or Cygwin. 
  • You want it on your mobile phone or tablet, 
    • BUT your phone runs Android 
    • AND it should be programmed with Java.
  • You don't want to write it anew in Java
  • You have cross compiled it with NDK
  • You don't want to root your phone or to make people root their phone to use your application. 
In this article I describe the steps that let you run your cross compiled binaries from an Android application. Since I assume you already have a cross compiled binary, I use a shell script as a binary.

  1. create an Android application for our experiment
  2. make a shell script
  3. modify the application in order to copy binaries from assets
  4. run the script
Create an Android application for our experiment using eclipse:
File>New Project...
Android> Android Application Project
Application Name: Test3
NEXT
NEXT
NEXT
NEXT
FINISH

Wait while eclipse is preparing the project... may take a bit of time...

To test it on your phone, if it is correctly configured for USB debugging...
Right click on Test3 in the Project Explorer tab in eclipse.
Run As>1. Android Application

Make a shell script:
Right click on the assets directory of your Test3 project.
New>File...
Give the File name: go.sh
Close the window that may just have opened.
Rightclick on it, open with>Test editor
Write the following in the file:
#!/system/bin/sh
echo "Hello from script"
exit 0


Close the file.

Modify the application in order to copy binaries from assets:
Open MainActivity.java
Add the following method in MainActivity class:
    /**
     * copy a file from the assets directory to the application dedicated
     * directory on the phone
     */
    private void copyAssets(String filename) {
        AssetManager assetManager = getAssets();

        InputStream src = null;
        OutputStream dst = null;
        Log.d(USER_SERVICE, "Copy from asset: " + filename);

        try {
            // get the destination path
            // i.e. path to where the application has write permissions
            String appFileDirectory = getFilesDir().getPath();
            Log.d(USER_SERVICE, "to: " + appFileDirectory);

            // open the source file
            src = assetManager.open(filename);

            // create destination file
            File outFile = new File(appFileDirectory, filename);
            dst = new FileOutputStream(outFile);

            // copy the file
            byte[] buf = new byte[4096];
            int rd_status;
            // copy until read EOF
            while ((rd_status = src.read(buf)) != -1) {
                dst.write(buf, 0, rd_status);
            }

            src.close();
            src = null;
            dst.flush();
            dst.close();
            dst = null;
        } catch (IOException e) {
            Log.e(USER_SERVICE, "ERROR while copying asset file: " + filename, e);
        }
        Log.d(USER_SERVICE, "Copy ok: " + filename);
    }


In method onCreate, call the copyAssets function:
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new PlaceholderFragment()).commit();
        }
       
        copyAssets("go.sh");
    }


This will copy the script from the assets directory to the directory where the application has write permission.

run the script 
Then we want to run that script.
To do that we need a Java method that runs system binaries. Add this method in the MainActivity class:
    public Boolean execProgram(String program) {
        Log.d(USER_SERVICE, "Start Exec: " + program);
        try {
            // load the program
            Runtime rt = Runtime.getRuntime();
            Process process = rt.exec(program);
          
            // catch input and output streams of the program
            DataOutputStream os = new DataOutputStream(process.getOutputStream());
            InputStreamReader is = new InputStreamReader(process.getInputStream());
            BufferedReader reader = new BufferedReader(is);
          
            // if necessary give stdin through
            // os.writeBytes( "some input\n" );
            os.flush();

            // execute the program
            process.waitFor();
          
            // get the stdout from the program
            String output = "[";
            int nb_words = 0;
            int read;
            char[] buffer = new char[4096];
            StringBuffer progOutput = new StringBuffer();
            while ((read = reader.read(buffer)) > 0) {
                progOutput.append(buffer, 0, read);
            }
            reader.close();
            output += progOutput;
            output += "]{" + Integer.toString(nb_words) + "}";

            Log.d(USER_SERVICE, "output=" + output);
            Log.d(USER_SERVICE, "finished Exec of " + program);
        } catch (IOException e) {
            Log.e(USER_SERVICE, "IO Error while running " + program, e);
            return false;
        } catch (InterruptedException e) {
            Log.e(USER_SERVICE, "IO Error while running " + program, e);
            return false;
        }
        return true;
    }


Modify again onCreate to make go.sh executable by calling chmod and then execute go.sh:
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new PlaceholderFragment()).commit();
        }
       
        copyAssets("go.sh");
        // make go.sh executable for everyone 777 or for me 744
        execProgram("/system/bin/chmod 744 " + getFilesDir().getPath() + "/go.sh");
        // execute go.sh
        execProgram(getFilesDir().getPath() + "/go.sh");

    }


Then you'll see the output on the LogCat messages.

Wednesday, May 07, 2014

How to JNI with ADT Eclipse Android Development Toolchain

Get the toolchain:
- download ADT: e.g.: adt-bundle-linux-x86-20140321.zip
- download NDK e.g.: android-ndk-r9d-linux-x86.tar.bz2

unzip adt-bundle-linux-x86-20140321.zip
tar -xjf android-ndk-r9d-linux-x86.tar.bz2

this will create two directories:
- adt-bundle-linux-x86-20140321
- android-ndk-r9d


run eclipse:
cd adt-bundle-linux-x86-20140321/eclipse
eclipse

Configure Eclipse:
in menu: Window>Preferences
then select: Android>NDK
there you'll give the location to: fiull/path/to/android-ndk-r9d

Create an empty new project:
File>New>Project...
Android>Android Application Project

Application Name: test2
NEXT
NEXT
NEXT
Blank Activity, then NEXT
FINISH

Add support for NDK to this app:
right click on application name in project explorer "test2".
Android Tools>Add Native Support...

Keep proposed library Name: test2
Finish

two files have been created in the subfolder jni:
- test2.cpp
- Android.mk

Write the C code:
In project explorer: open jni/test2.cpp

it already contains
#include


write the following code in jni/test2.cpp:
extern "C"
{
JNIEXPORT int JNICALL Java_com_example_test2_Test2Wrapper_myfunc
      (JNIEnv * je, jobject jobj)
    {
            return (3);
    }
}

The function name must be composed as follow:
- Java_
- package name (here com.example.test2) with dots replaced by underscores
- your class name that will receive the native code (here Test2Wrapper)
- the function name (here myfunc)

The function type here is int and it returns the value 3.
The function receive two obligaroty parameters:
JNIEnv * je, jobject jobj

To have more parameters, add them after those two, like this e.g. with two integers p1 and p2:
JNIEXPORT int JNICALL Java_com_example_test2_Test2Wrapper_myfunc
      (JNIEnv * je, jobject jobj, jint p1, jint p2)

Write makefile:
Android.mk is the makefile.
It is already written, so here, for the moment, nothing more to do ;-).
It should look like:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := test2
LOCAL_SRC_FILES := test2.cpp
include $(BUILD_SHARED_LIBRARY)


LOCAL_MODULEis given the name of the compiled library.
LOCAL_SRC_FILES is given a list of c or c++ files to be compiled.

write the Java wrapper:
For each function of the library you want to call from Java, you need to implement a native method of a wrapper class.
In our example we want to name the wrapper class: Test2Wrapper

In project explorer, open: src/com.example.test2
In this directory you have the MainActivity.java file.
Right click on  com.example.test2
new>Class
Name:  Test2Wrapper
FINISH

Modify the code as follow:
package com.example.test2;
public class Test2Wrapper {
    static {
        System.loadLibrary("test2");
    }
    public static native int func();

}







System.loadLibrary is given the name of the library compiled by Android.mk i.e. the one given to LOCAL_MODULE, here test2.


Call and use the wrapper:
Open the file: MainActivity.java
Modify the onCreate method as follow:
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new PlaceholderFragment()).commit();
        }

        // 2 lines added first to call the native function
        // then to display a message box with "3" written inside.
        String hello = Integer.toString(Test2Wrapper.func());
        new AlertDialog.Builder(this).setMessage(hello).show();

    }


Build and run the project:
Save all files. This should lead eclipse to compile automatically the files.
Right click on project name.
Run As>Android Application

That's all, folks!