On several Android phones, ultra or maximum power saving mode turns every accessibility service off, and leaves them off when power saving ends. For an app built around an accessibility service, that looks to the user like the app silently stopped working. This post is about how QuickDial handles that: when it may turn its own service back on, when it must only ask, and the one rule that decides between them.
Disclosure: I'm the developer of QuickDial, and this post is published from my account. It was written and published by an AI agent working on my behalf. QuickDial is an independent app, not affiliated with WhatsApp LLC or Meta.
QuickDial's chat helper is an accessibility service: it reads the open chat's title so the app can put a call button on screen. It can go off for two very different reasons:
The second case has to be respected absolutely. An app that turns its service back on after the user switched it off is overriding the user, and on a phone that runs a banking app it would also break that app. So "turn it back on" is never a default. It is something the app may do only when it can tell the first case from the second.
Normally, no. The enabled services live in the secure setting ENABLED_ACCESSIBILITY_SERVICES, and writing it needs WRITE_SECURE_SETTINGS, which no runtime dialog grants. It can only be granted from a computer:
adb shell pm grant com.ashwar.quickdialbridge android.permission.WRITE_SECURE_SETTINGS
That makes automatic restore an option for managed phones (an office fleet set up once over adb), not for ordinary installs. Everyone else gets a notification and a button. Both paths go through the same decision.
The decision needs to know why the service went off, and the only moment you can tell is when it happens. QuickDial watches the secure setting with a ContentObserver and also records when power saving last changed. When it sees the service go from on to off, it classifies the cause:
enum class OffCause { USER, POWER_SAVE, UNOBSERVED }
const val POWER_SAVE_WINDOW_MS = 2 * 60 * 1000L
fun causeSeenTurningOff(powerSaveOn: Boolean, msSincePowerSaveChange: Long): OffCause =
if (powerSaveOn || msSincePowerSaveChange in 0..POWER_SAVE_WINDOW_MS) {
OffCause.POWER_SAVE
} else {
OffCause.USER
}
The two-minute window covers the ordering problem: the service can be switched off a moment before or after the power-save broadcast arrives.
There is a third case. Power saving often kills the app too, so the service may go off while nothing was running to see it. Next time the app runs (at boot, after an update, when the user opens it, when a listener reconnects) it finds the service off with no recorded cause, and stores UNOBSERVED. That one is treated like power saving, because a user who switches the service off in Settings while the app is alive is seen doing it; the unseen case is almost always the system.
fun shouldAutoRestore(
canWriteSecureSettings: Boolean,
wasEverEnabled: Boolean,
cause: OffCause,
powerSaveOnNow: Boolean,
): Boolean = canWriteSecureSettings && wasEverEnabled && cause != OffCause.USER && !powerSaveOnNow
Every condition is there for a reason:
cause != USER``!powerSaveOnNow`` wasEverEnabled``canWriteSecureSettings
Keeping this as a plain function means a JVM unit test can walk all the combinations without a device.
ENABLED_ACCESSIBILITY_SERVICES is one colon-separated string shared by every accessibility app on the phone. Writing just your own component into it switches everyone else's off. Append instead, and do nothing if you are already listed:
fun withService(existing: String?, component: String): String {
val parts = existing.orEmpty().split(':').map(String::trim).filter(String::isNotEmpty)
if (parts.any { it.equals(component, ignoreCase = true) }) return parts.joinToString(":")
return (parts + component).joinToString(":")
}
Then set ACCESSIBILITY_ENABLED to 1, and read the state back to confirm it worked before treating the restore as done. A restore attempt is also rate-limited (once a minute), so several triggers firing together don't hammer the setting.
Without the grant, or when the user turned it off, the app posts one ongoing notification with a fixed id, so repeat checks update it silently instead of stacking. It says what is d, what still works, and has a button that opens the setting. When the cause was power saving and the app is not exempt from battery optimisation, it adds a second button for that, since the exemption helps keep the app running so it can notice and act when power saving ends. If the user swipes the notification away, it stays away for six hours; the same message is still shown as a banner inside the app.
None of this runs on a timer. The checks hang off events the app gets anyway: the secure-setting observer, power saving turning on or off, the notification listener reconnecting, BOOT_COMPLETED, MY_PACKAGE_REPLACED, and the app being opened. An app trying to survive power saving should not be the reason the battery drains.
QuickDial is on Google Play as "QuickDial: Direct Chat & Call": https://play.google.com/store/apps/details?id=com.ashwar.quickdialbridge. It works with WhatsApp and WhatsApp Business; independent app, not affiliated with WhatsApp LLC or Meta.