Introduction
Lors d'un précédent article, nous avons évoqué l'utilisation systématique de JNI lors de l'accès aux « couches basses » d'Android depuis une application Java. Le framework Android étant majoritairement écrit en Java, ce principe est également utilisé pour les services système (Wi-Fi, Bluethooth, téléphonie, …) et l'on parle alors de HAL pour Hardware Abstraction Layer. La HAL est donc une couche logicielle en espace utilisateur (et écrite en C/C++) permettant d'adapter les services Java aux différentes cibles supportées par Android/AOSP. La notion de HAL existe depuis longtemps et ce n'est pas une innovation Google !
Le but de cet article est de décrire la prise en compte de la HAL dans Android sur la base de l'exemple simple du LightsService qui gère entre autres la luminosité de l'écran. Concrètement nous étudierons le cas de l'émulateur Android puis celui d'une carte Beaglebone Black.
Architecture des services Android
L'architecture Android est relativement complexe si on la compare avec un système GNU/Linux. Techniquement parlant, la cohabitation obligatoire Java/C/C++ n'y est pas pour rien. Le schéma ci-dessous donne l'architecture d'Android pour la partie services et HAL. On distingue bien la partie AOSP (sous licence Apache 2 ASL) puis les couches basses fournies par les constructeurs. La HAL correspond à la partie grise.
Figure1. Services et HAL (schéma Karim Yaghmour)
Chaque service Android est séparé en plusieurs parties, soit :
-
La partie android.* qui sur le schéma correspond aux classes de développement, soit frameworks/base/core dans les sources AOSP. Notons que ce répertoire contient également du code Java (sous-répertoire java) et du C/C++ (sous-répertoire jni) pour les accès JNI.
-
La partie service écrite en Java. Cela correspond à la partie System Server sur le schéma et au répertoire frameworks/base/services/java dans les sources AOSP.
-
La partie interface JNI écrite en C/C++. Cela correspond à la partie Foo service JNI sur le schéma et au répertoire frameworks/base/services/jni dans AOSP. Cette interface permet d'accéder aux ressources matérielles correspondant au service mais ce de manière indépendante de la plate-forme.
-
La bibliothèque d'accès libfoo.so dépend de la plate-forme. Le code source (si il est disponible) est située sur device/<constructeur>/<nom> comme par exemple device/ti/beagleboneblack/liblight et device/generic/goldfish/lights.
-
Le répertoire hardware/libharwdare/include/hardware contient les définitions des types à utiliser dans la bibliothèque d'accès. A titre d'exemple, le type light_device_t est défini dans hardware/.../lights.h et utilisé dans les sources présents dans les répertoires device/.../liblight (Beaglebone Black) et device/.../lights (émulateur).
-
La partie /dev/foo du schéma correspond au pilote matériel intégré au noyau Linux. Il peut également correspondre à une fonctionnalité du noyau accessible par une entrée dans /sys, comme /sys/class/backlight dans le cas du LightsService.
Un autre point à prendre en compte est le coté légal puisque la majorité du code publié par Google est sous licence Apache 2, beaucoup plus « permissive » que la GPL/LGPL. Cette architecture utilisant en majorité du code en espace utilisateur, les bibliothèques dépendantes du matériel peuvent être fournies uniquement sous forme binaire et ce en toute légalité. La partie GPL reste limitée au noyau Linux.
Exemple du LightsService
Le LightsService a l'avantage d'être relativement simple pour une introduction. Sous Android, le réglage de la luminosité est accessible par le menu Settings/ Display/ Brightness.
Figure2. Réglage de luminosité
Le service Java correspond au fichier LightsService.java dans le répertoire frameworks/base/service/java/com/android/server. La classe Light fait référence à une fonction setLight_native() définie dans la couche JNI et déclarée dans le fichier source Java comme suit :
private static native void setLight_native(int ptr, int light, int color, int mode, int onMS, int offMS, int brightnessMode);
Dans la même classe, cette fonction est utilisée dans setLightLocked() :
private void setLightLocked(int color, int mode, int onMS, int offMS, int brightnessMode) { if (color != mColor || mode != mMode || onMS != mOnMS || offMS != mOffMS) { if (DEBUG) Slog.v(TAG, "setLight #" + mId + ": color=#" + Integer.toHexString(color)); mColor = color; mMode = mode; mOnMS = onMS; mOffMS = offMS; setLight_native(mNativePointer, mId, color, mode, onMS, offMS, brightnessMode); } }
La fonction setLightLocked() est utilisée dans les autres fonctions, dont le réglage de la luminosité.
public void setBrightness(int brightness, int brightnessMode) { synchronized (this) { int color = brightness & 0x000000ff; color = 0xff000000 | (color << 16) | (color << 8) | color; setLightLocked(color, LIGHT_FLASH_NONE, 0, 0, brightnessMode); } }
La fonction setLight_native() est définie dans le fichier frameworks/base/services/jni/com_android_server_LightsService.cpp. La fonction set_light() dépend de la cible définie dans le pointeur Devices *devices passé en paramètre par ptr. Notons que ce code est toujours indépendant de la plate-forme et donc fourni par AOSP.
static void setLight_native(JNIEnv *env, jobject clazz, int ptr, int light, int colorARGB, int flashMode, int onMS, int offMS, int brightnessMode) { Devices* devices = (Devices*)ptr; light_state_t state; if (light < 0 || light >= LIGHT_COUNT || devices->lights[light] == NULL) { return ; } memset(&state, 0, sizeof(light_state_t)); state.color = colorARGB; state.flashMode = flashMode; state.flashOnMS = onMS; state.flashOffMS = offMS; state.brightnessMode = brightnessMode; { ALOGD_IF_SLOW(50, "Excessive delay setting light"); devices->lights[light]->set_light(devices->lights[light], &state); } }
L'implémentation de la fonction set_light() est souvent fournie par le fabricant de matériel. Dans le cas de l'émulateur, il est bien entendu fourni par AOSP dans device/generic/goldfish/lights/lights_qemu.c.
/** Open a new instance of a lights device using name */ static int open_lights( const struct hw_module_t* module, char const *name, struct hw_device_t **device ) { void* set_light; if (0 == strcmp( LIGHT_ID_BACKLIGHT, name )) { set_light = set_light_backlight; } ... dev->set_light = set_light; ... *device = (struct hw_device_t*)dev; return 0; }
Dans le cas de la luminosité, nous utilisons la fonction set_light_backlight() qui envoie une commande à l'émulateur QEMU. Nous présentons ci-dessous un extrait de la fonction :
static int set_light_backlight( struct light_device_t* dev, struct light_state_t const* state ) { /* Get Lights service. */ int fd = qemud_channel_open( LIGHTS_SERVICE_NAME ); ... /* send backlight command to perform the backlight setting. */ if (qemud_channel_send( fd, buffer, -1 ) < 0) { E( "%s: could not query lcd_backlight: %s", __FUNCTION__, strerror(errno) ); close( fd ); return -1; } close( fd ); return 0; }
Dans le cas d'une carte réelle comme la Beaglebone Black, le réglage de la luminosité de l'écran utilise par contre une fonctionnalité du noyau Linux. On peut visualiser (ou modifier) la valeur de luminosité directement sur la console de la carte ou bien en utilisant une console « shell » via ADB. On constate alors la variation de luminosité.
root@android:/ # cat /sys/class/backlight/backlight.11/brightness 100 root@android:/ # echo 50 > /sys/class/backlight/backlight.11/brightness
Bien entendu cette méthode n'est pas la meilleure méthode à utiliser dans le cas d'Android. Pour la Beaglebone Black, nous utilisons une version modifiée d'AOSP dédiée aux processeurs TI Sitara (AM335x, AM35x, …). La construction de l'environnement Android pour cette carte sort du cadre de l'article mais nous constatons que l'implémentation de la fonction set_light() est située dans le fichier device/ti/beagleboneblack/liblight/lights.c. Nous présentons ci-dessous quelques extraits du code. La fonction open_lights() est très similaire à la précédente :
static int open_lights(const struct hw_module_t *module, char const *name, struct hw_device_t **device) { int (*set_light)(struct light_device_t *dev, struct light_state_t const *state); if (0 == strcmp(LIGHT_ID_BACKLIGHT, name)) set_light = set_light_backlight; else return -EINVAL; ... }
Par contre, la fonction set_light_backlight() utilise un accès direct au fichier /sys/class/backlight/backlight.11/brightness. Il faut noter que nous avons du modifier les entrées utilisées par la distribution initiale afin d'obtenir un fonctionnement correct car les noms des fichiers virtuels n'étaient pas corrects.
/* char const *const LCD7_FILE = "/sys/class/backlight/pwm-backlight/brightness"; char const *const LCD3_FILE = "/sys/class/backlight/tps65217-bl/brightness"; */ char const *const LCD7_FILE = "/sys/class/backlight/backlight.11/brightness"; char const *const LCD3_FILE = "/sys/class/backlight/backlight.11/brightness"; static int set_light_backlight(struct light_device_t *dev, struct light_state_t const *state) { ... /* Try to write to LCD7 Backlight node */ err = write_int(LCD7_FILE, brightness); if (err != 0) { /* LCD7 Backlight node not available, Try to write to LCD3 Backlight node */ err = write_int(LCD3_FILE, brightness); if (err != 0) /* LCD3 and LCD7 Backlight node not available */ ALOGI("write_int failed to open \n\t %s and %s\n", LCD7_FILE, LCD3_FILE); } ... }
Conclusion
Cet article nous a permis de décrire rapidement par un exemple le fonctionnement d'un service Android et surtout son adaptation à une cible matérielle spécifique via la HAL. Dans un prochain article nous décrirons l'adaptation d'AOSP à une nouvelle cible et la personnalisation d'une cible existante en utilisant le contenu du répertoire device.
Bibliographie