Running on Android BOOT_COMPLETED
Tags: android
Published: 2023-12-11 03:59:06

Hooking into `BOOT_COMPLETED` is important in Android because that's the only way you can reliably schedule actions to run at certain times. On each boot, all of your actions registered with `AlarmManager` are cleared away and you need to set them back up again.

However an increasingly large number of Android OEMs block `BOOT_COMPLETED` by default ([Samsung, OnePlus, Huawei, Xiaomi](https://dontkillmyapp.com/)), to prolong battery life on the phone. That is, unless you are lucky enough one of a handful of apps on the default allowlist.

The worst part here is that it's a non-standard permission that the OEMs have stapled onto Android, so there's no standard way to figure out if auto-launch is enabled, and no standard way to request the permissions. There's not even a standard way to open the auto-launch settings activity.

## Detection
What can we do? One thing is to be able to detect when `BOOT_COMPLETED` is being locked. To do this we need a few pieces of information.

1) When was the last time the system booted? [SystemClock.elapsedRealtime()](https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()) provides the answer there. `System.currentTimeMillis() - SystemClock.elapsedRealtime()` gives you the Unix timestamp in milliseconds.
2) When did the `BOOT_COMPLETED` receiver last run? You can put this in the receiver:

```kotlin
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
with(sharedPrefs.edit()) {
    putLong("LastBootReceiverTimestamp", System.currentTimeMillis())
    apply()
}
```

3) When was the app installed? You can write a similar piece of code to write another timestamp to shared prefs.

You can combine these pieces on app start to figure it out:

```kotlin
val installTimeMs = sharedPrefs.getLong("InstallTimestamp", 0)
if (installTimeMs > 0) {
    val now = System.currentTimeMillis()
    val elapsedSinceBootMs = SystemClock.elapsedRealtime()
    val lastBootTimeMs = now - elapsedSinceBootMs
    val lastBootReceiverTimeMs = sharedPrefs.getLong("LastBootReceiverTimestamp", 0)

    val bootedSinceInstalled = lastBootTimeMs > installTimeMs
    val receiverFiredSinceLastBoot = lastBootReceiverTimeMs > lastBootTimeMs

    // If enough time has passed since device boot that we should have expected the
    // boot receiver to fire, but it hasn't, then we need to ask for auto-launch permission
    if (bootedSinceInstalled
        && !receiverFiredSinceLastBoot
        && elapsedSinceBootMs > Duration.ofMinutes(5).toMillis()) {
        // Auto-launch has been disabled, warn the user
    }
}
```

## Solutions
[AutoStarter](https://github.com/judemanutd/AutoStarter) is a valiant effort, providing an API to help link to the auto-launch settings, that is supposed to encapsulate all the vendor-specific screens that control auto-launch.

It did not work with my OnePlus 8, and even though I found the package and activity I would need to launch, I was denied with a SecurityException. And there are similar crash reports for other devices like Huawei that have locked down the direct opening of this screen. Sadly I don't think it's a good idea to use this, since there doesn't seem to be a great way to work around the security issues or prevent it from throwing exceptions on new phone releases.

I think the only somewhat reliable action you can take is to explain the situation and give a link to open your app's details page, which should hopefully have a way to enable auto-launch there:

```kotlin
val packageName = BuildConfig.APPLICATION_ID
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
```

## The deeper issue
These phone makers are taking these aggressive measures to contain app activity to prolong battery life. I get where they're coming from, but a number of things really bug me about this:

* If these measures didn't unreasonably affect app functionality, they wouldn't need to allowlist popular apps.
* The allowlists further entrench already successful apps. Would you use a competitor to Whatsapp that often fails to tell you when you've gotten a text? They wouldn't get adoption, so they wouldn't get on the whitelist, so they'd never get adoption.
* Having the OEMs all go their own separate way and not through the official Android app model means it's outright impossible to have a smooth permission-granting experience, and extremely difficult to make even a bumpy one.

[This article about CTS-D](https://www.androidpolice.com/google-cts-d-app-killing/) chronicles some heroics to try to bring the OEMs in line and back to sanity. I think the ideal state here is to have good tools to tell you what's taking your battery, and have battery management handled in a central way by the Android app model. Crossing my fingers here; maybe it will be resolved someday.