Ludovic ROLAND

Blog technique sur mes expériences de développeur.

Les permissions sous Android (3/6) : Demander une permission (1/2)

6 mars 2016

Maintenant que la théorie a été vue, place à la pratique ! Je vous propose de voir, pas à pas, comment, depuis Android 6.0, il convient de demander une permission. Pour nous aider, nous allons nous baser sur un fil rouge : demander l’autorisation de passer un coup de téléphone après appuie sur un bouton.

Plan

Le fichier build.gradle

Avant d’aller plus loin, il convient de vérifier une toute petite chose au niveau du fichier build.gradle du module qui sera votre application. Pour profiter pleinement de ce tutoriel, il convient que les champs compileSdkVersion et targetSdkVersion soient, à minima, à 23.

Par exemple, voici ce à quoi ressemble le fichier de mon côté :

apply plugin: 'com.android.application'

android
{
  compileSdkVersion 23
  buildToolsVersion "23.0.2"

  defaultConfig
  {
    applicationId "com.domain.packagename"

    minSdkVersion 9
    targetSdkVersion 23

    versionCode 1
    versionName "1.0"
  }

  buildTypes
  {
    release
    {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}

dependencies
{
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
}

Le manifest

La première étape, quand on nécessite une permission au sein d’une application Android, est de la déclarer dans le fichier AndroidManifest.xml.

Lorsque vous créez un nouveau projet Android contenant au moins une activité via Android Studio, votre fichier AndroidManifest.xml doit sensiblement ressembler à ça :

<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.domain.packagename"
>
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
  >
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name"
      android:theme="@style/AppTheme.NoActionBar"
    >
      <intent-filter>
        <action android:name="android.intent.action.MAIN"></action>
        <category android:name="android.intent.category.LAUNCHER"></category>
      </intent-filter>
    </activity>
  </application>
</manifest>

Comme vous pouvez le constater, plusieurs éléments, représentés par des balises XML, composent ce fichier. Revenons rapidement dessus :

  • La balise <manifest /> encapsule tout le contenu du fichier. C’est ce qu’on appelle une balise root en XML. C’est au sein de cette balise qui encapsule tout le contenu du fichier.
  • La balise <application /> permet de décrire l’application comme par exemple l’icône de votre application, s’il s’agit d’un jeu ou non, le thème de celle-ci, une classe Application spécifique si celle-ci est personnalisée, si l’application supporte le notamment de personnaliser la classe Application de votre application si celle-ci supporte le RTL (right-to-left) ou non, etc. C’est au sein de cette balise que l’on va notamment déclarer les activités, les services, les receivers et les providers.
  • La balise <activity /> permet de déclarer une activité, c’est-à-dire un écran qui compose l’application.

C’est dans ce fichier que nous allons déclarer les permissions et plus particulièrement entre les balises <manifest />.

Une permission se déclare à l’aide de la balise <uses-permission /> et de l’attribut android:name. C’est au niveau de cet attribut que nous allons spécifier le nom de la permission requise par notre application.

Dans le cadre de notre fil rouge, à savoir passer un coup de fil, nous pouvons demander la permission CALL_PHONE.

Il convient donc d’ajouter la ligne suivante à notre fichier AndroidManifest.xml :

<uses-permission android:name="android.permission.CALL_PHONE" />

Le fichier complet doit donc maintenant ressembler à ça :

<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.domain.packagename"
>
  <uses-permission android:name="android.permission.CALL_PHONE" ></uses>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
  >
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name"
      android:theme="@style/AppTheme.NoActionBar"
    >
      <intent-filter>
        <action android:name="android.intent.action.MAIN"></action>
        <category android:name="android.intent.category.LAUNCHER"></category>
      </intent-filter>
    </activity>
  </application>
</manifest>

Un peu de Java

Le code de base

Puisque les bonnes pratiques nous recommandent de travailler dans des fragments plutôt que dans des activités, nous allons travailler dans un… fragment ! ;)

Voici le code de base sur lequel nous allons nous appuyer dans le reste de ce tutoriel :

package com.domain.packagename;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;

public final class MainActivityFragment
    extends Fragment
    implements OnClickListener
{

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
  {
    final View rootView = inflater.inflate(R.layout.fragment_main, container, false);
    final Button call = (Button) rootView.findViewById(R.id.call);
    call.setOnClickListener(this);
    return rootView;
  }

  @Override
  public void onClick(View view)
  {
    //todo : passer l'appel
  }

}

Et le layout associé :

<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="@dimen/activity_horizontal_margin"
>
  <Button
    android:id="@+id/call"
    android:text="Appeler"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
  />
</RelativeLayout>

Comme vous pouvez le constater, notre application est extrêmement simple puisqu’elle se compose d’un simple bouton, qui lorsqu’on clique dessus, permet de lancer un appel.

Une fois exécutée, voici ce à quoi ressemble l’application :

Dans les prochaines lignes de code, je vous propose que l’on mette en place le (très) petit morceau de code permettant de lancer un appel au clic sur l’unique bouton composant notre interface graphique, tout en ignorant les contraintes liées à Android 6.0.

Passer un appel est très simple puisqu’il nous faut uniquement :

  • une Intent avec l’action ACTION_CALL ;
  • une Uri avec le numéro avec le numéro de téléphone.

Puisque le code est relativement simple, je vous propose de vous livrer le contenu de la méthode onClick sans plus de détails que ça :

public final class MainActivityFragment
    extends Fragment
    implements OnClickListener
{

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
  {
    final View rootView = inflater.inflate(R.layout.fragment_main, container, false);
    final Button call = (Button) rootView.findViewById(R.id.call);
    call.setOnClickListener(this);
    return rootView;
  }

  @Override
  public void onClick(View view)
  {
    call();
  }

  private void call()
  {
    final Intent intent = new Intent(Intent.ACTION_CALL);
    intent.setData(Uri.parse("tel:0102030405"));
    startActivity(intent);
  }

}

Je vous propose que l’on exécute notre application sur deux terminaux tournant sur deux versions d’Android différentes :

  • un LG Nexus 4 sous Android 4.4.1 ;
  • un LG Nexus 5 sous Android 6.0.1.

Sur le LG Nexus 4 qui tourne sous Android 4.4.1, il n’y a aucun problème, l’appel se fait bien comme en témoigne la capture d’écran ci-dessous :

Si nous exécutons maintenant la même application sur le LG Nexus 5 sous Android 6.0.1, nous constatons, au moment du clic sur le bouton, un crash. Voici ce que nous dit la stacktrace :

FATAL EXCEPTION: main
Process: com.domain.packagename, PID: 25374
java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxx cmp=com.android.server.telecom/.components.UserCallActivity } from ProcessRecord{bccd16c 25374:com.domain.packagename/u0a27} (pid=25374, uid=10027) with revoked permission android.permission.CALL_PHONE
at android.os.Parcel.readException(Parcel.java:1620)
at android.os.Parcel.readException(Parcel.java:1573)
at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:2658)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1507)
at android.app.Activity.startActivityForResult(Activity.java:3930)
at android.app.Activity.startActivityForResult(Activity.java:3890)
at android.support.v4.app.FragmentActivity.startActivityFromFragment(FragmentActivity.java:849)
at android.support.v4.app.FragmentActivity$HostCallbacks.onStartActivityFromFragment(FragmentActivity.java:907)
at android.support.v4.app.Fragment.startActivity(Fragment.java:916)
at com.domain.packagename.MainActivityFragment.onClick(MainActivityFragment.java:32)
at android.view.View.performClick(View.java:5204)
at android.view.View$PerformClick.run(View.java:21153)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

Ce qu’il faut retenir de cette stacktrace, c’est la première ligne et plus particulièrement les mots clefs suivants :

  • SecurityException
  • Permission Denial
  • revoked permission
  • android.permission.CALL_PHONE

Ce qu’il faut comprendre de ces mots clefs, c’est que du fait que l’on soit sous Android 6.0 et que la permission CALL_PHONE soit considérée comme une permissions dangereuse, nous devons explicitement demander l’autorisation à l’utilisateur. Cette permission n’est plus automatiquement acceptée à l’installation de l’application.

Nous allons voir comment gérer Android 6.0 dans le chapitre suivant !

En résumé

  • Demander une permission dans les anciennes versions d’Android se faisait simplement à l’aide de l’ajout d’une ligne dans le fichier AndroidManifest.xml.
  • La gestion fine des permissions sous Android 6.0 rend les choses un peu plus compliqués.

Commentaires