• 20 hours
  • Medium

Free online content available in this course.

Paperback available in this course

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 2/26/19

La communication entre composants

Log in or subscribe for free to enjoy all this course has to offer!

C'est très bien tout ça, mais on ne sait toujours pas comment lancer une activité depuis une autre activité. C'est ce que nous allons voir dans ce chapitre, et même un peu plus. On va apprendre à manipuler un mécanisme puissant qui permet de faire exécuter certaines actions et de faire circuler des messages entre applications ou à l'intérieur d'une même application. Ainsi, chaque application est censée vivre dans un compartiment cloisonné pour ne pas déranger le système quand elle s'exécute et surtout quand elle plante. À l'aide de ces liens qui lient les compartiments, Android devient un vrai puzzle dont chaque pièce apporte une fonctionnalité qui pourrait fournir son aide à une autre pièce, ou au contraire qui aurait besoin de l'aide d'une autre pièce.

Les agents qui sont chargés de ce mécanisme d'échange s'appellent les intents. Par exemple, si l'utilisateur clique sur un numéro de téléphone dans votre application, peut-être souhaiteriez-vous que le téléphone appelle le numéro demandé. Avec un intent, vous allez dire à tout le système que vous avez un numéro qu'il faut appeler, et c'est le système qui fera en sorte de trouver les applications qui peuvent le prendre en charge. Ce mécanisme est tellement important qu'Android lui-même l'utilise massivement en interne.

Aspect technique

Un intent est en fait un objet qui contient plusieurs champs, représentés à la figure suivante.

Remarquez que le champ « Données » détermine le champ « Type » et que ce n'est pas réciproque
Remarquez que le champ « Données » détermine le champ « Type » et que ce n'est pas réciproque

La façon dont sont renseignés ces champs détermine la nature ainsi que les objectifs de l'intent. Ainsi, pour qu'un intent soit dit « explicite », il suffit que son champ composant soit renseigné. Ce champ permet de définir le destinataire de l'intent, celui qui devra le gérer. Ce champ est constitué de deux informations : le package où se situe le composant, ainsi que le nom du composant. Ainsi, quand l'intent sera exécuté, Android pourra retrouver le composant de destination de manière précise.

À l'opposé des intents explicites se trouvent les intents « implicites ». Dans ce cas de figure, on ne connaît pas de manière précise le destinataire de l'intent, c'est pourquoi on va s'appliquer à renseigner d'autres champs pour laisser Android déterminer qui est capable de réceptionner cet intent. Il faut au moins fournir deux informations essentielles :

  • Une action : ce qu'on désire que le destinataire fasse.

  • Un ensemble de données : sur quelles données le destinataire doit effectuer son action.

Il existe aussi d'autres informations, pas forcément obligatoires, mais qui ont aussi leur utilité propre le moment venu :

  • La catégorie : permet d'apporter des informations supplémentaires sur l'action à exécuter et le type de composant qui devra gérer l'intent.

  • Le type : pour indiquer quel est le type des données incluses. Normalement ce type est contenu dans les données, mais en précisant cet attribut vous pouvez désactiver cette vérification automatique et imposer un type particulier.

  • Les extras : pour ajouter du contenu à vos intents afin de les faire circuler entre les composants.

  • Les flags : permettent de modifier le comportement de l'intent.

Injecter des données dans un intent

Types standards

Nous avons vu à l'instant que les intents avaient un champ « extra » qui leur permet de contenir des données à véhiculer entre les applications. Un extra est en fait une clé à laquelle on associe une valeur. Pour insérer un extra, c'est facile, il suffit d'utiliser la méthode Intent putExtra(String key, X value) avec key la clé de l'extra et value la valeur associée. Vous voyez que j'ai mis un X pour indiquer le type de la valeur — ce n'est pas syntaxiquement exact, je le sais. Je l'utilise juste pour indiquer qu'on peut y mettre un peu n'importe quel type de base, par exemple int, String ou double[].

Puis vous pouvez récuperer tous les extras d'un intent à l'aide de la méthode Bundle getExtras(), auquel cas vos couples clé-valeurs sont contenus dans le Bundle. Vous pouvez encore récupérer un extra précis à l'aide de sa clé et de son type en utilisant la méthode X get{X}Extra(String key, X defaultValue), X étant le type de l'extra et defaultValue la valeur qui sera retournée si la clé passée ne correspond à aucun extra de l'intent. En revanche, pour les types un peu plus complexes tels que les tableaux, on ne peut préciser de valeur par défaut, par conséquent on devra par exemple utiliser la méthode float[] getFloatArrayExtra(String key) pour un tableau de float.

En règle générale, la clé de l'extra commence par le package duquel provient l'intent.

// On déclare une constante dans la classe FirstClass
public final static String NOMS = "sdz.chapitreTrois.intent.examples.NOMS";

…

// Autre part dans le code
Intent i = new Intent();
String[] noms = new String[] {"Dupont", "Dupond"};
i.putExtra(FirstClass.NOMS, noms);
		
// Encore autre part
String[] noms = i.getStringArrayExtra(FirstClass.NOMS);

Il est possible de rajouter un uniqueBundle en extra avec la méthode Intent putExtras(Bundle extras) et un uniqueIntent avec la méthode Intent putExtras(Intent extras).

Les parcelables

Cependant, Bundle ne peut pas prendre tous les objets, comme je vous l'ai expliqué précédemment, il faut qu'ils soient sérialisables. Or, dans le cas d'Android, on considère qu'un objet est sérialisable à partir du moment où il implémente correctement l'interface Parcelable. Si on devait entrer dans les détails, sachez qu'un Parcelable est un objet qui sera transmis à un Parcel, et que l'objectif des Parcel est de transmettre des messages entre différents processus du système.

Pour implémenter l'interface Parcelable, il faut redéfinir deux méthodes :

  • int describeContents(), qui permet de définir si vous avez des paramètres spéciaux dans votre Parcelable. En ce mois de juillet 2012 (à l'heure où j'écris ces lignes), les seuls objets spéciaux à considérer sont les FileDescriptor. Ainsi, si votre objet ne contient pas d'objet de type FileDescriptor, vous pouvez renvoyer 0, sinon renvoyez Parcelable.CONTENT_FILE_DESCRIPTOR.

  • void writeToParcel(Parcel dest, int flags), avec dest le Parcel dans lequel nous allons insérer les attributs de notre objet et flags un entier qui vaut la plupart du temps 0. C'est dans cette classe que nous allons écrire dans le Parcel qui transmettra le message.

Si on prend l'exemple simple d'un contact dans un répertoire téléphonique :

import android.os.Parcel;
import android.os.Parcelable;

public class Contact implements Parcelable{
  private String mNom;
  private String mPrenom;
  private int mNumero;

  public Contact(String pNom, String pPrenom, int pNumero) {
    mNom = pNom;
    mPrenom = pPrenom;
    mNumero = pNumero;
  }

  @Override
  public int describeContents() {
    //On renvoie 0, car notre classe ne contient pas de FileDescriptor
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    // On ajoute les objets dans l'ordre dans lequel on les a déclarés
    dest.writeString(mNom);
    dest.writeString(mPrenom);
    dest.writeInt(mNumero);
  }
}

Tous nos attributs sont désormais dans le Parcel, on peut transmettre notre objet.

C'est presque fini, cependant, il nous faut encore ajouter un champ statique de type Parcelable.Creator et qui s'appellera impérativement « CREATOR », sinon nous serions incapables de reconstruire un objet qui est passé par un Parcel :

public static final Parcelable.Creator<Contact> CREATOR = new Parcelable.Creator<Contact>() {
  @Override
  public Contact createFromParcel(Parcel source) {
    return new Contact(source);
  }

  @Override
  public Contact[] newArray(int size) {
    return new Contact[size];
  }
};

public Contact(Parcel in) {
  mNom = in.readString();
  mPrenom = in.readString();
  mNumero = in.readInt();
}

Enfin, comme n'importe quel autre objet, on peut l'ajouter dans un intent avec putExtra et on peut le récupérer avec getParcelableExtra.

Intent i = new Intent();
Contact c = new Contact("Dupont", "Dupond", 06);
i.putExtra("sdz.chapitreTrois.intent.examples.CONTACT", c);
		
// Autre part dans le code

Contact c = i.getParcelableExtra("sdz.chapitreTrois.intent.examples.CONTACT");

Les intents explicites

Créer un intent explicite est très simple puisqu'il suffit de donner un Context qui appartienne au package où se trouve la classe de destination :

Intent intent = new Intent(Context context, Class<?> cls);

Par exemple, si la classe de destination appartient au package du Context actuel :

Intent intent = new Intent(Activite_de_depart.this, Activite_de_destination.class);

À noter qu'on aurait aussi pu utiliser la méthode Intent setClass(Context packageContext, Class<?> cls) avec packageContext un Context qui appartient au même package que le composant de destination et cls le nom de la classe qui héberge cette activité.

Il existe ensuite deux façons de lancer l'intent, selon qu'on veuille que le composant de destination nous renvoie une réponse ou pas.

Sans retour

Si vous ne vous attendez pas à ce que la nouvelle activité vous renvoie un résultat, alors vous pouvez l'appeler très naturellement avec void startActivity (Intent intent) dans votre activité. La nouvelle activité sera indépendante de l'actuelle. Elle entreprendra un cycle d'activité normal, c'est-à-dire en commençant par un onCreate.

Voici un exemple tout simple : dans une première activité, vous allez mettre un bouton et vous allez faire en sorte qu'appuyer sur ce bouton lance une seconde activité :

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

public class MainActivity extends Activity {
  public final static String AGE = "sdz.chapitreTrois.intent.example.AGE";
	
  private Button mPasserelle = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    mPasserelle = (Button) findViewById(R.id.passerelle);
    
    mPasserelle.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        // Le premier paramètre est le nom de l'activité actuelle
        // Le second est le nom de l'activité de destination
        Intent secondeActivite = new Intent(MainActivity.this, IntentExample.class);
        
        // On rajoute un extra
        secondeActivite.putExtra(AGE, 31);

        // Puis on lance l'intent !
        startActivity(secondeActivite);
      }
    });
  }
}

La seconde activité ne fera rien de particulier, si ce n'est afficher un layout différent :

package sdz.chapitreTrois.intent.example;

import android.app.Activity;
import android.os.Bundle;

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

    // On récupère l'intent qui a lancé cette activité
    Intent i = getIntent();

    // Puis on récupère l'âge donné dans l'autre activité, ou 0 si cet extra n'est pas dans l'intent
    int age = i.getIntExtra(MainActivity.AGE, 0);

    // S'il ne s'agit pas de l'âge par défaut
    if(age != 0)
      // Traiter l'âge
      age = 2;
  }
}

Enfin, n'oubliez pas de préciser dans le Manifest que vous avez désormais deux activités au sein de votre application :

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="sdz.chapitreTrois.intent.example"
  android:versionCode="1"
  android:versionName="1.0" >

  <uses-sdk
    android:minSdkVersion="7"
    android:targetSdkVersion="7" />

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
      android:name=".MainActivity"
      android:label="@string/title_activity_main" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    
    <activity
      android:name=".IntentExample"
      android:label="@string/title_example" >
    </activity>
  </application>

</manifest>

Ainsi, dès qu'on clique sur le bouton de la première activité, on passe directement à la seconde activité, comme le montre la figure suivante.

En cliquant sur le bouton de la première activité, on passe à la seconde
En cliquant sur le bouton de la première activité, on passe à la seconde

Avec retour

Cette fois, on veut qu'au retour de l'activité qui vient d'être appelée cette dernière nous renvoie un petit feedback. Pour cela, on utilisera la méthode void startActivityForResult(Intent intent, int requestCode), avec requestCode un code passé qui permet d'identifier de manière unique un intent.

Quand l'activité appelée s'arrêtera, la première méthode de callback appelée dans l'activité précédente sera void onActivityResult(int requestCode, int resultCode, Intent data). On retrouve requestCode, qui sera le même code que celui passé dans le startActivityForResult et qui permet de repérer quel intent a provoqué l'appel de l'activité dont le cycle vient de s'interrompre. resultCode est quant à lui un code renvoyé par l'activité qui indique comment elle s'est terminée (typiquement Activity.RESULT_OK si l'activité s'est terminée normalement, ou Activity.RESULT_CANCELED s'il y a eu un problème ou qu'aucun code de retour n'a été précisé). Enfin, intent est un intent qui contient éventuellement des données.

Dans la seconde activité, vous pouvez définir un résultat avec la méthode void setResult(int resultCode, Intent data), ces paramètres étant identiques à ceux décrits ci-dessus.

Ainsi, l'attribut requestCode de void startActivityForResult(Intent intent, int requestCode) sera similaire au requestCode que nous fournira la méthode de callbackvoid onActivityResult(int requestCode, int resultCode, Intent data), de manière à pouvoir identifier quel intent est à l'origine de ce retour.

Le code de ce nouvel exemple sera presque similaire à celui de l'exemple précédent, sauf que cette fois la seconde activité proposera à l'utilisateur de cliquer sur deux boutons. Cliquer sur un de ces boutons retournera à l'activité précédente en lui indiquant lequel des deux boutons a été pressé. Ainsi, MainActivity ressemble désormais à :

package sdz.chapitreTrois.intent.example;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {
  private Button mPasserelle = null;
  // L'identifiant de notre requête
  public final static int CHOOSE_BUTTON_REQUEST = 0;
  // L'identifiant de la chaîne de caractères qui contient le résultat de l'intent
  public final static String BUTTONS = "sdz.chapitreTrois.intent.example.Boutons";

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    mPasserelle = (Button) findViewById(R.id.passerelle);
    
    mPasserelle.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent secondeActivite = new Intent(MainActivity.this, IntentExample.class);
        // On associe l'identifiant à notre intent
        startActivityForResult(secondeActivite, CHOOSE_BUTTON_REQUEST);
      }
    });
  }
  
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // On vérifie tout d'abord à quel intent on fait référence ici à l'aide de notre identifiant
    if (requestCode == CHOOSE_BUTTON_REQUEST) {
      // On vérifie aussi que l'opération s'est bien déroulée
      if (resultCode == RESULT_OK) {
        // On affiche le bouton qui a été choisi
      	Toast.makeText(this, "Vous avez choisi le bouton " + data.getStringExtra(BUTTONS), Toast.LENGTH_SHORT).show();
      }
    }
  }
}

Alors que la seconde activité devient :

package sdz.chapitreTrois.intent.example;

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

public class IntentExample extends Activity {
  private Button mButton1 = null;
  private Button mButton2 = null;
	
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.layout_example);
    
    mButton1 = (Button) findViewById(R.id.button1);
    mButton1.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent result = new Intent();
        result.putExtra(MainActivity.BUTTONS, "1");
        setResult(RESULT_OK, result);
        finish();
      }
    });
    
    mButton2 = (Button) findViewById(R.id.button2);
    mButton2.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        Intent result = new Intent();
        result.putExtra(MainActivity.BUTTONS, "2");
        setResult(RESULT_OK, result);
        finish();
      }
    });
  }
}

Et voilà, dès que vous cliquez sur un des boutons, la première activité va lancer un Toast qui affichera quel bouton a été pressé, comme le montre la figure suivante.

Un Toast affiche quel bouton a été pressé
Un Toast affiche quel bouton a été pressé

Les intents implicites

Ici, on fera en sorte d'envoyer une requête à un destinataire, sans savoir qui il est, et d'ailleurs on s'en fiche tant que le travail qu'on lui demande de faire est effectué. Ainsi, les applications destinataires sont soit fournies par Android, soit par d'autres applications téléchargées sur le Play Store par exemple.

Les données

L'URI

La première chose qu'on va étudier, c'est les données, parce qu'elles sont organisées selon une certaine syntaxe qu'il vous faut connaître. En fait, elles sont formatées à l'aide des URI. Un URI est une chaîne de caractères qui permet d'identifier un endroit. Par exemple sur internet, ou dans le cas d'Android sur le périphérique ou une ressource. Afin d'étudier les URI, on va faire l'analogie avec les adresses URL qui nous permettent d'accéder à des sites internet. En effet, un peu à la manière d'un serveur, nos fournisseurs de contenu vont répondre en fonction de l'URI fournie. De plus, la forme générale d'une URI rappelle fortement les URL. Prenons l'exemple du Site du Zéro avec une URL inventée : http://www.siteduzero.com/forum/android/aide.html. On identifie plusieurs parties :

  • http://

  • www.siteduzero.com

  • /forum/android/aide.html

Les URI se comportent d'une manière un peu similaire. La syntaxe d'un URI peut être analysée de la manière suivante (les parties entre accolades {} sont optionnelles) :

<schéma> : <information> { ? <requête> } { # <fragment> }
  • Le schéma décrit quelle est la nature de l'information. S'il s'agit d'un numéro de téléphone, alors le schéma sera tel, s'il s'agit d'un site internet, alors le schéma sera http, etc.

  • L'information est la donnée en tant que telle. Cette information respecte elle aussi une syntaxe, mais qui dépend du schéma cette fois-ci. Ainsi, pour un numéro de téléphone, vous pouvez vous contenter d'insérer le numéro tel:0606060606, mais pour des coordonnées GPS il faudra séparer la latitude de la longitude à l'aide d'une virgule geo:123.456789,-12.345678. Pour un site internet, il s'agit d'un chemin hiérarchique.

  • La requête permet de fournir une précision par rapport à l'information.

  • Le fragment permet enfin d’accéder à une sous-partie de l'information.

Pour créer un objet URI, c'est simple, il suffit d'utiliser la méthode statique Uri Uri.parse(String uri). Par exemple, pour envoyer un SMS à une personne, j'utiliserai l'URI :

Uri sms = Uri.parse("sms:0606060606");

Mais je peux aussi indiquer plusieurs destinataires et un corps pour ce message :

Uri sms = Uri.parse("sms:0606060606,0606060607?body=Salut%20les%20potes");

Comme vous pouvez le voir, le contenu de la chaîne doit être encodé, sinon vous rencontrerez des problèmes.

Type MIME

Le MIME est un identifiant pour les formats de fichier. Par exemple, il existe un type MIME text. Si une donnée est accompagnée du type MIME text, alors les données sont du texte. On trouve aussi audio et video par exemple. Il est ensuite possible de préciser un sous-type afin d'affiner les informations sur les données, par exemple audio/mp3 et audio/wav sont deux types MIME qui indiquent que les données sont sonores, mais aussi de quelle manière elles sont encodées.

Les types MIME que nous venons de voir son standards, c'est-à-dire qu'il y a une organisation qui les a reconnus comme étant légitimes. Mais si vous vouliez créer vos propres types MIME ? Vous n'allez pas demander à l'organisation de les valider, ils ne seront pas d'accord avec vous. C'est pourquoi il existe une petite syntaxe à respecter pour les types personnalisés : vnd.votre_package.le_type, ce qui peut donner par exemple vnd.sdz.chapitreTrois.contact_telephonique.

Pour les intents, ce type peut être décrit de manière implicite dans l'URI (on voit bien par exemple que sms:0606060606 décrit un numéro de téléphone, il n'est pas nécessaire de le préciser), mais il faudra par moments le décrire de manière explicite. On peut le faire dans le champ type d'un intent. Vous trouverez une liste non exhaustive des types MIME sur cette page Wikipédia.

Préciser un type est surtout indispensable quand on doit manipuler des ensembles de données, comme par exemple quand on veut supprimer une ou plusieurs entrées dans le répertoire, car dans ce cas précis il s'agira d'un pointeur vers ces données. Avec Android, il existe deux manières de manipuler ces ensembles de données, les curseurs (cursor) et les fournisseurs de contenus (content provider). Ces deux techniques seront étudiées plus tard, par conséquent nous allons nous cantonner aux données simples pour l'instant.

L'action

Une action est une constante qui se trouve dans la classe Intent et qui commence toujours par « ACTION_ » suivi d'un verbe (en anglais, bien sûr) de façon à bien faire comprendre qu'il s'agit d'une action. Si vous voulez voir quelque chose, on va utiliser l'action ACTION_VIEW. Par exemple, si vous utilisez ACTION_VIEW sur un numéro de téléphone, alors le numéro de téléphone s'affichera dans le composeur de numéros de téléphone.

Vous pouvez aussi créer vos propres actions. Pour cela, il vaux mieux respecter une syntaxe, qui est de commencer par votre package suivi de .intent.action.NOM_DE_L_ACTION :

public final static String ACTION_PERSO = "sdz.chapitreTrois.intent.action.PERSO";

Voici quelques actions natives parmi les plus usitées :

Intitulé

Action

Entrée attendue

Sortie attendue

ACTION_MAIN

Pour indiquer qu'il s'agit du point d'entrée dans l'application

/

/

ACTION_DIAL

Pour ouvrir le composeur de numéros téléphoniques

Un numéro de téléphone semble une bonne idée :-p

/

ACTION_DELETE*

Supprimer des données

Un URI vers les données à supprimer

/

ACTION_EDIT*

Ouvrir un éditeur adapté pour modifier les données fournies

Un URI vers les données à éditer

/

ACTION_INSERT*

Insérer des données

L'URI du répertoire où insérer les données

L'URI des nouvelles données créées

ACTION_PICK*

Sélectionner un élément dans un ensemble de données

L'URI qui contient un répertoire de données à partir duquel l'élément sera sélectionné

L'URI de l'élément qui a été sélectionné

ACTION_SEARCH

Effectuer une recherche

Le texte à rechercher

/

ACTION_SENDTO

Envoyer un message à quelqu'un

La personne à qui envoyer le message

/

ACTION_VIEW

Permet de visionner une donnée

Un peu tout. Une adresse e-mail sera visionnée dans l'application pour les e-mails, un numéro de téléphone dans le composeur, une adresse internet dans le navigateur, etc.

/

ACTION_WEB_SEARCH

Effectuer une recherche sur internet

S'il s'agit d'un texte qui commence par « http », le site s'affichera directement, sinon c'est une recherche dans Google qui se fera

/

Pour créer un intent qui va ouvrir le composeur téléphonique avec le numéro de téléphone 0606060606, j'adapte mon code précédent en remplaçant le code du bouton par :

mPasserelle.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    Uri telephone = Uri.parse("tel:0606060606");
    Intent secondeActivite = new Intent(Intent.ACTION_DIAL, telephone);
    startActivity(secondeActivite);
  }
});

Ce qui donne, une fois que j'appuie dessus, la figure suivante.

Le composeur téléphonique est lancé avec le numéro souhaité
Le composeur téléphonique est lancé avec le numéro souhaité

La résolution des intents

Quand on lance un ACTION_VIEW avec une adresse internet, c'est le navigateur qui se lance, et quand on lance un ACTION_VIEW avec un numéro de téléphone, c'est le composeur de numéros qui se lance. Alors, comment Android détermine qui doit répondre à un intent donné ?

Ce que va faire Android, c'est qu'il va comparer l'intent à des filtres que nous allons déclarer dans le Manifest et qui signalent que les composants de nos applications peuvent gérer certains intents. Ces filtres sont les nœuds <intent-filter>, nous les avons déjà rencontrés et ignorés par le passé. Un composant d'une application doit avoir autant de filtres que de capacités de traitement. S'il peut gérer deux types d'intent, il doit avoir deux filtres.

Le test de conformité entre un intent et un filtre se fait sur trois critères.

L'action

Permet de filtrer en fonction du champ Action d'un intent. Il peut y en avoir un ou plus par filtre. Si vous n'en mettez pas, tous vos intents seront recalés. Un intent sera accepté si ce qui se trouve dans son champ Action est identique à au moins une des actions du filtre. Et si un intent ne précise pas d'action, alors il sera automatiquement accepté pour ce test.

<activity>
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <action android:name="android.intent.action.SENDTO" />
  </intent-filter>
</activity>

Cette activité ne pourra intercepter que les intents qui ont dans leur champ action ACTION_VIEW et/ou ACTION_SENDTO, car toutes ses actions sont acceptées par le filtre. Si un intent a pour action ACTION_VIEW et ACTION_SEARCH, alors il sera recalé, car une de ses actions n'est pas acceptée par le filtre.

La catégorie

Cette fois, il n'est pas indispensable d'avoir une indication de catégorie pour un intent, mais, s'il y en a une ou plusieurs, alors pour passer ce test il faut que toutes les catégories de l'intent correspondent à des catégories du filtre. Pour les matheux, on dit qu'il s'agit d'une application « injective » mais pas « surjective ».

On pourrait se dire que par conséquent, si un intent n'a pas de catégorie, alors il passe automatiquement ce test, mais dès qu'un intent est utilisé avec la méthode startActivity(), alors on lui ajoute la catégorie CATEGORY_DEFAULT. Donc, si vous voulez que votre composant accepte les intents implicites, vous devez rajouter cette catégorie à votre filtre.

Pour les actions et les catégories, la syntaxe est différente entre le Java et le XML. Par exemple, pour l'action ACTION_VIEW en Java, on utilisera android.intent.action.VIEW et pour la categorie CATEGORY_DEFAULT on utilisera android.intent.category.DEFAULT. De plus, quand vous créez vos propres actions ou catégories, le mieux est de les préfixer avec le nom de votre package afin de vous assurer qu'elles restent uniques. Par exemple, pour l'action DESEMBROUILLER, on pourrait utiliser sdz.chapitreQuatre.action.DESEMBROUILLER.

<activity>
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <action android:name="android.intent.action.SEARCH" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="com.sdz.intent.category.DESEMBROUILLEUR" />
  </intent-filter>
</activity>

Il faut ici que l'intent ait pour action ACTION_VIEW et/ou ACTION_SEARCH. En ce qui concerne les catégories, il doit avoir CATEGORY_DEFAULTetCATEGORY_DESEMBROUILLEUR.

Voici les principales catégories par défaut fournies par Android :

Catégorie

Description

CATEGORY_DEFAULT

Indique qu'il faut effectuer le traitement par défaut sur les données correspondantes. Concrètement, on l'utilise pour déclarer qu'on accepte que ce composant soit utilisé par des intents implicites.

CATEGORY_BROWSABLE

Utilisé pour indiquer qu'une activité peut être appelée sans risque depuis un navigateur web. Ainsi, si un utilisateur clique sur un lien dans votre application, vous promettez que rien de dangereux ne se passera à la suite de l'activation de cet intent.

CATEGORY_TAB

Utilisé pour les activités qu'on retrouve dans des onglets.

CATEGORY_ALTERNATIVE

Permet de définir une activité comme un traitement alternatif dans le visionnage d'éléments. C'est par exemple intéressant dans les menus, si vous souhaitez proposer à votre utilisateur de regarder telles données de la manière proposée par votre application ou d'une manière que propose une autre application.

CATEGORY_SELECTED_ALTERNATIVE

Comme ci-dessus, mais pour des éléments qui ont été sélectionnés, pas seulement pour les voir.

CATEGORY_LAUNCHER

Indique que c'est ce composant qui doit s'afficher dans le lanceur d'applications.

CATEGORY_HOME

Permet d'indiquer que c'est cette activité qui doit se trouver sur l'écran d'accueil d'Android.

CATEGORY_PREFERENCE

Utilisé pour identifier les PreferenceActivity (dont nous parlerons au chapitre suivant).

Les données

Il est possible de préciser plusieurs informations sur les données que cette activité peut traiter. Principalement, on peut préciser le schéma qu'on veut avec android:scheme, on peut aussi préciser le type MIME avec android:mimeType. Par exemple, si notre application traite des fichiers textes qui proviennent d'internet, on aura besoin du type « texte » et du schéma « internet », ce qui donne :

<data android:mimeType="text/plain" android:scheme="http" /> 
<data android:mimeType="text/plain" android:scheme="https" />

Et il se passe quoi en interne une fois qu'on a lancé un intent ?

Eh bien, il existe plusieurs cas de figure:

  • Soit votre recherche est infructueuse et vous avez 0 résultat, auquel cas c'est grave et une ActivityNotFoundException sera lancée. Il vous faut donc penser à gérer ce type d'erreurs.

  • Si on n'a qu'un résultat, comme dans le cas des intents explicites, alors ce résultat va directement se lancer.

  • En revanche, si on a plusieurs réponses possibles, alors le système va demander à l'utilisateur de choisir à l'aide d'une boîte de dialogue. Si l'utilisateur choisit une action par défaut pour un intent, alors à chaque fois que le même intent sera émis ce sera toujours le même composant qui sera sélectionné. D'ailleurs, il peut arriver que ce soit une mauvaise chose parce qu'un même intent ne signifie pas toujours une même intention (ironiquement). Il se peut qu'avec ACTION_SEND, on cherche un jour à envoyer un SMS et un autre jour à envoyer un e-mail, c'est pourquoi il est possible de forcer la main à Android et à obliger l'utilisateur à choisir parmi plusieurs éventualités à l'aide de Intent createChooser(Intent target, CharSequence titre). On peut ainsi insérer l'intent à traiter et le titre de la boîte de dialogue qui permettra à l'utilisateur de choisir une application.

Dans tous les cas, vous pouvez vérifier si un composant va réagir à un intent de manière programmatique à l'aide du Package Manager. Le Package Manager est un objet qui vous permet d'obtenir des informations sur les packages qui sont installés sur l'appareil. On y fait appel avec la méthode PackageManager getPackageManager() dans n'importe quel composant. Puis on demande à l'intent le nom de l'activité qui va pouvoir le gérer à l'aide de la méthode ComponentName resolveActivity (PackageManager pm) :

Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("tel:0606060606"));

PackageManager manager = getPackageManager();

ComponentName component = intent.resolveActivity(manager);
// On vérifie que component n'est pas null
if(component != null)
  //Alors c'est qu'il y a une activité qui va gérer l'intent

Pour aller plus loin : navigation entre des activités

Une application possède en général plusieurs activités. Chaque activité est dédiée à un ensemble cohérent d'actions, mais toujours centrées vers un même objectif. Pour une application qui lit des musiques, il y a une activité pour choisir la musique à écouter, une autre qui présente les contrôles sur les musiques, encore une autre pour paramétrer l'application, etc.

Je vous avais présenté au tout début du tutoriel la pile des activités. En effet, comme on ne peut avoir qu'une activité visible à la fois, les activités étaient présentées dans une pile où il était possible d'ajouter ou d'enlever un élément au sommet afin de changer l'activité consultée actuellement. C'est bien sûr toujours vrai, à un détail près. Il existe en fait une pile par tâche. On pourrait dire qu'une tâche est une application, mais aussi les activités qui seront lancées par cette application et qui sont extérieures à l'application. Ainsi que les activités qui seront lancées par ces activités extérieures, etc.

Au démarrage de l'application, une nouvelle tâche est créée et l'activité principale occupe la racine de la pile.

Au lancement d'une nouvelle activité, cette dernière est ajoutée au sommet de la pile et acquiert ainsi le focus. L'activité précédente est arrêtée, mais l'état de son interface graphique est conservé. Quand l'utilisateur appuie sur le bouton Retour, l'activité actuelle est éjectée de la pile (elle est donc détruite) et l'activité précédente reprend son déroulement normal (avec restauration des éléments de l'interface graphique). S'il n'y a pas d'activité précédente, alors la tâche est tout simplement détruite.

Dans une pile, on ne manipule jamais que le sommet. Ainsi, si l'utilisateur appuie sur un bouton de l'activité 1 pour aller à l'activité 2, puis appuie sur un bouton de l'activité 2 pour aller dans l'activité 1, alors une nouvelle instance de l'activité 1 sera créée, comme le montre la figure suivante.

On passe de l'activité 1 à  l'activité 2, puis de l'activité 2 à l'activité 1, ce qui fait qu'on a deux différentes instances de l'activité 1 !
On passe de l'activité 1 à l'activité 2, puis de l'activité 2 à l'activité 1, ce qui fait qu'on a deux différentes instances de l'activité 1 !

Pour changer ce comportement, il est possible de manipuler l'affinité d'une activité. Cette affinité est un attribut qui indique avec quelle tâche elle préfère travailler. Toutes les activités qui ont une affinité avec une même tâche se lanceront dans cette tâche-là.

Ce comportement est celui qui est préférable la plupart du temps. Cependant, il peut arriver que vous ayez besoin d'agir autrement, auquel cas il y a deux façons de faire.

Modifier l'activité dans le Manifest

Il existe six attributs que nous n'avons pas encore vus et qui permettent de changer la façon dont Android réagit à la navigation.

android:taskAffinity

Cet attribut permet de préciser avec quelle tâche cette activité possède une affinité. Exemple :

<activity android:taskAffinity="sdz.chapitreTrois.intent.exemple.tacheUn" />
<activity android:taskAffinity="sdz.chapitreTrois.intent.exemple.tacheDeux" />
android:allowTaskReparenting

Est-ce que l'activité peut se déconnecter d'une tâche dans laquelle elle a commencé à travailler pour aller vers une autre tâche avec laquelle elle a une affinité ?

Par exemple, dans le cas d'une application pour lire les SMS, si le SMS contient un lien, alors cliquer dessus lancera une activité qui permettra d'afficher la page web désignée par le lien. Si on appuie sur le bouton Retour, on revient à la lecture du SMS. En revanche, avec cet attribut, l'activité lancée sera liée à la tâche du navigateur et non plus du client SMS.

La valeur par défaut est false.

android:launchMode

Définit comment l'application devra être lancée dans une tâche. Il existe deux modes : soit l'activité peut être instanciée plusieurs fois dans la même tâche, soit elle est toujours présente de manière unique.

Dans le premier mode, il existe deux valeurs possibles :

  • standard est le mode par défaut, dès qu'on lance une activité une nouvelle instance est créée dans la tâche. Les différentes instances peuvent aussi appartenir à plusieurs tâches.

  • Avec singleTop, si une instance de l'activité existe déjà au sommet de la tâche actuelle, alors le système redirigera l'intent vers cette instance au lieu de créer une nouvelle instance. Le retour dans l'activité se fera à travers la méthode de callbackvoid onNewIntent(Intent intent).

Le second mode n'est pas recommandé et doit être utilisé uniquement dans des cas précis. Surtout, on ne l'utilise que si l'activité est celle de lancement de l'application. Il peut prendre deux valeurs :

  • Avec singleTask, le système crée l'activité à la racine d'une nouvelle tâche. Cependant, si une instance de l'activité existe déjà, alors on ouvrira plutôt cette instance-là.

  • Enfin avec singleInstance, à chaque fois on crée une nouvelle tâche dont l'activité sera la racine.

android:clearTaskOnLaunch

Est-ce que toutes les activités doivent être enlevées de la tâche — à l'exception de la racine — quand on relance la tâche depuis l'écran de démarrage ? Ainsi, dès que l'utilisateur relance l'application, il retournera à l'activité d'accueil, sinon il retournera dans la dernière activité qu'il consultait.

La valeur par défaut est false.

android:alwaysRetainTaskState

Est-ce que l'état de la tâche dans laquelle se trouve l'activité — et dont elle est la racine — doit être maintenu par le système ?

Typiquement, une tâche est détruite si elle n'est pas active et que l'utilisateur ne la consulte pas pendant un certain temps. Cependant, dans certains cas, comme dans le cas d'un navigateur web avec des onglets, l'utilisateur sera bien content de récupérer les onglets qui étaient ouverts.

La valeur par défaut est false.

android:finishOnTaskLaunch

Est-ce que, s'il existe déjà une instance de cette activité, il faut la fermer dès qu'une nouvelle instance est demandée ?

La valeur par défaut est false.

Avec les intents

Il est aussi possible de modifier l'association par défaut d'une activité à une tâche à l'aide des flags contenus dans les intents. On peut rajouter un flag à un intent avec la méthode Intent addFlags(int flags).

Il existe trois flags principaux :

  • FLAG_ACTIVITY_NEW_TASK permet de lancer l'activité dans une nouvelle tâche, sauf si l'activité existe déjà dans une tâche. C'est l'équivalent du mode singleTask.

  • FLAG_ACTIVITY_SINGLE_TOP est un équivalent de singleTop. On lancera l'activité dans une nouvelle tâche, quelques soient les circonstances.

  • FLAG_ACTIVITY_CLEAR_TOP permet de faire en sorte que, si l'activité est déjà lancée dans la tâche actuelle, alors au lieu de lancer une nouvelle instance de cette activité toutes les autres activités qui se trouvent au-dessus d'elle seront fermées et l'intent sera délivré à l'activité (souvent utilisé avec FLAG_ACTIVITY_NEW_TASK). Quand on utilise ces deux flags ensemble, ils permettent de localiser une activité qui existait déjà dans une autre tâche et de la mettre dans une position où elle pourra répondre à l'intent.

Pour aller plus loin : diffuser des intents

On a vu avec les intents comment dire « Je veux que vous traitiez cela, alors que quelqu'un le fasse pour moi s'il vous plaît ». Ici on va voir comment dire « Cet évènement vient de se dérouler, je préviens juste, si cela intéresse quelqu'un ». C'est donc la différence entre « Je viens de recevoir un SMS, je cherche un composant qui pourra permettre à l'utilisateur de lui répondre » et « Je viens de recevoir un SMS, ça intéresse une application de le gérer ? ». Il s'agit ici uniquement de notifications, pas de demandes. Concrètement, le mécanisme normal des intents est visible pour l'utilisateur, alors que celui que nous allons étudier est totalement transparent pour lui.

Nous utiliserons toujours des intents, sauf qu'ils seront anonymes et diffusés à tout le système. Ce type d'intent est très utilisé au niveau du système pour transmettre des informations, comme par exemple l'état de la batterie ou du réseau. Ces intents particuliers s'appellent des broadcast intents. On utilise encore une fois un système de filtrage pour déterminer qui peut recevoir l'intent, mais c'est la façon dont nous allons recevoir les messages qui est un peu spéciale.

La création des broadcast intents est similaire à celle des intents classiques, sauf que vous allez les envoyer avec la méthode void sendBroadcast(Intent intent). De cette manière, l'intent ne sera reçu que par les broadcast receivers, qui sont des classes qui dérivent de la classe BroadcastReceiver. De plus, quand vous allez déclarer ce composant dans votre Manifest, il faudra que vous annonciez qu'il s'agit d'un broadcast receiver :

<receiver android:name="CoucouReceiver">
  <intent-filter>
    <action android:name="sdz.chapitreTrois.intent.action.coucou" />
  </intent-filter>
</receiver>

Il vous faudra alors redéfinir la méthode de callbackvoid onReceive (Context context, Intent intent) qui est lancée dès qu'on reçoit un broadcast intent. C'est dans cette classe qu'on gérera le message reçu.

Par exemple, si j'ai un intent qui transmet à tout le système le nom de l'utilisateur :

public class CoucouReceiver extends BroadcastReceiver {
  private static final String NOM_USER = "sdz.chapitreTrois.intent.extra.NOM";

  // Déclenché dès qu'on reçoit un broadcast intent qui réponde aux filtres déclarés dans le Manifest
  @Override
  public void onReceive(Context context, Intent intent) {
    // On vérifie qu'il s'agit du bon intent
    if(intent.getAction().equals("ACTION_COUCOU")) {
      // On récupère le nom de l'utilisateur
      String nom = intent.getExtra(NOM_USER);
      Toast.makeText(context, "Coucou " + nom + " !", Toast.LENGTH_LONG).show();
    }
  }
}

Un broascast receiver déclaré de cette manière sera disponible tout le temps, même quand l'application n'est pas lancée, mais ne sera viable que pendant la durée d'exécution de sa méthode onReceive. Ainsi, ne vous attendez pas à retrouver votre receiver si vous lancez un thread, une boîte de dialogue ou un autre composant d'une application à partir de lui.

De plus, il ne s'exécutera pas en parallèle de votre application, mais bien de manière séquentielle (dans le même thread, donc), ce qui signifie que, si vous effectuez de gros calculs qui prennent du temps, les performances de votre application pourraient s'en trouver affectées.

Mais il est aussi possible de déclarer un broadcast receiver de manière dynamique, directement dans le code. Cette technique est surtout utilisée pour gérer les évènements de l'interface graphique.

Pour procéder, vous devrez créer une classe qui dérive de BroadcastReceiver, mais sans l'enregistrer dans le Manifest. Ensuite, vous pouvez lui rajouter des lois de filtrage avec la classe IntentFilter, puis vous pouvez l'enregistrer dans l'activité voulue avec la méthode Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) et surtout, quand vous n'en n'aurez plus besoin, il faudra la désactiver avec void unregisterReceiver(BroadcastReceiver receiver).

Ainsi, si on veut recevoir nos broadcast intents pour dire coucou à l'utilisateur, mais uniquement quand l'application se lance et qu'elle n'est pas en pause, on fait :

import android.app.Activity;
import android.content.IntentFilter;
import android.os.Bundle;

public class CoucouActivity extends Activity {
  private static final String COUCOU = "sdz.chapitreTrois.intent.action.coucou";
  private IntentFilter filtre = null;
  private CoucouReceiver receiver = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    filtre = new IntentFilter(COUCOU);
    receiver = new CoucouReceiver();
  }
  
  @Override
  public void onResume() {
    super.onResume();
    registerReceiver(receiver, filtre);
  }

  /** Si vous déclarez votre receiver dans le onResume, n'oubliez pas qu'il faut l'arrêter dans le onPause **/
  @Override
  public void onPause() {
    super.onPause();
    unregisterReceiver(receiver);
  }
}

De plus, il existe quelques messages diffusés par le système de manière native et que vous pouvez écouter, comme par exemple ACTION_CAMERA_BUTTON qui est lancé dès que l'utilisateur appuie sur le bouton de l'appareil photo.

Sécurité

N'importe quelle application peut envoyer des broadcast intents à votre receiver, ce qui est une faiblesse au niveau sécurité. Vous pouvez aussi faire en sorte que votre receiver déclaré dans le Manifest ne soit accessible qu'à l'intérieur de votre application en lui ajoutant l'attribut android:exported="false" :

<receiver android:name="CoucouReceiver"
  android:exported="false">
  <intent-filter>
    <action android:name="sdz.chapitreTrois.intent.action.coucou" />
  </intent-filter>
</receiver>

Notez que cet attribut est disponible pour tous les composants.

De plus, quand vous envoyez un broadcast intent, toutes les applications peuvent le recevoir. Afin de déterminer qui peut recevoir un broadcast intent, il suffit de lui ajouter une permission à l'aide de la méthode void sendBroadcast (Intent intent, String receiverPermission), avec receiverPermission une permission que vous aurez déterminée. Ainsi, seuls les receivers qui déclarent cette permission pourront recevoir ces broadcast intents :

private String COUCOU_BROADCAST = "sdz.chapitreTrois.permission.COUCOU_BROADCAST";

…

sendBroadcast(i, COUCOU_BROADCAST);

Puis dans le Manifest, il suffit de rajouter :

<uses-permission android:name="sdz.chapitreTrois.permission.COUCOU_BROADCAST"/>
  • Les intents sont des objets permettant d'envoyer des messages entre vos activités, voire entre vos applications. Ils peuvent, selon vos préférences, contenir un certain nombre d'informations qu'il sera possible d'exploiter dans une autre activité.

  • En général, les données contenues dans un intent sont assez limitées mais il est possible de partager une classe entière si vous étendez la classe Parcelable et que vous implémentez toutes les méthodes nécessaires à son fonctionnement.

  • Les intents explicites sont destinés à se rendre à une activité très précise. Vous pourriez également définir que vous attendez un retour suite à cet appel via la méthode void startActivityForResult(Intent intent, int requestCode).

  • Les intents implicites sont destinés à demander à une activité, sans que l'on sache laquelle, de traiter votre message en désignant le type d'action souhaité et les données à traiter.

  • Définir un nœud <intent-filter> dans le nœud d'une <activity> de votre fichier Manifest vous permettra de filtrer vos activités en fonction du champ d'action de vos intents.

  • Nous avons vu qu'Android gère nos activités via une pile LIFO. Pour changer ce comportement, il est possible de manipuler l'affinité d'une activité. Cette affinité est un attribut qui indique avec quelle tâche elle préfère travailler.

  • Les broadcast intents diffusent des intents à travers tout le système pour transmettre des informations de manière publique à qui veut. Cela se met en place grâce à un nœud <receiver> filtré par un nœud <intent-filter>.

Example of certificate of achievement
Example of certificate of achievement