Los permisos de Android que llegaron con la versión 6.0 fueron una gran noticia para el usuario, pero también un pequeño quebradero de cabeza para los desarrolladores.

Además, el API tampoco acompañaba mucho, ya que estaba muy acoplado a la Activity y esto no nos permitía desacoplarnos demasiado del framework de Android.

Con Android 11, llegan novedades a los permisos y también a la forma de pedirlos.

Permisos en Android 11. ¿Cuáles son las novedades?

Como te comentaba en otro artículo sobre las novedades de Android 11 para desarrolladores, esta nueva versión nos ha traído algunas limitaciones extra.

Habilitar un permiso «Solo esta vez»

El usuario tendrá la opción de habilitar ciertos permisos para usarse «Solo esta vez». Si se vuelve a requerir ese permiso, se tendrá que aceptar de nuevo.

Estos permisos son:

  • Localización
  • Micrófono
  • Cámara

A partir de ese momento, tendrás un periodo de tiempo en el que el permiso está activo:

  • Mientras la App está abierta, tendrás acceso
  • Si la App se va a un segundo plano, seguirás teniendo acceso durante un periodo corto de tiempo
  • Esto se puede alargar si lanzas un servicio en primer plano (esos que mantienes activos con una notificación)
  • El usuario en cualquier momento puede irse a ajustes y revocarlo, aunque haya un servicio en primer plano ejecutándose.

Los permisos se auto resetean si la App no se usa durante un tiempo

No se indica una cifra específica, más allá de unos pocos meses. Pero si tu usuario no utiliza la App y al cabo de unos meses la abre, estos permisos se tienen que pedir otra vez.

Como desarrolladores tenemos la opción de pedirle al usuario que desactive el auto reset de nuestros permisos. Luego explicaremos cómo.

También podemos comprobar en el código si esta opción está activa para nuestra App con la función isAutoRevokeWhitelisted()

El permiso de localización en segundo plano se debe pedir aparte

Android 11 obliga a pedir primero la localización en primer plano y luego en segundo plano, para que el usuario esté seguro en todo momento de qué Apps le están pidiendo permiso para obtener la localización cuando no está usando la App.

Existe un nuevo permiso para acceder al API de números de teléfono

Anteriormente teníamos que usar READ_PHONE_STATE, pero ahora tenemos uno más específico, READ_PHONE_NUMBERS, que nos da acceso a un par de métodos para estas tareas.

¿Qué tenemos que cambiar en nuestro código para que los nuevos permisos funcionen?

Si estamos siguiendo la guía de buenas prácticas de petición de permisos, entonces no tienes que cambiar nada.

Estas guías nos vienen a decir que:

  • No pidas permisos cuando se abre la App, sino cuando realmente vas a usar el permiso. Imagina al usuario teniendo que seleccionar «Solo una vez» cada vez que abre tu App porque le pides ahí el servico de localización
  • Pide el permiso cada vez que lo necesites, no lo cachees ni lo guardes en las SharedPreferences ni nada por el estilo. Puedes perder el permiso en cualquier momento.

Nueva forma de pedir permisos: RequestPermission

Seguramente si llevas tiempo desarrollando en Android, ya has tenido que lidiar con los permisos y conoces la forma antigua.

Aquí te voy a explicar la nueva forma, que aunque aún está en Alpha, es la que recomienda Google de aquí en adelante. Como siempre, todo lo que esté en alpha, úsalo con cuidado.

Añade las dependencias necesarias

Lo primero que necesitas es añadir la dependencia de androidx.activity (revisa la última versión en el maven de Google)

implementation 'androidx.activity:activity-ktx:1.2.0-alpha06'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'

Ten cuidado y no te olvides de la dependencia del fragment aunque no uses fragments, o perderás 5 horas de tu vida 🤣. La librería de AppCompat los usa y no funcionarán los permisos.

Utiliza el nuevo API de registerForActivityResult

Hay una nueva forma de lanzar activities y recuperar resultados que es mucho más escalable que lo que teníamos en el pasado, y que conseguimos gracias a las librerías que hemos añadido.

Si quieres que hable sobre ella a fondo para, por ejemplo, recuperar imágenes, o hacer una foto, etc, déjamelo en los comentarios.

Hoy me voy a centrar en la petición de permisos. Lo primero que necesitas es crearte una property en la clase correspondiente con este aspecto:

private val requestPermissionLauncher: ActivityResultLauncher<String> =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        if (isGranted) {
            toast("Permission Granted")
        } else {
            toast("Permission Rejected")
        }
    }

Lo que hacemos es registrar un contrato de petición de permisos, de tal forma que cuando se nos devuelve el resultado, podemos comprobar si se ha autorizado el permiso o no, y actuar en consecuencia.

Este registro nos devuelve un ActivityResultLauncher, que luego lanzaremos cuando queramos que se pida el permiso:

button.setOnClickListener {
    requestPermissionLauncher.launch(Manifest.permission.ACCESS_COARSE_LOCATION)
}

Aquí no estamos teniendo en cuenta algunos casos, como que el permiso ya esté dado, o que debamos mostrar más información sobre para qué necesitamos el permiso.

Este segundo caso es un poco especial, y ocurre cuando el usuario ya ha denegado el permiso una vez. Lo ideal sería mostrar en la interfaz las razones por las que se lo pedimos, para qué el usuario comprenda por qué se lo pedimos.

when {
    ContextCompat.checkSelfPermission(
        applicationContext,
        Manifest.permission.ACCESS_COARSE_LOCATION
    ) == PackageManager.PERMISSION_GRANTED ->
        toast("Permission is already granted")
    shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION) ->
        showRationaleUI()
    else ->
        requestPermissionLauncher.launch(Manifest.permission.ACCESS_COARSE_LOCATION)

}

Vamos a simplificar esto un poco

La verdad es que, a pesar de que las cosas han mejorado mucho, sigue siendo un código un poco difícil de seguir, y lo podemos simplificar un poco dando por hecho que estamos tratando con permisos.

Esto se puede solucionar de muchas maneras. Mi objetivo es poder hacer algo así. Tener una Property que defina el permiso:

val coarseLocation = PermissionRequester(this, ACCESS_COARSE_LOCATION)

Y luego poder crear bloques seguros que solo puedan ejecutarse si tengo el permiso otorgado:

coarseLocation.runWithPermission{
    toast("Success")
}

Para ello nos podemos crear esta clase:

class PermissionRequester(
    activity: ComponentActivity,
    private val permission: String
) {
    private var onGranted: () -> Unit = {}

    private val launcher =
        activity.registerForActivityResult(RequestPermission()) { isGranted ->
                if(isGranted) onGranted()
        }

    fun runWithPermission(onGranted: () -> Unit) {
        this.onGranted = onGranted
        launcher.launch(permission)
    }
}

Si queremos tener en cuenta los casos en los que se deniega el permiso o que hay que mostrar el rationale (totalmente recomendado), podemos añadir un par de funciones al constructor:

class PermissionRequester(
    activity: ComponentActivity,
    private val permission: String,
    onDenied: () -> Unit = {},
    onShowRationale: () -> Unit = {}
)

Y reescribir la parte donde se ejecuta la acción:

when {
    isGranted -> onGranted()
    activity.shouldShowRequestPermissionRationale(permission) -> onShowRationale()
    else -> onDenied()
}

Finalmente, en la definición:

    private val coarseLocationRequest = PermissionRequester(this, ACCESS_COARSE_LOCATION,
        onDenied = { toast("Permission Denied") },
        onShowRationale = { toast("Should show Rationale") })

Cómo pedir al usuario que desactive el auto reset de permisos

Ten en cuenta que esto solo deberías hacerlo si tu App está hecha para ejecutarse prácticamente siempre en segundo plano. Si no, no te merece la pena.

Lo único que realmente puedes hacer es enviarle a la pantalla de Settings de tu App, y educarle sobre cómo debe activarlo. Para ello, ejecuta el siguiente código:

Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
    addCategory(Intent.CATEGORY_DEFAULT)
    data = Uri.parse("package:$packageName")
}.let(::startActivity)

Si algunos de estos conceptos sobre Kotlin te suenan a chino, te animo a que te apuntes a mi training gratuito donde te daré los pasos para convertirte en un experto en el lenguaje

Desde ahí, navegará a los ajustes de tu App, y eligiendo Permisos, verá esta opción:

Tu usuario debería desactivar esta opción manualmente.

Si lo haces bien, no hace falta cambiar nada para Android 11

Todo el código que hemos creado antes no requiere de adaptación específica para Android 11.

Como no estamos cacheando el permiso, sino que siempre comprobamos antes de usarlo, no importa si el usuario nos permite usarlo solo una vez o para siempre.

Cuando lo necesitemos intentaremos pedirlo.

Espero que te sea de ayuda, y te simplifique la vida con los permisos.

Si quieres ver el código, te dejo un Gist aquí.

Author: Antonio Leiva

Soy un apasionado de Kotlin. Hace ya más de dos años que estudio el lenguaje y su aplicación a Android para ayudarte a ti a aprenderlo de la forma más sencilla posible.