Permisos en Android 6

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

1.   Crea un nueva proyecto con los siguientes datos:

Phone and Tablet / Scrolling Activity
Name: Permisos
Package name: org.example.permisos
Language: Java ó Kotlin
Minimum API level: API 21 Android 5.0 (Lollipop)

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

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

3.  Añade el siguiente metodo:

void borrarLlamada() {
   getContentResolver().delete(CallLog.Calls.CONTENT_URI,
                                     "number='555555555'", null);
   Snackbar.make(binding.vistaPrincipal.getRoot(),
       "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.<

4.  En la etiqueta <include> de activity_scrolling.xml añade el código subrayado:

 
<include android:id="@+id/vista_principal"
         layout="@layout/content_scrolling" />

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

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

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

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

8.  Añade en AndroidManifest.xml antes de <application>:

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

9.  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 (si no dispones de uno utiliza un emulador).

10.  Si ejecutas ahora en un dispositivo con versión 6.0 o superior, observarás que el error continúa.

11.   Para entender lo que ha ocurrido, ve a Ajustes / Aplicaciones / 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.

12. Activa el permiso para la app Permisos:

13.  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

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. Primero quita el permiso que has concedido manualmente.

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) {
                        super.onRequestPermissionsResult(requestCode,permissions,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) {
                    super.onRequestPermissionsResult(requestCode,permissions,grantResults)
   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 Intenciones implícitas en Mis Lugares se utilizó una intención asociada a ACTION_DIAL para realizar una llamada de teléfono. No fue necesario solicitar permiso, dado que la intención no llega a realizar la llamada. Solo marca el teléfono y es el usuario quien confirma la llamada.

Reemplaza ACTION_DIAL por ACTION_CALL. Ahora la llamada se realizará directamente sin que el usuario la confirme y, por lo tanto, esta acción sí que se considera peligrosa. Para verificar que es así, añade el permiso correspondiente en AndroidManifest y asigna el permiso manualmente en Ajustes / Aplicaciones / Mis Lugares / Permisos. Ejecuta la aplicación y verifica que la llamada se hace directamente.

Introduce el código para que verifique el permiso y, si es necesario, se solicite al usuario, tal y como se ha realizado en el ejercicio anterior.

Preguntas de repaso : Permisos en Android 6.0