Permisos en Android 6 Marshmallow

En un dispositivo con versión de Android anterior a Marshmallow un usuario concede los permisos a una aplicación en el momento de la instalación. Si no está de acuerdo con algún permiso, la única alternativa para el usuario es no instalar la aplicación. Una vez instalada la aplicación, puede realizar las acciones asociadas a estos permisos tantas veces como desee y cuando desee. Esta forma de trabajar dejaba a los usuarios indefensos ante posibles abusos. Por ejemplo, si queremos utilizar WhatsApp o jugar a Apalabrados tenemos que aceptar la larga lista de permisos innecesarios que nos solicitan. El usuario se resigna y acaba aceptando prácticamente cualquier permiso.

En la versión 6 se introducen importantes novedades a la hora de conceder los permisos a las aplicaciones. En primer lugar los permisos son divididos en normales y peligrosos. A su vez los permisos peligrosos se dividen en 9 grupos: almacenamiento, localización, teléfono, SMS, contactos, calendario, cámara, micrófono y sensor de ritmo cardíaco. En el proceso de instalación el usuario da el visto bueno a los permisos normales, de la misma forma como se hacía en la versión anterior. Por el contrario, los permisos peligrosos no son concedidos  en la instalación. La aplicación consultará al usuario si quiere conceder un permiso peligroso en el momento de utilizarlo:

Además se recomienda que la aplicación indique para que lo necesita. De esta forma el usuario tendrá más elementos de juicio para decidir si da o no el permiso. Si el usuario no concede el permiso la aplicación ha de tratar de continuar el proceso sin este permiso. Otro aspecto interesante es que el usuario podrá configurar en cualquier momento que permisos concede y cuáles no. Por ejemplo, podemos ir al administrador de aplicaciones y seleccionar la aplicación Navegador. En el apartado permisos se nos mostrará los grupos de permisos que podemos conceder:


 

Observa como de los grupos de permisos solicitados, en este momento solo concedemos el permiso de Ubicación.

El usuario concede o rechaza los permisos por grupos. Si en el manifiesto se ha pedido leer y escribir en la SD, concedemos los dos permisos o ninguno. Es decir, no podemos conceder permiso de lectura, pero denegar el de escritura.

video[Tutorial] Permisos en Android 6.0 Marshmallow

Para reforzar los conceptos que acabamos de exponer es recomendable que hagas el siguiente ejercicio:

Ejercicio:Trabajando con permisos en Android Marshmallow

1.   Crea un nueva proyecto con los siguientes datos:

Phone and Tablet / Basic Activity
Name: Permisos en Marshmallow
Package name: org.example.permisosenmarshmallow
Language: Java ó Kotlin
Minimum API level: API 19 Android 4.4 (KitKat)

2.   En el método onCreate() de MainActivityelimina las líneas tachadas, y en su lugar, añade las subrayadas.

public void onClick(View view) {
    borrarLlamada();
    Snackbar.make(view, "Replace with your …", Snackbar.LENGTH_LONG)
            .setAction("Action", null).show();
} 

3.   En la etiqueta <ConstraintLayout> de ccontent_main.xml añade:

android:id="@+id/vista_principal" 

4.   En Java declara la siguiente variable al principio de la clase:

private View vista_principal;

En el método onCreate() añade:

vista = findViewById(R.id.content_main);

5.  Añade el siguiente metodo:

void borrarLlamada() {
   getContentResolver().delete(CallLog.Calls.CONTENT_URI,
                                     "number='555555555'", null);
   Snackbar.make(vista_principal, "Llamadas borradas del registro.",
                                         Snackbar.LENGTH_SHORT).show();
} 
fun borrarLlamada() {
   contentResolver.delete(CallLog.Calls.CONTENT_URI,
                          "number='555555555'", null)
   Snackbar.make(vista_principal, "Llamadas borradas del registro.",
        Snackbar.LENGTH_SHORT).show()
} 
 

Como se describirá en el capítulo 9, este código elimina del registro de llamadas del teléfono todas las llamadas cuyo número sea 555555555. La segunda línea muestra un cuadro de texto tipo Snackbar para avisar que la acción se ha realizado.<

6.  Observa cómo el sistema nos advierte de que estamos actuando de forma no correcta:

7.  Ignora esta advertencia y ejecuta el proyecto. Si pulsas en el botón flotante, aparecerá el siguiente  error:

8.  Abre el Log cat para verificar la causa del error:<

Caused by: java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.CallLogProvider from … requires android.permission.READ_CALL_LOG or android.permission.WRITE_CALL_LOG 

Es decir, la aplicación se ha detenido porque está realizando una acción que requiere de la solicitud de un permiso

9.  Añade en AndroidManifest.xml:

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

10.  Si ejecutas de nuevo el proyecto en un dispositivo con una versión anterior a la 6.0, podrás verificar que ya no se produce el error.

11.  Si ejecutas ahora en un dispositivo con versión 6.0 o superior (si no dispones de uno utiliza un emulador), observarás que el error continúa.

12.Para entender lo que ha ocurrido, ve a Ajustes / Aplicaciones / Permisos en Marshmallow / Permisos:

Desde aquí podrás configurar los permisos peligrosos que quieres otorgar a la aplicación. Observa como el grupo de permisos referentes al teléfono está desactivado. Cuando instalamos una aplicación no se le concede ningún permiso peligroso.

13. Activa el permiso:

14.  Vuelve a ejecutar la aplicación y verificar que ya no se produce el error:

Como acabamos de comprobar la aplicación anterior va a funcionar correctamente en dispositivos con una versión anterior a la 6.0. Sin embargo, cuando se ejecute en las nuevas versiones, se producirá un error. Aunque hemos visto cómo el usuario puede evitarlo, no es desde luego la forma correcta de trabajar.

A partir de Android Marshmallow trabajar con acciones que necesiten de un permiso va a suponer un esfuerzo adicional para el programador. Antes de realizar la acción tendremos que verificar si tenemos el permiso. En caso negativo hay que exponer al usuario para qué lo queremos y pedírselo. Si el usuario no nos diera el permiso, tendremos qué decidir qué hacer. ¿Podemos realizar la acción solicitada aunque no dispongamos de cierta información? ¿Dejamos de hacer la acción solicitada? ¿O salimos de la aplicación? En el siguiente ejercicio veremos cómo realizar esta tarea.

video[Tutorial] Trabajando con permisos en Android 6.0

Ejercicio:Solicitud de permisos en Android Marshmallow

1.   El primer paso va a ser verificar que tenemos el permiso adecuado antes de realizar una acción que lo requiera. Resulta sencillo, simplemente has de añadir el if que se muestra a continuación en borrarLlamada():

if (ActivityCompat.checkSelfPermission(this,Manifest.permission
                  .WRITE_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
     getContentResolver().delete(CallLog.Calls.CONTENT_URI, 
                                           "number='555555555'", null);
     Snackbar.make(vista_principal, "Llamadas borradas del registro.",
                                           Snackbar.LENGTH_SHORT).show();
} 

2. Ejecuta de nuevo la aplicación en un dispositivo con versión 6.0 o superior y verifica que ya no se produce el error.

3. Esto no resuelve el problema. Nuestra aplicación no puede limitarse a no realizar la acción cuando no disponga del permiso. Ha de avisar al usuario y solicitar el permiso. Para ello añade una sección else a el if anterior.>

if (ContextCompat.checkSelfPermission(this,
            Manifest.permission.WRITE_CALL_LOG)
            == PackageManager.PERMISSION_GRANTED) {
   …
} else {
    solicitarPermiso(Manifest.permission.WRITE_CALL_LOG, "Sin el permiso"+
        " administrar llamadas no puedo borrar llamadas del registro.",
        SOLICITUD_PERMISO_WRITE_CALL_LOG, this);
} 

4. Añade el siguiente método:

public static void solicitarPermiso(final String permiso, String
         justificacion, final int requestCode, final Activity actividad) {
   if (ActivityCompat.shouldShowRequestPermissionRationale(actividad, 
                                                                permiso)){
     new AlertDialog.Builder(actividad)
        .setTitle("Solicitud de permiso")
        .setMessage(justificacion)
        .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int whichButton) {
                ActivityCompat.requestPermissions(actividad,
                        new String[]{permiso}, requestCode);
            }}).show();
   } else {
      ActivityCompat.requestPermissions(actividad,
              new String[]{permiso}, requestCode);
   }
} 
fun solicitarPermiso(permiso: String, justificacion: String, 
                                  requestCode: Int, actividad: Activity) {
   if (ActivityCompat.shouldShowRequestPermissionRationale(actividad,
                                                               permiso)) {
      AlertDialog.Builder(actividad)
            .setTitle("Solicitud de permiso")
            .setMessage(justificacion)
            .setPositiveButton("Ok", DialogInterface.OnClickListener { 
                dialog, whichButton -> ActivityCompat.requestPermissions(
                    actividad, arrayOf(permiso), requestCode )
            }).show()
   } else {
      ActivityCompat.requestPermissions(
            actividad,arrayOf(permiso), requestCode)
   }
} 

Es posible que tengas que solicitar permisos desde diferentes puntos de la aplicación. Por esta razón se ha declarado este método público y estático. Además, se ha pasado a parámetros toda la información que necesita: el permiso a solicitar, la justificación de porque lo necesitamos, un código de solicitud y la actividad que recogerá la respuesta. Una vez el usuario decida si da el permiso, se llamará al método onRequestPermissionsResult(), que  tendrás que declarar en la actividad que se pasa en el cuarto parámetro. El código es un valor numérico que permitirá identificar diferentes solicitudes.

Android nos recomienda que indiquemos al usuario para qué le estamos solicitando el permiso. Si consideras que no es necesario, puedes eliminar la primera parte del método y dejar solo el código que aparece dentro del else.  Antes de mostrar la explicación usando un AlertDialog, se verifica en el if si interesa mostrar esta información. Si el usuario ha indicado que no nos da el permiso y además ha marcado la casilla de que no quiere que volvamos a preguntar, no es conveniente insistir. El sistema se encarga de recordar esta información, nosotros simplemente tenemos que usar el método shouldShowRequestPermissionRationale().

NOTA: Este código se ejecuta en el hilo principal, por lo tanto nunca utilices un método para preguntar al usuario que pueda bloquear el hilo. Observa como en el ejemplo se utilizan llamadas asíncronas.

El trabajo más importante lo hace el método requestPermissions() que muestra un cuadro de diálogo como el siguiente y registra el permiso según la respuesta del usuario:

5. Una vez que el usuario escoja se realizará una llamada a onRequestPermissionsResult(). Aquí podremos procesar la respuesta. Añade el siguiente método:

@Override public void onRequestPermissionsResult(int requestCode, 
                        String[] permissions, int[] grantResults) {
   if (requestCode == SOLICITUD_PERMISO_WRITE_CALL_LOG) {
      if (grantResults.length == 1 && 
          grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            borrarLlamada();
      } else {
         Toast.makeText(this, "Sin el permiso, no puedo realizar la " +
                              "acción", Toast.LENGTH_SHORT).show();
      }
   }
} 
override fun onRequestPermissionsResult(requestCode: Int,
                    permissions: Array<String>, grantResults: IntArray) {
   if (requestCode == SOLICITUD_PERMISO_WRITE_CALL_LOG) {
      if (grantResults.size == 1 && 
          grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            borrarLlamada()
        } else {
            Toast.makeText(this, "Sin el permiso, no puedo realizar la " + 
                                 "acción", Toast.LENGTH_SHORT).show()
        }
    }
} 

Este método ha de estar declarado en una actividad. En caso de que el usuario nos conceda el permiso, tenemos que volver a realizar la acción que no pudo realizarse (en el ejemplo, borrarLlamada()). En caso de que hayas solicitado el permiso desde diferentes acciones o que hayas solicitado diferentes permisos, el valor de requestCode permitirá diferenciar cada caso.

6. Declara la siguiente variable al principio de la clase:

private static final int SOLICITUD_PERMISO_WRITE_CALL_LOG = 0; 
val SOLICITUD_PERMISO_WRITE_CALL_LOG = 0 

7. Verifica que la aplicación funciona correctamente.
 

Práctica: Solicitud de permisos en Mis Lugares

En el ejercicio Añadiendo fotografías desde la galería era imprescindible disponer del permiso para acceder a la memoria externa. Para poder ejecutar la aplicación en un dispositivo con una versión de Android 6 o superior, tuvimos que conceder este permiso manualmente. Un usuario final no debe realizar esta solicitud desde Ajustes. Va a ser imprescindible, solicitar este permiso desde la aplicación

Cuando el usuario intente obtener una fotografía desde la galería o tomar una fotografía, solicita el permiso READ_EXTERNAL_STORAGE tal y como se ha realizado en el ejercicio anterior

Preguntas de repaso : Permisos en Android 6.0 Marshmallow