¿Qué es un ForegroundService?
Un ForegroundService es un servicio Android que muestra una notificación persistente al usuario, indicando que la app está haciendo algo en background. A cambio de esa notificación, el sistema le garantiza que no será matado por necesitar memoria.
Cuándo usar ForegroundService:
- Reproducir música mientras el usuario usa otras apps
- Tracking de ubicación GPS continuo (delivery, running)
- Grabar audio o video en background
- Downloads o uploads grandes que el usuario inició y quiere seguir
- Llamadas VoIP activas
La notificación no es opcionalUn ForegroundService sin notificación visible es una contradicción — el sistema lo termina. La notificación es el contrato con el usuario: "la app sigue corriendo, esto es lo que está haciendo". Si tu caso de uso no justifica una notificación visible, probablemente WorkManager sea la herramienta correcta.
Tipos de ForegroundService (Android 10+)
Desde Android 10, debés declarar el tipo de servicio. Cada tipo tiene permisos específicos:
<!-- AndroidManifest.xml -->
<service
android:name=".MusicaService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<service
android:name=".UbicacionService"
android:foregroundServiceType="location"
android:exported="false" />
<!-- Tipos disponibles:
camera - acceso a cámara en background
connectedDevice - conexión con dispositivos (BT, USB, WiFi)
dataSync - sincronización de datos
health - apps de salud y fitness
location - acceso a ubicación en background
mediaPlayback - reproducción de audio/video
mediaProjection - captura de pantalla
microphone - acceso al micrófono en background
phoneCall - llamadas activas
remoteMessaging - mensajería
shortService - tareas cortas y urgentes (máx 3 minutos)
specialUse - casos especiales con justificación
systemExempted - solo para apps del sistema
-->
Crear la notificación obligatoria
// NotificationChannel — obligatorio desde Android 8.0
private fun crearNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"Reproducción de música",
NotificationManager.IMPORTANCE_LOW // LOW para no interrumpir con sonido
).apply {
description = "Muestra la pista en reproducción"
setShowBadge(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
// Construir la notificación
private fun construirNotificacion(titulo: String, artista: String): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(titulo)
.setContentText(artista)
.setSmallIcon(R.drawable.ic_music_note)
.setOngoing(true) // No se puede deslizar para cerrar
.setPriority(NotificationCompat.PRIORITY_LOW)
// Intent para abrir la app al tocar la notificación
.setContentIntent(
PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
)
// Acciones en la notificación
.addAction(R.drawable.ic_pause, "Pausar",
crearPendingIntent(ACTION_PAUSE))
.addAction(R.drawable.ic_next, "Siguiente",
crearPendingIntent(ACTION_NEXT))
.build()
}
companion object {
const val CHANNEL_ID = "musica_channel"
const val NOTIFICATION_ID = 1
const val ACTION_PAUSE = "ar.pensa.app.PAUSE"
const val ACTION_NEXT = "ar.pensa.app.NEXT"
}
Implementar el servicio
class MusicaService : Service() {
private var mediaPlayer: MediaPlayer? = null
override fun onCreate() {
super.onCreate()
crearNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_PLAY -> {
val url = intent.getStringExtra("url") ?: return START_NOT_STICKY
reproducir(url)
}
ACTION_PAUSE -> pausar()
ACTION_STOP -> detener()
}
// START_STICKY: el sistema reinicia el servicio si lo mata, con intent null
// START_NOT_STICKY: no reiniciar si el sistema lo mata
// START_REDELIVER_INTENT: reiniciar con el último intent
return START_STICKY
}
private fun reproducir(url: String) {
// Llamar a startForeground() lo antes posible (en los primeros 5 segundos)
startForeground(
NOTIFICATION_ID,
construirNotificacion("Cargando...", ""),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK // Android 10+
)
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
prepare()
start()
}
// Actualizar la notificación con los metadatos reales
val manager = getSystemService(NotificationManager::class.java)
manager.notify(NOTIFICATION_ID, construirNotificacion("Canción", "Artista"))
}
private fun detener() {
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
stopForeground(STOP_FOREGROUND_REMOVE) // remueve la notificación
stopSelf()
}
override fun onBind(intent: Intent?): IBinder? = null // no es un bound service
override fun onDestroy() {
super.onDestroy()
mediaPlayer?.release()
}
}
Manifest y permisos
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Permiso específico según el tipo (Android 14+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application ...>
<service
android:name=".MusicaService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
</application>
Iniciar y detener desde la Activity
// Iniciar (desde foreground — app visible)
val intent = Intent(this, MusicaService::class.java).apply {
action = MusicaService.ACTION_PLAY
putExtra("url", "https://ejemplo.com/cancion.mp3")
}
startForegroundService(intent) // obligatorio en Android 8+
// Detener
val stopIntent = Intent(this, MusicaService::class.java).apply {
action = MusicaService.ACTION_STOP
}
startService(stopIntent)
// O directamente:
stopService(Intent(this, MusicaService::class.java))
Android 12+ — restricción de inicio desde backgroundEn Android 12+, las apps en background no pueden iniciar ForegroundServices directamente. Las excepciones incluyen: el servicio fue iniciado por el usuario, FCM de alta prioridad, accesibilidad activa, y algunos otros casos. Para trabajo en background sin estas excepciones, usá WorkManager con expedited work.
Comunicación entre Service y UI
// La forma moderna: SharedFlow en un objeto singleton o repositorio
object MusicaRepository {
private val _estado = MutableStateFlow(MusicaEstado())
val estado: StateFlow<MusicaEstado> = _estado.asStateFlow()
fun actualizarEstado(nuevo: MusicaEstado) {
_estado.value = nuevo
}
}
data class MusicaEstado(
val reproduciendo: Boolean = false,
val titulo: String = "",
val artista: String = "",
val progreso: Int = 0
)
// El Service actualiza el estado:
MusicaRepository.actualizarEstado(
MusicaEstado(reproduciendo = true, titulo = "Canción", artista = "Artista")
)
// La UI observa:
viewModel.musicaEstado.collect { estado ->
binding.tvTitulo.text = estado.titulo
binding.btnPlay.isSelected = estado.reproduciendo
}