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.
The two ways a service goes off
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 system turned it off. Power saving mode switched it off along with every other service.
- The user turned it off. On purpose, perhaps because another app (a banking app is the usual case) refuses to run while an accessibility service is on.
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.
Can an app turn it on at all?
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.
Record the cause when you see it go off
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.
The rule, as one pure function
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: never undo the user's own choice. -
!powerSaveOnNow: restoring inside power saving gets undone at once, so wait until it ends. The power-save broadcast is one of the triggers, so the check runs again when it does. -
wasEverEnabled: if the user never set the service up, that is an onboarding problem, not a restore. -
canWriteSecureSettings: without the adb grant, fall through to asking.
Keeping this as a plain function means a JVM unit test can walk all the combinations without a device.
Append, never replace
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.
When you can't restore, say what is paused
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 paused, 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.
No polling
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.
Takeaways
- A service the user switched off stays off. Build the restore path around that rule, not the other way round.
- Record why it went off at the moment it happens; afterwards you can only guess.
- Treat "went off while the app wasn't running" as the system, not the user.
- Don't restore while power saving is still on; wait for it to end.
- The enabled-services setting is shared. Append to it, never overwrite it.
- Without the adb-only grant, ask clearly, once, and say what still works.
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.
Top comments (0)