← Back to everything else

Case study · Android debugging

The phone that deleted
my alarms

I built an Android app that runs a schedule on my own phone. It worked after every restart, then quietly stopped. No crash, no error. The phone was deleting its alarms, and the usual fixes didn't stop it.

Role  solo: design, build, debugging Stack  Kotlin · Android device-owner APIs · AlarmManager · adb Device  TECNO KM6, HiOS on Android 15

01

A schedule that only worked after a restart

The app is a device owner, the highest level of control Android gives an app. At set times it locks and unlocks other apps. Each run sets an AlarmManager alarm for the next one, so the schedule is a chain. After a restart the chain ran. Then it stopped, and nothing in the app's own logs said why.

The phone matters here. TECNO, Infinix and itel are what most people in Nigeria actually carry. An app that dies on them dies for most of its users.

02

The answer was in someone else's log tag

Reading the whole of logcat around each run, not just my own lines, turned up one I hadn't written: Hiber/hiber: proxy alarm (uid=…, pkgName=…), about five seconds after every run. Hiber is HiOS's background freezer. It freezes an app that isn't in front and takes its alarms away.

When the app is unfrozen, Hiber gives back its services, its jobs and its sensors. It doesn't give back its alarms. So the next link in the chain never fires, and the schedule ends without an error.

Nothing failed. The alarm just wasn't there any more.

WITHOUT A WIDGET run sets next alarm Hiber freezes ~5 s later alarm removed "proxy alarm" next run never happens WITH A WIDGET PLACED run sets next alarm Hiber skips it "importantcase: widget" alarm pending next run on time the chain keeps going

The schedule is a chain of alarms. Hiber cuts it a few seconds after each run, unless the app has a widget on the home screen.

03

Four fixes that should have worked

Each of these is the standard advice for keeping an Android app alive. I tried each one and watched for the proxy alarm line.

A foreground service. The app was frozen with the service running and isForeground=true.
The battery-optimisation exemption. Frozen anyway.
Device-owner status. The strongest privilege an app can hold. Frozen anyway.
The EXEMPTED standby bucket. Frozen anyway.

Moving the work to WorkManager wasn't a way out either. Hiber proxies JobScheduler jobs the same way (p_job).

"Exempt from battery optimisation" doesn't mean exempt from the manufacturer's freezer.

04

The one thing Hiber respects

A placed home-screen widget. With one on the screen, Hiber logs can't transition from R to F, importantcase is : widget and leaves the app alone. The alarms stay pending and the chain keeps running.

So the widget became part of the product, not decoration. It shows what the schedule is doing right now, and it's what keeps the schedule alive. The app only asks for it on the phones that need it.

05

Then it broke again with the screen off

The widget's protection only holds while the screen is on. With the screen off, Hiber freezes the app again, drops its alarms, and holds broadcasts meant for it in a restricted queue (visible in dumpsys activity broadcasts) that may never be delivered.

There was no exemption to find this time, so I changed the design. The app now treats the first sign of life after a freeze as a reason to check the schedule again. It asks PowerManager.isInteractive instead of trusting a screen-on broadcast that may never arrive. And it assumes an alarm might not fire, instead of assuming it will.

06

What I'd tell another Android developer

If your users are in Africa, test on a TECNO, an Infinix or an itel, not only on a Pixel. Read the manufacturer's own log tags, not just yours. The answer was in logcat the whole time, under a name I didn't know to look for. And design for the alarm that doesn't fire, because on these phones, some of them won't.

The smaller surprises on the same phone: the launcher draws widget images at about 87.5% of their box, so tap areas drift out of line. It ignores custom fonts in widgets, so text has to be drawn as a bitmap. And one device-policy call throws on this phone instead of returning, which had been silently cutting a function short.