¿Cuándo usar AlarmManager?

AlarmManager está diseñado para un caso de uso muy específico: ejecutar código en un momento exacto en el tiempo. A diferencia de WorkManager (que puede diferir el trabajo), AlarmManager dispara en el tiempo pedido incluso durante Doze.

Casos de uso legítimos:

  • Alarma de despertador — debe sonar a las 7:00 AM exactas
  • Recordatorio de calendario — notificar 10 minutos antes de una reunión
  • Timer de cocina — alertar cuando pasan 30 minutos exactos
  • Medicación — recordatorio a hora fija

No uses AlarmManager para sincronización periódicaSi lo que necesitás es "sincronizar datos cada hora aproximadamente", WorkManager es la herramienta correcta. AlarmManager consume más batería y tiene restricciones severas en versiones modernas de Android. El uso incorrecto de alarmas exactas es causal de rechazo en Play Store.

Tipos de alarma

val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager

// RTC_WAKEUP — hora real del reloj, despierta el dispositivo en Doze
// RTC — hora real del reloj, NO despierta en Doze (diferida)
// ELAPSED_REALTIME_WAKEUP — tiempo desde el boot, despierta en Doze
// ELAPSED_REALTIME — tiempo desde el boot, NO despierta en Doze

// set() — inexacta, el sistema puede agrupar alarmas para ahorrar batería
alarmManager.set(
    AlarmManager.RTC_WAKEUP,
    tiempoEnMillis,
    pendingIntent
)

// setExact() — exacta pero puede ser diferida en Doze
alarmManager.setExact(
    AlarmManager.RTC_WAKEUP,
    tiempoEnMillis,
    pendingIntent
)

// setExactAndAllowWhileIdle() — exacta Y dispara durante Doze
// Usar para alarmas que el usuario configuró explícitamente
alarmManager.setExactAndAllowWhileIdle(
    AlarmManager.RTC_WAKEUP,
    tiempoEnMillis,
    pendingIntent
)

// setAlarmClock() — la más exacta, para alarmas de despertador
// Muestra un ícono en la barra de estado
val alarmInfo = AlarmManager.AlarmClockInfo(tiempoEnMillis, pendingIntentMostrarApp)
alarmManager.setAlarmClock(alarmInfo, pendingIntentDisparo)

Alarmas exactas — permiso en Android 12+

Desde Android 12 (API 31), las alarmas exactas requieren un permiso especial que el usuario debe conceder manualmente:

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- O desde Android 13, el permiso más restrictivo: -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<!-- USE_EXACT_ALARM solo para alarmas, calendarios y apps de timer -->
// Verificar si el permiso fue concedido
fun puedeUsarAlarmasExactas(): Boolean {
    val alarmManager = getSystemService(AlarmManager::class.java)
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        alarmManager.canScheduleExactAlarms()
    } else {
        true  // En versiones anteriores siempre se puede
    }
}

// Si no tiene el permiso, llevar al usuario a la configuración
fun pedirPermisoAlarmas() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
            data = Uri.parse("package:$packageName")
        }
        startActivity(intent)
    }
}

BroadcastReceiver — el receptor de la alarma

// El receiver que se ejecuta cuando dispara la alarma
class AlarmaReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val alarmaId = intent.getIntExtra("alarma_id", -1)
        val mensaje = intent.getStringExtra("mensaje") ?: "Alarma"

        // Mostrar notificación
        mostrarNotificacionAlarma(context, alarmaId, mensaje)

        // O iniciar un servicio si necesitás hacer trabajo más largo
        // val serviceIntent = Intent(context, AlarmaService::class.java)
        // context.startForegroundService(serviceIntent)
    }

    private fun mostrarNotificacionAlarma(context: Context, id: Int, mensaje: String) {
        val notification = NotificationCompat.Builder(context, "alarmas")
            .setContentTitle("Recordatorio")
            .setContentText(mensaje)
            .setSmallIcon(R.drawable.ic_alarm)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)
            .build()

        val manager = context.getSystemService(NotificationManager::class.java)
        manager.notify(id, notification)
    }
}

// Registrar en el Manifest:
// <receiver android:name=".AlarmaReceiver" android:exported="false" />

Ejemplo completo: alarma de recordatorio

class AlarmaManager(private val context: Context) {

    private val alarmManager = context.getSystemService(AlarmManager::class.java)

    fun programarRecordatorio(id: Int, mensaje: String, tiempoMs: Long) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            if (!alarmManager.canScheduleExactAlarms()) {
                // Pedir permiso al usuario
                return
            }
        }

        val intent = Intent(context, AlarmaReceiver::class.java).apply {
            putExtra("alarma_id", id)
            putExtra("mensaje", mensaje)
        }

        val pendingIntent = PendingIntent.getBroadcast(
            context,
            id,  // request code único por alarma
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP,
            tiempoMs,
            pendingIntent
        )
    }
}

Cancelar alarmas

fun cancelarAlarma(id: Int) {
    val intent = Intent(context, AlarmaReceiver::class.java)
    val pendingIntent = PendingIntent.getBroadcast(
        context, id, intent,
        PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
    )
    pendingIntent?.let {
        alarmManager.cancel(it)
        it.cancel()
    }
}

Sobrevivir al reinicio del dispositivo

Las alarmas se pierden cuando el dispositivo se reinicia. Para restaurarlas, registrá un receiver para el broadcast de boot:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<receiver
    android:name=".BootReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // Leer las alarmas guardadas en Room/DataStore y reprogramarlas
            val alarmaManager = AlarmaManager(context)
            val alarmasGuardadas = AlarmaDatabase.getInstance(context).alarmaDao().getAll()
            alarmasGuardadas.forEach { alarma ->
                if (alarma.tiempoMs > System.currentTimeMillis()) {
                    alarmaManager.programarRecordatorio(
                        alarma.id, alarma.mensaje, alarma.tiempoMs
                    )
                }
            }
        }
    }
}