wandres.dev
SERVICIOS Y SEGUNDO PLANO · los límites del sistema

Límites de inicio: no puedes irrumpir desde el fondo

Desde Android 8 una aplicación que no está en primer plano no puede arrancar un servicio ordinario, y desde Android 10 tampoco puede lanzar una Activity encima de lo que el usuario esté haciendo. Ambas prohibiciones responden al mismo problema, la irrupción no solicitada, pero se manifiestan de forma opuesta: una lanza una excepción ruidosa y la otra falla en silencio dejando solo una línea en el registro. Esta lección detalla qué cuenta como primer plano a efectos de la plataforma, cuál es la ventana de gracia que se abre al salir, el catálogo de excepciones legítimas para cada caso, la trampa de las notificaciones de pantalla completa y el patrón correcto para pedir la atención del usuario sin robársela.

⏱ 20 min

Las dos restricciones de esta lección son las que más código roto han producido en la historia reciente de Android, y también las que más se malinterpretan, porque su lógica no es técnica sino territorial. La pantalla del teléfono en un instante dado pertenece a la aplicación que el usuario eligió abrir, y el foco de atención del usuario es un recurso escaso del que ninguna aplicación puede servirse por iniciativa propia. Durante años Android permitió que cualquier proceso, aunque llevara semanas sin ser abierto, arrancara una actividad y se colocara delante de lo que fuera que estuviese ocurriendo en ese momento, incluida una llamada, un mapa en marcha o un formulario a medio rellenar. Se usó exactamente como cabía esperar. La respuesta de la plataforma fue invertir la carga de la prueba: ya no hay que demostrar que una irrupción es abusiva para bloquearla, hay que demostrar que es legítima para permitirla.

🎯 Al terminar esta lección sabrás
  • Definir con precisión qué considera la plataforma que es estar en primer plano y qué ventana de gracia concede al salir.
  • Explicar por qué la prohibición de arrancar servicios falla ruidosamente y la de lanzar actividades falla en silencio.
  • Enumerar las excepciones legítimas de cada prohibición y saber cuál corresponde a cada caso de uso real.
  • Sustituir cualquier irrupción por el patrón correcto de notificación con intención pendiente.

Qué cuenta como primer plano y cuánto dura la gracia

La plataforma no razona con la palabra abierta sino con la importancia del proceso, que es un valor que el gestor de actividades mantiene y actualiza continuamente. Se considera en primer plano un proceso que tiene una actividad visible, que tiene un servicio ligado a otra aplicación en primer plano, que ejecuta un receptor o un servicio de una aplicación en primer plano, o que ya mantiene un servicio promocionado a primer plano. Esa lista es más ancha de lo que parece y explica por qué algunos casos que uno cree ilegales funcionan sin problema.

Al abandonar el primer plano no se cae inmediatamente en la prohibición: hay una ventana de gracia de unos pocos minutos durante la cual el proceso conserva los privilegios como si siguiera delante. Esa ventana existe para no romper el patrón legítimo de una aplicación que inicia trabajo y el usuario sale inmediatamente después, y es la razón exacta por la que tantos fallos de este tipo no se reproducen en desarrollo: el desarrollador prueba pulsando el botón y volviendo a mirar antes de que la gracia expire.

Existen además situaciones que reponen temporalmente el privilegio sin que el usuario abra la aplicación. Recibir un mensaje de alta prioridad del servicio de mensajería en la nube concede una ventana breve. Que el sistema entregue a tu aplicación un anuncio dirigido, o que otra aplicación con privilegio te ceda el suyo explícitamente al arrancar un componente, produce el mismo efecto. Conocer esta lista es lo que separa el diseño que aprovecha canales legítimos del que intenta sortear la regla.

👀

Primer plano real

Actividad visible, servicio ligado a una aplicación visible o servicio ya promocionado. El privilegio es completo.

Ventana de gracia

Unos minutos tras salir. Es la causa de que el fallo no se reproduzca nunca en el escritorio del desarrollador.

📨

Privilegio cedido

Un mensaje de alta prioridad o un anuncio dirigido del sistema devuelven una ventana breve de permiso.

🔇

Fondo puro

Sin ventana ni cesión. Arrancar un servicio lanza excepción y lanzar una actividad no hace absolutamente nada.

Servicios: la excepción que sí se ve

La prohibición de arrancar servicios en segundo plano tiene la virtud pedagógica de ser ruidosa. Si tu proceso está en el fondo y llama al arranque de un servicio ordinario, el sistema lanza una excepción de estado ilegal y el proceso muere. Si llama al arranque de un servicio en primer plano desde el fondo en Android 12 o posterior sin caer en una excepción autorizada, recibe una excepción específica que nombra el problema con claridad. Nadie descubre estos fallos tarde, porque revientan en el momento.

Las excepciones autorizadas para arrancar un servicio en primer plano desde el fondo forman un catálogo tasado que conviene conocer entero, porque casi cualquier necesidad legítima está en él. Comprende, entre otras, el caso de una aplicación exenta de optimización de batería, el de un mensaje de alta prioridad recibido, el de una alarma exacta que se dispara, el de una interacción del usuario con una notificación, el de un cambio de estado en un dispositivo conectado y el de una aplicación con permiso de ventana superpuesta concedido.

// Correcto: el usuario acaba de pulsar, estamos en primer plano
fun alPulsarDescargar() {
    ContextCompat.startForegroundService(
        contexto, Intent(contexto, DescargaService::class.java),
    )
}

// Incorrecto: desde un receptor sin privilegio esto lanza excepcion
class ReceptorDeRed : BroadcastReceiver() {
    override fun onReceive(contexto: Context, intent: Intent) {
        // Sustituir por trabajo declarado con condicion de red
        WorkManager.getInstance(contexto).enqueue(
            OneTimeWorkRequestBuilder<SincronizarWorker>()
                .setConstraints(
                    Constraints.Builder()
                        .setRequiredNetworkType(NetworkType.CONNECTED)
                        .build(),
                )
                .build(),
        )
    }
}
⚠️
El receptor que arranca un servicio es el antipatrón más caro

El patrón heredado consistía en declarar un receptor en el manifiesto y usarlo para arrancar un servicio que hiciera el trabajo. Hoy ese patrón está roto por partida doble: la mayoría de los anuncios implícitos ya no se entregan a receptores declarados en el manifiesto, y aunque el anuncio llegue, el receptor no siempre tiene privilegio suficiente para arrancar el servicio. Peor aún, un receptor solo puede ejecutar código durante unos segundos antes de que el sistema considere terminado su cometido, de modo que el trabajo que lance directamente desde él morirá a medias. La sustitución correcta es invariable: el receptor encola trabajo declarado con sus condiciones y termina de inmediato.

Actividades: la prohibición que falla en silencio

El caso de las actividades es más traicionero porque no lanza excepción. Cuando un proceso sin privilegio llama al arranque de una actividad, el sistema descarta la petición, escribe una línea en el registro con el texto de que se ha bloqueado un inicio de actividad desde segundo plano, y sigue como si nada. No hay excepción que capturar, no hay valor de retorno que comprobar, no hay callback de fallo. La aplicación simplemente no hace nada, y en producción eso se traduce en funcionalidades que un porcentaje de usuarios reporta como que no pasa nada al recibir la alerta.

Las excepciones legítimas incluyen que la aplicación tenga alguna actividad visible o recientemente visible, que el usuario haya interactuado con una notificación de la aplicación, que exista un servicio ligado desde una aplicación en primer plano, que la aplicación tenga permiso de dibujar sobre otras aplicaciones concedido explícitamente por el usuario, o que se trate de una aplicación de sistema o del asistente predeterminado.

Merece atención propia la notificación de pantalla completa, que es el mecanismo previsto para el único caso realmente urgente: una llamada entrante o una alarma que el usuario fijó. Se construye adjuntando una intención de pantalla completa a la notificación, y el sistema decide. Si la pantalla está apagada o bloqueada, muestra la actividad directamente; si el usuario está usando el dispositivo, la degrada a una notificación destacada en la parte superior, que es exactamente el comportamiento deseable. Desde Android 14 esa capacidad exige un permiso propio que la plataforma concede automáticamente solo a aplicaciones de llamadas y alarmas, y que el resto debe solicitar al usuario.

val aviso = NotificationCompat.Builder(contexto, CANAL_LLAMADAS)
    .setSmallIcon(R.drawable.ic_llamada)
    .setContentTitle("Llamada entrante")
    .setPriority(NotificationCompat.PRIORITY_HIGH)
    .setCategory(NotificationCompat.CATEGORY_CALL)
    .setFullScreenIntent(intencionDeLlamada, true)
    .setContentIntent(intencionDeLlamada)
    .build()
flowchart TD
A[El proceso quiere abrir una pantalla] --> B{Hay actividad visible o gracia reciente}
B -->|si| C[Se abre la Activity con normalidad]
B -->|no| D{Existe excepcion legitima}
D -->|notificacion tocada por el usuario| C
D -->|permiso de ventana superpuesta| C
D -->|intencion de pantalla completa valida| E[El sistema decide segun el estado de la pantalla]
D -->|ninguna| F[Peticion descartada en silencio]
E --> G[Pantalla bloqueada: se muestra la Activity]
E --> H[Usuario activo: se degrada a aviso destacado]
style F fill:#f38ba8,color:#11111b
style H fill:#a6e3a1,color:#11111b

El patrón correcto: pedir la atención en lugar de tomarla

La sustitución universal para cualquier irrupción desde el fondo es la misma y tiene dos piezas. Primero se publica una notificación que describa con honestidad qué ha ocurrido y por qué merece atención. Segundo se adjunta una intención pendiente que, cuando el usuario toque, abra la pantalla correspondiente. En ese momento el toque del usuario concede el privilegio que faltaba y la actividad se abre sin ninguna restricción.

Este patrón parece una concesión y en realidad es superior en todo. Respeta lo que el usuario está haciendo, deja constancia persistente del suceso aunque el usuario esté ocupado, funciona en el dispositivo bloqueado, se integra con el resto de notificaciones del sistema y permite al usuario decidir el momento. Además elimina de raíz una clase entera de fallos difíciles, porque la actividad se abre siempre desde un contexto con privilegio y nunca hay que preguntarse si esta vez habrá funcionado.

val intencion = PendingIntent.getActivity(
    contexto, 0,
    Intent(contexto, DetalleActivity::class.java)
        .putExtra("pedidoId", id)
        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)

NotificationManagerCompat.from(contexto).notify(
    id,
    NotificationCompat.Builder(contexto, CANAL_PEDIDOS)
        .setSmallIcon(R.drawable.ic_pedido)
        .setContentTitle("Tu pedido va en camino")
        .setContentIntent(intencion)
        .setAutoCancel(true)
        .build(),
)
La atención del usuario es el recurso que ninguna aplicación puede asignarse a sí misma

Los límites de inicio en segundo plano suelen leerse como una restricción técnica y son en realidad la formalización de una regla de propiedad. Cuando un sistema operativo permite que cualquier proceso se coloque delante de lo que el usuario está haciendo, ha declarado implícitamente que la pantalla en primer plano es un bien de acceso libre, y todo bien de acceso libre con valor comercial acaba sobreexplotado hasta perder su utilidad. Lo notable del caso de Android es que la sobreexplotación no se manifestó como una degradación gradual sino como una carrera armamentística: cada aplicación que irrumpía obtenía una ventaja mensurable en interacción, lo que obligaba a las demás a irrumpir también para no quedarse atrás, y el equilibrio final fue un dispositivo en el que ninguna interacción del usuario estaba a salvo. La solución de la plataforma es interesante porque no intenta arbitrar mérito, cosa imposible, sino que reasigna el derecho: la atención pertenece al usuario, y solo un acto del usuario puede transferirla. De ahí que todas las excepciones legítimas compartan la misma estructura profunda, aunque parezcan heterogéneas. Tocar una notificación es un acto del usuario. Conceder el permiso de ventana superpuesta es un acto del usuario. Fijar una alarma exacta es un acto del usuario. Incluso el mensaje de alta prioridad, que parece la excepción más laxa, descansa en que el usuario instaló la aplicación y aceptó recibir avisos suyos. No hay ninguna excepción que se conceda por la naturaleza del trabajo o por su urgencia objetiva, porque la urgencia objetiva no existe: existe la urgencia según quien escribe el código, y esa es precisamente la magnitud que el sistema no puede verificar. La lección de diseño que se lleva uno de aquí trasciende Android por completo. Cuando construyas cualquier cosa que compita por la atención de una persona, la pregunta correcta nunca es si tu mensaje merece interrumpir, sino qué acto previo de esa persona te autorizó a hacerlo. Si no encuentras ese acto, lo que tienes entre manos no es una notificación sino una intrusión, y la plataforma acabará bloqueándola aunque hoy todavía no lo haga.

⚔️ Caza tus irrupciones y conviértelas en invitaciones
  1. Filtra el registro del sistema buscando bloqueos de inicio de actividad desde segundo plano mientras usas tu aplicación un día entero y documenta cada aparición.
  2. Reproduce el fallo a propósito esperando a que expire la ventana de gracia antes de intentar el arranque, y compara el resultado con el intento inmediato.
  3. Toma la funcionalidad de tu aplicación que abre una pantalla sin que el usuario la haya pedido y reescríbela con notificación e intención pendiente.
  4. Implementa una notificación de pantalla completa y observa su comportamiento con la pantalla bloqueada, desbloqueada y con el usuario escribiendo en otra aplicación.
  5. Busca en tu manifiesto todos los receptores y decide, uno por uno, si siguen recibiendo algo y si el trabajo que lanzan sobreviviría a los pocos segundos de vida del receptor.