Contrary to standard "vanilla Linux", Android requires more than just proper device drivers to function on hardware. It in fact defines a new Hardware Abstraction Layer (HAL) which defines an API for each type of hardware supported by Android's core. In order for an ODM's hardware to properly interface with Android, it must provide a hardware "module" (unrelated to kernel modules) which conforms to the API specified for that type of hardware. This blog shows you how to extend Android's HAL by adding your own new type hardware to the Android stack.

Generally, each type of hardware supported by Android has a corresponding "System Service" and HAL definition. There's a Lights system service and a lights HAL definition. There's a WiFi manager system service and wifi HAL definition. The same goes for power management, location, sensors, etc. The following figure from our CC-BY-SA Embedded Android courseware illustrates this architecture:

There are two general categories of HAL modules, those explicitly loaded (through a runtime dlopen()) and those automatically loaded by the dynamic linker (libhardware_legacy.so). The API for the former are in <aosp>/hardware/libhardware/include/hardware and the API for the latter are in <aosp>/hardware/libhardware_legacy/include/hardware_legacy. The trend seems to be that Android is moving away from "legacy". The interface between those ".so" files and the actual drivers through /dev entries or otherwise is up to the ODM to specify. Android doesn't care about that. It only cares about the HAL .so modules.

One of the questions I often get is "how do I add support for my own type of hardware in Android?" To illustrate this, I've created a opersys-hal-hw type and have posted the code that implements this HAL type on github along with a very basic circular buffer driver.

The Code

If you copy the content of the opersys-hal-hw project over an existing 2.3.7_r1 release of the AOSP and build it for the emulator, you should get yourself an image that comes up with the "opersys" service. That service has a very simple interface consisting of a read() and a write() call. If you follow the code, you will see how each of these are implemented at every layer of Android with the core definition of the new hardware type being in <aosp>hardware/libhardware/include/hardware/opersyshw.h:

#ifndef ANDROID_OPERSYSHW_INTERFACE_H
#define ANDROID_OPERSYSHW_INTERFACE_H

#include 
#include 
#include 

#include 

__BEGIN_DECLS

#define OPERSYSHW_HARDWARE_MODULE_ID "opersyshw"

struct opersyshw_device_t {
    struct hw_device_t common;

    int (*read)(char* buffer, int length);
    int (*write)(char* buffer, int length);
    int (*test)(int value);
};

__END_DECLS

#endif // ANDROID_OPERSYSHW_INTERFACE_H

The code for the hardware module implementing this interface is in <aosp>sdk/emulator/opersyshw/opersyshw_qemu.c. And as you can see by looking at this code, it essentially just does the "read" and "write" by opening /dev/circchar and reading/writing from/to it. In fact, if you've built the driver I pointed to earlier and have it in your emulator's /system/lib/modules/, the modified init configuration files I provide in the opersys-hal-hw project will automatically load it before system services are started and, therefore, the opersys hardware module will be able to talk to the "device" when the system services start.

The opersys hw module is itself loaded by the opersys system service's JNI code in <aosp>/framework/base/services/jni/com_android_server_OpersysService.cpp. The system service itself is in this case implemented in Java and has been added to the same location as the other Java-coded system services as <aosp>/framework/base/services/java/com/android/server/OpersysService.java. The loading of the HAL module by the system service's JNI code is usually done through a call to hw_get_module(), which essentially results in a dlopen():

static jint init_native(JNIEnv *env, jobject clazz)
{
    int err;
    hw_module_t* module;
    opersyshw_device_t* dev = NULL;
    
    err = hw_get_module(OPERSYSHW_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
    if (err == 0) {
        if (module->methods->open(module, "", ((hw_device_t**) &dev)) != 0)
           return 0;
    }

    return (jint)dev;
}

Yes, you're reading this right, I'm returning a pointer as an int to the Java layer. A student attending one of my classes actually asked it this way: "Does Android run on 64-bit machines?" Have a look at the Lights service, the above is inspired by it. And the pointer does come back:

static int read_native(JNIEnv *env, jobject clazz, int ptr, jbyteArray buffer)
{
    opersyshw_device_t* dev = (opersyshw_device_t*)ptr;
    jbyte* real_byte_array;
    int length;

    real_byte_array = env->GetByteArrayElements(buffer, NULL);

    if (dev == NULL) {
        return 0;
    }

    length = dev->read((char*) real_byte_array, env->GetArrayLength(buffer));

    env->ReleaseByteArrayElements(buffer, real_byte_array, 0);

    return length;
}

That's for how a system service talks to a new type of hardware through the HAL. There's also the matter of exposing the new system service's API to the upper layers. To do this, you need something like <aosp>/frameworks/base/core/java/android/os/IOpersysService.aidl:

package android.os;
interface IOpersysService {
/**
* {@hide}
*/
String read(int maxLength);
int write(String mString);
}

This allows you to use something like the following HelloOpersysInternal app that builds as part of the AOSP to call on the new system service:

package com.opersys.hellointernal;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.os.ServiceManager;   // Will only work in AOSP
import android.os.IOpersysService;  // Interface "hidden" in SDK

public class HelloOpersysInternalActivity extends Activity {
    private static final String DTAG = "HelloOpersysInternal";

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        IOpersysService om = IOpersysService.Stub.asInterface(ServiceManager.getService("opersys"));
        try {
        	Log.d(DTAG, "Going to write to the \"opersys\" service");
        	om.write("Hello Opersys");
        	Log.d(DTAG, "Service returned: " + om.read(20));
        }
        catch (Exception e) {
        	Log.d(DTAG, "FAILED to call service");
        	e.printStackTrace();
        }
    }

That, though, uses APIs which are only available within the AOSP's build. What you likely want is to generate a new SDK and call the new system service using the standard developer API calls:

package com.opersys.hello;

import android.app.Activity;
import android.os.Bundle;
import android.os.OpersysManager;
import android.util.Log;

public class HelloOpersysActivity extends Activity {
    private static final String DTAG = "HelloOpersysInternal";

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        OpersysManager om = (OpersysManager) getSystemService(OPERSYS_SERVICE);

        Log.d(DTAG, "Going to write to the \"opersys\" service");
    	om.write("Hello Opersys");
    	Log.d(DTAG, "Service returned: " + om.read(20));
    }

That's taken care of by modifiying the getSystemService() implementation, the addition of OPERSYS_SERVICE to the Context implementation and the creation of an OpersysManager class which is essentially a wrapper around the calls made to the opersys system service. Have a look at the files and modifications in <aosp>/frameworks/base/core/ for the details of this.

In Closing

Obviously there's way much more to be said on this topic and there's only so much I can fit in a blog. Nevertheless, this should give you a head start as to how to add support for your own type of hardware to Android. It should also enable you to start digging into the Android sources and understand the inner workings of how Android supports your favorite hardware.

Note that the above example takes a few shortcuts. Mainly, I'm adding support for the new type of hardware in the AOSP as if it was meant to be upstreamed. Usually, custom extensions are better added into a <aosp>/device/manufacturer-name/product-name/ directory. That, though, is a topic for a separate post. And, for what it's worth, as much as you'd like to isolate your changes to the AOSP, adding a new type of hardware to it isn't something you'll be able to shove underneath a directory in <aosp>/device/. It's too fundamental.