ldx's blog

Posted p 23 március 2012

Embedding OSGi into an Android application part 1

OSGi is a modular service platform written in Java. Individual components (called bundles) may provide services, and can be deployed or removed without restarting the whole OSGi framework. Knopflerfish is a complete open source implementation of the OSGi R4 v4.2 specifications maintained by Makewave.

Knopflerfish logo

The great thing about Android is that it can seamlessly integrate existing Java code and libraries. Even though it uses a VM, Dalvik, that utilizes a bytecode format that is not compatible with the original Java bytecode specification, existing jars and classes can be converted into Dalvik bytecode easily and thus used by Android applications. Since the OSGi framework itself and the bundles are just plain jars (with some extra manifest headers), they are also supposed to just work in Android. Indeed, in most cases they do, however, there are a few catches.

Note: this is the first part of a series of posts about how to embed OSGi into an Android application.

If the only thing you need is an OSGi framework running on an Android device, you can simply compile Knopflerfish for Android and copy it to your device, then it can be started from the command line just like on a regular Linux distribution. For more information see the Knopflerfish Android tutorial http://www.knopflerfish.org/releases/3.2.0/docs/android_dalvik_tutorial.html.

Android logo

We will now take a look at what is required to embed Knopflerfish with various bundles into an Android application, and start/manage the framework programatically from the app. It is assumed that you already know how to create an Android application; see http://developer.android.com/guide/developing/index.html if you don't.

Starting up the OSGi framework programatically roughly consists of the following steps:

  1. Create the framework (e.g. via a framework factory), providing any framework properties
  2. Initialize the framework
  3. Set the initlevel and start/install any bundles on that particular initlevel
  4. Repeat the previous step for all initlevels
  5. Set the startlevel
  6. Start the framework

Eclipse build path

So create an Android app, it can contain just an Activity. If we import framework.jar into our app (add it to the build path in Eclipse, or if you use the command line just put it inside libs/) we can create the OSGi framework using a FrameworkFactory (step 1):

import org.knopflerfish.framework.FrameworkFactoryImpl;
import org.osgi.framework.BundleException;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
...
private Framework mFramework;
...
Dictionary fwprops = new Hashtable();
// add any framework properties to fwprops
FrameworkFactory ff = new FrameworkFactoryImpl();
mFramework = ff.newFramework(fwprops);
try {
    mFramework.init();
} catch (BundleException be) {
    // framework initialization failed
}

The jar file does not need to be "dexified", this will be done automatically during the build.

The bundle jars can be added to the application as either raw resources (under res/raw) or as assets (under e.g. assets/bundles). The latter has the advantage that they don't need to be renamed, resource names have limitations. Bundle jars need to be converted into the Dalvik dex format first. Here's a short shell function to do that:

dexify() {
    for f in $*; do
        tmpdir="`mktemp -d`"
        tmpfile="${tmpdir}/classes.dex"
        dx --dex --output=${tmpfile} ${f}
        aapt add ${f} ${tmpfile}
        rm -f ${tmpfile}
        rmdir ${tmpdir}
    done
}

You need the Android SDK installed, add /opt/android-sdk-linux/platform-tools to your PATH. Then you can convert your bundles via dexify assets/bundles/*. If you compiled Knopflerfish for Android as suggested by the Knopflerfish tutorial, then you don't need to dexify bundles (but you have to remove classes.dex from framework.jar).

Here are a few helper methods to install and start bundles and to set the initlevel/startlevel:

private void startBundle(String bundle) {
    Log.d(TAG, "starting bundle " + bundle);
    InputStream bs;
    try {
        bs = getAssets().open("bundles/" + bundle);
    } catch (IOException e) {
        Log.e(TAG, e.toString());
        return;
    }

    long bid = -1;
    Bundle[] bl = mFramework.getBundleContext().getBundles();
    for (int i = 0; bl != null && i < bl.length; i++) {
        if (bundle.equals(bl[i].getLocation())) {
            bid = bl[i].getBundleId();
        }
    }

    Bundle b = mFramework.getBundleContext().getBundle(bid);
    if (b == null) {
        Log.e(TAG, "can't start bundle " + bundle);
        return;
    }

    try {
        b.start(Bundle.START_ACTIVATION_POLICY);
        Log.d(TAG, "bundle " + b.getSymbolicName() + "/" + b.getBundleId() + "/"
                + b + " started");
    } catch (BundleException be) {
        Log.e(TAG, be.toString());
    }

    try {
        bs.close();
    } catch (IOException e) {
        Log.e(TAG, e.toString());
    }
}

private void installBundle(String bundle) {
    Log.d(TAG, "installing bundle " + bundle);
    InputStream bs;
    try {
        bs = getAssets().open("bundles/" + bundle);
    } catch (IOException e) {
        Log.e(TAG, e.toString());
        return;
    }

    try {
        mFramework.getBundleContext().installBundle(bundle, bs);
        Log.d(TAG, "bundle " + bundle + " installed");
    } catch (BundleException be) {
        Log.e(TAG, be.toString());
    }

    try {
        bs.close();
    } catch (IOException e) {
        Log.e(TAG, e.toString());
    }
}

private void setStartLevel(int startLevel) {
    ServiceReference sr = mFramework.getBundleContext()
        .getServiceReference(StartLevel.class.getName());
    if (sr != null) {
        StartLevel ss =
            (StartLevel)mFramework.getBundleContext().getService(sr);
        ss.setStartLevel(startLevel);
        mFramework.getBundleContext().ungetService(sr);
    } else {
        Log.e(TAG, "No start level service " + startLevel);
    }
}

private void setInitlevel(int level) {
    ServiceReference sr = mFramework.getBundleContext()
        .getServiceReference(StartLevel.class.getName());
    if (sr != null) {
        StartLevel ss =
            (StartLevel)mFramework.getBundleContext().getService(sr);
        ss.setInitialBundleStartLevel(level);
        mFramework.getBundleContext().ungetService(sr);
        Log.d(TAG, "initlevel " + level + " set");
    } else {
        Log.e(TAG, "No start level service " + level);
    }
}

Now we can install and start bundles:

setInitlevel(1);
installBundle("event_all-3.0.4.jar");
startBundle("event_all-3.0.4.jar");
// install/start other bundles...

setStartLevel(10);

try {
    mFramework.start();
} catch (BundleException be) {
    Log.e(TAG, be.toString());
    // framework start failed
}

Log.d(TAG, "OSGi framework running, state: " + mFramework.getState());

If you craft an app around the code above and start it you will see that it's not quite working yet: as the framework classloader is loading bundles at runtime Dalvik will try to place optimized dex classes into a system folder (under /data/dalvik-cache), but plain application privileges don't make it possible to write there.

In the next part we will see how to fix this problem.

Category: android, development, java, osgi

Comments