Foreground services: el privilegio se paga con notificación
Un servicio en primer plano es la única forma legítima de que tu proceso ejecute trabajo continuo y prolongado sin que el sistema lo aplace ni lo mate por presión de memoria, y su precio es exacto: una notificación permanente que el usuario ve, entiende y puede usar para detenerte. Esta lección desmonta el mecanismo completo: la ventana de gracia para publicar la notificación y qué ocurre si la incumples, el catálogo de tipos obligatorios desde Android 14 con el permiso y la justificación que exige cada uno, la diferencia entre el permiso de notificaciones y los permisos de tipo, el ciclo de vida real del servicio con sus tres formas de detenerse, y los topes temporales que la plataforma empezó a imponer a los tipos menos defendibles.
El servicio en primer plano es el trato más honesto que ofrece Android y también el peor entendido. La plataforma te dice, en esencia: puedo darte un proceso que no se congela, que no se aplaza y que el asesino de baja memoria tratará casi como si el usuario lo estuviera mirando, pero el usuario tiene que estar enterado de que existe. Esa notificación permanente que tantos desarrolladores viven como un estorbo de diseño no es un requisito burocrático ni un castigo: es literalmente la contrapartida del privilegio, la prueba visible de que no estás consumiendo recursos a espaldas de nadie. Desde Android 14 el trato se volvió aún más explícito, porque ya no basta con mostrar algo: hay que declarar de antemano a qué categoría de trabajo perteneces, pedir el permiso asociado a esa categoría y aceptar las reglas particulares que la plataforma le impone.
- Distinguir cuándo un servicio en primer plano es la herramienta correcta y cuándo es un abuso que se romperá.
- Publicar la notificación dentro de la ventana permitida y entender la excepción que se lanza al incumplirla.
- Declarar el
foregroundServiceTypeadecuado con su permiso y su justificación ante la tienda. - Gestionar el ciclo de vida completo, incluidas las tres formas de detener el servicio y los topes temporales.
Cuándo corresponde y cuándo no
El criterio no es la duración del trabajo ni su importancia para tu producto: es si el usuario lo inició conscientemente y espera que continúe mientras hace otra cosa. Reproducir música, grabar audio, guiar una ruta con navegación paso a paso, transferir un archivo grande que el usuario acaba de soltar, medir un entrenamiento deportivo, mantener una llamada. En todos esos casos hay un acto previo del usuario y una expectativa clara de continuidad, y por eso la notificación no le sorprende: le confirma lo que ya sabía.
El abuso tiene una firma reconocible. Consiste en usar el servicio en primer plano para tareas que el usuario no pidió y no espera, escondiéndolas tras una notificación deliberadamente insulsa del tipo la aplicación está funcionando. Eso no solo es hostil, sino que es cada vez menos viable: la plataforma exige tipos declarados, la tienda pide justificación en la ficha, y el usuario tiene a un toque de distancia la pantalla que le dice qué aplicaciones han corrido en primer plano y cuánto han consumido.
Hay una tercera categoría, la más frecuente en revisiones de código, que consiste en usar un servicio en primer plano para trabajo que en realidad es diferible. Sincronizar el catálogo, subir estadísticas, limpiar cachés. Funciona, pasa las pruebas y es un error de diseño: gasta batería, molesta al usuario con una notificación que no le dice nada útil y prescinde de las garantías de reintento y persistencia que tendrías gratis con un planificador. La pregunta que zanja el caso es simple: si esto se ejecutara dentro de seis horas mientras el teléfono carga por la noche, ¿perdería algo el usuario? Si la respuesta es no, no es un servicio en primer plano.
Legítimo
El usuario lo inició, sabe que sigue y quiere que siga. Reproducción, grabación, navegación, ejercicio, llamada.
Diferible disfrazado
Sincronizaciones y subidas rutinarias. Pertenecen a un planificador, con condiciones y reintentos, no a un servicio visible.
Abuso
Trabajo que el usuario no pidió, oculto tras una notificación vacía. La plataforma y la tienda lo cierran progresivamente.
Con tope
Tipos como sincronización de datos tienen un presupuesto diario. Superarlo detiene el servicio, no lo ralentiza.
La notificación, la ventana de gracia y el tipo obligatorio
El mecanismo tiene un orden estricto. Se arranca el servicio, y dentro de la ventana de gracia el servicio debe llamar al método que lo promociona entregando el identificador de notificación, la notificación construida y el tipo. Si no lo hace a tiempo, el sistema lanza una excepción que termina el proceso. Esa ventana existe para impedir el truco de arrancar un servicio, hacer el trabajo rápido y desaparecer sin mostrar nada, que era la forma clásica de saltarse la visibilidad.
La notificación tiene que pertenecer a un canal cuya importancia no la relegue al silencio absoluto, y debe describir el trabajo real en términos que el usuario reconozca. Incluir una acción de detener no es obligatorio y es lo que separa una implementación decente de una hostil: si el usuario ve que algo corre y no tiene forma de pararlo desde ahí, la única salida que le queda es desinstalar.
class SubidaService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, id: Int): Int {
val parar = PendingIntent.getService(
this, 0,
Intent(this, SubidaService::class.java).setAction("PARAR"),
PendingIntent.FLAG_IMMUTABLE,
)
val aviso = NotificationCompat.Builder(this, CANAL)
.setSmallIcon(R.drawable.ic_subida)
.setContentTitle("Subiendo tus fotos")
.setContentText("3 de 12")
.setOngoing(true)
.addAction(R.drawable.ic_stop, "Detener", parar)
.build()
ServiceCompat.startForeground(
this, ID_AVISO, aviso,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
}
Desde Android 14 el tipo es obligatorio y se declara en dos sitios que deben coincidir: el manifiesto y la llamada de promoción. Cada tipo arrastra un permiso normal propio que hay que declarar, y algunos exigen además permisos de tiempo de ejecución previos: la cámara y el micrófono requieren sus permisos concedidos antes de promocionar, y la ubicación requiere el permiso de ubicación correspondiente. Si el permiso no está concedido en el momento de la promoción, la llamada falla.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<service
android:name=".RutaService"
android:foregroundServiceType="location"
android:exported="false" />
Los tipos disponibles cubren cámara, conexión con dispositivos, sincronización de datos, salud, ubicación, reproducción multimedia, proyección de pantalla, micrófono, llamadas telefónicas, mensajería de corta duración, procesamiento especial y uso de sistema exento. La elección no es cosmética: determina qué permisos necesitas, qué justificación tendrás que dar en la ficha de la tienda y qué restricciones adicionales se te aplican.
Declarar dataSync porque parece el cajón de sastre menos comprometido es la trampa más habitual y sale cara por dos vías. La primera es que ese tipo concreto es de los que la plataforma somete a tope temporal diario, de modo que un servicio que en tus pruebas corre indefinidamente empezará a detenerse solo en dispositivos reales de usuarios intensivos. La segunda es que la ficha de la tienda pregunta por el caso de uso declarado, y una discordancia entre lo que declaras y lo que se observa que haces es motivo de rechazo. El tipo debe describir la actividad real, aunque el tipo real exija un permiso más incómodo de pedir.
Ciclo de vida, detención y topes
Un servicio en primer plano tiene tres formas de terminar y conviene no mezclarlas. Puede detenerse a sí mismo cuando acaba el trabajo, puede ser detenido por un componente externo, y puede perder únicamente el estado de primer plano conservando el servicio vivo, lo cual casi nunca es lo que quieres. La regla operativa es que el servicio debe morir en cuanto el trabajo termina: mantener vivo un servicio ocioso con su notificación es la forma más rápida de aparecer en la lista de aplicaciones que consumen batería.
El valor devuelto por el método de arranque decide qué ocurre si el sistema mata el proceso. La opción no pegajosa no vuelve a arrancar el servicio, que es lo correcto cuando el trabajo tiene un origen concreto y reintentarlo a ciegas no tiene sentido. La opción pegajosa lo vuelve a arrancar sin la intención original, lo que suele producir un servicio que no sabe qué debía hacer. La tercera opción reentrega la última intención, y es la única razonable cuando la intención contiene todo lo necesario para reanudar.
override fun onStartCommand(intent: Intent?, flags: Int, id: Int): Int {
if (intent?.action == "PARAR") {
scope.coroutineContext.cancelChildren()
stopSelf()
return START_NOT_STICKY
}
scope.launch {
try {
subir(intent?.getStringExtra("uri"))
} finally {
ServiceCompat.stopForeground(this@SubidaService, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
return START_REDELIVER_INTENT
}
flowchart TD
A[El usuario pulsa iniciar] --> B[startForegroundService desde primer plano]
B --> C{Se publica la notificacion a tiempo}
C -->|no| D[Excepcion y muerte del proceso]
C -->|si| E[Servicio promocionado con tipo declarado]
E --> F{Permisos del tipo concedidos}
F -->|no| G[Fallo en la promocion]
F -->|si| H[Trabajo continuo protegido]
H --> I{Termina el trabajo o el usuario detiene}
I --> J[stopForeground y stopSelf]
H --> K[Se agota el tope temporal del tipo]
K --> J
style D fill:#f38ba8,color:#11111b
style J fill:#a6e3a1,color:#11111bLos topes temporales merecen atención propia porque cambian el modelo mental. La plataforma empezó a imponer un presupuesto diario a los tipos cuya continuidad es menos defendible, señaladamente el de sincronización de datos. Cuando se agota, el sistema detiene el servicio y no basta con volver a arrancarlo: hay que esperar a que el presupuesto se renueve o a que el usuario devuelva la aplicación al primer plano. Un diseño que asuma ejecución indefinida bajo ese tipo funcionará en desarrollo y fallará en el dispositivo de quien más lo usa, que es justamente el usuario que menos conviene perder.
Hay una manera de leer el servicio en primer plano que lo convierte de tema aislado en principio general de diseño de plataformas. La pregunta que Android tuvo que responder era esta: cómo autorizar consumo continuo de recursos compartidos sin poder inspeccionar el propósito del código que lo solicita. Auditar intenciones no escala, porque las intenciones no son verificables y porque cualquier heurística automática que intente distinguir el trabajo legítimo del abusivo será sorteada en semanas. La respuesta que encontró la plataforma es elegante justamente porque renuncia a saber: no juzga si tu trabajo merece correr, sino que exige que sea visible para la única entidad capaz de juzgarlo con autoridad, que es el dueño del dispositivo. La notificación permanente no es entonces un peaje molesto adosado a un permiso, es el mecanismo de autorización completo, porque delega la decisión en quien paga la batería y le da el instrumento para revocarla. De ahí se sigue una consecuencia que reordena el diseño: si tu notificación no puede describir con honestidad lo que estás haciendo en términos que el usuario entienda y apruebe, no es que te falte una buena redacción, es que el trabajo no debería estar corriendo así. La calidad del texto es un test de legitimidad disfrazado de detalle de interfaz. Y el corolario predictivo es directo. Cada tipo de servicio en primer plano cuya notificación resulta difícil de justificar ante el usuario es exactamente el tipo que la plataforma acabará limitando con topes, permisos adicionales o revisión en la tienda, porque su existencia señala un hueco entre lo que se declara y lo que se hace. Los tipos que sobrevivirán intactos son aquellos en los que el usuario, al leer la notificación, piensa naturalmente que sí, que eso es justo lo que pidió.
- Implementa un servicio en primer plano con notificación descriptiva, progreso real y acción de detener que cancele el trabajo de verdad y no solo oculte el aviso.
- Provoca deliberadamente el incumplimiento de la ventana de gracia y documenta la excepción exacta y el estado en que queda el proceso.
- Declara un tipo que exija permiso de tiempo de ejecución, revócalo desde los ajustes con el servicio corriendo y observa qué ocurre en la siguiente promoción.
- Compara los tres valores de retorno del método de arranque matando el proceso a mitad del trabajo y anota qué recibe el servicio al resucitar en cada caso.
- Mide durante una semana el tiempo total que tu servicio permanece promocionado y contrástalo con el tope del tipo que has declarado.