Gestionar permisos para versiones 6.0 o posteriores

Cómo pregunto si el usuario ha asignado el permiso. ¿Lo ha hecho ya? Si lo denegó, ¿le puedo llevar a donde lo puede activar? Le pregunto siempre o sólo la primera vez? Aquí veremos cómo hacer esto a partir de la versión 6.0 de Android.Vamos a utilizar las funciones CheckPermission() y IsOlderPermissionVersion() que vimos en ¿Tengo permiso en tiempo de ejecución? y en ¿Que versión del SDK está ejetutando mi app? respectivamente.

Vamos a trabajar con el supuesto del permiso CALL_PHONE para realizar una llamada.

Lo primero que podemos hacer es plantearnos si queremos preguntar siempre al usuario que se va a hacer una llamada o sólo la primera vez.  El siguiente esqueleto mostraría cómo preguntar por el permiso sólo la primera vez:

if (CheckPermission(permiso)){
   //tiene el permiso
   ... hacemos la llamada
} else {
  SolicitarPermiso();
}

Tenemos una supuest función SolicitarPermiso() que realizará la solicitud del permiso.

Si no preguntamos por CheckPermission() en el código anterior, siempre que lleguemos ahí se llamará a SolicitarPermiso() .. y el usuario siempre tendrá que aceptar o denegar el  permiso para hacer una llamada.

Ahora vamos a ver una forma de solicitar permiso al usuario. Veamos el código y luego lo explicamos:

//Intentamos hacer la llamada verificando la versión y los permisos.
String phoneNumber = this.etPhone.getText().toString();
Intent intentCall = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + phoneNumber));
if (IsOlderPermissionVersion()) {
    //Estamos en una versión vieja
    if (ChekPermission(Manifest.permission.CALL_PHONE))
        startActivity(intentCall);
    else
        Toast.makeText(this, "You don't have permissions to run this action", Toast.LENGTH_LONG).show();

} else {
    //Estamos en una versión nueva, tenemos que utilizar requestPermissions() que es un método asincrono que pregunta al
    //usuario si da su permiso.
    //Tras su respuesta se lanzará un callback llamado onRequestPermisionsResults, que tendremos que sobrecargar en nuestra activity.
    //Pasamos un array con los permisos que solicitamos y un código de solicitud que definimos nosotros
    if (ChekPermission(Manifest.permission.CALL_PHONE)) {
        //Ha aceptado el permiso.
        startActivity(intentCall);
    }
    else{
        //O ha denegado o no se le ha pregundado nunca
        if (!shouldShowRequestPermissionRationale(Manifest.permission.CALL_PHONE)){
            //No se le ha preguntado aún, le preguntamos ahora
            requestPermissions(new String[]{Manifest.permission.CALL_PHONE}, PHONE_CALL_CODE);
            //... el flujo de ejecución termina y nos lleva al callback donde realizaremos la acción
        }
        else{
            //Ha denegado previamente. Puede activar el permiso desde Settings->Apps->Permissions
            Toast.makeText(this,"Por favor, activa el permiso para poder realizar la llamada",Toast.LENGTH_LONG).show();
            //Pero también podemos llevarle al sitio donde debe activarlo, lo hacemos con un implicit intent.
            Intent intentSettings=new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            //de la categoría default
            intentSettings.addCategory(Intent.CATEGORY_DEFAULT);
            //y como datos un uri que apunta a nuestra aplicación. La obtenemos así:
            intentSettings.setData(Uri.parse("package:"+getPackageName()));
            intentSettings.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intentSettings.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
            intentSettings.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
            startActivity(intentSettings);
        }

    }
}

Como vemos lo primero que hacemos es preparar el intent para hacer la llamada.

Luego detectamos la versión del API que está corriendo para proceder de una forma u otra. En este caso suponemos que estamos ejecutando la versión 6.0 por lo que nos vamos por el primer else.

Hemos decicido preguntar sólo la primera vez, por lo que usamos CheckPermission(), que si devuelve true es que ya lo concedió anteriormente y lanzamos la llamada.

Si resulta que no ha concedido previamente el permiso puede ser porque no se le preguntó antes, o porque denegó el permiso anteriormente.

Si no se le preguntó antes lo sabemos con la función shouldShowRequestPermissionRationale(). Esta función devuelve true si al usuario se le enseño antes el motivo de solicitar el permiso y dijo que No. Por eso ahora no se lo mostramos de nuevo pero si que le realizamos la solicitud del permiso con la función requestPermissions() a la que se le pasa un array de uno o más permisos (en el ejemplo sólo le pasamos uno) y una constante que nos inventamos y declaramos global a nuestra Activity para saber que es desde esta llamada a requestPermissions() desde donde estamos llamando.

private final int PHONE_CALL_CODE = 10;

Esto es necesario porque en ese momento Android muestra un cuadro de diálogo al usuario para que acepte o deniegue el permiso. Nuestro código continua y termina la función. Estaremos a la espera de que se lanze un Callback para recoger el valor que el usuario dió al cuadro de diálogo de solicitud.  El callback que se pone en ejecución es onRequestPermissionsResult() del que hablaermos luego, vamos a terminar con el código anterior…

Si resultase que se denegó previamente el permiso, entonces podremos lanzar un Toast indicando que debería hacerlo desde los ajustes de su móvil, es decir, desde Settings>Apps>Permissions. El usuario podrá activar ese permiso si quiere, pero como seguramente no sepa o no se acuerde desde donde se activa eso, lo podemos llevar nosotros usando un explicit intent, que como vemos configuramos con los flags para que no se quede en el callstack. En el setData() colocamos una Uri de nuestra propia aplicación. Esto se hace con un Uri.parse() al que se le pasa un string que comienza con “package:” (una uri de tipo package) y para obtener el nombre de nuestro package lo hacemos con la función getPackageName().

Vamos ahora a ver cómo reaccionamos en el callback que se pone en ejecución una vez que el usuario acepta o deniega el permiso. Veamos el código y luego lo explicamos:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 //Este método callback se pone en ejecución tras cualquier llamada previa a requestPermissions()
 //Recibimos: el código de petición que pasamos al llamar
 //           un array de permisos solicitados
 //           un array de resultados (granted or denied) correspondiente al array de permisos solicitados
 switch (requestCode) {
     case PHONE_CALL_CODE:
 //Chequeamos el array de permisos, en este caso solo solicitamos uno, el índice cero del array.
 //Podríamos iterar en bucle si hubiese más...
 String permission = permissions[0];
 int result = grantResults[0];
 if (permission.equals(Manifest.permission.CALL_PHONE)) {
     //El primer permiso es el CALL_PHONE. Esto ya lo sabíamos pero lo comprobamos :-)
     //Comprobamos si ha sido aceptado o denegado.
     if (result == PackageManager.PERMISSION_GRANTED) {
         //Permiso concedido
         //Obtenemos el número, montamos el intent y lo lanzamos.
         String phoneNumber = this.etPhone.getText().toString();
         Intent intentCall = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + phoneNumber));
         //AndroidStudio avisa que hay que poner esta comprobación, que es para determinar si
         //tenemos el permiso asignado. 
         if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
             return;
         }
         startActivity(intentCall);
     }
     else {
         //Permiso denegado
         Toast.makeText(this, "No se concedió el permiso", Toast.LENGTH_LONG).show();
     }
 }

 break;
     default:
 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 break;
 }
}

Como este callback se pone en ejecución para cualquier llamada en el código a reuquestPermissions() tenemos que discernir con la constante o requestCode que recibimos y que usamos en la llamada.

En el ejemplo tenemos un único permiso en el array, por lo que leemos el primer elemento, tanto del permiso como del resultado para ese permiso (que es el segundo array).

Ahora comprobamos que el permiso sea el que decimos… nunca está de más pero podríamos obviarlo ya que la llamada la hicimos con es array que nosostros quisimos, pero así lo dejamos ya preparado por si tuviesemos más y tuvieramos que iterar en bucle. Una vez que sabemos que el permiso es el CALL_PHONE vemos si el resultado es PERMISSION_GRANTED. En ese caso montamos el implicit intent para realizar la llamada y pregunamos de nuevo si el permiso está concedido, esto es casi una obligación del editor de código, aunque podemos indicarle en la alerta que nos dá que no la muestre, o bien como es el caso del ejemplo que añada el código necesario previa a la llamada a StartActivity.

En los ejercicios  podéis descargar un código completo de la aplicación ImplicitPermissions para poder probarlo. Tener en cuenta que para poder probar las dos variantes hay que crear un Virtual Device para la versión API22 por ejemplo y otra para la API23.