Ludovic ROLAND

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

Les permissions sous Android (4/6) : demander une permission (2/2)

17 avril 2016

Comme nous l’avons vu dans le chapitre précédent, dans le cas d’Android 6.0, la demande de permission au sein du fichier AndroidManifest.xml n’est plus suffisante. Il convient, dans le cas des permissions dites dangereuses, de demander explicitement l’autorisation à l’utilisateur. C’est ce que l’on appelle la demande de permission au runtime.

Plan

Demander une permission au runtime

Nous allons donc voir comment demander la permission à l’utilisateur de passer un appel uniquement quand la permission n’est pas déjà acceptée et comment gérer les cas difficiles. C’est parti ! ;)

Vérifier si la permission est déjà acceptée

La première étape consiste à vérifier si l’application dispose d’ores-et-déjà de la permission nécessaire à notre action. Pour cela, nous pouvons utiliser la méthode statique checkSelfPermission de la classe ActivityCompat . Cette méthode accepte deux paramètres :

  • un contexte ;
  • la permission à vérifier.

Elle nous renvoie ensuite l’une des deux valeurs suivantes :

  • PackageManager.PERMISSION_GRANTED si la permission est actuellement acceptés par l’utilisateur ;
  • PackageManager.PERMISSION_DENIED si la permission n’est actuellement pas acceptée par l’utilisateur.

Suite à la modification du code, voici ce à quoi doit normalement ressembler la méthode onClick de notre fragment :

@Override
public void onClick(View view)
{
  if (ActivityCompat.checkSelfPermission(getContext(), permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED)
  {
    //demander la permission
  }
  else
  {
    call();
  }
}

Vérifier si l’on doit immédiatement demander l’autorisation à l’utilisateur

Contrairement à ce que laisse croire le commentaire du code précédent, dans le cas où la permission n’est actuellement pas acceptée par l’utilisateur, nous n’allons pas immédiatement afficher une pop-up à l’utilisateur lui demandant d’accepter ou non notre permission. En effet, Google juge ce comportement un poil agressive et le proscrit.

Ainsi, il convient de vérifier si nous avons déjà demandé à l’utilisateur d’accepter ou non la permission. En fonction de sa réponse, nous allons devoir mettre en place deux comportements différents au sein de notre application :

  • l’application n’a jamais demandé l’autorisation : nous la demandons à l’aide d’une pop-up système ;
  • l’application a déjà demandé l’autorisation : nous affichons un mot à l’utilisateur lui expliquant pourquoi l’autorisation est nécessaire et lui nous lui proposons d’activer la permission. S’il clique sur notre bouton, nous lui affichons alors la pop-up système.

Pour savoir si l’application a déjà demandé ou non l’autorisation à l’utilisateur, il convient de d’utiliser la méthode shouldShowRequestPermissionRationale de notre Fragment. Cette méthode qui ne prend qu’un paramètre (la permission), nous renvoie deux valeurs possibles :

  • true : dans le cas où la permission a déjà été demandée à l’utilisateur ;
  • false : dans le cas où la permission n’a pas déjà été demandée à l’utilisateur.

Suite à la modification du code, voici ce à quoi doit normalement ressembler la méthode onClick de notre fragment :

@Override
public void onClick(View view)
{
  if (ActivityCompat.checkSelfPermission(getContext(), permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED)
  {
    if(shouldShowRequestPermissionRationale(permission.CALL_PHONE) == true)
    {
      //expliquer pourquoi l'autorisation est nécessaire
    }
    else
    {
      //demander l'autorisation
    }
  }
  else
  {
    call();
  }
}

Donner quelques explications à l’utilisateur

Avant de nous attaquer à la demande d’autorisation, je vous propose de traiter le cas qui consiste à expliquer pourquoi l’autorisation est nécessaire au bon fonctionnement de l’application.

Pour mettre en place ce message, il n’y a pas de bonne ou mauvaise façon de faire, il convient simplement que le widget graphique utilisé soit raccord avec le design général de votre application. Dans notre cas, nous allons utiliser le composant Snackbar. Il convient alors de modifier le layout de notre fragment :

<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/container"
  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>

Vous remarquerez que j’ai simplement ajouté un identifiant à mon RelativeLayout.

Il convient maintenant, au sein de notre code java, de récupérer une référence vers ce RelativeLayout et d’afficher notre Snackbar avec un bouton d’action :

public final class MainActivityFragment
    extends Fragment
    implements OnClickListener
{

  private RelativeLayout containerView;

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

  @Override
  public void onClick(View view)
  {
    if (ActivityCompat.checkSelfPermission(getContext(), permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED)
    {
      if (shouldShowRequestPermissionRationale(permission.CALL_PHONE) == true)
      {
        explain();
      }
      else
      {
        //demander l'autorisation
      }
    }
    else
    {
      call();
    }
  }

  private void explain()
  {
    Snackbar.make(containerView, "Cette permission est nécessaire pour appeler", Snackbar.LENGTH_LONG).setAction("Activer", new OnClickListener()
    {
      @Override
      public void onClick(View view)
      {
        //demander l'autorisation
      }
    }).show();
  }

  //...

}

Graphiquement, voici le rendu dans notre application :

Demander l’autorisation

Maintenant que tout est en place pour demander l’autorisation à notre utilisateur dans de bonnes conditions, nous allons pouvoir le faire. :)

Demander une permission est très simple, puisqu’il convient d’appeler la méthode requestPermissions de la classe Fragment. Cette méthode accepte deux paramètres :

  • un tableau de permissions ;
  • un code unique qui nous permettra d’identifier notre demande quand nous voudrons analyser la réponse de l’utilisateur.

Voici le code du fragment mis à jour :

public final class MainActivityFragment
    extends Fragment
    implements OnClickListener
{

  //...

  @Override
  public void onClick(View view)
  {
    if (ActivityCompat.checkSelfPermission(getContext(), permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED)
    {
      if (shouldShowRequestPermissionRationale(permission.CALL_PHONE) == true)
      {
        explain();
      }
      else
      {
        askForPermission();
      }
    }
    else
    {
      call();
    }
  }

  private void askForPermission()
  {
    requestPermissions(new String[] { permission.CALL_PHONE }, 2);
  }

  private void explain()
  {
    Snackbar.make(containerView, "Cette permission est nécessaire pour appeler", Snackbar.LENGTH_LONG).setAction("Activer", new OnClickListener()
    {
      @Override
      public void onClick(View view)
      {
        askForPermission();
      }
    }).show();
  }

  //...

}

Graphiquement, voici le rendu que vous devriez avoir dans votre application :

Gérer la réponse de l’utilisateur

Comme vous pouvez le constater, à l’affichage de la pop-up système, deux choix s’offre à l’utilisateur :

  • autoriser ;
  • refuser.

Nous allons donc devoir récupérer la réponse de l’utilisateur et vérifier que la permission a été acceptée ou non afin de pouvoir passer notre appel.

La récupération de la réponse de l’utilisateur doit se faire au sein de la méthode onRequestPermissionsResult qui est une méthode de la classe Fragment qu’il convient alors de surcharger :

public final class MainActivityFragment
    extends Fragment
    implements OnClickListener
{

  //...

  @Override
  public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
  {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  }

  //...

}

Comme vous pouvez le constater dans le morceau de code ci-dessus, cette méthode prend 3 paramètres :

  • requestCode : il s’agit du fameux code unique permettant d’identifier la demande de permission dont je vous parlais plus haut ;
  • permissions : il s’agit des permissions qui ont fait l’objet d’une demande ;
  • grandResults : il s’agit du résultat de notre demande de permission.

Afin de vérifier la réponse de l’utilisateur, il convient dans un premier temps de filtrer la requête à l’aide du code unique. Dans notre cas, nous avions mis 2 :

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
  if(requestCode == 2)
  {
    //il s'agit de notre requête
  }

  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Gérer une autorisation

Une fois notre requête identifiée, nous allons vérifier l’état de la permission en vérifiant la valeur de la première case du tableau grantResults. Si la valeur est égale à PackageManager.PERMISSION_GRANTED, c’est que la permission a été acceptée par l’utilisateur et que nous pouvons passer l’appel :

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
  if(requestCode == 2)
  {
    if(grantResults[0] == PackageManager.PERMISSION_GRANTED)
    {
      //on appelle
      call();
    }
    else
    {
      //expliquer pourquoi nous avons besoin de la permission
    }
  }

  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Gérer un refus

Il nous reste encore un cas à traiter : celui ou l’utilisateur n’a pas accepté la permission.

Contrairement à ce que laisse penser mon commentaire, dans le cas où l’utilisateur n’a pas accepté la permission, il ne convient pas forcément de lui expliquer pourquoi la permission est nécessaire à l’aide de notre méthode explain. En effet, dans le cas où ce n’est pas la première fois que la pop-up système est affichée à l’utilisateur, celle-ci se dote d’une case à cocher “Ne plus jamais demander” comme en témoigne la capture d’écran ci-dessous :

Dans le cas où l’utilisateur coche la case, l’appel à la méthode requestPermissions n’aura plus aucun effet et notre pop-up système ne s’affichera plus. Le seul moyen pour que l’utilisateur puisse accepter de nouveau la permission, c’est qu’il le fasse directement depuis l’écran système d’information de l’application.

Nous allons devoir gérer les deux cas suivants :

  • si l’utilisateur a cliqué sur “refuser” : nous allons appeler la méthode explain ;
  • si l’utilisateur a cliqué sur “refuser” et qu’il a coché la case “Ne plus jamais demander”, nous appellerons une méthode displayOptions qu’il conviendra alors d’écrire.

J’y viens ! ;)

Pour différencier les résultats, il convient, dans le cas d’un refus de l’utilisateur, de rappeler la méthode shouldShowRequestPermissionRationale et de vérifier son résultat.

  • Si elle renvoie false, c’est qu’on ne peut plus justifier notre utilisation de la permission. C’est donc que l’utilisateur a refusé et qu’il a coché “Ne plus jamais demander”.
  • Si elle renvoie true, c’est que l’utilisateur refusé la permission sans cocher “Ne plus jamais demander”.

Voici ce à quoi ressemble notre méthode onRequestPermissionsResult une fois modifiée :

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
  if(requestCode == 2)
  {
    if(grantResults[0] == PackageManager.PERMISSION_GRANTED)
    {
      //on appelle
      call();
    }
    else if(shouldShowRequestPermissionRationale(permissions[0]) == false)
    {
      displayOptions();
    }
    else
    {
      explain();
    }
  }

  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Il nous reste maintenant à écrire la fameuse méthode displayOptions. Pour ne pas faire dans l’originalité, je vous propose, à l’image de notre méthode explain, d’afficher un message dans une Snackbar, à la différence près que lorsque l’utilisateur cliquera sur le bouton d’action, on ne fera pas une demande de permission, mais on ouvrira l’écran d’information système de l’application :

private void displayOptions()
{
  Snackbar.make(containerView, "Vous avez désactivé la permission", Snackbar.LENGTH_LONG).setAction("Paramètres", new OnClickListener()
  {
    @Override
    public void onClick(View view)
    {
      final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
      final Uri uri = Uri.fromParts("package", getActivity().getPackageName(), null);
      intent.setData(uri);
      startActivity(intent);
    }
  }).show();
}

Et voici le rendu graphique :

Au clic sur le bouton “Paramètres” l’utilisateur sera alors redirigé vers l’écran d’information de l’application via lequel il pourra activer manuelle la permission.

En résumé

  • Demander une permission est complexe car nécessite de vérifier plusieurs cas.
  • A tout moment un utilisateur peut activer ou désactiver une permission.
  • Il convient de respecter l’utilisateur et de vérifier tous les cas d’usage avant de demander une permission.

Pour ceux qui en aurait besoin, voici le code complet du fragment :

package com.domain.packagename;

import android.Manifest.permission;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
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;
import android.widget.RelativeLayout;

public final class MainActivityFragment
    extends Fragment
    implements OnClickListener
{

  private RelativeLayout containerView;

  @Override
  public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
  {
    if (requestCode == 2)
    {
      if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
      {
        //on appelle
        call();
      }
      else if (shouldShowRequestPermissionRationale(permissions[0]) == false)
      {
        displayOptions();
      }
      else
      {
        explain();
      }
    }

    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  }

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

  @Override
  public void onClick(View view)
  {
    if (ActivityCompat.checkSelfPermission(getContext(), permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED)
    {
      if (shouldShowRequestPermissionRationale(permission.CALL_PHONE) == true)
      {
        explain();
      }
      else
      {
        askForPermission();
      }
    }
    else
    {
      call();
    } 
  }

  private void displayOptions()
  {
    Snackbar.make(containerView, "Vous avez désactivé la permission", Snackbar.LENGTH_LONG).setAction("Paramètres", new OnClickListener()
    {
      @Override
      public void onClick(View view)
      {
        final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        final Uri uri = Uri.fromParts("package", getActivity().getPackageName(), null);
        intent.setData(uri);
        startActivity(intent);
      }
    }).show();
  }

  private void askForPermission()
  {
    requestPermissions(new String[] { permission.CALL_PHONE }, 2);
  }

  private void explain()
  {
    Snackbar.make(containerView, "Cette permission est nécessaire pour appeler", Snackbar.LENGTH_LONG).setAction("Activer", new OnClickListener()
    {
      @Override
      public void onClick(View view)
      {
        askForPermission();
      }
    }).show();
  }

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

}

Commentaires