¿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
}