wandres.dev
NOTIFICACIONES · canales y push

El permiso de Android 13: pedirlo cuando toca

Desde Android 13 las notificaciones dejaron de ser un derecho por instalación y pasaron a ser un permiso en tiempo de ejecución que el usuario concede o deniega en un diálogo de dos botones. Esta lección analiza qué cambió exactamente en el ciclo de vida del permiso, por qué el momento de la petición determina la tasa de concesión mucho más que cualquier otro factor, cómo se construye un antecedente que justifique la petición sin manipular, y qué hacer con la denegación permanente que la plataforma impone tras dos rechazos. Cubre el comportamiento diferenciado según el nivel de API objetivo, la trampa del diálogo automático al crear un canal, la señal de la explicación previa, la reevaluación periódica del estado y el diseño de una aplicación que siga siendo íntegramente útil para quien dijo que no.

⏱ 18 min

Durante quince años, instalar una aplicación en Android equivalía a autorizarla a interrumpirte. El permiso de notificaciones era normal, es decir, se concedía por el hecho de declararlo, y ninguna pantalla se lo consultaba nunca al usuario. Android 13 terminó con esa herencia y colocó la notificación donde ya estaban la cámara, el micrófono y la ubicación: detrás de un diálogo de dos botones que aparece una vez, decide durante mucho tiempo y no admite insistencia. El cambio parece pequeño en superficie —una llamada más, un estado más que gestionar— y es en realidad una reordenación del producto entero, porque introduce en el diseño una variable que antes no existía: el momento. La misma petición, con el mismo texto y el mismo código, obtiene tasas de aceptación radicalmente distintas según cuándo se lance, y esa diferencia no se recupera después. Esta lección trata de esa decisión y de lo que viene detrás cuando sale mal.

🎯 Al terminar esta lección sabrás
  • Describir el ciclo de vida completo del permiso de notificaciones y su comportamiento según el nivel de API objetivo.
  • Identificar el momento óptimo de la petición y construir el antecedente que la justifica.
  • Gestionar la denegación permanente sin caer en insistencia ni en dependencias rotas.
  • Diseñar una aplicación que conserve su valor íntegro para quien nunca concede el permiso.

Qué cambió exactamente y qué se hereda

El permiso se llama POST_NOTIFICATIONS, se declara en el manifiesto y se solicita en tiempo de ejecución. Sin él concedido, tus llamadas de emisión no fallan ni lanzan excepción: simplemente no producen nada visible. Ese silencio es deliberado y es también la razón de que muchos equipos descubran el problema tarde, cuando el registro no muestra ningún error y las métricas de apertura se desploman sin causa aparente.

El comportamiento depende de dos ejes que conviene separar con cuidado, porque su combinación produce cuatro casos distintos y confundirlos lleva a diagnósticos erróneos. El primer eje es la versión del dispositivo. El segundo es el nivel de API que tu aplicación declara como objetivo.

Si tu aplicación apunta a Android 13 o superior, tú controlas el momento de la petición y el sistema no hace nada por su cuenta. Si apunta por debajo pero corre sobre un dispositivo con Android 13 o superior, el sistema toma la decisión por ti y muestra el diálogo la primera vez que crees un canal de notificación o emitas una notificación, lo que en la práctica significa que aparecerá durante el arranque inicial, sin contexto, en el peor momento imaginable. Esa es la trampa que más caro se paga durante las migraciones: no hace falta actualizar el nivel objetivo para verse afectado, basta con que el usuario actualice su teléfono.

Hay además dos casos de concesión implícita que conviene conocer para no confundirse al medir. Un usuario que actualiza el sistema teniendo ya la aplicación instalada y con las notificaciones activas conserva el permiso sin que se le pregunte. Y una aplicación que crea una conversación asociada a un atajo compartido, o que emite una notificación de servicio en primer plano en ciertos contextos, recibe tratamientos diferenciados. Ninguno de esos casos justifica prescindir de la comprobación explícita en el código.

🎯

Objetivo 33 o superior

Tú eliges el instante. El sistema no interviene. Es la única configuración que permite diseñar el momento.

Objetivo inferior en dispositivo moderno

El sistema pide por ti al crear el primer canal o emitir la primera notificación. Contexto cero.

🔁

Actualización de sistema

Quien ya tenía la app con notificaciones activas conserva el permiso concedido sin diálogo alguno.

🚫

Sin permiso

Las emisiones no fallan: se descartan en silencio. No hay excepción, no hay registro, no hay señal.

El momento lo es todo

La literatura sobre permisos en móviles converge en un resultado robusto y poco discutido: la tasa de concesión de una petición contextualizada duplica holgadamente la de una petición fría, y la diferencia se sostiene en el tiempo. La razón es cognitiva antes que técnica. Un diálogo que aparece en el arranque le pide al usuario que valore un beneficio abstracto contra un coste concreto y conocido; un diálogo que aparece justo después de que el usuario haya expresado interés en algo concreto le pide que confirme una decisión que acaba de tomar.

De ahí sale la regla que ordena todo lo demás: la petición debe ir enganchada a una acción del usuario que la haga obviamente necesaria. Ha activado el aviso de que un producto vuelve a estar disponible. Ha pulsado seguir a alguien. Ha configurado un recordatorio. Ha completado un pedido cuyo estado querrá conocer. En todos esos casos el permiso no interrumpe una tarea sino que la completa.

El anti patrón simétrico es igual de claro y sigue siendo mayoritario: pedirlo en la primera pantalla, junto a la ubicación y al acceso a los contactos, antes de que el usuario haya visto nada de valor. Esa secuencia optimiza la comodidad del equipo de desarrollo y destruye el activo que pretende conseguir.

Entre el momento y el diálogo del sistema conviene interponer una pantalla propia, llamada antecedente o preparación, cuya función es explicar en una frase qué se va a recibir y permitir decir ahora no sin gastar el intento del sistema. Esta pieza tiene una propiedad valiosa y poco intuitiva: al filtrar a quien iba a rechazar, protege la posibilidad de volver a preguntar más adelante, porque el rechazo en tu pantalla no consume ninguna de las dos oportunidades que concede la plataforma.

@Composable
fun ActivadorDeAvisos(alConceder: () -> Unit) {
    val contexto = LocalContext.current
    val lanzador = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission(),
    ) { concedido ->
        registrarResultadoDePermiso(concedido)
        if (concedido) alConceder() else ofrecerAlternativa()
    }

    Button(onClick = {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
            alConceder()
        } else {
            lanzador.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
    }) { Text("Avisarme cuando llegue") }
}
💡
La pantalla previa no es una capa de persuasión

Un antecedente honesto describe qué llegará y con qué frecuencia, y ofrece una salida sin fricción. Un antecedente deshonesto oculta el botón de rechazo, usa un lenguaje que sugiere que la app no funcionará sin el permiso o imita el aspecto del diálogo del sistema. Lo segundo mejora la métrica del trimestre y destruye la del año, porque el usuario que concede sin querer desactiva en cuanto recibe el primer aviso, y esa desactivación es mucho más difícil de revertir que un rechazo inicial.

La denegación y su irreversibilidad

La plataforma impone una regla dura: tras dos rechazos, el permiso pasa a estado denegado permanentemente y las peticiones posteriores devuelven el resultado negativo de inmediato, sin mostrar ningún diálogo. No hay ninguna llamada que reabra esa puerta. La única vía es que el usuario vaya a los ajustes del sistema y lo conceda a mano.

La señal que permite distinguir los estados es la explicación previa. Cuando devuelve verdadero, significa que el usuario rechazó una vez y el sistema considera oportuno que le expliques por qué insistes. Cuando devuelve falso, hay dos situaciones opuestas que la llamada no distingue: o nunca se ha preguntado, o el permiso está permanentemente denegado. Desambiguarlas exige guardar tu propio registro de si alguna vez lanzaste la petición.

enum class EstadoPermiso { CONCEDIDO, SIN_PREGUNTAR, RECHAZADO_UNA_VEZ, BLOQUEADO }

fun estadoDelPermiso(actividad: Activity, yaSePregunto: Boolean): EstadoPermiso {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return EstadoPermiso.CONCEDIDO
    val concedido = ContextCompat.checkSelfPermission(
        actividad, Manifest.permission.POST_NOTIFICATIONS,
    ) == PackageManager.PERMISSION_GRANTED

    return when {
        concedido -> EstadoPermiso.CONCEDIDO
        ActivityCompat.shouldShowRequestPermissionRationale(
            actividad, Manifest.permission.POST_NOTIFICATIONS,
        ) -> EstadoPermiso.RECHAZADO_UNA_VEZ
        yaSePregunto -> EstadoPermiso.BLOQUEADO
        else -> EstadoPermiso.SIN_PREGUNTAR
    }
}
flowchart TD
A[El usuario realiza una accion que necesita avisos] --> B[Pantalla propia de antecedente]
B -->|ahora no| C[No se gasta ningun intento del sistema]
B -->|de acuerdo| D[Dialogo del sistema]
D -->|concede| E[Emision normal de notificaciones]
D -->|rechaza primera vez| F[Estado con explicacion disponible]
F --> G[Segunda peticion solo con motivo nuevo y real]
G -->|rechaza| H[Bloqueado de forma permanente]
H --> I[Unica salida los ajustes del sistema]
C --> J[Alternativa dentro de la app]
H --> J
style H fill:#f38ba8,color:#11111b
style E fill:#a6e3a1,color:#11111b

Ante el estado bloqueado, la respuesta correcta no es un diálogo recurrente sino una entrada discreta y permanente en los ajustes de la aplicación, con un enlace directo a la pantalla del sistema correspondiente. Ese enlace debe llevar al ajuste concreto y no a la ficha general de la aplicación, porque cada pantalla intermedia que el usuario tiene que atravesar reduce a la mitad la probabilidad de que llegue.

fun abrirAjustesDeNotificaciones(contexto: Context) {
    val intencion = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
        .putExtra(Settings.EXTRA_APP_PACKAGE, contexto.packageName)
        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    contexto.startActivity(intencion)
}

Sobre la segunda petición conviene ser explícito porque es donde más se abusa. La plataforma concede dos intentos, no dos oportunidades de insistir con el mismo argumento. Volver a preguntar solo se justifica cuando ha cambiado algo material desde el primer rechazo: el usuario ha activado una función que sin avisos queda coja, ha configurado una alerta de precio, ha entrado en una conversación. Repetir la petición ante el mismo estado del mundo no es persistencia sino desgaste, y su resultado más probable es consumir el intento restante y perder el canal para siempre.

Hay un tercer estado que se olvida y produce fallos de diagnóstico: el permiso concedido con canales desactivados. Un usuario puede haber aceptado la petición global y luego haber apagado el canal concreto, o el sistema entero puede estar en un modo de concentración. Por eso la comprobación previa a emitir debe combinar el permiso con el estado del canal, tal como se vio en la primera lección, y por eso el estado del permiso debe releerse cada vez que la aplicación vuelve al primer plano en lugar de cachearse al arrancar: el usuario pudo cambiarlo desde los ajustes sin pasar por tu código.

Diseñar para quien dice que no

La consecuencia de producto más importante de este cambio es que la notificación dejó de ser un canal garantizado y se convirtió en un canal probabilístico. Cualquier flujo que dependa de ella para funcionar está roto para una fracción sustancial de tu base, y esa fracción no es un caso extremo sino una porción de dos dígitos.

La disciplina que resuelve el problema es la degradación funcional, no la insistencia. Toda información que se entrega por notificación debe existir también dentro de la aplicación, en un lugar descubrible: un centro de avisos, una insignia sobre la pestaña correspondiente, un banner en la pantalla principal. La notificación es la capa de aceleración, nunca la única vía de acceso.

Esa arquitectura tiene un efecto secundario que compensa el esfuerzo: al construir el historial dentro de la app dejas de necesitar que la notificación transporte el significado completo, lo que a su vez permite hacerla más breve, menos frecuente y menos invasiva. Los equipos que diseñan para el usuario sin permiso terminan molestando menos al que sí lo concedió.

Conviene además instrumentar el proceso como cualquier otro embudo de producto. Los cuatro números que informan de todo son: cuántos usuarios llegan al antecedente, cuántos aceptan verlo, cuántos conceden en el diálogo del sistema y cuántos siguen con el permiso activo treinta días después. El cuarto es el único que mide algo real, y suele ser mucho más bajo de lo que el segundo sugiere.

El fin del consentimiento por instalación

El permiso de notificaciones es el capítulo más reciente de un movimiento que lleva una década reescribiendo la relación entre aplicaciones y usuarios, y merece leerse en esa escala y no como un requisito de migración. El modelo original de Android era de consentimiento agregado en el instante de la instalación: una lista de permisos, un botón de aceptar y una decisión de todo o nada tomada antes de haber usado nada. Ese modelo fracasó por razones que hoy son evidentes y que en su momento no lo fueron tanto. Fracasó porque nadie puede evaluar de forma significativa una lista de doce capacidades técnicas sobre un producto que aún no conoce; porque la única alternativa al aceptar era no instalar, lo que convierte el consentimiento en coacción; y porque una decisión tomada una vez no puede seguir siendo válida cuando la aplicación cambia de propósito tres actualizaciones después. La respuesta de la plataforma, aplicada primero a los datos sensibles y ahora a la atención, invierte las tres propiedades: pregunta en el momento del uso y no en el de la instalación, permite el rechazo sin sacrificar el acceso al producto, y admite revocación posterior en cualquier instante. Lo que hace que este caso concreto sea particularmente instructivo es que la atención no es un dato: no se puede robar, no se puede filtrar y no aparece en ninguna normativa de privacidad. Que la plataforma la haya clasificado igualmente como recurso protegido equivale a una afirmación fuerte, la de que interrumpir a una persona es una intrusión de la misma naturaleza que leer su ubicación, aunque su daño sea difuso y acumulativo en lugar de puntual y demostrable. Para el ingeniero, la traducción práctica es un cambio de supuesto por defecto que conviene interiorizar más allá de las notificaciones: toda capacidad valiosa acabará siendo consentida, revocable y medida, y por tanto ninguna arquitectura debería descansar sobre la disponibilidad garantizada de un canal que depende de la voluntad renovada de otra persona. Diseñar suponiendo el permiso concedido no es optimismo, es construir sobre un supuesto que la propia dirección de la plataforma ya ha declarado falso.

⚔️ Instrumenta y rediseña tu petición de permiso
  1. Localiza dónde pide hoy tu aplicación el permiso y clasifícalo como contextualizado o frío. Si es frío, identifica las tres acciones del usuario que serían mejores anclas.
  2. Construye una pantalla de antecedente honesta, con salida sin fricción, y mide qué proporción de usuarios llega al diálogo del sistema y cuál concede.
  3. Implementa la máquina de cuatro estados incluyendo la desambiguación entre nunca preguntado y bloqueado, y comprueba cada transición en un dispositivo real.
  4. Ejecuta tu app con un nivel objetivo inferior a 33 sobre un dispositivo con Android 13 o superior y documenta en qué instante exacto aparece el diálogo automático.
  5. Elige el flujo de tu producto que más depende de las notificaciones y rediséñalo para que sea completamente funcional sin ellas. Anota qué has tenido que añadir dentro de la aplicación.