The matching part of NotiAlarm is the easy half. A notification arrives, you compare it against some rules, one matches. Fine. The hard half is what happens next, because "make a sound the user cannot miss" is not a thing an Android app can simply do.

Every layer between your code and the user's attention has an opinion. The ringer is on silent. Do Not Disturb is active. The screen is off and locked. Another app holds audio focus. And since Android 14, the specific mechanism alarm apps rely on to take over the screen requires a permission that is not granted when you install the app.

This post is about those layers, because understanding them is what makes the difference between "the rule fired" and "I woke up."

Alarm audio is a different stream, and that matters more than volume

The first thing that goes wrong in a naive implementation: playing the alert on the wrong audio stream.

Android routes audio through usage types. A notification sound plays with USAGE_NOTIFICATION. An alarm plays with USAGE_ALARM. They are governed by different volume sliders, and — critically — they are treated differently by silent mode and by Do Not Disturb. A phone set to silent still rings its alarm clock. That is not a special case for the clock app; it is what the alarm usage means.

So NotiAlarm builds its audio attributes explicitly:

AudioAttributes.Builder()
    .setUsage(AudioAttributes.USAGE_ALARM)
    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
    .build()

Getting this wrong is subtle because it works on your desk. Your test phone is not on silent. Your test phone is not in Do Not Disturb. You ship, and then you get a report from someone who set a rule specifically so they would be woken at night — the one situation where the phone is guaranteed to be in a mode that suppresses notification audio.

We also request audio focus with AUDIOFOCUS_GAIN_TRANSIENT, not the ducking variant. Ducking lowers the other app's volume and plays over it. For an alarm, that is the wrong choice: if someone is listening to a podcast, quietly mixing a beep underneath it is exactly how the alarm gets missed. We take focus, the podcast pauses, the alarm plays.

Do Not Disturb has to be asked, not assumed

Do Not Disturb blocks by category. An app can mark a notification with CATEGORY_ALARM, and DND policy commonly allows alarms through. But "commonly" is doing work in that sentence — the user's DND configuration decides, and a user can absolutely have a policy that suppresses alarms too.

There is also ACCESS_NOTIFICATION_POLICY, which lets an app read and modify DND state after the user grants it. NotiAlarm reads it. It does not modify it. An app that silently turns off your Do Not Disturb because it decided something was important is an app that will eventually wake you for a promotional message, and there is no way to build enough trust in a rule engine to justify that.

What we do instead is check NotificationManager.getCurrentInterruptionFilter() and tell you, at rule creation time, whether the alarm you just configured would actually get through your current DND setting. It is better to say "this rule will be suppressed at night unless you allow alarms in your Do Not Disturb settings" while someone is setting it up than to let them discover it at 3 a.m.

The full-screen intent permission is the one that breaks setups

An alarm that only makes noise is half an alarm. You also want the screen to come on, showing something you can act on — dismiss, snooze, open the notification that triggered it. The Android mechanism for that is a full-screen intent: a notification that, when the device is locked or the screen is off, launches an activity instead of posting quietly.

Before Android 14, declaring USE_FULL_SCREEN_INTENT in the manifest was enough; it was a normal permission granted at install.

Android 14 changed this. The permission is now restricted to apps whose core function is calling or alarms, and for everything else it is not granted by default. NotificationManager.canUseFullScreenIntent() tells you your current state, and ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT opens the settings screen where the user can grant it.

If the permission is missing, your full-screen intent silently degrades to a heads-up notification. It does not throw. It does not warn. On a locked screen at night, that is the difference between an alarm and a small banner nobody sees until morning.

NotiAlarm checks canUseFullScreenIntent() on every launch and on every rule creation, and refuses to describe a rule as an alarm when it cannot present one. If the permission is not there, the app says so and links directly to the settings screen. We would rather show a warning that annoys people than have a rule that quietly does less than they believe it does.

Getting past the lock screen

Assuming the full-screen intent fires, the activity it launches has to survive the keyguard. The old flags — FLAG_SHOW_WHEN_LOCKED and FLAG_TURN_SCREEN_ON — were deprecated in API 27 in favour of activity methods:

setShowWhenLocked(true)
setTurnScreenOn(true)

These are calls, not window flags, and they need to happen in onCreate before the window is shown. There is also KeyguardManager.requestDismissKeyguard(), which asks the system to prompt for unlock — appropriate when the alarm screen needs to hand off to something behind the lock, and inappropriate as a default, since it puts an unlock prompt in front of someone who is half awake.

We do not use a wake lock to keep the screen on. setTurnScreenOn plus the normal screen timeout is enough, and holding a wake lock is a good way to end up in a battery report.

What to set up, in order

The rule engine is the part people expect to spend time on. In practice the permissions are where a working setup is won or lost, so do them first.

Grant notification access. Without it NotiAlarm sees nothing at all. This is the permission with the strongest-worded system dialog, and it deserves the wording — it grants read access to every notification on the device.

Check the full-screen intent state. On Android 14 and later, if this is not granted, your alarms will be banners.

Decide about Do Not Disturb. Look at whether your DND policy allows alarms. If your reason for installing NotiAlarm is night-time alerts, this is the setting that decides whether it works.

Then write one rule. One. Pick a case where missing the notification has a real cost: a message from a specific person, a delivery status for something you are waiting on, an alert from a system you are on call for. Give the rule a name that describes the event rather than the app, because in six months "Package arriving" is legible and "Shopping app" is not.

Test it while you are awake. Trigger the notification if the source app lets you, or wait for a real one, and watch what happens with the screen off and the phone face down across the room. That is the actual test. A rule verified with the app in the foreground has verified nothing.

Where the tool is the wrong answer

If everything is an alarm, nothing is. We see setups with fifteen rules covering most of the phone's notification volume, and those users are not better informed, they are just startled more often.

There is also a category we tell people to avoid: NotiAlarm is not a good replacement for an on-call paging system. If the cost of a missed alert is professional rather than personal, you want something with delivery confirmation and escalation, not a listener service on a phone that Android is free to kill. NotiAlarm is best at the personal version of the same problem — the alert that matters to you and is not worth building infrastructure for.

What we are still fighting

Manufacturer battery management is the running battle. Several OEM Android builds will stop a notification listener from being rebound after a process kill, and there is no API that reports this. The app looks connected. Nothing arrives.

We detect a suspiciously quiet listener and surface it, and we can call requestRebind(), and neither is a guarantee. The honest position is that on some devices, keeping a notification-driven alarm reliable requires the user to exempt the app in a manufacturer settings screen that we cannot open programmatically and that moves between OS versions. We document the paths we know. We do not pretend it is solved.