I built a reminder app called Alarvo where the important reminders behave like an alarm clock: full screen, through Do Not Disturb and Silent mode, ringing until you tap Done. I thought the hard part would be the alarm screen. It wasn't. The hard part was making sure the alarm fires at all.
Alarvo runs on Android and iPhone, and the two platforms could hardly be more different here. Here's what I learned, in case you're building anything time-critical on either.
Android: the alarm is easy, firing it is not
1. Pick the right AlarmManager API
Most reminder apps schedule with setExactAndAllowWhileIdle. It looks right: exact, and allowed while idle. In practice:
- Doze can still defer it.
- While the device is idle, it's rate-limited to roughly once every 9 minutes per app.
- The user can't see it, so when it fails, nobody knows why.
setAlarmClock is different. The platform treats it as a real, user-facing alarm:
val info = AlarmManager.AlarmClockInfo(triggerAtMillis, showIntent)
alarmManager.setAlarmClock(info, firePendingIntent)
It's exempt from Doze and App Standby buckets, and OEM battery managers are least likely to touch it. As a bonus, it puts the alarm icon in the status bar. That icon is a free, always-on signal to the user that the app is armed.
The catch: setAlarmClock is for things the user genuinely asked to be woken for. I use it only for Critical reminders and for tasks the user explicitly set to ring. Quieter reminders go through setExactAndAllowWhileIdle or plain notifications. Using the strong API for everything is abusing the privilege, and invites the platform (and Play review) to take it away.
On Android 12+, check before you schedule:
val canSchedule = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarmManager.canScheduleExactAlarms()
setAlarmClock throws a SecurityException without the permission. Catch it and tell the user, instead of crashing or silently dropping their reminder.
2. Alarms disappear more often than you think
Scheduled alarms don't survive several everyday events. Most apps handle BOOT_COMPLETED and stop there. My receiver also listens for:
-
MY_PACKAGE_REPLACED: an app update clears your alarms. Without this, every release silently disarms every reminder for every user until they next open the app. -
LOCKED_BOOT_COMPLETED: fires before the user unlocks a direct-boot device, much earlier thanBOOT_COMPLETED. Handling both is harmless if rescheduling is idempotent. -
TIMEZONE_CHANGEDandTIME_SET: if you store wall-clock times ("8:00 PM"), flying somewhere changes when that actually is. Recompute.
3. Ringing through Do Not Disturb
For the alarm itself:
- Play the sound with
AudioAttributes.USAGE_ALARM, so it uses the alarm stream and cuts through silent mode. UseUSAGE_NOTIFICATIONfor quieter levels; the alarm stream stays audible when the user has deliberately silenced notifications, and only the reminders that earned it should use it. - Show it with a full-screen intent over the lock screen. On Android 14+,
USE_FULL_SCREEN_INTENTis only granted at install to apps whose core function is alarms or calls, and the user can revoke it. CheckNotificationManager.canUseFullScreenIntent(). - Run the ringing in a foreground service, so it isn't killed mid-alarm.
4. The part no API fixes: phone makers
Even with all of the above, OEM battery managers can kill your app: Samsung's "sleeping apps", Xiaomi's Autostart, and others. None of them tell the user they've done it.
You can't fix this in code, so I made it visible instead. Alarvo has a reliability screen that checks:
- exact-alarm permission
- notifications enabled
- full-screen intent allowed
- Do Not Disturb access (
isNotificationPolicyAccessGranted) - battery optimisation (
PowerManager.isIgnoringBatteryOptimizations) - the maker-specific settings, with step-by-step instructions for each brand
5. Prove it: ship a test alarm
The last piece is a test alarm: the user locks the phone, puts it down, and 15 seconds later a real alarm rings, through exactly the same path real reminders use (same scheduler, same receiver, same service). If anything in the chain is broken on that particular phone, they find out now and not at 3 AM.
I think every app that promises to wake you should ship one.
iPhone: AlarmKit changes the rules
On iPhone, the problem used to be the opposite. Background execution is tightly controlled, and a third-party app could only remind you with a notification: a short sound that Silent mode and Focus would happily mute. There was no way to build a real alarm.
AlarmKit (iOS 26) fixes that. Your app asks once for permission:
let state = try await AlarmManager.shared.requestAuthorization()
and then schedules alarms that the system itself owns and rings: full screen on the lock screen, through Silent mode and Focus, whether or not your app is running.
_ = try await AlarmManager.shared.schedule(id: id, configuration: configuration)
The buttons on the alarm are App Intents. I pass a stopIntent for Done and a secondaryIntent for Snooze, so the app learns which one you pressed and can mark the reminder done, or bring it back, even if it was never opened.
What you don't get is the Android-style escalation ladder: the system rings the alarm, and you don't control it once it's going. That's a fair trade. Because the OS owns the alarm, there's no battery manager to fight and no reliability screen needed.
The app
Alarvo has three reminder levels (quiet notification, sound, or full alarm), an AI planner that turns "gym at 6, meds at 9" into reminders you check before saving, repeats like "3 days after I last did it," and one account that syncs across Android and iPhone.
- Website: https://alarvo.app
- Google Play: https://play.google.com/store/apps/details?id=app.alarvo
If you've fought background limits on either platform, I'd love to hear what worked for you, especially on Android brands I haven't tested.