Android reminders that actually fire: exact alarms, SCHEDULE_EXACT_ALARM and rescheduling after reboot
Which Android scheduler to use for "at this minute" versus "some time today", SCHEDULE_EXACT_ALARM versus USE_EXACT_ALARM on Android 14, asking for Alarms & reminders access, a fallback when it is denied, and the receiver that reschedules after a reboot or clock change.

A reminder is a promise with a time on it. The user said "remind me at 9:00", and if the notification arrives at 9:40, or after the next reboot, or never, the promise is broken. Android gives you several ways to run code later, and each one trades precision for battery. This post picks the right one for each job and makes reminders survive the three things that kill them: Doze, reboots and permission changes.
The menu of "later"
| Need | Tool | Precision |
|---|---|---|
| Deferrable work: sync, cleanup, a daily digest | WorkManager | Minutes to hours; runs when constraints allow |
| "Around this time" | AlarmManager.set() / setAndAllowWhileIdle() | Within an hour on Android 12+, unless battery saving delays it |
| "Within this window" | AlarmManager.setWindow() | Inside the window; short windows are usually stretched to 10 minutes |
| "At this minute", user-set | setExactAndAllowWhileIdle() | Nearly exact, even in Doze; needs exact alarm access |
| An alarm clock | setAlarmClock() | Exact; shown to the user as an upcoming alarm |
Those figures come from Google's alarm documentation. For set() and setAndAllowWhileIdle(), the alarm never fires early, and on Android 12 (API 31) and higher the system fires it within one hour of the trigger time unless battery saver or Doze is in effect. For setWindow() on apps targeting Android 12+, windows under 600,000 milliseconds are typically clipped to 10 minutes.
An hour late is fine for "back up my notes overnight" and useless for "remind me before the meeting". A user-set reminder at a specific minute is exactly what exact alarms exist for.
Two permissions, very different terms
SCHEDULE_EXACT_ALARM | USE_EXACT_ALARM | |
|---|---|---|
| Granted by | The user, in "Alarms & reminders" | Automatically at install |
| Can the user revoke it? | Yes | No |
| Who should declare it | Apps with a user-facing feature that needs precise timing | Calendar and alarm clock apps |
| Google Play | Allowed for genuine precise-timing features | Restricted by Play policy to limited use cases |
Two details matter most. On Android 14 and higher, SCHEDULE_EXACT_ALARM is no longer pre-granted to most newly installed apps that target Android 13 or higher; it starts out denied. And without the access, setExact(), setExactAndAllowWhileIdle() and setAlarmClock() throw a SecurityException.
A notes app with a reminder feature is not a calendar or an alarm clock. It should declare SCHEDULE_EXACT_ALARM, ask for it in context, and fall back to an inexact alarm when the user says no. Declaring USE_EXACT_ALARM just to skip the question puts the Play listing at risk for a feature that works without it.
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
Asking for "Alarms & reminders"
SCHEDULE_EXACT_ALARM is a special app access, not a runtime permission. There is no dialog: you send the user to a settings page and check the result when they come back. Ask at the moment it matters, when the user sets a reminder for a specific time.
fun canScheduleExact(context: Context): Boolean {
if (Build.VERSION.SDK_INT < 31) return true
val am = context.getSystemService(AlarmManager::class.java)
return am.canScheduleExactAlarms()
}
fun requestExactAlarmAccess(context: Context) {
if (Build.VERSION.SDK_INT < 31) return
context.startActivity(
Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
Uri.fromParts("package", context.packageName, null))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
Explain why in your own UI first, then send the intent. One line under the time picker is enough: "Reminders may arrive up to an hour late. Allow exact reminders?" with an Allow button. If the user ignores it, the reminder is still saved and still fires, just less precisely.
When the user grants access, the system sends AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED. Listen for it, call canScheduleExactAlarms() again rather than trusting the broadcast alone, and reschedule pending reminders as exact ones. The user can also revoke access at any time, and the system then cancels all of the app's future exact alarms.
Database first, alarm second
Store every reminder in your database first (note ID, trigger time, state), then schedule the alarm. The database is the source of truth; the alarm is only the wake-up call. That split is what makes rescheduling possible.
class ReminderScheduler(private val context: Context) {
private val am = context.getSystemService(AlarmManager::class.java)
fun schedule(r: Reminder) {
val pi = firePendingIntent(r.noteId)
val at = r.triggerAt.toEpochMilli()
if (canScheduleExact(context)) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, at, pi)
} else {
am.setWindow(AlarmManager.RTC_WAKEUP, at, 10 * 60 * 1000L, pi)
}
}
fun cancel(noteId: Long) = am.cancel(firePendingIntent(noteId))
private fun firePendingIntent(noteId: Long) = PendingIntent.getBroadcast(
context, noteId.toInt(),
Intent(context, ReminderFireReceiver::class.java)
.setData("fieldnotes://note/$noteId".toUri()),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
RTC_WAKEUP uses wall-clock time and wakes the device, which is right for "9:00 tomorrow". For "in 20 minutes", ELAPSED_REALTIME_WAKEUP with SystemClock.elapsedRealtime() is immune to the user changing the clock. The data URI makes each reminder's PendingIntent unique, so cancel() cancels exactly one.
setExact* can still throw if access was revoked between your check and your call. Wrap the exact call in try/catch and fall back to the inexact path in the catch.
Surviving a reboot
Google's alarm documentation puts it plainly: "By default, all alarms are canceled when a device shuts down." The fix is to reschedule from your database when the system finishes booting, and also when the clock, the time zone or the exact-alarm access changes:
<receiver
android:name=".reminders.RescheduleReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.TIME_SET" />
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
<action android:name=
"android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>
The receiver must be exported because the system sends these broadcasts, and the boot broadcast reaches your app only if the user has launched it at least once. Whatever the trigger, it does one thing: read every pending reminder and schedule it again.
class RescheduleReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_TIME_CHANGED,
Intent.ACTION_TIMEZONE_CHANGED,
AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED -> Unit
else -> return
}
val pending = goAsync()
CoroutineScope(Dispatchers.IO).launch {
try {
val app = context.applicationContext as FieldNotesApp
val scheduler = ReminderScheduler(context)
val now = Instant.now()
app.reminders.pending().forEach { r ->
if (r.triggerAt.isBefore(now)) app.reminders.markMissed(r.noteId)
else scheduler.schedule(r)
}
} finally {
pending.finish()
}
}
}
}
One trap: the action string for a clock change is android.intent.action.TIME_SET, and the Kotlin constant that holds it is Intent.ACTION_TIME_CHANGED. The names differ, which catches people out. Reminders whose time passed while the phone was off are marked missed and shown in the app rather than fired late.
Testing the denied path
On an emulator, turn the app's access off in Settings, Apps, Special app access, Alarms & reminders. The app should fall back to inexact alarms and show its one-line explanation, and canScheduleExactAlarms() should report false. Then reboot the emulator and confirm pending reminders were rescheduled.
Checklist
- Each job uses the least precise scheduler that still keeps its promise.
SCHEDULE_EXACT_ALARMis requested in context, neverUSE_EXACT_ALARMfor a non-calendar app.- Reminders are stored first and scheduled second.
- Exact calls are wrapped in
try/catchwith an inexact fallback. - A receiver reschedules on boot, clock, time zone and access changes.
