DEV Community

Amit Raz
Amit Raz

Posted on

Why Your Android Reminder App Is Silently Failing You (And How to Fix It)

You set a reminder. You go about your day. The time passes. Nothing fires.

You open the app and the task is sitting there, overdue, with no notification ever sent. Sound familiar?

This isn't a bug in any one app. It's a systemic problem with how most reminder apps handle alarms on Android, and once you understand why it happens, you can't unsee it.

The Root Cause: Android's Battery Optimization

Android has gotten increasingly aggressive about killing background processes to save battery. This is mostly good for users, but it creates a real problem for apps that need to wake up at a specific time and do something.

The standard approach most apps use is WorkManager or Handler.postDelayed() or scheduled jobs. These are perfectly fine for non-time-critical background work. But for a reminder that needs to fire at exactly 9:00am, they're not reliable. Android can, and will, defer or skip them entirely when Doze mode is active or battery saver is on.

Doze mode kicks in when the device is stationary and unplugged for a while. Battery saver can be triggered manually or automatically. On some manufacturers (Samsung, Xiaomi, OnePlus especially) there's additional proprietary battery optimization on top of Android's own system that makes this even worse.

The result: your reminder app looks fine. It shows the task. It shows the time. But the alarm never fires.

The Right API for Time-Critical Alarms

Android has a specific API designed for exactly this use case: AlarmManager.

There are several methods, and the difference between them matters a lot:

// Inexact, deferrable. Android can batch and delay this.
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)

// Exact, but still deferrable during Doze mode.
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)

// Exact, fires even during Doze mode. This is what you want.
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)

// Treated like a clock alarm by the system. Highest priority.
alarmManager.setAlarmClock(alarmClockInfo, pendingIntent)
Enter fullscreen mode Exit fullscreen mode

setAlarmClock() is the one I ended up using in Sticky Tasks. It shows a clock icon in the status bar (which is actually useful UX, users can see their next alarm is set) and Android won't suppress it. It's the same mechanism the built-in clock app uses.

setExactAndAllowWhileIdle() is a solid alternative if you don't want the status bar indicator. Both will fire reliably during Doze mode and with battery saver active.

Permissions You Need to Declare

Starting from Android 12 (API 31), you need to explicitly request the SCHEDULE_EXACT_ALARM permission:

<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
Enter fullscreen mode Exit fullscreen mode

From Android 13 (API 33), you should also handle the case where this permission is revoked by the user. Check it before scheduling:

if (alarmManager.canScheduleExactAlarms()) {
    // schedule the alarm
} else {
    // direct user to settings to grant permission
    val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
    context.startActivity(intent)
}
Enter fullscreen mode Exit fullscreen mode

For full-screen notifications (the kind that show even when the screen is off), you also need:

<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
Enter fullscreen mode Exit fullscreen mode

Since Android 14, you need to request this at runtime, not just declare it in the manifest.

Surviving Reboots

Here's one that catches a lot of developers off guard. AlarmManager alarms don't survive a device restart. The moment the phone reboots, all your scheduled alarms are gone.

The fix is a BroadcastReceiver that listens for BOOT_COMPLETED and re-registers all pending alarms:

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // fetch all pending tasks from your database
            // re-schedule their alarms
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it in your manifest:

<receiver android:name=".BootReceiver" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
Enter fullscreen mode Exit fullscreen mode

And add the permission:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
Enter fullscreen mode Exit fullscreen mode

Without this, any user who restarts their phone loses all their scheduled reminders silently. They'll never know why.

Testing This Properly

Manual testing is painful here. You can't just wait for alarms to fire. A few adb commands that help:

# Force device into Doze mode immediately
adb shell dumpsys deviceidle force-idle

# Check current Doze state
adb shell dumpsys deviceidle

# Simulate battery saver on
adb shell settings put global low_power 1

# Turn it off
adb shell settings put global low_power 0

# Step through Doze states manually
adb shell dumpsys deviceidle step
Enter fullscreen mode Exit fullscreen mode

Test your alarm fires correctly in each of these states before shipping. It's tedious but worth it. This is exactly the kind of thing that fails silently in production and you'll never see it in your crash logs.

What This Looks Like in Practice

I ran into all of this while building Sticky Tasks, a reminder app I built because I kept missing notifications from other apps. Once I switched to setAlarmClock() and added the boot receiver, the reliability difference was immediate.

Alarms fire with battery saver on. They fire after restarts. They fire on Samsung devices with aggressive battery optimization enabled.

It's not magic. It's just using the right API for the job.

If you're building anything time-critical on Android, whether it's reminders, medication alerts, scheduled notifications, or anything that has to fire at a specific moment, this is the approach. The standard background job APIs aren't designed for this and they'll let you down.


I'm Amit Raz, a Software Architect and AI consultant based in Israel. I build Android apps under the RZApps brand and write about what I learn along the way. More at rzailabs.com.

Top comments (0)