Les services Android

Le projet d’exemple pour ce chapitre

Vous pouvez télécharger le projet contenant des exemples complets de services Android : android-demo-service.zip

Un service est un composant Android qui s’exécute sans interface graphique. Il dispose de son propre cycle de vie géré par le système. Un service est idéal pour implémenter un traitement long pour lequel on ne souhaite pas bloquer l’activité de l’utilisateur. Par exemple, une application peut avoir besoin d’envoyer en masse des données à un serveur. Pour cela, une activité peut démarrer un service qui aura la charge de l’envoi pendant que l’utilisateur peut continuer à interagir avec l’application (il peut même fermer les activités) sans interrompre le service. Un service peut également être utilisé pour initier des traitements indépendamment de toutes interactions graphiques avec l’utilisateur. Si une application doit interagir avec des périphériques en Bluetooth, elle peut attendre de capter un échange Bluetooth pour une demande de connexion. Ce type de comportement est réalisé à partir d’un service et ne nécessite pas d’interaction avec l’utilisateur.

Lorsqu’on conçoit un service Android, on imagine la plupart du temps un service s’exécutant en tâche de fond (background), c’est-à-dire un service qui n’interrompt pas l’interaction entre l’utilisateur et les activités. Cependant, un service Android n’est pas par défaut exécuté en tâche de fond. Si vous souhaitez implémenter un service qui doit réaliser des tâches d’une longue durée, vous devez implémenter vous-même une exécution de votre code en tâche de fond. Sinon, le système risque d’interrompre votre service en estimant qu’il consomme trop de ressources ou qu’il est actif depuis trop longtemps.

Note

Avec Android, il existe même la possibilité de créer des services en avant-plan (foreground service). Il s’agit de services qui n’ont pas besoin d’une activité mais qui réalisent un traitement pour l’utilisateur. Par exemple, un service de traduction automatique audio n’a pas besoin d’une activité pour fonctionner. Cependant, il rend bien un service directement à l’utilisateur en captant la conversation via le micro pour le retranscrire en audio dans un casque par exemple.

Un service Android peut également être appelé directement depuis un autre composant (une activité, un broadcast receiver ou même un autre service) en exposant une interface. On retrouve alors un modèle d’application multi-couches dans lequel l’activité prend en charge la présentation et l’interaction avec l’utilisateur et le service réalise les traitements métiers de l’application. Dans ce cas, on parle de service lié (bound service) à une activité. Ce modèle permet de développer des applications plus complexes puisqu’un même service peut être utilisé par différentes activités.

Dans ce chapitre nous verrons comment créer un service en tâche de fond (background service) et comment permettre aux autres composants de l’application de se lier à un service (bound service).

Service en tâche de fond

Nous allons voir comment implémenter et démarrer un service en tâche de fond.

Implémentation et déclaration du service

Un service Android est un composant qui hérite directement ou indirectement de la classe Service. Puisqu’un service est un composant, il doit donc être déclaré dans le fichier manifeste AndroidManifest.xml du module.

Déclaration d’un service dans le fichier AndroidManifest.xml
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="dev.gayerie.monappli">
  <application>
    <service android:name=".MonService" android:exported="false">
    </service>
  </application>
</manifest>

À la ligne 5, on déclare le service. On utilise l’attribut android:exported pour indiquer si le service peut être sollicité par une autre application. Pour des raisons de sécurité, il est conseillé de positionner cet attribut à false et de n’exporter que les services pour lesquels l’export est souhaitable.

Structure d’une classe de 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
26
27
28
29
30
31
32
33
34
35
package dev.gayerie.monappli;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class MonService extends Service {

  /*
  * La redéfinition de cette méthode est obligatoire car elle est déclarée
  * abstraite dans la classe Service. Néanmoins, cette méthode n'est utilisée
  * que pour les services liés (Bound Services).
  */
  @Override
  public IBinder onBind(Intent intent) {
    return null;
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    // TODO Implémenter le service
    return START_STICKY;
  }

  @Override
  public void onCreate() {
    // TODO Implémenter les actions à la création du service
  }

  @Override
  public void onDestroy() {
    // TODO Implémenter les actions à la destruction du service
  }

}

La classe ci-dessus présente une structure simple pour un service. La méthode onBind à la ligne 15 est la seule obligatoire car elle est déclarée abstract dans la classe Service. Cependant, la méthode onBind est utilisée uniquement pour les services liés (Bound Service) que nous verrons plus bas. Donc, nous nous contentons de faire retourner null à cette méthode.

Le cycle de vie

Tout comme les activités, les services ont un cycle de vie qui est géré par le système. Lorsque le service vient d’être créé, le système appelle sa méthode onCreate. Lorsque le système veut interrompre le service (à sa demande ou pour récupérer des ressources), le système appelle préalablement la méthode onDestroy. Vous n’êtes pas obligé de redéfinir les méthodes onCreate et onDestroy dans votre service (leur implémentation par défaut est vide).

Entre ces étapes de création et de destruction, le cycle de vie varie selon l’usage du service. Pour les services en tâche de fond (background service), le système appelle également la méthode onStartCommand pour informer le service qu’il a reçu une demande d’exécution.

Important

Afin de limiter l’usage des ressources, si on demande au système de démarrer une instance d’un service alors qu’une instance existe déjà, alors le système réutilisera l’instance existante. Il faut garder à l’esprit que la méthode onStartCommand peut être appelée plusieurs fois pour une instance alors que les méthodes onCreate et onDestroy ne sont appelées qu’une seule fois par instance.

../_images/background_service_lifecycle.png

Le cycle de vie d’un service en tâche de fond (extrait de la documentation officielle)

La méthode onStartCommand signale au service qu’il doit s’exécuter. Cette méthode prend trois paramètres :

intent

Un intent qui a été utilisé pour démarrer le service (Cf. ci-dessous)

flag

Un indicateur pour savoir si le service a été redémarré par le système. Il peut prendre comme valeur les constantes START_FLAG_REDELIVERY ou START_FLAG_RETRY qui permettent de préciser les conditions de redémarrage du service.

startId

Un entier qui identifie de manière unique l’appel à la méthode onStartCommand du service. Cet entier permet de gérer l’arrêt du service en s’assurant que toutes les commandes ont été traitées.

La méthode onStartCommand doit également retournée un entier pour décrire au système la stratégie à appliquer si le service est interrompu par le système. Cela peut se produire si le système estime qu’il doit libérer des ressources (par exemple pour économiser de l’énergie). Cet entier correspond à une constante parmi les suivantes:

START_STICKY

Si le système arrête le service, il devra le redémarrer plus tard en appelant à nouveau la méthode onStartCommand en passant une référence null pour l’intent.

START_STICKY_COMPATIBILITY

Si le système arrête le service, il devra le redémarrer plus tard et il pourra éventuellement appeler à nouveau la méthode onStartCommand en passant une référence null pour l’intent. Cet appel n’est pas obligatoire.

START_NOT_STICKY

Si le système arrête le service, il n’aura pas à le redémarrer.

START_REDELIVER_INTENT

Si le système arrête le service, il devra le redémarrer en passant à nouveau l’intent en paramètre. Dans ce cas, le paramètre flag vaudra START_FLAG_REDELIVERY lors du nouvel appel.

Un service en tâche de fond ne s’arrête que lorsqu’on appelle sa méthode stopSelf en passant en paramètre son identifiant d’exécution (le startId reçu en paramètre de onStartCommand). À moins de vouloir conserver un service en mémoire, il est indispensable d’appeler la méthode stopSelf lorsqu’un traitement est terminé.

Exécution en tâche de fond

Même si un service est souvent utilisé pour réaliser des traitements en tâche de fond, la classe Service ne fournit pas directement de mécanisme pour cela. Un service utilise, par défaut, le même thread d’exécution que les activités de l’application (appelé le thread principal). Si le système détecte un service qui utilise depuis trop longtemps le thread principal, il peut décider d’interrompre ce service.

Pour réaliser un traitement en tâche de fond, il existe différents mécanismes disponibles dans l’API Android. Pour une présentation détaillée, reportez-vous au chapitre Running Android tasks in background threads dans la documentation officielle. Pour des traitements qui n’ont pas besoin de recevoir des messages du système, vous pouvez simplement utiliser l’API Java Standard java.util.concurrent et, notamment, une implémentation de ExecutorService.

Ci-dessous un exemple pour l’implémentation d’un service utilisant des exécuteurs :

Un service s’exécutant en tâche de fond
 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
65
66
67
68
69
70
71
72
73
74
75
76
77
package dev.gayerie.monappli;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BackgroundService extends Service {

  public static final String TAG = "PremierService";
  private ExecutorService executorService;

  /*
  * La redéfinition de cette méthode est obligatoire car elle est déclarée
  * abstraite dans la classe Service. Néanmoins, cette méthode n'est utilisée
  * que pour les services liés (Bound Services).
  */
  @Override
  public IBinder onBind(Intent intent) {
    return null;
  }

  @Override
  public void onCreate() {
    Log.i(TAG, "Lancement du service");
    // Création d'un service d'exécution avec cinq threads.
    this.executorService = Executors.newFixedThreadPool(5);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    Log.i(TAG, "Appel à onStartCommand");
    String message = intent.getStringExtra("message");
    // Création de l'exécuteur et ajout dans la file d'attente du service.
    // L'exécuteur sera lancé dans un thread à part dès que possible.
    this.executorService.execute(new LogMessageExecutor(message, startId));
    return START_REDELIVER_INTENT;
  }

  @Override
  public void onDestroy() {
    Log.i(TAG, "Destruction du service");
    // arrêt du service d'exécution
    this.executorService.shutdown();
  }

  /**
  * Classe interne fournissant le traitement à réaliser
  * en tâche de fond
  */
  private class LogMessageExecutor implements Runnable {

    private String message;
    private int startId;

    LogMessageExecutor(String message, int startId) {
      this.message = message;
      this.startId = startId;
    }

    @Override
    public void run() {
      try {
        Log.i(TAG, "Je suis un service de log avec comme message : " + this.message);
        // On attend 3 secondes pour simuler un traitement
        Thread.sleep(3000);
        Log.i(TAG, "Fin de l'exécution");
      } catch (InterruptedException e) {
        // Si le thread est interrompu par un signal (rien de spécial à faire)
      } finally {
        stopSelf(this.startId);
      }
    }
  }
}

Dans la méthode onCreate, on crée une instance de ExecutorService (ligne 29).

Dans la méthode onStartCommand, on crée et on ajoute un exécuteur (ligne 38).

L’exécuteur est une instance de LogMessageExecutor qui est une classe interne. Cette classe implémente l’interface Runnable et réalise un traitement long. Pour les besoins de l’exemple, on se contente d’endormir le thread pendant trois secondes.

À la fin de son exécution, à la ligne 73, l’exécuteur appelle la méthode stopSelf qui est en fait la méthode de la classe englobante BackgroundService. Ainsi, dès que l’exécuteur a terminé sa tâche, on demande au système d’arrêter le service s’il n’existe aucune demande d’exécution postérieure au startId passé à la construction de l’exécuteur.

La méthode onDestroy est redéfinie de manière à fermer l’instance de ExecutorService (ligne 46).

Notez qu’on peut très facilement passer des paramètres à l’exécution d’un service sous la forme d’extras dans l’intent reçu par la méthode onStartCommand.

Note

Si votre service a besoin de réaliser des appels Web, vous pouvez utiliser la bibliothèque Volley comme nous l’avons vu dans un chapitre précédent. Volley gère déjà l’exécution de requête en tâche de fond. Si votre service n’a pas de traitement long à réaliser avant ou après la requête, vous pouvez implémenter le mécanisme de tâche de fond en utilisant uniquement le mécanisme de listeners fourni par la bibliothèque Volley.

Interaction avec l’utilisateur

Normalement, un service en tâche de fond n’a pas d’interaction avec l’utilisateur puisque sa fonction est précisément de réaliser un traitement en arrière plan. Néanmoins, il est souvent très utile de pouvoir informer l’utilisateur de la progression du traitement ou de sa fin. Un service correspond lui-même à un contexte d’exécution, il peut donc être passé en paramètre lorsqu’une instance de Context est attendue. Il est donc très facile de faire apparaître des messages pour l’utilisateur sous la forme de Toast, de Snackbar ou de notification (Cf. le chapitre sur l’interface graphique).

Important

Si vous voulez invoquer une méthode utilisant un Context depuis un thread, vous devez vous assurer que ce thread dispose d’un accès au thread principal qui gère les messages pour l’affichage graphique. Vous devez pour cela utiliser un handler pour mettre en place un échange de messages entre le thread principal de votre application et le thread réalisant le traitement en tâche de fond.

Lancement du service

Pour lancer un service, il suffit d’appeler la méthode startService depuis une activité, un broadcast receiver ou même depuis un autre service. L’appel à cette méthode attend une instance de Intent identifiant le service voulu.

Exemple d’appel d’un service depuis une activité
Intent intent = new Intent(this, BackgroundService.class);
this.startService(intent);

Service lié

Un service lié (Bound Service) est un service qui est attaché à un autre composant Android (le plus souvent une activité). Il permet de fournir une interface de communication avec le service pour appeler directement ses méthodes. Un service lié est très adapté pour implémenter un service pour lequel on attend un résultat synchrone.

Il n’y a pas spécifiquement de différence de nature entre un service lié et un service en tâche de fond. Un service lié n’a pas besoin d’implémenter la méthode onStartCommand mais il doit impérativement fournir une implémentation complète de la méthode onBind.

Note

Il est tout à fait possible d’implémenter un service pouvant être à la fois lié et exécuté en tâche de fond. Il suffit pour cela de fournir une implémentation pour ses méthodes onBind et onStartCommand.

Déclaration du service

Un service lié doit également être déclaré dans le fichier manifeste AndroidManifest.xml de l’application :

Déclaration d’un service dans le fichier AndroidManifest.xml
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="dev.gayerie.monappli">
  <application>
    <service android:name=".MonService" android:exported="false">
    </service>
  </application>
</manifest>

Note

Dans l’exemple ci-dessus, nous déclarons l’attribut android:exported à false. Cela signifie que le service ne sera pas accessible à d’autres applications. Il est possible de créer des services liés accessibles à d’autres applications mais il faut pour cela suivre un modèle de développement particulier utilisant un Messenger. Pour plus d’information sur ce modèle avancé de service, consultez la documentation officielle consacrée aux Bound Services.

Le cycle de vie

Comme pour un service en tâche de fond, lorsque le service vient d’être créé, le système appelle sa méthode onCreate. Lorsque le système veut interrompre le service (à sa demande ou pour récupérer des ressources), le système appelle préalablement la méthode onDestroy. Vous n’êtes pas obligé de redéfinir les méthodes onCreate et onDestroy dans votre service (leur implémentation par défaut est vide).

Entre ces étapes de création et de destruction, le cycle de vie varie selon l’usage du service. Quand un autre composant veut se lier au service, le système appelle la méthode onBind de ce dernier. Cette méthode doit retourner une instance de l’interface IBinder. Lorsque le composant se délie, le système appelle la méthode onUnbind du service.

../_images/bound_service_lifecycle.png

Le cycle de vie d’un service lié (extrait de la documentation officielle)

Implémentation de la méthode onBind

La méthode onBind doit retourner une instance de l’interface IBinder. Une instance de IBinder est un objet permettant de se lier au service. Lorsque le service reste local à l’application, l’implémentation peut se contenter de retourner une instance d’une classe qui hérite de la classe Binder. Cette dernière implémente déjà pour nous les méthodes de l’interface IBinder.

Nous utiliserons un exemple de service trivial pour illustrer cette présentation. Néanmoins, le même mécanisme peut être utilisé pour réaliser des services plus complexes.

Exemple d’implémentation d’un service lié
 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
package dev.gayerie.monappli;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import java.util.Arrays;
import java.util.Random;

public class RandomService extends Service {

  public class RandomServiceBinder extends Binder {
    RandomService getService() {
      return RandomService.this;
    }
  }

  private RandomServiceBinder binder = new RandomServiceBinder();

  @Override
  public IBinder onBind(Intent intent) {
    return this.binder;
  }

  public String getRandomWord() {
    String[] array = {"bonjour", "méchant", "loup", "maison", "pain d'épice"};
    return array[new Random().nextInt(array.length)];
  }

}

Le service ci-dessus n’offre qu’une méthode réellement utile : getRandomWord. Cette méthode retourne un mot au hasard parmi une liste prédéfinie. La méthode onBind retourne une instance de RandomServiceBinder (ligne 22). La classe RandomServiceBinder est une classe interne qui étend la classe Binder (lignes 12 à 16). Elle fournit la méthode getService qui retourne l’instance du service.

Pour cet exemple, nous n’avons pas besoin de fournir une implémentation pour la méthode onUnbind car il n’y a pas de traitement à réaliser lorsque le service est délié.

Lier le service à une activité

Pour se lier à un service, il faut utiliser la méthode bindService. Pour se délier d’un service, il faut utiliser la méthode unbindService. Ces méthodes ont besoin d’une connexion vers le service qui est représentée par une implémentation de l’interface ServiceConnection. Le lien avec un service est asynchrone, on ne peut pas créer le service directement, on peut simplement ouvrir une connexion vers un service et attendre que le système nous prévienne quand le service a été éventuellement démarré et a été correctement lié.

Exemple de liaison du service depuis une activité
 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
package dev.gayerie.monappli;

import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;

public class ClientActivity extends AppCompatActivity {

  private RandomService randomService;

  private ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
      RandomService.RandomServiceBinder binder = (RandomService.RandomServiceBinder) service;
      randomService = binder.getService();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
      randomService = null;
    }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_client);
  }

  @Override
  protected void onStart() {
    super.onStart();
    Intent intent = new Intent(this, RandomService.class);
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
  }

  @Override
  protected void onStop() {
    super.onStop();
    unbindService(serviceConnection);
  }

  // ...

}

Dans l’exemple ci-dessus, on crée une classe interne anonyme pour fournir une implémentation à l’interface ServiceConnection (lignes 15 à 26). Il faut pour cela implémenter les méthodes onServiceConnected et onServiceDisconnected. Il s’agit de méthodes de callback qui seront appelées respectivement quand le service sera lié et quand le service sera délié. Pour notre implémentation, nous récupérons en paramètre de onServiceConnected l’instance de IBinder que nous trans-typons en RandomService.RandomServiceBinder pour pouvoir appeler la méthode getService que nous avons précédemment implémentée (lignes 18 et 19). Nous pouvons ainsi initialiser l’attribut randomService (ligne 19). La méthode onServiceDisconnected se contente de remettre la valeur de l’attribut randomService à null (ligne 24).

Un service fait partie des ressources que l’activité peut acquérir. Il est donc conseillé d’ouvrir une connexion vers le service dans la redéfinition de la méthode onStart et de fermer la connexion vers le service dans la redéfinition de la méthode onStop.

Dans la redéfinition de onStart, nous appelons la méthode bindService en fournissant un intent pour identifier le service voulu ainsi que la connexion à utiliser. Le dernier paramètre passé à la méthode est un flag pour décrire comment la liaison doit se faire. Pour notre exemple, nous utilisons la constante BIND_AUTO_CREATE qui est le comportement par défaut : si le service n’existe pas encore en mémoire, il est créé. Sinon on établit une liaison avec le service existant.

Dans la redéfinition de onStop, nous appelons la méthode unbindService pour libérer la liaison avec le service.

Comme l’établissement de la liaison d’un service est asynchrone, partout dans le code de l’activité où nous voulons solliciter le service, nous sommes obligés de vérifier d’abord s’il n’est pas null.

Exemple d’utilisation du service pour l’activité
if (this.randomService != null) {
    String word = this.randomService.getRandomWord();
    // ...
}