mars
2010
A l’instar des Activities, des Intents, les services font partie des briques essentielles d’Android. Ils ne disposent pas d’interface utilisateur mais fonctionnent en arrière plan pour une période de temps indéfinie. L’exemple le plus courant est le lecteur de musique, qui vous permet d’écouter vos mp3 sans bloquer la navigation sur internet ou consultre la liste des vos contacts. Un service peut également rapatrier des données sur internet tels que des flux RSS.
Dans ce long article nous aborderons :
- Les services (threads, cycle de vie…)
- Services Locaux (LocalService)
- Déclaration du service
- Implémentation des listeners
- Implémentation d’un binder
- Services Distants (RemoteService)
- Déclaration du service
- Mise en place des Callbacks
- Démarrer le service au boot du téléphone
- Exemple : Connexion au service MP3
> Les services (threads, cycle de vie…)
Les services ont pour but de réaliser des tâches de fond sans aucune interaction avec l’utilisateur pour une durée indéfinie. Il existe deux type de services :
- les services locaux (ou LocalService) qui s’exécutent dans le même processus que votre application
- Les services distants (ou RemoteService) qui s’exécutent dans un processus différent de celui de application
Les services s’exécutent dans le Thread principal du processus parent. Ils doivent être déclarés dans le fichier AndroidManifest.xml
: <service android:name=".subpackagename.ServiceName"/>
Ils doivent étendre la classe Service dont vous devrez surcharger les méthodes suivantes en fonction de vos besoins :
1
2
3
4
5
6
7
8
9 void onCreate(); // initialisation des ressources
void onStart(Intent intent); // SDK<2.0 la tâche de fond démarre
void onStartCommand(Intent intent, int flags, int startId); // SDK>2.0 la tâche de fond démarre
void onDestroy(); // libération des ressources
IBinder onBind(Intent intent); // connexion client distant
boolean onUnbind(Intent intent); // déconnexion d'un client
void onRebind(Intent intent)
Google a publié un post sur la méthode onStartCommand()
apparue avec le SDK 2.0 :
http://android-developers.blogspot.com/2010/02/service-api-changes-starting-with.html
La méthode onStart()
est dépréciée mais doit être redéfinie pour une compatibilité avec les SDK antérieurs (si nécessaire).
Quant au cycle de vie d’un service, Google l’illustre de la manière suivante :
Pour interagir (demarrer/arrêter…) avec un service, deux possibilités s’offrent à nous :
- Soit on appelle la méthode
startService()
qui invoque la méthodeonCreate()
puisonStart()
service.startService() | -> onCreate() – > onStartCommand() [service running]
L’appel de la méthodestopService()
invoque la méthode onDestroy() - Soit on appelle la méthode
bindService()
qui appelle uniquement la méthodeonCreate()
activity.bindService() | ->onCreate() [service created]
Il est possible de voir la liste des services exécutés en allant dans Menu > Settings > Applications > Running Services > du téléphone:
> Services locaux (LocalService)
>> Déclaration du service
Un service Local n’est accessible que par les Activity
de l’application.
Pour l’exemple, notre service initialise un Timer et une tâche qui sera exécutée toutes les minutes. Nous allons créer une classe héritant de Service et surcharger les méthodes onCreate()
, onStart()
et onDestroy()
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 public class BackgroundService extends Service {
private Timer timer ;
@Override
protected void onCreate() {
super.onCreate();
timer = new Timer();
Log.d(this.getClass().getName(), "onCreate");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(this.getClass().getName(), "onStart");
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
// Executer de votre tâche
}
}, 0, 60000);
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
Log.d(this.getClass().getName(), "onDestroy");
this.timer.cancel();
}
Nous déclarons le service dans le fichier AndroidManifest.xml
à l’intérieur de la balise <application>
:
1 <service android:name=".BackgroundService" />
Pour le démarrer, nous faisons appel à la méthode startService(Intent)
de l’Activity
prenant en paramêtre un Intent. Ce dernier peut être initialisé de deux manières :
- Soit en lui passant explicitement le context de l’application et la class du service.
1
2Intent intent = new Intent(this,BackgroundService.class);
startService(intent); - Soit en déclarant une action dans le fichier
AndroidManifest.xml
…
1
2
3
4
5<service android:enabled="true" android:name=".BackgroundService">
<intent-filter>
<action android:name=".BackgroundService.ACTION" />
</intent-filter>
</service>… que nous passons au constructeur de l’intent
1
2Intent intent = new Intent(".BackgroundService.ACTION");
startService(intent);
Il est possible de passer des objets complets serialisables en paramètre avec les méthodes intent.putExtra(…). Les objets pourront être récupérer dans la méthode onStart(Intent intent, int startId)
du service avec intent.getExtras().get(String key). Le fonctionnement est similaire à une table de Hash.
Pour arrêter notre service :
1
2 Intent intent = new Intent(this,BackgroundService.class);
stopService(intent);
>> Implémentation des listeners
Dans un premier temps, nous allons implémenter un système de listeners très simple. L’interface IBackgroundServiceListener
implémentée par notre Activity pour écouter les mises à jour du service est la suivante :
1
2
3 public interface IBackgroundServiceListener {
public void dataChanged(Object o);
}
Nous créons l’interface IBackgroundService
:
1
2
3
4 public interface IBackgroundService {
public void addListener(IBackgroundServiceListener listener);
public void removeListener(IBackgroundServiceListener listener);
}
Nous implémentons l’interface dans le service :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 private List<IBackgroundServiceListener> listeners = null;
// Ajout d'un listener
public void addListener(IBackgroundServiceListener listener) {
if(listeners == null){
listeners = new ArrayList<IGoogleWeatherListener>();
}
listeners.add(listener);
}
// Suppression d'un listener
public void removeListener(IBackgroundServiceListener listener) {
if(listeners != null){
listeners.remove(listener);
}
}
// Notification des listeners
private void fireDataChanged(Object data){
if(listeners != null){
for(IBackgroundServiceListener listener: listeners){
listener.dataChanged(data);
}
}
}
Pour accéder au service depuis l’Activity nous rajoutons une variable static IBackgroundService service
que nous initialiserons dans la méthode onCreate()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class BackgroundService extends Service implements IBackgroundService {
...
private static IBackgroundService service;
@Override
public void onCreate() {
...
service = this;
}
public static IBackgroundService getService() {
return service;
}
...
}
L’emploi d’une variable static n’est pas la meilleure solution. Nous verrons plus tard comment nous en passer. Mais à ce stade, il n’est pas possible d’accéder aux méthodes du service et d’appeler un setter.
Nous poursuivons en définissant notre listener dans notre Activity:
1
2
3
4
5
6
7
8
9
10
11 Intent intent = new
// Lancement du service
Intent(this,BackgroundService.class);
startService(intent);
// Ajout de l'activity à la liste des listeners du service
BackgroundService.getService().addListener(new IBackgroundServiceListener() {
public void dataChanged(Object o) {
// mise à jour de l'interface graphique
}
});
Au moment voulu, dans la méthode run()
du Timer nous ferons appel à la méthode privée fireDataChanged(data)
pour notifier les listeners (dans notre cas, l’Activity) de la mise à jour des données.
Attention ! Si nous nous contentons d’implémenter la méthode dataChanged()
, l’exception suivante sera levée au runtime :
1
2 android.view.ViewRoot$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views.
En effet, la méthode du listener dataChanged()
est appelée par le Timer, et donc exécutée dans le Thread de celui-ci. L’exception nous indique que toute mise à jour de l’interface utilisateur ne peut se faire que par le Thread responsable de la création des View, Button, TextView… Nous utilisons donc la méthode runOnUiThread de la classe Activity pour poster des instructions dans l’UiThread :
1
2
3
4
5
6
7
8
9 // Ajout de l'activity à la liste des listeners du service
BackgroundService.getService().addListener(new IBackgroundServiceListener() {
public void dataChanged(final Object o) {
MyActivity.this.runOnUiThread(new Runnable() {
public void run() {
// Mise à jour de l'UI
}
});
}});
Il nous reste à mettre à jour la méthode onDestroy()
du service pour vider la liste des listeners :
1
2
3
4
5
6 @Override
public void onDestroy() {
this.listeners.clear();
this.timer.cancel();
Log.d(this.getClass().getName(), "onDestroy");
}
A ce stade, nous avons vu comment déclarer notre service et y accéder, définir une tâche de fond, et mettre à jour notre interface utilisateur. Le défaut de la solution proposée est l’utilisation d’une variable static pour accéder au service depuis l’Activity. Malheureusement, la méthode startService(Intent)
n’offre pas d’accès direct aux méthodes du services. Pour se faire, nous allons avoir recours au binding.
>> Implémentation d’un binder
Comme avec un RemoteService, nous allons nous connecter au service et récupérer un Binder. A travers cet objet nous accéderons aux méthodes public du service via l’interface IBackgroundService
que nous avons définie plus haut. L’avantage de cette solution est d’unifier l’utilisation des LocalService et RemoteService mais surtout de récupérer l’instance du service.
Nous redéfinissons la méthode onBind()
, modifions l’implémentation du listener et déportons notre tâche de fond dans la méthode _onStart()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 public class BackgroundService extends Service implements IBackgroundService {
private Timer timer ;
private BackgroundServiceBinder binder ;
private List<IBackgroundServiceListener> listeners = null;
@Override
public void onCreate() {
timer = new Timer();
binder = new BackgroundServiceBinder(this);
_onStart();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public void _onStart(){
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
if(listeners != null){
fireDataChanged(new Object());
}
}}, 0, 60000);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
_onStart();
return START_NOT_STICKY;
}
// Ajout d'un listener
public void addListener(IBackgroundServiceListener listener) {
if(listeners == null){
listeners = new ArrayList<IGoogleWeatherListener>();
}
listeners.add(listener);
}
// Suppression d'un listener
public void removeListener(IBackgroundServiceListener listener) {
if(listeners != null){
listeners.remove(listener);
}
}
// Notification des listeners
private void fireDataChanged(Object data){
if(listeners != null){
for(IBackgroundServiceListener listener: listeners){
listener.dataChanged(data);
}
}
}
@Override
public void onDestroy() {
this.listeners.clear();
this.timer.cancel();
}
}
Nous définissons le binder qui nous permettra de récupérer l’instance de notre service :
1
2
3
4
5
6
7
8
9
10
11
12
13 public class BackgroundServiceBinder extends Binder{
private IBackgroundService service = null;
public BackgroundServiceBinder(IBackgroundService service) {
super();
this.service = service;
}
public IBackgroundService getService(){
return service;
}
};
L’avantage de l’interface IBackgroundService
est de masquer les méthodes onCreate()
, onBind()...
et de ne proposer uniquement addListener()
, removeListener()
et pourquoi pas d’autres méthodes métiers.
Il nous reste à nous connecter au service, accéder aux méthodes exposées par l’interface et s’ajouter comme listener pour mettre à jour l’interface utilisateur :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 Intent intent = new Intent(this,BackgroundService.class);
final IBackgroundServiceListener listener = new IBackgroundServiceListener() {
public void dataChanged(final Object data) {
MyActivity.this.runOnUiThread(new Runnable() {
public void run() {
// Mise à jour de l'UI
}
});
}
};
ServiceConnection connection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i("BackgroundService", "Connected!");
IBackgroundService service = ((BackgroundServiceBinder)service).getService();
service.addListener(listener);
}
public void onServiceDisconnected(ComponentName name) {
Log.i("BackgroundService", "Disconnected!");
}
};
bindService(intent,connection, Context.BIND_AUTO_CREATE);
La connexion au service se fait via la méthode bindService (Intent service, ServiceConnection conn, int flags) de la classe Context dont hérite Activity. L’argument flags peut prendre soit 0
soit Context.BIND_AUTO_CREATE pour forcer le service à démarrer, s’il ne l’est pas, au moment de la connexion.
La partie sur les LocalService est terminée. Je ne suis pas personnellement convaincu de leur intéret. Les classes Service, ServiceConnection, IBinder… sont relativement contraignantes, complexifient le code et n’offrent pas vraiment d’aide à la réalisation de services. Mais si un jour votre LocalService doit se transformer en RemoteService alors vous serez prets.
> Services distants (RemoteService)
>> Déclaration du service
Contrairement aux LocalService qui s’exécutent dans le processus de l’application et plus particulièrement dans le thread principal, les RemoteService s’exécutent dans un processus totalement différent de celui de votre application.
Afin que deux processus différents puissent communiquer et s’échanger des données, Google a implémenté son IDL appelé AIDL (Android Interface Definition Language) pour décrire les méthodes publiques et les données échangées. Cette description se fait dans un fichier .aidl qui devra être connu des deux processus !
Pourront être échangées entre deux processus, seulement les types primitifs java (int
, boolean
, etc), les String
, List
, Map
et CharSequence
. Pour des objects personnalisés, vous devrez implémenter l’interface Parcelable.
Un RemoteService peut être sollicité par plusieurs applications différentes. C’est pourquoi je préfère le déporter dans un nouveau projet Android sous Eclipse. Je conseille également la réalisation d’une petite interface graphique avec deux boutons pour le démarrer et l’arrêter. Si vous ne souhaitez pas créer un projet particulier, il faudra ajouter android:process=":remote"
à la déclaration de votre service (<service>
) dans le fichier AndroidManifest.xml
.
Après avoir créer un projet BackgroundService (package de l’application com.android23.backgroundservice
), nous créons notre service com.android23.backgroundservice.service.BackgroundService
le plus simple qui soit, avec un Timer
et un objet Data
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 public class BackgroundService extends Service {
private Timer timer ;
private BackgroundServiceBinder binder ;
private Data data;
@Override
public void onCreate() {
binder = new BackgroundServiceBinder(this);
timer = new Timer();
_onStart();
}
public void _onStart(){
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
data = new Data(18, Data.Weather.ENSOLEILLE);
}
}, 0, 60000);
}
@Override
public void onDestroy() {
super.onDestroy();
this.binder = null;
this.timer.cancel();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public Data getData() {
return data;
}
}
Pour chaque RemoteService il est nécessaire de déclarer un fichier .aidl que nous plaçons dans le même package (com.android23.backgroundservice.service
).
Voici le contenu de notre fichier IRemoteBackgroundService.aidl
:
1
2
3
4
5
6
7
8
9 package com.android23.backgroundservice.service;
import com.android23.backgroundservice.bo.Data;
interface IRemoteBackgroundService {
int getPid(); // Renvoie le PID du processus du service
Data getData(); // Renvoie notre objet mis à jour
//void updateData(in Data data);
}
Il n’y pas de règle concernant le nom du fichier. Ce fichier sert d’interface entre le client et le service. Nous y déclarons seulement les méthodes public. La syntaxe ressemble fortement au Java bien que cela n’en soit pas. Les mots clefs ‘in
‘, ‘out
‘, ‘inout
‘ sont ajoutés aux paramètres des méthodes. ‘//
‘ permet d’ajouter des commentaires. Enfin on peut spécifier ‘oneway
‘ devant interface
.
Nous créons dans le package com.android23.backgroundservice.bo
, notre objet java Data implémentant l’interface Parcelable :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 public class Data implements Parcelable {
public static enum Weather {NUAGEUX,PLUVIEUX,ENSOLEILLE,BROUILLARD}
private int temperature;
private Weather today;
public static final Parcelable.Creator<Data> CREATOR = new Parcelable.Creator<Data>() {
public Data createFromParcel(Parcel in) {
return new Data(in);
}
public Data[] newArray(int size) {
return new Data[size];
}
};
public Data(int temperature, Weather today) {
super();
this.temperature = temperature;
this.today = today;
}
public int getTemperature() {
return temperature;
}
public Weather getToday() {
return today;
}
private Data(Parcel in) {
readFromParcel(in);
}
public void readFromParcel(Parcel in) {
temperature = in.readInt();
today = (Weather)in.readSerializable();
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel arg0, int arg1) {
arg0.writeInt(temperature);
arg0.writeSerializable(today);
}
}
Notre objet contient le temps et la température, deux types java simples. Le code est calqué sur l’exemple que vous trouverez sur http://developer.android.com/guide/developing/tools/aidl.html.
L’interface Parcelable nous oblige à définir la méthode writeToParcel()
dans laquelle nous ajoutons à l’objet Parcel tous les attributs de notre objet Data
. Un Parcel peut être vue comme l’unité de communication entre deux processus. Attention, l’ordre dans lequel nous ajoutons les attributs à une importance. C’est ordre doit être respecté dans la méthode readFromParcel()
.
L’attribut static CREATOR
est obligatoire. Il implémente l’interface Parcelable.Creator. L’implémentation de cette interface nous force a écrire la méthode readFromParcel()
.
Pour que notre objet Data
puisse voyager d’un processus à l’autre, il nous reste à déclarer son fichier Data.aidl
:
1
2 package com.android23.backgroundservice.bo;
parcelable Data;
Le mot clef parcelable
spécifie que notre objet Data
implémente bien le protocole Google pour les communication IPC.
Les erreurs contenues dans les fichiers .aidl devraient disparaitre. Eclipse génère alors l’interface Java IRemoteBackgroundService
dans le répertoire source « gen » du projet :
Il nous reste ensuite à déclarer notre BackgroundServiceBinder
qui permettra au client d’accéder aux méthodes déclarées dans le fichier aidl. Notre binder hérite de IRemoteBackgroundService.Stub
généré par Eclipse :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class BackgroundServiceBinder extends IRemoteBackgroundService.Stub{
private BackgroundService service = null;
public BackgroundServiceBinder(BackgroundService service) {
super();
this.service = service;
}
public Data getData() throws RemoteException {
return service.getData();
}
public int getPid() throws RemoteException {
return Process.myPid();
}
};
N’oublions pas de déclarer notre service dans le fichier AndroidManifest.xml
:
1
2
3 <application android:icon="@drawable/icon" android:label="@string/app_name">
<service android:exported="true" android:name=".service.BackgroundService" />
</application>
Il est possible d’ajouter une permission :
1
2
3
4
5 <permission android:name="com.android23.backgroundservice.BACKGROUNDSERVICE_PERMISSION" />
<application android:icon="@drawable/icon" android:label="BackgroundService">
<service android:exported="true" android:enabled="true"
android:name=".service.BackgroundService" android:permission="com.android23.backgroundservice.BACKGROUNDSERVICE_PERMISSION"/>
</application>
Les clients devront déclarer cette permission dans leur fichier manifest pour se connecter au service. Sans quoi une SecurityException sera levée à l’appel des méthodes startService
ou bindService
. Pour plus d’informations concernant les permissions, jetez un coup d’oeil à la documentation Google:
- http://developer.android.com/guide/topics/manifest/manifest-intro.html#perms
- http://developer.android.com/guide/topics/security/security.html
Revenons à notre projet Eclipse principal. Pour réaliser notre appel distant au service, nous devons y copier/coller les fichier .aidl Data.aidl
, et IRemoteBackgroundService.aidl
dans le même package que le projet BackgroundService
, ainsi que votre objet java Data
! Eclipse créera automatiquement les interfaces d’accès.
Pour nous connecter au service, rien de bien compliqué :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 Intent intent = new Intent();
intent.setClassName("com.android23.backgroundservice", "com.android23.backgroundservice.service.BackgroundService");
ServiceConnection remoteConnection = new ServiceConnection() {
public void onServiceDisconnected(ComponentName name) {
}
public void onServiceConnected(ComponentName name, IBinder service) {
IRemoteBackgroundService service = IRemoteBackgroundService.Stub.asInterface(service);
try {
service.getPid();
service.getData();
} catch (RemoteException e) {
// TODO Auto-generated catch block
}
}
};
bindService(intent, remoteConnection, Context.BIND_AUTO_CREATE);
Nous passons au constructeur de l’Intent le package principal de notre projet BackgroundService
puis la classe du service.
IRemoteBackgroundService.Stub.asInterface()
nous permet de récupérer notre service. A noter également que chaque appel de méthode peut lever une RemoteException
.
Pour nous déconnecter au service, nous faisons appel à la méthode unbindService(ServiceConnection)
qui prend en paramêtre l’object ServiceConnection
que nous avons créé :
unbindService(remoteConnection);
Il est indispensable d’ajouter la permission dans le fichier AndroidManifest.xml
, au même niveau que la balise <application>
sans quoi notre Activity
ne se connectera jamais :
1 <uses-permission android:name="com.android23.backgroundservice.BACKGROUNDSERVICE_PERMISSION"></uses-permission>
Il n’est pas possible de débugger à la fois le service et notre application. Pour tester, notre service il sera nécessaire de développer dans notre projet BackgroundService
une Activity avec 2 boutons : un pour démarrer le service, et l’autre pour l’arrêter.
Pour tester notre application, nous déployons le service en premier puis nous lançons notre application en mode debug.
>> Mise en place d’un syteme de Callback/Listeners
Comme pour les LocalService, les RemoteService peuvent également notifier leurs clients grâce une liste RemoteCallbackList. Pour cela nous allons apporter quelques modifications.
Pour commencer, retournons dans le projet BackgroundService, et modifions le fichier IRemoteBackgroundService.aidl
en supprimant la méthode getData()
et en rajoutant les méthodes registerCallback
et unregisterCallback
qui permettrons aux clients de s’enregistrer auprès du service :
1
2
3
4
5
6
7
8
9
10
11 package com.android23.backgroundservice.service;
import com.android23.backgroundservice.service.IRemoteBackgroundServiceCallback;
interface IRemoteBackgroundService {
int getPid();
void registerCallback(IRemoteBackgroundServiceCallback cb);
void unregisterCallback(IRemoteBackgroundServiceCallback cb);
}
A travers cette interface, les clients pourront s’enregistrer. Une fois enregistrés il leur faudra implémenter l’interface IRemoteBackgroundServiceCallback
que nous déclarons dans le fichier IRemoteBackgroundServiceCallback.aidl
:
1
2
3
4
5
6
7 package com.android23.backgroundservice.service;
import com.android23.backgroundservice.bo.Data;
interface IRemoteBackgroundServiceCallback {
void dataChanged(in Data data);
}
Nous déclarons dans notre service, la liste des clients à l’écoute :
1
2
3
4
5 final RemoteCallbackList<IRemoteBackgroundServiceCallback> callbacks = new RemoteCallbackList<IRemoteBackgroundServiceCallback>();
public RemoteCallbackList<IRemoteBackgroundServiceCallback> getCallbacks() {
return callbacks;
}
Nous mettons à jour la méthode _onStart()
afin de créer un nouvel objet Data toutes les minutes et notifier les clients :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 public void _onStart() {
Log.i(getClass().getSimpleName(), "_onStart()");
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
data = new Data(++counter, Data.Weather.BROUILLARD);
// Broadcast to all clients the new value.
final int N = callbacks.beginBroadcast();
for (int i = 0; i < N; i++) {
try {
callbacks.getBroadcastItem(i).dataChanged(data);
}
catch (RemoteException e) {}
}
callbacks.finishBroadcast();
}
}, 0,60000);
}
callbacks.beginBroadcast() et callbacks.finishBroadcast();
encadre le début et la fin de la notification des clients. callbacks.getBroadcastItem(i) vous permet d’appeler les méthodes de l’intarface IRemoteBackgroundServiceCallback
et prévenir les clients. Elle ne peut être appelée qu’après beginBroadcast()
Nous désactivons la liste des callbacks dans la méthode onDestroy()
:
1
2
3
4
5
6
7 @Override
public void onDestroy() {
super.onDestroy();
this.binder = null;
this.callbacks.kill(); // Désactive tous les éléments de la liste
this.timer.cancel();
}
N’oublions pas de mettre à jour notre Binder implémentant l’interface IRemoteBackgroundService.Stub
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 public class BackgroundServiceBinder extends IRemoteBackgroundService.Stub{
private BackgroundService service = null;
public BackgroundServiceBinder(BackgroundService service) {
super();
this.service = service;
}
@Override
public int getPid() throws RemoteException {
return Process.myPid();
}
@Override
public void registerCallback(IRemoteBackgroundServiceCallback cb) throws RemoteException {
if(cb != null){
service.getCallbacks().register(cb);
}
}
@Override
public void unregisterCallback(IRemoteBackgroundServiceCallback cb) throws RemoteException {
if(cb != null){
service.getCallbacks().unregister(cb);
}
}
};
Retournons maintenant du côté de notre application principale pour y copier les fichiers IRemoteBackgroundService.aidl
et IRemoteBackgroundServiceCallback.aidl
dans le package com.android23.backgroundservice.service
.
Puis mettons à jour la manière dont nous nous connectons au service en déclarant un IRemoteBackgroundServiceCallback
et en l’inscrivant auprès du service dans la méthode onServiceConnected()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 Intent intent = new Intent();
intent.setClassName("com.android23.backgroundservice", "com.android23.backgroundservice.service.BackgroundService");
final IRemoteBackgroundServiceCallback callback = new IRemoteBackgroundServiceCallback.Stub() {
public void dataChanged(Data data) throws RemoteException {
Log.d( getClass().getSimpleName(), "Temperature : " + data.getTemperature());
Log.d( getClass().getSimpleName(), "Temps : " + data.getToday().toString());
}
};
ServiceConnection remoteConnection = new ServiceConnection() {
public void onServiceDisconnected(ComponentName name) {
Log.d( getClass().getSimpleName(), "onServiceConnected()" );
}
public void onServiceConnected(ComponentName name, IBinder service) {
remoteBackgroundService = IRemoteBackgroundService.Stub.asInterface(service);
try {
remoteBackgroundService.getPid();
remoteBackgroundService.registerCallback(callback);
}
catch (RemoteException e) {}
}
};
bindService(intent, remoteConnection, Context.BIND_AUTO_CREATE);
Pour tester, nous déployons le service et puis nous lançons en mode debug notre application.
>> Démarrer le service au boot du téléphone
Dans le projet BackgroundService, nous rajoutons un BroadcastReceiver :
1
2
3
4
5
6
7
8 public class BackgroundServiceReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
Intent serviceIntent = new Intent();
serviceIntent.setClassName("com.android23.backgroundservice", "com.android23.backgroundservice.service.BackgroundService");
context.startService(serviceIntent);
}
}
Nous le rajoutons dans le fichier AndroidManifest.xml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <permission android:name="com.android23.backgroundservice.BACKGROUNDSERVICE_PERMISSION" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<service android:exported="true" android:name=".service.BackgroundService" android:permission="com.android23.backgroundservice.BACKGROUNDSERVICE_PERMISSION" />
<receiver android:name=".service.BackgroundServiceReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.HOME" />
</intent-filter>
</receiver>
</application>
<uses-permission android:name="com.android23.backgroundservice.BACKGROUNDSERVICE_PERMISSION"/>
Nous avons également précisé que notre service demande la permission BACKGROUNDSERVICE_PERMISSION
pour pouvoir démarrer tout seul.
Déployons le service sur l’émulateur en faisant un clic droit sur le projet : Run As > Android Application. Une fois déployé, re-démarrons l’émulateur en cliquant sur le bouton dans la barre d’outil d’Eclipse puis sur le bouton start dans la fenêtre qui apparait. Une fois l’émulateur lancé, il suffit d’aller dans Menu > Settings > Applications > Running Services > pour constater que notre BackgroundService
est bien en route.
>> Exemple : Connexion au service MP3
Le lecteur MP3 d’Android est lui aussi un RemoteService. Il est donc très simple de s’y connecter pour récupérer la chanson en cours, l’artiste… Pour l’exemple n’oubliez pas de lancer la lecture d’un fichier mp3.
Profitons du fait qu’Android soit open-source pour nous rendre sur Google Code et récupérer le fichier IMediaPlaybackService.aidl.
Une fois le fichier récupéré, nous créons le package "com.android.music"
dans lequel nous insérons le fichier .aidl. Eclipse génère alors automatiquement l’interface IMediaPlaybackService
. Le service ne renvoyant que des types primitifs, seul ce fichier suffit au client.
Nous nous connectons au lecteur mp3 comme nous nous connections à notre BackgroundService
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 Intent intent = new Intent();
intent.setClassName("com.android.music", "com.android.music.MediaPlaybackService");
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName componentname) {
}
@Override
public void onServiceConnected(ComponentName componentname, IBinder ibinder) {
IMediaPlaybackService service = IMediaPlaybackService.Stub.asInterface(ibinder);
try {
Log.i("ServiceConnection", "Playing track: " + service.getTrackName());
Log.i("ServiceConnection", "By artist: " + service.getArtistName());
} catch (Exception e) {
}
}
};
this.bindService(intent, conn, 0);
Malheureusement, si Google modifie l’interface .aidl de son lecteur MP3 il faudra mettre à jour le fichier IMediaPlaybackService.aidl
dans notre projet.
> Conclusion
Le post est relativement long mais regroupe tout ce que j’ai pu apprendre en parcourant le net. Je vais réfléchir à le publier au format PDF dans la rubrique tutorial.
L’intérêt des LocalService reste flou à mes yeux. J’ai la fâcheuse impression de gagner plus en complexité qu’en productivité. A contrario, les RemoteService ont une véritable valeur ajoutée. Ainsi on peut imaginer un service météo commun à toutes nos applications et nos widgets. Les possibilités sont nombreuses dès lors que l’OS gère le multi-taches
Si vous souhaitez que certains points soient approfondis, ou corrigés, n’hésitez pas à m’en faire part.
> Ressources
-
http://developer.android.com/reference/android/app/Service.html
-
http://blog.7touchgroup.com/tag/android-services-versus-broadcast-receivers/
-
http://www.brighthub.com/mobile/google-android/articles/34861.aspx
-
http://saigeethamn.blogspot.com/2009/09/android-developer-tutorial-part-9.html
-
http://www.androidcompetencycenter.com/2009/06/start-service-at-boot/
-
http://www.alexc.me/android-music-app-service-currently-playing-song/231/
Bonjour!
Tres Merci pour cette article compréhensive, mais j’ai un problème au niveau de ce ligne la!
listeners = new ArrayList();
L’erreur est: IGoogleWeatherListener cannot be resolved to a type
Est ce que c’est une service publique de Google?! je ne peux trouver aucun lien sur Internet a propos de ca!?
Bonjour!
Tres Merci pour cet example compréhensif, mais j’ai un problème au niveau de ce ligne la!
listeners = new ArrayList();
L’erreur est: IGoogleWeatherListener cannot be resolved to a type
Est ce que c’est une service publique de Google?! je ne peux trouver aucun lien sur Internet a propos de ca!?
En tant que débutant sous Android, je suis d’accord avec Capslock : serait-il possible, svp, d’avoir le code complet?
J’aime bien lire un bon article avec le code source complet à coté.
J’ai beau regarder, mais je ne vois de lien pour le code de la demo …
A propos des binder pour les services locaux. jai implémenté un service local comme decrit dans le tuto. Jai créé deux activité : La premiere se connecte à l’activité comme dans le tuto. De cette activité, je passe à la seconde activité. j’aiconnecté la 2eme activité au même service, mais dans cette nouvelle activité, je n’arrive pas à recuperer l’instance du service. jai un NullPointerException. Normallement, plusieurs activités devraient pouvoir se connecter au même service grace au binding. Pouvez vous m’aider à resoudre ce pb?
merci
désolé pour le double post, j’ai trouvé la solution :
pour appeler le service par un objet Intent, mettre directement le nom de la classe du service plutôt que d’utiliser la méthode ‘setClasseName()’, cela marche beaucoup.
Super tuto sinon.
Bonjour,
J’ai testé ce tutoriel mais lors du test sur l’émulateur android, j’ai un warning :
‘unable to start service intent : « nomDeMonPackage » : not found ‘
alors que le nom du package est bien celui que j’ai … je ne comprends pas.
D’ailleurs, il y a un point sur le tutoriel qui est ambigüe :
public void onServiceConnected(ComponentName name, IBinder service) {
IRemoteBackgroundService service = IRemoteBackgroundService.Stub.asInterface(service);
try {
service.getPid();
service.getData();
} catch (RemoteException e) {
// TODO Auto-generated catch block
}
}
le ‘service’ du type IRemoteBackgroundService doit être différent du ‘service’ type IBinder non ?
re
Est ce que les objets contenus dans l’objet qui sera échangé entre les processus devront eux aussi être déclaré dans des AIDL différents et implémenter l’interface Parcelable ?
Par exemple dans ta classe Data si Weather n’était pas de type primitif je l’aurai déclaré dans un AIDL et lui faire implémenter parcelable ?
re
Est ce que les objets contenus dans l’objet qui sera échangé entre les processus devront eux aussi être déclaré dans des AIDL différents et implémenter l’interface Parcelable ?
Par exemple dans ta classe Data si Weather n’était pas de type primitif je l’aurai déclaré dans un AIDL et lui faire implémenter parcelable ?
Il suffit de faire un bind une fois pour que l’application soit « connectée » au service.
Merci pour ce riche tutoriel.
Mon activité doit être amenée à invoquer régulièrement des méthodes distantes. Dois-je pour ça appeler à chaque fois la méthode bindService() ?
« Les services s’exécutent dans le Thread principal du processus parent »
C’est pourquoi on crée un thread dans le localService. Ainsi avoir un vrai tâche de fond. Mais qui sera détruite quand l’application sera fermée.
« Les services distants (ou RemoteService) qui s’exécutent dans un processus différent de celui de application »
Tout a fait. Mais en aucun cas son processus parent est le processus de l’application. Sinon ca n’aurait pas grand intérêt. Le RemoteService est une autre application Android sans IHM
Merci pour ce tutorial.
Cependant, je ne suis toujours pas sûr d’avoir bien compris le principe du service. Une contradiction dans le premier paragraphe me désarme complétement dans le choix de mon implémentation d’une tâche de fond dans mon appli.
La phrase :
« Les services s’exécutent dans le Thread principal du processus parent. »
est pour moi très contradictoire avec la phrase qui la précède :
« Les services distants (ou RemoteService) qui s’exécutent dans un processus différent de celui de application »
Du coup, si je considère la première phrase, ma tâche de fond sera interrompue lors de la destruction de l’activité qui l’a lancé (ce que je ne souhaite pas).
Si je considère la deuxième phrase, un RemoteService me permet d’exécuter ma tâche de fond dans un processus indépendant de l’application. Seulement, son processus parent est le processus de mon application.
Voilà la source de ma grande détresse. Help