Resposta rápida: para executar código em um horário exato no Android — despertador, lembrete de remédio, alarme de pomodoro, fechamento de pedido — use AlarmManager com setExactAndAllowWhileIdle() (ou setAlarmClock() para alarmes de usuário). Desde o Android 12 (API 31), alarmes exatos exigem a permissão SCHEDULE_EXACT_ALARM declarada no manifest e aprovada na Play Store, e o app precisa checar canScheduleExactAlarms() em runtime. Para quase todo o resto — sincronização, retry, upload de foto, trabalho periódico — o certo é WorkManager, que foi feito para trabalho adiado e não precisa de horário exato.
Alarme exato é a exceção, não a regra. O sistema tenta economizar bateria agrupando tarefas (Doze, App Standby), e cada alarme exato fura esse otimização. Este guia mostra quando o alarme exato é justificável, como implementá-lo em Kotlin com AlarmManager e BroadcastReceiver, como tratar a permissão no Android 12+ e o que mudou no Android 14, e como migrar um fluxo que hoje abusa de alarme para WorkManager ou foreground service.
Alarme exato vs. trabalho adiado: a decisão
Antes de escrever qualquer linha, responda: o usuário percebe se o horário atrasar 5 minutos?
| Necessidade | API recomendada | Alarme exato? |
|---|---|---|
| Despertador, timer de cozinha, pomodoro | setAlarmClock() ou setExactAndAllowWhileIdle() | Sim |
| Lembrete de medicação em horário marcado | setExactAndAllowWhileIdle() | Sim |
| Fechar expiração de pedido/cota em horário fixo | setExactAndAllowWhileIdle() | Sim |
| Sincronizar dados, retry, backup | WorkManager | Não |
| Trabalho periódico que pode atrasar | WorkManager (PeriodicWorkRequest) | Não |
| Ação disparada por evento do servidor | FCM push | Não |
| Tarefa longa visível ao usuário | Foreground service | Depende |
Regra de bolso: se ninguém percebe o atraso, não é alarme exato. setWindow(), setAndAllowWhileIdle() e o WorkManager cobrem a vasta maioria dos casos com muito menos atrito de permissão e bateria.
Permissões: o que mudou no Android 12, 13 e 14
O AlarmManager existe desde a API 1, mas o contrato mudou:
- Android 12 (API 31): alarmes exatos passaram a exigir
SCHEDULE_EXACT_ALARMno manifest. Apps recém-instalados do zero (target 31+) não recebem a concessão automaticamente — ela fica desligada até o usuário ativar em Configurações > Apps > Permissões especiais. - Android 13 (API 33): sem mudança relevante para alarmes; a onda foi de permissões de notificação (lembre-se de pedir
POST_NOTIFICATIONSpara o lembrete aparecer). - Android 14 (API 34): apps direcionados à Play Store precisam declarar um caso de uso justificado no Play Console para usar
SCHEDULE_EXACT_ALARM. A alternativa declarável sem justificativa éUSE_EXACT_ALARM, reservada a categorias cuja função principal é alarme (despertador, calendário) — o Google aprova caso a caso.
No manifest:
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
android:maxSdkVersion="32" />
<!-- Android 13+ usa a mesma SCHEDULE_EXACT_ALARM; USE_EXACT_ALARM só
para apps cujo propósito central é alarme (ex.: despertador): -->
<!-- <uses-permission android:name="android.permission.USE_EXACT_ALARM" /> -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
RECEIVE_BOOT_COMPLETED entra porque alarmes são cancelados quando o aparelho reinicia — apps de alarme re-agendam no boot.
Em runtime, antes de agendar, cheque a concessão:
val alarmManager = context.getSystemService(AlarmManager::class.java)
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(/* ... */)
} else {
// leve o usuário até Ajustes ou caia para setWindow/WorkManager
val intent = Intent(
Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
Uri.parse("package:${context.packageName}")
)
context.startActivity(intent)
}
Se o usuário revogar a permissão com o app aberto, o sistema mata o processo. Trate isso no próximo onResume revalidando canScheduleExactAlarms() — é o erro mais comum em produção.
Implementação em Kotlin: receptor + PendingIntent
O fluxo tem três peças: o BroadcastReceiver que executa a ação, o PendingIntent que aponta para ele, e a chamada do AlarmManager. Como o alvo de um alarme é um receptor, nada de ViewModel ou composables aqui.
1. O receptor
class LembreteReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(EXTRA_LEMBRETE_ID, -1L)
if (id == -1L) return
// Roda na main thread: despache para uma coroutine scope do app
val work = LembreteNotifier(context).mostrarLembrete(id)
goAsync().use { pendingResult ->
CoroutineScope(Dispatchers.Default).launch {
try {
work.join()
} finally {
pendingResult.finish()
}
}
}
}
companion object {
const val EXTRA_LEMBRETE_ID = "lembrete_id"
fun pendingIntent(context: Context, id: Long): PendingIntent {
val intent = Intent(context, LembreteReceiver::class.java)
.putExtra(EXTRA_LEMBRETE_ID, id)
return PendingIntent.getBroadcast(
context,
id.hashCode(), // requestCode único por lembrete
intent,
PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_IMMUTABLE
)
}
}
}
Duas armadilhas nesses 30 linhas: FLAG_IMMUTABLE é obrigatório desde o Android 12, e o requestCode precisa ser distinto por alarme — usar o mesmo código para dois lembretes faz o segundo PendingIntent sobrescrever o primeiro.
Registre no manifest (alarmes disparados por AlarmManager exigem registro explícito, não vale receiver dinâmico):
<receiver android:name=".LembreteReceiver" android:exported="false" />
<receiver android:name=".BootReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
2. Agendando o alarme exato
fun agendarLembrete(
context: Context,
id: Long,
quando: LocalDateTime,
) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
val triggerAtMillis = quando.atZone(ZoneId.systemDefault())
.toInstant().toEpochMilli()
when {
// alarme de usuário (despertador): aparece no relógio do sistema
alarmManager.canScheduleExactAlarms() ->
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
LembreteReceiver.pendingIntent(context, id)
)
else ->
// sem concessão: melhor esforço, janela de 15 min
alarmManager.setWindow(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
TimeUnit.MINUTES.toMillis(15),
LembreteReceiver.pendingIntent(context, id)
)
}
}
RTC_WAKEUP acorda o aparelho e usa o relógio de parede (o correto para horários que o usuário enxerga). ELAPSED_REALTIME* usa tempo desde o boot — prefira para timers internos, imunes a o usuário mudar a hora do aparelho.
3. Re-agendando após reboot
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
val repositorio = LembreteRepository(context)
val trabalho = CoroutineScope(Dispatchers.Default).launch {
repositorio.proximosAtivos().forEach { lembrete ->
agendarLembrete(context, lembrete.id, lembrete.quando)
}
}
goAsync().use { pending ->
CoroutineScope(Dispatchers.Default).launch {
try { trabalho.join() } finally { pending.finish() }
}
}
}
}
O armazenamento dos lembretes pendentes pode ser Room ou DataStore — o importante é que o app consiga reconstruir a agenda sozinho, porque o sistema não faz isso por você.
Despertador de verdade: setAlarmClock()
Para despertador e timer de cozinha, setAlarmClock() é mais honesto que setExactAndAllowWhileIdle(): ele mostra o ícone de alarme na status bar, trata o caso “usuário mudou o fuso” e não depende da permissão especial:
alarmManager.setAlarmClock(
AlarmManager.AlarmClockInfo(triggerAtMillis, mostrarIntent),
LembreteReceiver.pendingIntent(context, id)
)
O mostrarIntent abre a tela do seu app quando o usuário toca no ícone de alarme da status bar. setAlarmClock() pode disparar até pouco antes do horário (o sistema antecipa para não atrasar) — aceitável para despertador, inaceitável para expiração de oferta, onde antecipar quebra a regra de negócio.
Repetição: alarme que se re-agenda
O setRepeating() moderno não é exato — desde o Android 4.4 ele é inexact por design, e em Doze pode deslizar bastante. O padrão para alarme exato recorrente é re-agendar no próprio receptor:
override fun onReceive(context: Context, intent: Intent) {
val proximo = calcularProximaOcorrencia() // ex.: +24h no fuso do usuário
agendarLembrete(context, id, proximo)
// ...executa a ação do alarme atual
}
Calcule sempre a próxima ocorrência a partir do horário planejado, não do horário disparado — senão um atraso de 2 minutos vira deriva permanente no alarme diário de 7h.
Como testar
- Adb: force as condições com
adb shell dumpsys battery unplug,adb shell am set-standby-bucket <pkg> restrictede confiraadb shell dumpsys alarm | grep <pkg>para ver o alarme registrado. - Doze:
adb shell su root am broadcast -a android.intent.action.DEVICE_IDLE_MODE_ENABLEDou o comando de força de Doze nos aparelhos de teste. - Robolectric/E omit: para o receptor, um teste de unidade valida a extração de extras e o cálculo da próxima ocorrência — o scheduling em si fica para testes instrumentados, junto com o que já se usa em testes de UI no Android.
- Revogação: instale, agende, revogue a permissão especial em Ajustes e reabra o app — o fluxo de fallback para
setWindow/WorkManager precisa sobreviver ao processo morto.
Erros comuns
- Alarme exato para tudo. Sincronização com
setExactAndAllowWhileIdlea cada 15 minutos drena bateria e dá motivo para a Play Store negar a declaração de caso de uso. Use WorkManager. - Esquecer
FLAG_IMMUTABLE. Crash imediato no Android 12+ comSecurityException. requestCoderepetido. DoisPendingIntents com o mesmo request code e o mesmo intent são o mesmo alarme — o primeiro some.- Não checar
canScheduleExactAlarms()em runtime. A concessão pode ser revogada a qualquer momento; agendar exato sem ela lançaSecurityExceptionno Android 12+. - Ignorar o reboot. Alarmes não sobrevivem a reinício. Sem
BOOT_COMPLETED+ re-agendamento, o despertador falha exatamente no dia em que o usuário mais precisa. - Trabalho longo no
onReceive. O receptor vive ~10 segundos. Notificação curta, ok; upload ou sincronização, nunca — despache para WorkManager ou foreground service. - Mudança de fuso ignorada. Guarde o horário como
LocalDateTime+ zona ou como instante absoluto, e recalcule na exibição. Viagem para outro fuso não deve disparar o remédio às 3h da manhã.
Perguntas frequentes
Preciso de permissão para alarmes não exatos? Não. set(), setWindow() e setRepeating() funcionam sem qualquer permissão especial — pagam apenas com precisão.
WorkManager não substitui AlarmManager? Não para horário exato. WorkManager garante execução eventual com restrições (rede, bateria), sem promessa de horário. Para precisão de minuto, só AlarmManager exato.
O que declarar no Play Console para passar na revisão do Android 14? Um caso de uso da lista permitida (alarme, calendário, temporizador) com descrição concreta do comportamento visível ao usuário. “Sincronizar dados em horário exato” é recusa quase certa — e sinal de que a API certa é outra.
E se o usuário negar a permissão especial? Caia para setWindow() com a maior janela aceitável ou para WorkManager, e comunique o impacto (“seus lembretes podem atrasar até 15 minutos”). Não bloqueie o app.
Conclusão
AlarmManager com alarme exato é uma ferramenta de nicho e poderosa: horário que o usuário percebe, permissão declarada e justificada, canScheduleExactAlarms() checado em runtime, re-agendamento no reboot e nada de trabalho longo no receptor. Para sincronização, retry e periódicos, o caminho continua sendo WorkManager; para evento vindo do servidor, notificação push com FCM; para execução longa visível, foreground service. Escolha a ferramenta pelo custo de atraso — e na dúvida, escolha a mais barata para a bateria do usuário.