Licence CC BY-NC-SA

Les services

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Nous savons désormais faire du travail en arrière-plan, mais de manière assez limitée quand même. En effet, toutes les techniques que nous avons vues étaient destinées aux opérations courtes et/ou en interaction avec l'interface graphique, or ce n'est pas le cas de toutes les opérations d'arrière-plan. C'est pourquoi nous allons voir le troisième composant qui peut faire partie d'une application : les services.

Contrairement aux threads, les services sont conçus pour être utilisés sur une longue période de temps. En effet, les threads sont des éléments sommaires qui n'ont pas de lien particulier avec le système Android, alors que les services sont des composants et sont par conséquent intégrés dans Android au même titre que les activités. Ainsi, ils vivent au même rythme que l'application. Si l'application s'arrête, le service peut réagir en conséquence, alors qu'un thread, qui n'est pas un composant d'Android, ne sera pas mis au courant que l'application a été arrêtée si vous ne lui dites pas. Il ne sera par conséquent pas capable d'avoir un comportement approprié, c'est-à-dire la plupart du temps de s'arrêter.

Qu'est-ce qu'un service ?

Tout comme les activités, les services possèdent un cycle de vie ainsi qu'un contexte qui contient des informations spécifiques sur l'application et qui constitue une interface de communication avec le restant du système. Ainsi, on peut dire que les services sont des composants très proches des activités (et beaucoup moins des receivers, qui eux ne possèdent pas de contexte). Cette configuration leur prodigue la même grande flexibilité que les activités. En revanche, à l'opposé des activités, les services ne possèdent pas d'interface graphique : c'est pourquoi on les utilise pour effectuer des travaux d'arrière-plan.

Un exemple typique est celui du lecteur de musique. Vous laissez à l'utilisateur l'opportunité de choisir une chanson à l'aide d'une interface graphique dans une activité, puis il est possible de manipuler la chanson dans une seconde activité qui nous montre un joli lecteur avec des commandes pour modifier le volume ou mettre en pause. Mais si l'utilisateur veut regarder une page web en écoutant la musique ? Comme une activité a besoin d'afficher une interface graphique, il est impossible que l'utilisateur regarde autre chose que le lecteur quand il écoute la musique. On pourrait éventuellement envisager de passer par un receiver, mais celui-ci devrait résoudre son exécution en dix secondes, ce n'est donc pas l'idéal pour un lecteur. La solution la plus évidente est bien sûr de faire jouer la musique par un service, comme ça votre client pourra utiliser une autre application sans pour autant que la musique s'interrompe. Un autre exemple est celui du lecteur d'e-mails qui va vérifier ponctuellement si vous avez reçu un nouvel e-mail.

Il existe deux types de services :

  • Les plus courants sont les services locaux (on trouve aussi le terme started ou unbound service), où l'activité qui lance le service et le service en lui-même appartiennent à la même application.
  • Il est aussi possible qu'un service soit lancé par un composant qui appartient à une autre application, auquel cas il s'agit d'un service distant (on trouve aussi le terme bound service). Dans ce cas de figure, il existe toujours une interface qui permet la communication entre le processus qui a appelé le service et le processus dans lequel s'exécute le service. Cette communication permet d'envoyer des requêtes ou récupérer des résultats par exemple. Le fait de communiquer entre plusieurs processus s'appelle l'IPC. Il peut bien sûr y avoir plusieurs clients liés à un service.
  • Il est aussi possible que le service expérimente les deux statuts à la fois. Ainsi, on peut lancer un service local et lui permettre d'accepter les connexions distantes par la suite.

Vous vous en doutez peut-être, mais un service se lance par défaut dans le même processus que celui du composant qui l'a appelé. Ce qui peut sembler plus étrange et qui pourrait vous troubler, c'est que les services s'exécutent dans le thread UI. D'ailleurs, ils ne sont pas conçus pour être exécutés en dehors de ce thread, alors n'essayez pas de le délocaliser. En revanche, si les opérations que vous allez mener dans le service risquent d'affecter l'interface graphique, vous pouvez très bien lancer un thread dans le service. Vous voyez la différence ? Toujours lancer un service depuis le thread principal ; mais vous pouvez très bien lancer des threads dans le service.

Gérer le cycle de vie d'un service

De manière analogue aux activités, les services traversent plusieurs étapes pendant leur vie et la transition entre ces étapes est matérialisée par des méthodes de callback. Heureusement, le cycle des services est plus facile à maîtriser que celui des activités puisqu'il y a beaucoup moins d'étapes. La figure suivante est un schéma qui résume ce fonctionnement.

Ce cycle est indépendant du cycle du composant qui a lancé le service

Vous voyez qu'on a deux cycles légèrement différents : si le service est local (lancé depuis l'application) ou distant (lancé depuis un processus différent).

Les services locaux

Ils sont lancés à partir d'une activité avec la méthode ComponentName startService(Intent service). La variable retournée donne le package accompagné du nom du composant qui vient d'être lancé.

1
2
Intent intent = new Intent(Activite.this, UnService.class);
startService(intent);

Si le service n'existait pas auparavant, alors il sera créé. Or, la création d'un service est symbolisée par la méthode de callback void onCreate(). La méthode qui est appelée ensuite est int onStartCommand(Intent intent, int flags, int startId).

Notez par ailleurs que, si le service existait déjà au moment où vous en demandez la création avec startService(), alors onStartCommande() est appelée directement sans passer par onCreate().

En ce qui concerne les paramètres, on trouve intent, qui a lancé le service, flags, dont nous discuterons juste après, et enfin startId, pour identifier le lancement (s'il s'agit du premier lancement du service, startId vaut 1, s'il s'agit du deuxième lancement, il vaut 2, etc.).

Ensuite comme vous pouvez le voir, cette méthode retourne un entier. Cet entier doit en fait être une constante qui détermine ce que fera le système s'il est tué.

START_NOT_STICKY

Si le système tue le service, alors ce dernier ne sera pas recréé. Il faudra donc effectuer un nouvel appel à startService() pour relancer le service.

Ce mode vaut le coup dès qu'on veut faire un travail qui peut être interrompu si le système manque de mémoire et que vous pouvez le redémarrer explicitement par la suite pour recommencer le travail. Si vous voulez par exemple mettre en ligne des statistiques sur un serveur distant. Le processus qui lancera la mise en ligne peut se dérouler toutes les 30 minutes, mais, si le service est tué avant que la mise en ligne soit effectuée, ce n'est pas grave, on le fera dans 30 minutes.

START_STICKY

Cette fois, si le système doit tuer le service, alors il sera recréé mais sans lui fournir le dernier Intent qui l'avait lancé. Ainsi, le paramètre intent vaudra null. Ce mode de fonctionnement est utile pour les services qui fonctionnent par intermittence, comme par exemple quand on joue de la musique.

START_REDELIVER_INTENT

Si le système tue le service, il sera recréé et dans onStartCommand() le paramètre intent sera identique au dernier intent qui a été fourni au service. START_REDELIVER_INTENT est indispensable si vous voulez être certains qu'un service effectuera un travail complètement.

Revenons maintenant au dernier paramètre de onStartCommand(), flags. Il nous permet en fait d'en savoir plus sur la nature de l'intent qui a lancé le service :

  • 0 s'il n'y a rien de spécial à dire.
  • START_FLAG_REDELIVERY si l'intent avait déjà été délivré et qu'il l'est à nouveau parce que le service avait été interrompu.
  • Enfin vous trouverez aussi START_FLAG_RETRY si le service redémarre alors qu'il s'était terminé de manière anormale.

Enfin, il faut faire attention parce que flags n'est pas un paramètre simple à maîtriser. En effet, il peut très bien valoir START_FLAG_REDELIVERY et START_FLAG_RETRY en même temps ! Alors comment ce miracle peut-il se produire ? Laissez-moi le temps de faire une petite digression qui vous servira à chaque fois que vous aurez à manipuler des flags, aussi appelés drapeaux.

Vous savez écrire les nombres sous la forme décimale : « 0, 1, 2, 3, 4 » et ainsi de suite. On parle de numération décimale, car il y a dix unités de 0 à 9. Vous savez aussi écrire les nombres sous la forme hexadécimale : « 0, 1, 2, 3, …, 8, 9, A, B, C, D, E, F, 10, 11, 12, …, 19, 1A, 1B », et ainsi de suite. Ici, il y a seize unités de 0 à F, on parle donc d'hexadécimal. Il existe une infinité de systèmes du genre, ici nous allons nous intéresser au système binaire qui n'a que deux unités : 0 et 1. On compte donc ainsi : « 0, 1, 10, 11, 100, 101, 110, 111, 1000 », etc.

Nos trois flags précédents valent en décimal (et dans l'ordre de la liste précédente) 0, 1 et 2, ce qui fait en binaire 0, 1 et 10. Ainsi, si flags contient START_FLAG_REDELIVERY et START_FLAG_RETRY, alors il vaudra 1 + 2 = 3, soit en binaire 1 + 10 = 11. Vous pouvez voir qu'en fait chaque 1 correspond à la présence d'un flag : le premier à droite dénote la présence de START_FLAG_REDELIVERY (car START_FLAG_REDELIVERY vaut 1) et le plus à gauche celui de START_FLAG_RETRY (car START_FLAG_RETRY vaut 10).

On remarque tout de suite que le binaire est pratique puisqu'il permet de savoir quel flag est présent en fonction de l'absence ou non d'un 1. Mais comment demander à Java quels sont les 1 présents dans flags ? Il existe deux opérations de base sur les nombres binaires : le « ET » (« & ») et le « OU » (« | »). Le « ET » permet de demander « Est-ce que ce flag est présent dans flags ou pas ? », car il permet de vérifier que deux bits sont similaires. Imaginez, on ignore la valeur de flags (qui vaut « YX », on va dire) et on se demande s'il contient START_FLAG_REDELIVERY (qui vaut 1, soit 01 sur deux chiffres). On va alors poser l'opération comme vous le faites d'habitude :

1
2
3
4
flags    YX
      &  01
      ------
Résultat 0X

Le résultat fait « 0X » et en fonction de X on saura si flags contient ou non START_FLAG_REDELIVERY :

  • Si X vaut 0, alors flags ne contient pas START_FLAG_REDELIVERY.
  • Si X vaut 1, alors il contient START_FLAG_REDELIVERY.

Il suffit maintenant de vérifier la valeur du résultat : s'il vaut 0, c'est que le flag n'est pas présent !

En Java, on peut le savoir de cette manière:

1
2
3
4
if((flags & Service.START_FLAG_REDELIVERY) != 0)
  // Ici, START_FLAG_REDELIVERY est présent dans flags
else
  // Ici, START_FLAG_REDELIVERY n'est pas présent dans flags

Je vais maintenant vous parler du « OU ». Il permet d'ajouter un flag à un nombre binaire s'il n'était pas présent auparavant :

1
2
3
4
flags    YX
      |  10
      ------
Résultat 1X

Quelle que soit la valeur précédente de flags, il contient désormais START_FLAG_RETRY. Ainsi, si on veut vérifier qu'il ait START_FLAG_REDELIVERY et en même temps START_FLAG_RETRY, on fera :

1
2
3
4
5
if((flags & (Service.START_FLAG_REDELIVERY | Service.START_FLAG_RETRY) != 0)
  // Les deux flags sont présents
else
  // Il manque un des deux flags (voire les deux)
}

J'espère que vous avez bien compris le concept de flags parce qu'on le retrouve souvent en programmation. Les flags permettent même d'optimiser quelque peu certains calculs pour les fous furieux, mais cela ne rentre pas dans le cadre de ce cours.

Une fois sorti de la méthode onStartCommand(), le service est lancé. Un service continuera à fonctionner jusqu'à ce que vous l'arrêtiez ou qu'Android le fasse de lui-même pour libérer de la mémoire RAM, comme pour les activités. Au niveau des priorités, les services sont plus susceptibles d'être détruits qu'une activité située au premier plan, mais plus prioritaires que les autres processus qui ne sont pas visibles. La priorité a néanmoins tendance à diminuer avec le temps : plus un service est lancé depuis longtemps, plus il a de risques d'être détruit. De manière générale, on va apprendre à concevoir nos services de manière à ce qu'ils puissent gérer la destruction et le redémarrage.

Pour arrêter un service, il est possible d'utiliser void stopSelf() depuis le service ou boolean stopService(Intent service) depuis une activité, auquel cas il faut fournir service qui décrit le service à arrêter.

Cependant, si votre implémentation du service permet de gérer une accumulation de requêtes (un pool de requêtes), vous pourriez vouloir faire en sorte de ne pas interrompre le service avant que toutes les requêtes aient été gérées, même les nouvelles. Pour éviter ce cas de figure, on peut utiliser boolean stopSelfResult(int startId)startId correspond au même startId qui était fourni à onStartCommand(). On l'utilise de cette manière : vous lui passez un startId et, s'il est identique au dernier startId passé à onStartCommand(), alors le service s'interrompt. Sinon, c'est qu'il a reçu une nouvelle requête et qu'il faudra la gérer avant d'arrêter le service.

Comme pour les activités, si on fait une initialisation qui a lieu dans onCreate() et qui doit être détruite par la suite, alors on le fera dans le onDestroy(). De plus, si un service est détruit par manque de RAM, alors le système ne passera pas par la méthode onDestroy().

Les services distants

Comme les deux types de services sont assez similaires, je ne vais présenter ici que les différences.

On utilisera cette fois boolean bindService(Intent service, ServiceConnection conn, int flags) afin d'assurer une connexion persistante avec le service. Le seul paramètre que vous ne connaissez pas est conn qui permet de recevoir le service quand celui-ci démarrera et permet de savoir s'il meurt ou s'il redémarre.

Un ServiceConnection est une interface pour surveiller l'exécution du service distant et il incarne le pendant client de la connexion. Il existe deux méthodes de callback que vous devrez redéfinir :

  1. void onServiceConnected(ComponentName name, IBinder service) qui est appelée quand la connexion au service est établie, avec un IBinder qui correspond à un canal de connexion avec le service.
  2. void onServiceDisconnected(ComponentName name) qui est appelée quand la connexion au service est perdue, en général parce que le processus qui accueille le service a planté ou a été tué.

Mais qu'est-ce qu'un IBinder ? Comme je l'ai déjà dit, il s'agit d'un pont entre votre service et l'activité, mais au niveau du service. Les IBinder permettent au client de demander des choses au service. Alors, comment créer cette interface ? Tout d'abord, il faut savoir que le IBinder qui sera donné à onServiceConnected(ComponentName, IBinder) est envoyé par la méthode de callback IBinder onBind(Intent intent) dans Service. Maintenant, il suffit de créer un IBinder. Nous allons voir la méthode la plus simple, qui consiste à permettre à l'IBinder de renvoyer directement le Service de manière à pouvoir effectuer des commandes dessus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class MonService extends Service {
  // Attribut de type IBinder
  private final IBinder mBinder = new MonBinder(); 

  // Le Binder est représenté par une classe interne 
  public class MonBinder extends Binder {
    // Le Binder possède une méthode pour renvoyer le Service
    MonService getService() {
      return MonService.this;
    }
  }

  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
}

Le service sera créé s'il n'était pas déjà lancé (appel à onCreate() donc), mais ne passera pas par onStartCommand().

Pour qu'un client puisse se détacher d'un service, il peut utiliser la méthode void unbindService(ServiceConnection conn) de Context, avec conn l'interface de connexion fournie précédemment à bindService().

Ainsi, voici une implémentation typique d'un service distant :

 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
// Retient l'état de la connexion avec le service
private boolean mBound = false;
// Le service en lui-même
private MonService mService;
// Interface de connexion au service
private ServiceConnection mConnexion = new ServiceConnection() {
  // Se déclenche quand l'activité se connecte au service
  public void onServiceConnected(ComponentName className, IBinder service) {
    mService = ((MonService.MonBinder)service).getService();
  }

  // Se déclenche dès que le service est déconnecté
  public void onServiceDisconnected(ComponentName className) {
    mService = null;
  }
};

@Override
protected void onStart() {
  super.onStart();
  Intent mIntent = new Intent(this, MonService.class);
  bindService(mIntent, mConnexion, BIND_AUTO_CREATE);
  mBound = true;
}

@Override
protected void onStop() {
  super.onStop();
  if(mBound) {
    unbindService(mConnexion);
    mBound = false;
  }
}

À noter aussi que, s'il s'agit d'un service distant, alors il aura une priorité supérieure ou égale à la priorité de son client le plus important (avec la plus haute priorité). Ainsi, s'il est lié à un client qui se trouve au premier plan, il y a peu de risques qu'il soit détruit.

Créer un service

Dans le Manifest

Tout d'abord, il faut déclarer le service dans le Manifest. Il peut prendre quelques attributs que vous connaissez déjà tels que android:name qui est indispensable pour préciser son identifiant, android:icon pour indiquer un drawable qui jouera le rôle d'icône, android:permission pour créer une permission nécessaire à l'exécution du service ou encore android:process afin de préciser dans quel processus se lancera ce service. Encore une fois, android:name est le seul attribut indispensable :

1
2
3
4
<service android:name="MusicService"
  android:process=":player" ></service>

De cette manière, le service se lancera dans un processus différent du reste de l'application et ne monopolisera pas le thread UI. Vous pouvez aussi déclarer des filtres d'intents pour savoir quels intents implicites peuvent démarrer votre service.

En Java

Il existe deux classes principales depuis lesquelles vous pouvez dériver pour créer un service.

Le plus générique : Service

La classe Service permet de créer un service de base. Le code sera alors exécuté dans le thread principal, alors ce sera à vous de créer un nouveau thread pour ne pas engorger le thread UI.

Le plus pratique : IntentService

En revanche la classe IntentService va créer elle-même un thread et gérer les requêtes que vous lui enverrez dans une file. À chaque fois que vous utiliserez startService() pour lancer ce service, la requête sera ajoutée à la file et tous les éléments de la file seront traités par ordre d'arrivée. Le service s'arrêtera dès que la file sera vide. Usez et abusez de cette classe, parce que la plupart des services n'ont pas besoin d'exécuter toutes les requêtes en même temps, mais plutôt les unes après les autres. En plus, elle est plus facile à gérer puisque vous aurez juste à implémenter void onHandleIntent(Intent intent) qui recevra toutes les requêtes dans l'ordre sous la forme d'intent, ainsi qu'un constructeur qui fait appel au constructeur d'IntentService :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ExampleService extends IntentService {
  public ExampleService() {
    // Il faut passer une chaîne de caractères au superconstructeur
    super("UnNomAuHasard");
  }

  @Override
  protected void onHandleIntent(Intent intent) {
    // Gérer la requête
  }
}

Vous pouvez aussi implémenter les autres méthodes de callback, mais faites toujours appel à leur superimplémentation, sinon votre service échouera lamentablement :

1
2
3
4
5
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
  // Du code
  return super.onStartCommand(intent, flags, startId);
}

On veut un exemple, on veut un exemple !

Je vous propose de créer une activité qui va envoyer un chiffre à un IntentService qui va afficher la valeur de ce chiffre dans la console. De plus, l'IntentService fera un long traitement pour que chaque fois que l'activité envoie un chiffre les intents s'accumulent, ce qui fera que les messages seront retardés dans la console.

J'ai une activité toute simple qui se lance au démarrage de l'application :

 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
package sdz.chapitreQuatre.intentservice.example;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {
  private Button mBouton = null;
  private TextView mAffichageCompteur = null;

  private int mCompteur = 0;

  public final static String EXTRA_COMPTEUR = "sdz.chapitreQuatre.intentservice.example.compteur";

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mAffichageCompteur = (TextView) findViewById(R.id.affichage);

    mBouton = (Button) findViewById(R.id.bouton);
    mBouton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent i = new Intent(MainActivity.this, IntentServiceExample.class);
        i.putExtra(EXTRA_COMPTEUR, mCompteur);

        mCompteur ++;
        mAffichageCompteur.setText("" + mCompteur);

        startService(i);
      }
    });
  }
}

Cliquer sur le bouton incrémente le compteur et envoie un intent qui lance un service qui s'appelle IntentServiceExample. L'intent est ensuite reçu et traité :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package sdz.chapitreQuatre.intentservice.example;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

public class IntentServiceExample extends IntentService {
  private final static String TAG = "IntentServiceExample";

  public IntentServiceExample() {
    super(TAG);
  }

  @Override
  protected void onHandleIntent(Intent intent) {
    Log.d(TAG, "Le compteur valait : " + intent.getIntExtra(MainActivity.EXTRA_COMPTEUR, -1));
    int i = 0;
    // Cette boucle permet de rajouter artificiellement du temps de traitement
    while(i < 100000000)
      i++;
  }
}

Allez-y maintenant, cliquez sur le bouton. La première fois, le chiffre s'affichera immédiatement dans la console, mais si vous continuez vous verrez que le compteur augmente, et pas l'affichage, tout simplement parce que le traitement prend du temps et que l'affichage est retardé entre chaque pression du bouton. Cependant, chaque intent est traité, dans l'ordre d'envoi.

Les notifications et services de premier plan

Distribuer des autorisations

Les PendingIntents sont des Intents avec un objectif un peu particulier. Vous les créez dans votre application, et ils sont destinés à une autre application, jusque là rien de très neuf sous le soleil ! Cependant, en donnant à une autre application un PendingIntent, vous lui donnez les droits d'effectuer une opération comme s'il s'agissait de votre application (avec les mêmes permissions et la même identité).

En d'autres termes, vous avez deux applications : celle de départ, celle d'arrivée. Vous donnez à l'application d'arrivée tous les renseignements et toutes les autorisations nécessaires pour qu'elle puisse demander à l'application de départ d'exécuter une action à sa place.

Comment peut-on indiquer une action à effectuer ?

Vous connaissez déjà la réponse, j'en suis sûr ! On va insérer dans le PendingIntent… un autre Intent, qui décrit l'action qui sera à entreprendre. Le seul but du PendingIntent est d'être véhiculé entre les deux applications (ce n'est donc pas surprenant que cette classe implémente Parcelable), pas de lancer un autre composant.

Il existe trois manières d'appeler un PendingIntent en fonction du composant que vous souhaitez démarrer. Ainsi, on utilisera l'une des méthodes statiques suivantes :

1
2
3
4
5
PendingIntent PendingIntent.getActivity(Context context, int requestCode, Intent intent, int flags);

PendingIntent PendingIntent.getBroadcast(Context context, int requestCode, Intent intent, int flags);

PendingIntent PendingIntent.getService(Context context, int requestCode, Intent intent, int flags);

Comme vous l'aurez remarqué, les paramètres sont toujours les mêmes :

  • context est le contexte dans lequel le PendingIntent devrait démarrer le composant.
  • requestCode est un code qui n'est pas utilisé.
  • intent décrit le composant à lancer (dans le cas d'une activité ou d'un service) ou l'Intent qui sera diffusé (dans le cas d'un broadcast).
  • flags est également assez peu utilisé.

Le PendingIntent sera ensuite délivré au composant destinataire comme n'importe quel autre Intent qui aurait été appelé avec startActivityForResult() : le résultat sera donc accessible dans la méthode de callback onActivityResult().

Voici un exemple qui montre un PendingIntent qui sera utilisé pour revenir vers l'activité principale :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package sdz.chapitreQuatre.pending.example;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;

public class MainActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Intent explicite qui sera utilisé pour lancer à nouveau MainActivity
    Intent intent = new Intent();
    // On pointe vers l'activité courante en précisant le package, puis l'activité
    intent.setComponent(new ComponentName("sdz.chapitreQuatre.pending.example", "sdz.chapitreQuatre.pending.example.MainActivity"));

    PendingIntent mPending = PendingIntent.getService(this, 0, intent, 0);
  }
}

Notifications

Une fois lancé, un service peut avertir l'utilisateur des évènements avec les Toasts ou des notifications dans la barre de statut, comme à la figure suivante.

Ma barre de statut contient déjà deux notifications représentées par deux icônes à gauche

Comme vous connaissez les Toasts mieux que certaines personnes chez Google, je ne vais parler que des notifications.

Une notification n'est pas qu'une icône dans la barre de statut, en fait elle traverse trois étapes :

  1. Tout d'abord, à son arrivée, elle affiche une icône ainsi qu'un texte court que Google appelle bizaremment un « texte de téléscripteur ».
  2. Ensuite, seule l'icône est lisible dans la barre de statut après quelques secondes.
  3. Puis il est possible d'avoir plus de détails sur la notification en ouvrant la liste des notifications, auquel cas on peut voir une icône, un titre, un texte et un horaire de réception.

Si l'utilisateur déploie la liste des notifications et appuie sur l'une d'elles, Android actionnera un PendingIntent qui est contenu dans la notification et qui sera utilisé pour lancer un composant (souvent une activité, puisque l'utilisateur s'attendra à pouvoir effectuer quelque chose). Vous pouvez aussi configurer la notification pour qu'elle s'accompagne d'un son, d'une vibration ou d'un clignotement de la LED.

Les notifications sont des instances de la classe Notification. Cette classe permet de définir les propriétés de la notification, comme l'icône, le message associé, le son à jouer, les vibrations à effectuer, etc.

Il existe un constructeur qui permet d'ajouter les éléments de base à une notification : Notification(int icon, CharSequence tickerText, long when)icon est une référence à un Drawable qui sera utilisé comme icône, tickerText est le texte de type téléscripteur qui sera affiché dans la barre de statut, alors que when permet d'indiquer la date et l'heure qui accompagneront la notification. Par exemple, pour une notification lancée dès qu'on appuie sur un bouton, on pourrait avoir :

1
2
3
4
5
6
// L'icône sera une petite loupe
int icon = R.drawable.ic_action_search;
// Le premier titre affiché
CharSequence tickerText = "Titre de la notification";
// Daté de maintenant
long when = System.currentTimeMillis();

La figure suivante représente la barre de statut avant la notification.

Avant la notification

La figure suivante représente la barre de statut au moment où l'on reçoit la notification.

Au moment de la notification

Ajouter du contenu à une notification

Une notification n'est pas qu'une icône et un léger texte dans la barre de statut, il est possible d'avoir plus d'informations quand on l'affiche dans son intégralité et elle doit afficher du contenu, au minimum un titre et un texte, comme à la figure suivante.

La notification contient au moins un titre et un texte

De plus, il faut définir ce qui va se produire dès que l'utilisateur cliquera sur la notification. Nous allons rajouter un PendingIntent à la notification, et dès que l'utilisateur cliquera sur la notification, l'intent à l'intérieur de la notification sera déclenché.

Notez bien que, si l'intent lance une activité, alors il faut lui rajouter le flag FLAG_ACTIVITY_NEW_TASK. Ces trois composants, titre, texte et PendingIntent sont à définir avec la méthode void setLatestEventInfo(Context context, CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent), où contentTitle sera le titre affiché et contentText, le texte. Par exemple, pour une notification qui fait retourner dans la même activité que celle qui a lancé la notification :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// L'icône sera une petite loupe
int icon = R.drawable.ic_action_search;
// Le premier titre affiché
CharSequence tickerText = "Titre de la notification";
// Daté de maintenant
long when = System.currentTimeMillis();

// La notification est créée
Notification notification = new Notification(icon, tickerText, when);

// Intent qui lancera vers l'activité MainActivity
Intent notificationIntent = new Intent(MainActivity.this, MainActivity.class);
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

PendingIntent contentIntent = PendingIntent.getActivity(MainActivity.this, 0, notificationIntent, 0);

notification.setLatestEventInfo(MainActivity.this, "Titre", "Texte", contentIntent);

Enfin, il est possible de rajouter des flags à une notification afin de modifier son comportement :

  • FLAG_AUTO_CANCEL pour que la notification disparaisse dès que l'utilisateur appuie dessus.
  • FLAG_ONGOING_EVENT pour que la notification soit rangée sous la catégorie « En cours » dans l'écran des notifications, comme à la figure suivante. Ainsi, l'utilisateur saura que le composant qui a affiché cette notification est en train de faire une opération.

La notification est rangée sous la catégorie « En cours » dans l'écran des notifications

Les flags s'ajoutent à l'aide de l'attribut flags qu'on trouve dans chaque notification :

1
notification.flags = FLAG_AUTO_CANCEL | FLAG_ONGOING_EVENT;

Gérer vos notifications

Votre application n'est pas la seule à envoyer des notifications, toutes les applications peuvent le faire ! Ainsi, pour gérer toutes les notifications de toutes les applications, Android fait appel à un gestionnaire de notifications, représenté par la classe NotificationManager. Comme il n'y a qu'un NotificationManager pour tout le système, on ne va pas en construire un nouveau, on va plutôt récupérer celui du système avec une méthode qui appartient à la classe Context : Object getSystemService(Context.NOTIFICATION_SERVICE). Alors réfléchissons : cette méthode appartient à Context, pouvez-vous en déduire quels sont les composants qui peuvent invoquer le NotificationManager ? Eh bien, les Broadcast Receiver n'ont pas de contexte, alors ce n'est pas possible. En revanche, les activités et les services peuvent le faire !

Il est ensuite possible d'envoyer une notification avec la méthode void notify(int id, Notification notification)id sera un identifiant unique pour la notification et où on devra insérer la notification.

Ainsi, voici le code complet de notre application qui envoie une notification pour que l'utilisateur puisse la relancer en cliquant sur une notification :

 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
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity {
  public int ID_NOTIFICATION = 0;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button b = (Button) findViewById(R.id.launch);
    b.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        // L'icône sera une petite loupe
        int icon = R.drawable.ic_action_search;
        // Le premier titre affiché
        CharSequence tickerText = "Titre de la notification";
        // Daté de maintenant
        long when = System.currentTimeMillis();

        // La notification est créée
        Notification notification = new Notification(icon, tickerText, when);

        Intent notificationIntent = new Intent(MainActivity.this, MainActivity.class);
        notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent contentIntent = PendingIntent.getActivity(MainActivity.this, 0, notificationIntent, 0);

        notification.setLatestEventInfo(MainActivity.this, "Titre", "Texte", contentIntent);

        // Récupération du Notification Manager
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        manager.notify(ID_NOTIFICATION, notification);
      }
    });
  }
}

Les services de premier plan

Pourquoi avons-nous appris tout cela ? Cela n'a pas grand-chose à voir avec les services ! En fait, tout ce que nous avons appris pourra être utilisé pour manipuler des services de premier plan.

Mais cela n'a pas de sens, pourquoi voudrait-on que nos services soient au premier plan ?

Et pourquoi pas ? En fait, parler d'un service de premier plan est un abus de langage, parce que ce type de services reste un service, il n'a pas d'interface graphique, en revanche il a la même priorité qu'une activité consultée par un utilisateur, c'est-à-dire la priorité maximale. Il est donc peu probable que le système le ferme.

Il faut cependant être prudent quand on les utilise. En effet, ils ne sont pas destinés à tous les usages. On ne fait appel aux services de premier plan que si l'utilisateur sait pertinemment qu'il y a un travail en cours qu'il ne peut pas visualiser, tout en lui laissant des contrôles sur ce travail pour qu'il puisse intervenir de manière permanente. C'est pourquoi on utilise une notification qui sera une passerelle entre votre service et l'utilisateur. Cette notification devra permettre à l'utilisateur d'ouvrir des contrôles dans une activité pour arrêter le service.

Par exemple, un lecteur multimédia qui joue de la musique depuis un service devrait s'exécuter sur le premier plan, de façon à ce que l'utilisateur soit conscient de son exécution. La notification pourrait afficher le titre de la chanson, son avancement et permettre à l'utilisateur d'accéder aux contrôles dans une activité.

Pour faire en sorte qu'un service se lance au premier plan, on appelle void startForeground(int id, Notification notification). Comme vous pouvez le voir, vous devez fournir un identifiant pour la notification avec id, ainsi que la notification à afficher.

 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
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity {
  public int ID_NOTIFICATION = 0;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button b = (Button) findViewById(R.id.launch);
    b.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        // L'icône sera une petite loupe
        int icon = R.drawable.ic_action_search;
        // Le premier titre affiché
        CharSequence tickerText = "Titre de la notification";
        // Daté de maintenant
        long when = System.currentTimeMillis();

        // La notification est créée
        Notification notification = new Notification(icon, tickerText, when);

        Intent notificationIntent = new Intent(MainActivity.this, MainActivity.class);
        notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent contentIntent = PendingIntent.getActivity(MainActivity.this, 0, notificationIntent, 0);

        notification.setLatestEventInfo(MainActivity.this, "Titre", "Texte", contentIntent);

        startForeground(ID_NOTIFICATION, notification)
      }
    });
  }
}

Vous pouvez ensuite enlever le service du premier plan avec void stopForeground(boolean removeNotification), ou vous pouvez préciser si vous voulez que la notification soit supprimée avec removeNotification (sinon le service sera arrêté, mais la notification persistera). Vous pouvez aussi arrêter le service avec les méthodes traditionnelles, auquel cas la notification sera aussi supprimée.

Pour aller plus loin : les alarmes

Il arrive parfois qu'on ait besoin de lancer des travaux à intervalles réguliers. C'est même indispensable pour certaines opérations : vérifier les e-mails de l'utilisateur, programmer une sonnerie tous les jours à la même heure, etc. Avec notre savoir, il existe déjà des solutions, mais rien qui permette de le faire de manière élégante !

La meilleure manière de faire est d'utiliser les alarmes. Une alarme est utilisée pour déclencher un Intent à intervalles réguliers.

Encore une fois, toutes les applications peuvent envoyer des alarmes, Android a donc besoin d'un système pour gérer toutes les alarmes, les envoyer au bon moment, etc. Ce système s'appelle AlarmManager et il est possible de le récupérer avec Object context.getSystemService(Context.ALARM_SERVICE), un peu comme pour NotificationManager.

Il existe deux types d'alarme : les uniques et celles qui se répètent.

Les alarmes uniques

Pour qu'une alarme ne se déclenche qu'une fois, on utilise la méthode void set(int type, long triggerAtMillis, PendingIntent operation) sur l'AlarmManager.

On va commencer par le paramètre triggerAtMillis, qui définit à quel moment l'alarme se lancera. Le temps doit y être exprimé en millisecondes comme d'habitude, alors on utilisera la classe Calendar, que nous avons vue précédemment.

Ensuite, le paramètre type permet de définir le comportement de l'alarme vis à vis du paramètre triggerAtMillis. Est-ce que triggerAtMillis va déterminer le moment où l'alarme doit se déclencher (le 30 mars à 08:52) ou dans combien de temps elle doit se déclencher (dans 25 minutes et 55 secondes) ? Pour définir une date exacte on utilisera la constante RTC, sinon pour un compte à rebours on utilisera ELAPSED_REALTIME. De plus, est-ce que vous souhaitez que l'alarme réveille l'appareil ou qu'elle se déclenche d'elle-même quand l'appareil sera réveillé d'une autre manière ? Si vous souhaitez que l'alarme réveille l'appareil rajoutez _WAKEUP aux constantes que nous venons de voir. On obtient ainsi RTC_WAKEUP et ELAPSED_REALTIME_WAKEUP.

Enfin, operation est le PendingIntent qui contient l'Intent qui sera enclenché dès que l'alarme se lancera.

Ainsi, pour une alarme qui se lance maintenant, on fera :

1
2
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
manager.set(RTC, System.currentTimeMillis(), pending);

Pour une alarme qui se lancera pour mon anniversaire (notez-le dans vos agendas !), tout en réveillant l'appareil :

1
2
3
4
5
6
Calendar calendar = Calendar.getInstance();
// N'oubliez pas que les mois commencent à 0, contrairement aux jours !
// Ne me faites pas de cadeaux en avril surtout !
calendar.set(1987, 4, 10, 17, 35);

manager.set(RTC_WAKEUP, calendar.getTimeInMillis(), pending);

Et pour une alarme qui se lance dans 20 minutes et 50 secondes :

1
2
3
4
calendar.set(Calendar.MINUTE, 20);
calendar.set(Calendar.SECOND, 50);

manager.set(ELAPSED_REALTIME, calendar.getTimeInMillis(), pending);

Les alarmes récurrentes

Il existe deux méthodes pour définir une alarme récurrente. La première est void setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation) qui prend les mêmes paramètres que précédemment à l'exception de intervalMillis qui est l'intervalle entre deux alarmes. Vous pouvez écrire n'importe quelle durée, cependant il existe quelques constantes qui peuvent vous aider :

  • INTERVAL_FIFTEEN_MINUTES représente un quart d'heure.
  • INTERVAL_HALF_HOUR représente une demi-heure.
  • INTERVAL_HOUR représente une heure.
  • INTERVAL_HALF_DAY représente 12 heures.
  • INTERVAL_DAY représente 24 heures.

Vous pouvez bien entendu faire des opérations, par exemple INTERVAL_HALF_DAY = INTERVAL_DAY / 2. Pour obtenir une semaine, on peut faire INTERVAL_DAY * 7.

Si une alarme est retardée (parce que l'appareil est en veille et que le mode choisi ne réveille pas l'appareil par exemple), une requête manquée sera distribuée dès que possible. Par la suite, les alarmes seront à nouveau distribuées en fonction du plan originel.

Le problème de cette méthode est qu'elle est assez peu respectueuse de la batterie, alors si le délai de répétition est inférieur à une heure, on utilisera plutôt void setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation), auquel cas l'alarme n'est pas déclenchée au moment précis si c'est impossible.

Une alarme ne persiste pas après un redémarrage du périphérique. Si vous souhaitez que vos alarmes se réactivent à chaque démarrage du périphérique, il vous faudra écouter le Broadcast Intent appelé ACTION_BOOT_COMPLETED.

Annuler une alarme

Pour annuler une alarme, il faut utiliser la méthode void cancel(PendingIntent operation)operation est le même PendingIntent qui accompagnait l'alarme. Si plusieurs alarmes utilisent le même PendingIntent, alors elles sont toutes annulées.

Il faut que tous les champs du PendingIntent soient identiques, à l'exception du champ Données. De plus, les deux PendingIntent doivent avoir le même identifiant.


  • Les services sont des composants très proches des activités puisqu'ils possèdent un contexte et un cycle de vie similaire mais ne possèdent pas d'interface graphique. Ils sont donc destinés à des travaux en arrière-plan.
  • Il existe deux types de services :
    • Les services locaux où l'activité qui lance le service et le service en lui-même appartiennent à la même application.
    • Les services distants où le service est lancé par l'activité d'une activité d'une autre application du système.
  • Le cycle de vie du service est légèrement différent selon qu'il soit lancé de manière locale ou distante.
  • La création d'un service se déclare dans le manifest dans un premier temps et se crée dans le code Java en étendant la classe Service ou IntentService dans un second temps.
  • Bien qu'il soit possible d'envoyer des notifications à partir d'une activité, les services sont particulièrement adaptés pour les lancer à la fin du traitement pour lequel ils sont destinés, par exemple.
  • Les alarmes sont utiles lorsque vous avez besoin d'exécuter du code à un intervalle régulier.