The OEM autostart rabbit hole: deep-linking into a dozen undocumented settings screens
Here is a sentence that is true and that no documentation will tell you: on a large share of the Android phones actually in use, your foreground service does not decide whether it keeps running. The manufacturer's battery manager does, and it has no API, no callback, and no way to ask it what it thinks.
The SMS gateway I have been writing about is exactly the kind of app these managers exist to kill. It is a background process. It runs forever. It talks to the network. From a battery manager's point of view it is indistinguishable from a badly written app leaking wakelocks, and the manager is not wrong to be suspicious, it is just wrong about this one.
So the app has a screen whose only job is to walk an operator through turning those protections off, and behind that screen is one of the least elegant pieces of code in the project. I am showing it because everybody writing this kind of app writes it, usually badly, and because the interesting part is not the list of component names. It is what you do when the list misses.
keep an ordered list of undocumented component names, test each one with resolveActivity before launching it, and fall back to the app details page.
- There is no platform API for autostart. It is a manufacturer feature and it is invisible to you.
- Battery optimisation and OEM autostart are two different protections. Winning one does not win the other.
- Transsion (Tecno, Infinix), which is very large in Africa, exposes no component at all, so the fallback is not an edge case, it is a primary path.
- You cannot query the autostart state, so the only honest signal is a watchdog that notices the service stopped.
Two protections, not one
The first thing to get straight is that there are two separate mechanisms with similar-sounding names, and they are handled by completely different code.
| Battery optimisation (Doze) | OEM autostart or protected apps | |
|---|---|---|
| Who owns it | Android platform, since Marshmallow | The manufacturer's own app manager |
| Can you query it? | Yes: PowerManager.isIgnoringBatteryOptimizations | No. Nothing to ask. |
| Can you request it? | Yes: a documented intent with a documented permission | No. You can only try to open a screen. |
| What it does when active | Defers your background work in idle windows | Stops your app outright, often on a schedule |
An app can be perfectly exempt from Doze and still be killed every night at 2am by a manufacturer's "phone manager" doing its cleanup round. Conversely, getting onto the OEM allowlist does not exempt you from Doze. You need both, and on several ROMs there is a third thing, pinning the app in the recents list, that has no API whatsoever.
The part that has an API
Battery optimisation is the well-behaved half. The app declares REQUEST_IGNORE_BATTERY_OPTIMIZATIONS in the manifest and then walks a three-step ladder, each rung less specific than the last:
@android.annotation.SuppressLint("BatteryLife")
private fun requestIgnoreBatteryOptimizations(): Boolean {
return try {
if (isIgnoringBatteryOptimizations()) return true
val i = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:$packageName")
}
startActivity(i)
true
} catch (e: Exception) {
openBatteryOptimizationSettings()
}
}
private fun openBatteryOptimizationSettings(): Boolean {
return try {
startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
true
} catch (e: Exception) {
openAppDetailsSettings()
}
}
Three things in there are deliberate. The first line checks whether the exemption is already granted, because launching a settings dialog at somebody who has already granted it is how an app teaches its user to dismiss its dialogs. The lint suppression is honest: Google flags direct use of this intent because they do not want apps demanding it casually, and this is one of the cases where demanding it is the entire point. And each catch falls through to something less specific rather than reporting failure, so the worst case is that the operator lands on the app details page and can still get there manually.
The state is queryable, which means the dashboard can show it live rather than assuming:
private fun isIgnoringBatteryOptimizations(): Boolean {
return try {
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
pm.isIgnoringBatteryOptimizations(packageName)
} catch (e: Exception) {
false
}
}
That single boolean sits on the gateway's dashboard next to the queue counts, because the operator needs to know it is still true. Users turn things off. Phones get reset. A setting granted in March is not a setting that is granted today.
The part that has no API
Now the other half. There is no Settings.ACTION_AUTOSTART. What exists instead is, on each ROM, an activity somewhere inside the manufacturer's security or system-manager package, with a name that is not documented, is not stable across versions, and is not the same on two brands that share an underlying ROM.
The only approach available is to hold a list, try each candidate, and check whether the device can actually resolve it before launching:
for (c in candidates) {
try {
val i = Intent().apply {
component = c
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
if (packageManager.resolveActivity(i, 0) != null) {
startActivity(i)
return true
}
} catch (e: Exception) {
// try next
}
}
// Fallback: at least open our app's settings so the user can find autostart.
return openAppDetailsSettings()
The resolveActivity check is what makes this tolerable rather than reckless. Without it you are firing explicit intents at components that may not exist, catching ActivityNotFoundException a dozen times, and hoping none of the ROMs decides to crash instead of throwing. With it, you ask first and only launch what the device says it has.
The real list in the shipped app:
| Brand | Package | Activity |
|---|---|---|
| Xiaomi, MIUI, Redmi, POCO | com.miui.securitycenter | com.miui.permcenter.autostart.AutoStartManagementActivity |
| Oppo, ColorOS, Realme | com.coloros.safecenter | ...safecenter.permission.startup.StartupAppListActivity |
| Oppo, older ColorOS | com.coloros.safecenter | ...safecenter.startupapp.StartupAppListActivity |
| Oppo, older still | com.oppo.safe | ...safe.permission.startup.StartupAppListActivity |
| Vivo | com.vivo.permissionmanager | ...permissionmanager.activity.BgStartUpManagerActivity |
| Vivo, iQOO | com.iqoo.secure | ...secure.ui.phoneoptimize.AddWhiteListActivity |
| Vivo, iQOO variant | com.iqoo.secure | ...secure.ui.phoneoptimize.BgStartUpManager |
| Huawei, Honor | com.huawei.systemmanager | ...systemmanager.startupmgr.ui.StartupNormalAppListActivity |
| Huawei, older | com.huawei.systemmanager | ...systemmanager.optimize.process.ProtectActivity |
| Samsung | com.samsung.android.lool | ...sm.ui.battery.BatteryActivity |
| Samsung, China ROM | com.samsung.android.sm_cn | ...sm.ui.battery.BatteryActivity |
| Asus | com.asus.mobilemanager | ...mobilemanager.MainActivity |
| Letv | com.letv.android.letvsafe | ...letvsafe.AutobootManageActivity |
| Transsion: Tecno, Infinix, itel | No public component. Falls through. | |
Look at how many rows are the same brand at a different ROM generation. Oppo appears three times, Vivo three times, Huawei twice, Samsung twice. That is the shape of the problem: this is not thirteen manufacturers, it is a handful of manufacturers times however many times they have renamed the screen. The list is not something you write once. It is something that decays, and the only way you find out it has decayed is that a phone in a shop somewhere stops forwarding payments.
The brand that is not on the list
Transsion, the group behind Tecno, Infinix and itel, exposes nothing. There is no component to resolve, so every candidate fails and the ladder falls through to the app details page.
If you build for a European or North American market, that reads like a minor gap. If you build for Africa, it is not a gap, it is a large share of the phones your software will actually run on. Transsion brands are everywhere in the market this gateway serves, and they are exactly the price bracket a shop buys when it needs a second phone to sit on the counter. Which means the fallback path is not a courtesy for unusual devices. It is the main path.
That reframing changes what you build. If the fallback is an edge case, a toast saying "please enable autostart" is acceptable. If the fallback is the common case, it has to be a proper screen: the app's Reliability tab, with steps in plain language, in an order that matches what the operator will actually see, and a live indicator of the things that can be checked so at least part of the walkthrough verifies itself.
Designing for the thing you cannot check
You cannot ask whether the app is on the autostart allowlist. You can, however, notice when it stops running, and that is what the gateway does instead.
The foreground service writes a timestamp to shared preferences every 60 seconds, and the dashboard treats the service as stale after three minutes of silence. Separately, the five-minute heartbeat means the backend also notices, which is the version that matters, because the person who can drive to the shop is not the person holding the phone.
This is the general shape of the answer whenever a platform hides state from you. Stop trying to read the setting. Measure the outcome the setting affects, and alarm on that. It is less satisfying, it catches every cause including the ones you have not enumerated, and it keeps working when the manufacturer renames the screen again.
The one case it does not fix is a force-stop, because after an OEM force-stop the broadcast receiver will not fire until a human opens the app. There is no code that works around that. What the app does instead is make reopening it do the right thing: the UI bootstrap calls startService on every launch, harmlessly if it is already running, so the single action an operator is most likely to take, opening the app to see whether it is working, is also the action that fixes it.
// Ensure the gateway service is running (also recovers after a force-stop,
// the only time the user reopens the app). Harmless if already running.
try {
await NativeBridge().startService();
} catch (e) {
AppLog.w('Bootstrap', 'startService failed: $e');
}
What I would do differently
I would report the manufacturer in the heartbeat and act on it centrally. The app already reads Build.MANUFACTURER and a full device fingerprint for its About screen, and the heartbeat already carries device health. Putting the manufacturer in that payload would mean the backend could answer a question no individual phone can: are Infinix devices in this fleet going quiet more often than Samsungs? That is the only realistic way to find out that a component name has gone stale, and it is a small change I have not made.
I would also stop treating the candidate list as code. It is data that decays independently of every release, and it is currently compiled into the APK, which means updating it requires shipping a new build to every phone in the field. Fetching it from the same backend that serves the version feed would let a new ROM be supported the day someone reports it. The trade is that you are then launching activities named by a remote server, which is a genuinely uncomfortable thing to do and would need the list signed and constrained to a known set of packages. I have not decided that the convenience is worth it, which is why the list is still hard-coded.
What I would not change is the bottom of the ladder. Every branch of that code ends somewhere the operator can act, and nothing ever tells them it failed and leaves them there. For the reliability work that all of this protects, see the six failure modes article, and for the wider picture of what this phone is actually doing, the gateway overview.
Frequently asked questions
- Is there an official Android API to open the OEM autostart screen?
- No. Autostart and protected-app allowlists are manufacturer additions with no entry in the platform settings API. The only way in is to construct an explicit intent for an undocumented component name that varies by brand and by ROM version, check whether it resolves on this device, and fall back when it does not.
- What do I do about Tecno and Infinix phones?
- Fall back to a walkthrough. Transsion brands expose no public component for their autostart screen, so there is nothing to deep-link to. The honest design is to detect that no candidate resolved, open the app details settings page, and put a plain-language instruction list in the app so the operator can find the setting themselves. This matters more in Africa than the market share tables from elsewhere would suggest.
- Is REQUEST_IGNORE_BATTERY_OPTIMIZATIONS enough on its own?
- No. The Doze exemption stops the platform's own battery optimisation from deferring your work. It has no effect on a manufacturer's separate app-management layer, which will still stop the app on its own schedule. You need both, and on some ROMs you also need the app pinned in the recents list.
- How do I know whether the exemption actually worked?
- Query it rather than trusting the intent result. PowerManager.isIgnoringBatteryOptimizations returns the real state, so the app can show a live indicator on its dashboard instead of assuming that launching a settings screen changed anything. For the OEM autostart allowlist there is no equivalent query, which is why the app has to watch for the service dying instead.