Permisos
Al terminar: Pedir un permiso en tiempo de ejecución y reaccionar a la respuesta del usuario.
Al terminar esta lección, Marcador tendrá un botón Activar avisos de gol que abre el diálogo real del sistema para pedir permiso, reacciona a la respuesta del usuario (sí o no) y recuerda la decisión. Verás qué pasa si dice que no y cómo lo compruebas por terminal. Las notificaciones en sí, el aviso «¡Gol!» en la barra del móvil, llegan en la lección siguiente: esta prepara el permiso que necesitan.
Importa porque, desde Android 13, una app no puede publicar notificaciones sin que el usuario lo autorice, y este mismo patrón (declarar, comprobar, pedir, reaccionar) sirve para la cámara, la ubicación y el resto de permisos. Tema: el Manifest y los permisos en tiempo de ejecución.
Permisos: declarar no basta
Un permiso es una autorización que Android exige antes de que la app toque algo delicado: la cámara, la ubicación, las notificaciones. Hay dos clases:
| Clase | Cómo se concede | Ejemplos |
|---|---|---|
| Normal | Al instalar: basta declararlo en el Manifest | Internet (lo usarás en Retrofit y la primera llamada) |
| Peligroso (en tiempo de ejecución) | Declarado y además pedido al usuario mientras usa la app | Cámara, ubicación y, desde Android 13, notificaciones |
En esta lección trabajas con el de notificaciones; el resto de permisos peligrosos siguen el mismo flujo.
El flujo para un permiso peligroso, en cuatro pasos:
- Declarar el permiso en el Manifest con
<uses-permission>. Sin esta línea, el diálogo nunca aparece y el sistema deniega siempre. - Comprobar si ya lo tienes (
checkSelfPermission). Si sí, sigues. - Pedirlo con un launcher (un objeto que abre el diálogo del sistema): la respuesta llega después, de forma asíncrona.
- Reaccionar a las dos respuestas: si dice que sí, activas la función; si dice que no, explicas y sigues sin ella, sin insistir.
Un ejemplo con otra app: una tienda que usa la cámara
Para ver los cuatro pasos juntos, una app de tienda inventada que quiere escanear códigos con la cámara. No es el código de Marcador. Primero, el paso 1, en el manifiesto:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- uses-permission: un elemento propio, dentro de <manifest> y ANTES de <application> -->
<uses-permission android:name="android.permission.CAMERA" />
<application>
<!-- … lo de siempre … -->
</application>
</manifest>Y los pasos 2, 3 y 4, en Kotlin:
// Paso 2: ¿ya lo tengo? Devuelve true o false.
fun tienePermisoDeCamara(contexto: Context): Boolean =
// Manifest.permission.CAMERA: el nombre del permiso, guardado como constante
// (así no escribes el texto "android.permission.CAMERA" a mano y te equivocas).
ContextCompat.checkSelfPermission(contexto, Manifest.permission.CAMERA) == // ¿qué respondió el usuario?
PackageManager.PERMISSION_GRANTED // true si ya está concedido
class TiendaActivity : ComponentActivity() {
// Paso 3: el launcher. Se crea al construir la Activity; la lambda recibe la respuesta.
private val pedirCamara =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { concedido -> // concedido: Boolean
// Paso 4: reaccionar a las dos respuestas.
if (concedido) {
// dijo que sí: abrir el escáner
} else {
// dijo que no: explicar que sin cámara no hay escáner, y seguir sin él
}
}
private fun escanear() {
if (tienePermisoDeCamara(this)) {
// ya lo tenía: abrir el escáner directamente
} else {
pedirCamara.launch(Manifest.permission.CAMERA) // abre el diálogo; la respuesta llega a la lambda de arriba
}
}
}Lo que se ve en pantalla: la primera vez que el usuario toca «Escanear», el sistema pone un diálogo encima de la app («¿Permitir que Tienda haga fotos y grabe vídeo?», con dos botones). Si toca Permitir, la lambda recibe true. Si toca No permitir, recibe false. La app no ve nada más: no puede saltarse el diálogo ni cambiar sus textos. La segunda vez que toca «Escanear», si ya dijo que sí, no se vuelve a preguntar: la comprobación del paso 2, en escanear, abre el escáner directamente.
Qué es cada pieza:
- Un contrato (
ActivityResultContracts.RequestPermission()) es el objeto que describe qué se pide al sistema y qué respuesta vuelve: este pide un permiso (se le pasa su nombre) y devuelve unBoolean:truesi el usuario concede. registerForActivityResulttiene que llamarse antes de que la Activity llegue aSTARTED(el estado de El ciclo de vida de la Activity en que ya se ve), por eso va en una propiedad y no dentro de unonClick. El botón solo llama alaunch.Manifest.permission.CAMERA: aquíManifest(conimport android.Manifest) no es el ficheroAndroidManifest.xml, sino una clase de Android que guarda los nombres de los permisos como constantes.- Nivel de API: el número de versión de Android que viste en Instalar Android Studio y el SDK (Android 13 es el nivel 33, Android 8 el 26). Tu proyecto tiene
minSdk = 24: la app se instala en móviles de nivel 24 o superior (Android 7.0), y por eso hay que cuidar el código que solo existe en niveles más nuevos.Build.VERSION.SDK_INTes el nivel del móvil donde corre la app, yBuild.VERSION_CODESes una lista de nombres para esos niveles:TIRAMISUes el 33 (Android 13). Antes de la 33 el permiso de notificaciones se concede solo.
Reto · Ejercicio: decidir qué hacer con el permiso
En el Kotlin Playground, fuera de tu proyecto. Modela la decisión sin Android.
- Modela las dos respuestas posibles del usuario como un tipo con solo dos valores: concedido y denegado.
- Escribe una función que reciba la versión de Android y si el permiso ya está concedido, y devuelva el texto
activarsi la versión es menor que 33 (no hace falta permiso) o si ya está concedido, ypediren los demás casos. - Escribe otra que reciba una respuesta del usuario y devuelva
activarsi concede yseguir sin avisossi deniega. - Imprime, en este orden, lo que devuelve la primera función para Android 30 sin el permiso concedido, para Android 35 sin concederlo y para Android 35 ya concedido, y lo que devuelve la segunda cuando el usuario deniega. Lo que debe salir:
activar
pedir
activar
seguir sin avisosVer solución
enum class Respuesta { CONCEDIDO, DENEGADO }
fun queHacer(versionDeAndroid: Int, yaConcedido: Boolean): String =
if (versionDeAndroid < 33 || yaConcedido) "activar" else "pedir"
fun alResponder(r: Respuesta): String = when (r) {
Respuesta.CONCEDIDO -> "activar"
Respuesta.DENEGADO -> "seguir sin avisos"
}
fun main() {
println(queHacer(30, false)) // activar
println(queHacer(35, false)) // pedir
println(queHacer(35, true)) // activar
println(alResponder(Respuesta.DENEGADO)) // seguir sin avisos
}Es la misma lógica que usarás en el paso, con las funciones puedeNotificar y activarAvisos: por debajo de la 33 no hay permiso que pedir, y el when sobre un enum obliga a decidir qué hacer con cada respuesta.
Paso · Activar avisos de gol
Objetivo. Crear Notificaciones.kt con la comprobación del permiso, declarar el permiso y pedirlo desde un botón. Al terminar: el botón Activar avisos de gol abre el diálogo del sistema; al conceder, pasa a Avisos de gol: activados; y al denegar sale un mensaje breve. (Los avisos «¡Gol!» llegan en Notificaciones.)
Notificaciones.kt va en core.notifications: avisar es un servicio de toda la app, no de una pantalla. Se crea como core.ui en Intents implícitos y filtros: en el panel, app → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador (el que no lleva (test) ni (androidTest)) → New → Package, escribe .core.notifications al final (con el punto delante) y pulsa Intro. Las carpetas de código llevan nombres en inglés por costumbre de Android (notifications, domain, ui); así los verás en casi cualquier proyecto. Después, clic derecho sobre notifications, dentro de core → New → Kotlin Class/File, elige File, escribe Notificaciones y pulsa Intro.
No hay que instalar ninguna librería nueva. ContextCompat forma parte de androidx.core, y :app ya la declara desde la plantilla: en Gradle Scripts → build.gradle.kts (Module :app) está la línea implementation(libs.androidx.core.ktx), la que viste en Los ficheros de Gradle de tu proyecto. Manifest y Build vienen con el propio Android.
El manifiesto: <uses-permission>
Es una entrada nueva del manifiesto: no un atributo, sino un elemento propio, una línea con su etiqueta. <uses-permission android:name="…" /> le dice a Android «esta app va a necesitar este permiso». Va dentro de <manifest>, antes de <application, no dentro de ella. Ábrelo con doble clic en app → manifests → AndroidManifest.xml: arriba verás <manifest …> con dos atributos de xmlns y, debajo, <application. La línea nueva va entre las dos.
Dos fallos típicos: ponerla dentro de <application> (no es el sitio de ese elemento) o escribir mal el nombre del permiso (POST_NOTIFICATION sin la S final), con lo que el diálogo nunca aparece y el sistema deniega siempre.
Qué hay que hacer en cada fichero (obligatorio; el código completo está más abajo):
AndroidManifest.xml: una línea<uses-permission>dentro de<manifest>, antes de<application.Notificaciones.kt: una función,puedeNotificar, con sus imports.MainActivity.kt, cuatro cambios: losimportde arriba; el bloque del launcher yactivarAvisos()dentro de la clase, tras las dos propiedades de los goles (golesQueSePierdenygolesQueSobreviven, de Rotación y muerte del proceso); una línea enonCreate, tras el bloque derepeatOnLifecycley antes degolesQueSobreviven = savedInstanceState…; y un botón en laColumn, tras los de los intents. Para los imports, Android Studio ofrece añadirlos al colocarte sobre el nombre en rojo: Option+Enter. Los bloques deMainActivity.ktde abajo se añaden donde dice cada uno, sobre lo que ya tienes de las lecciones anteriores.
Piezas nuevas de este paso: el launcher, el contrato y Manifest.permission.POST_NOTIFICATIONS son los del ejemplo de la cámara. Quedan dos:
ContextCompat.checkSelfPermission(contexto, permiso) == PackageManager.PERMISSION_GRANTED: ¿ya está concedido? (el paso 2 del flujo).ContextCompates la versión deContextque funciona igual en móviles antiguos y nuevos (compat, de «compatible»).Toast.makeText(...).show(): un mensaje breve que aparece abajo en la pantalla y desaparece solo.
Intenta escribirlo tú, en este orden:
- El permiso de publicar notificaciones, declarado en el manifiesto.
Notificaciones.kt: una función que dice si la app puede notificar, que en versiones anteriores a Android 13 siempre es que sí (puedeNotificar).- En
MainActivity, un botón «Activar avisos de gol» que, si ya hay permiso, pasa a decir «Avisos de gol: activados», y si no, lo pide al sistema. Si el usuario lo deniega, un mensaje breve, «Sin permiso no hay avisos de gol», y nada más.
Lo que debes ver: al pulsar Activar avisos de gol por primera vez, el diálogo del sistema.
Los títulos de los bloques de abajo son las rutas de los ficheros en tu proyecto; en la vista Android están en app → kotlin+java (o app → manifests para el manifiesto).
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
En AndroidManifest.xml, la línea va antes de <application. El resto del fichero no cambia.
<!-- Permiso PELIGROSO desde la API 33: declararlo aqui no basta, ademas se pide
en tiempo de ejecucion. Sin la linea, el dialogo nunca aparece y el
sistema deniega siempre. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />package com.sebastiannarvaez.marcador.core.notifications
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
// Desde Android 13 (API 33) publicar notificaciones es un permiso PELIGROSO: hay
// que pedirlo en tiempo de ejecucion. Antes de la 33 se concede solo.
fun puedeNotificar(contexto: Context): Boolean =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(contexto, Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTEDEn MainActivity.kt, los imports nuevos. Van arriba, tras el package, con los demás. El resto del fichero no cambia.
import androidx.activity.result.contract.ActivityResultContracts
import android.Manifest
import android.os.Build
import android.widget.Toast
import com.sebastiannarvaez.marcador.core.notifications.puedeNotificarEl launcher y activarAvisos(), dentro de la clase:
// El dialogo del permiso es asincrono: pides y la respuesta llega despues, a este
// lambda. Hay que registrarlo ANTES de que la Activity llegue a STARTED (por eso es
// una propiedad, no algo dentro de un onClick).
private val pedirPermisoDeAvisos =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { concedido ->
avisosActivados = concedido
if (!concedido) {
// El usuario dijo que no: se explica y se sigue sin avisos. No se insiste.
Toast.makeText(this, "Sin permiso no hay avisos de gol", Toast.LENGTH_LONG).show()
}
}
private var avisosActivados by mutableStateOf(false)
private fun activarAvisos() {
if (puedeNotificar(this)) {
avisosActivados = true
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pedirPermisoDeAvisos.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}Dos aclaraciones sobre este bloque. Dentro de la lambda de registerForActivityResult, this a secas es la Activity, porque esa lambda no tiene un receptor propio que lo tape (lo que sí cambia el significado de this son las funciones de extensión y los bloques con receptor, como apply). Y avisosActivados está declarada después de usarse en la lambda: en una clase, el orden de las propiedades no importa para el código que se ejecuta más tarde, y esa lambda solo corre cuando el usuario responde al diálogo, cuando la propiedad ya existe.
Dentro de onCreate, después del bloque de repeatOnLifecycle, una línea que mira si el permiso ya estaba concedido al abrir la pantalla:
avisosActivados = puedeNotificar(this)Y el botón, dentro de la Column, tras los de los intents:
Button(onClick = { activarAvisos() }) {
Text(if (avisosActivados) "Avisos de gol: activados" else "Activar avisos de gol")
}Probarlo, y qué pasa si dices que no
Necesitas un emulador con Android 13 o superior (nivel 33+). Para saber la versión del tuyo, abre View → Tool Windows → Device Manager: junto al nombre de cada dispositivo verás su nivel de API, que debe ser 33 o más. Si es menor, crea otro dispositivo con una imagen más reciente como en Instalar Android Studio y el SDK; cómo arrancarlo, en La Activity. Ejecuta la app con Run, con app elegido arriba. Los textos del diálogo dependen del idioma del sistema; aquí se muestran en inglés. Los comandos adb se escriben en la Terminal de Android Studio. Hoy usas pm, el gestor de paquetes, que sabe qué permisos tiene cada app; está en la Chuleta de adb:
- Quita el permiso para empezar de cero:
adb shell pm revoke com.sebastiannarvaez.marcador android.permission.POST_NOTIFICATIONS. Siadbno se reconoce, mira Instalar Android Studio y el SDK. Si en Context y Application ya denegaste el diálogo que lanzó LeakCanary, este comando lo deja como nuevo; si aun así el diálogo no sale, desinstala la app conadb uninstall com.sebastiannarvaez.marcadory vuelve a ejecutarla desde Android Studio. - Abre la app y toca Activar avisos de gol. Sale el diálogo del sistema:
Allow Marcador to send you notifications?
[Allow] [Don’t allow]- Don’t allow. No pasa nada visible salvo un
Toastcon «Sin permiso no hay avisos de gol»; el botón sigue diciendo «Activar avisos de gol». Si lo tocas otra vez, el diálogo vuelve a salir: la primera denegación aún no es definitiva. Según la documentación citada arriba, tras denegar más de una vez el diálogo deja de mostrarse y hay que mandar al usuario a los Ajustes de la app; puedes comprobarlo tú denegando dos veces. - Allow. El botón pasa a Avisos de gol: activados.
Punto de control
- Tras
pm revoke, Activar avisos de gol muestra el diálogoAllow Marcador to send you notifications?. - Con Don’t allow sale el
Toast«Sin permiso no hay avisos de gol»; con Allow, el botón dice «Avisos de gol: activados». - En la Terminal de Android Studio,
./gradlew :app:assembleDebugtermina enBUILD SUCCESSFUL. - Todavía no sale ninguna notificación: de eso trata Notificaciones. El proyecto sigue con los mismos 137 tests de
:torneo(ábrelos con el informe, como en Bucles y tu primer test): esta lección no toca:torneo.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.Declaras
POST_NOTIFICATIONSen el Manifest y no llamas al launcher. En Android 13 o superior, ¿qué pasa al publicar una notificación?2.¿Por qué
registerForActivityResult(...)va en una propiedad de la Activity y no dentro delonClickdel botón?3.El usuario toca «Don’t allow» una sola vez y luego vuelve a tocar «Activar avisos de gol». ¿Qué ocurre?
4.Un usuario tiene un móvil con Android 12 (nivel de API 32). ¿Hace falta mostrarle el diálogo para el permiso de notificaciones?