13

Your guide to foreground services on Android

 1 year ago
source link: https://www.hellsoft.se/your-guide-to-foreground-services-on-andorid/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Android Featured

Your guide to foreground services on Android

Android foreground services are difficult to get right. In this post, I'll explain the restrictions and what you need to do to get them working properly.

Erik Hellman

Mar 13, 2023 • 10 min read
Your guide to foreground services on Android

Photo by Erik Mclean / Unsplash

The Service component have been around on Android since the very beginning. It is the component we need in order to be able to perform operations that are not immediately triggered through an Activity. However, over the years Google has restricted how services can be launched and how long they can run. This is mostly to prevent excessive battery consumption, but also for user privacy. Google also recommends developers consider using the Work Manager instead, which is usually a good recommendation.

However, there are legitimate reasons for using a Service in your app today. Your app might be playing audio (like music, podcasts, or audiobooks), or it might be a companion app for a Bluetooth peripheral (like a smartwatch, eBike, or smart lock on your front door). While you still should consider using the Work Manager for any background work, you sometimes must resort to a service that keeps your app running indefinitely.

In later Android versions, Google introduced the concept of foreground services. These are services started with the intention of performing actions on the users' behalf without presenting a full UI. In order to use them, they must be bound to an active notification so the user is aware that there is something running in the background.

Let's look at how to properly implement a foreground service, and how to do it correctly for all Android versions.

Starting a foreground service

The first thing to know is how to start a foreground service. Before this concept, we only had Context.startService() for starting services, but that has now changed. When you need a Service that should keep running when the UI is no longer active, you need to instead use the method Context.startForegroundService().

In fact, as from API level 26 (Android 8), you should never use the legacy Context.startService() method at all. If you look at the documentation for this method you'll find the following note:

Screenshot-2023-02-26-at-11.24.24.png

The Android API docs explain why you should avoid frequent calls to startService()

In essence, you should avoid controlling your service by passing an Intent and starting it, as this is an expensive operation. If you need to do frequent interactions with a Service, use a bound service instead.

Starting a foreground service begins by simply calling Content.startForegroundService() with an explicit Intent.

fun launchService(context: Context) {
    val intent = Intent(this, MyForegroundService::class)
    context.startForegroundService(intent)
}

Next in our service, we must implement onStartCommand() properly. This is where a common source for making a mistake.

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    super.onStartCommand(intent, flags, startId)
    
    createNotificationChannel(applicationContext)
    val notification = createNotification(applicationContext)
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        startForeground(
            NOTIFICATION_ID,
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
        )
    } else {
    	startForeground(NOTIFICATION_ID, notification)
    }
    
    return START_STICKY
}

The basics for how your onStartCommand() in a foreground service should look like. 

The important part is when you call startForeground() in onStartCommand(). This needs to be done as soon as possible and never deferred until later. If you're concerned about the content of the notification you should use default values at this point and update the notification later when the values are available.

Never call startForeground() from a background thread, like a coroutine. Always do it as soon as possible from onStartCommand().

It is also important to use the version check and call the correct Service.startForeground() method. A new method was introduced in API level 29, and that one must be used if your app targets API level 29 or later. This method adds a third parameter, foregroundServiceType, which is either a subset of what you've specified in the manifest (see below) or the value ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST which tells the system to use all the service types you specified in the manifest. Unless you start your service in different ways depending on the situation, I recommend always using ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST in order to make your implementation simpler.

In your manifest, you have to include the foregroundServiceType attribute for the entry on your service, or it will not function as a foreground service from API level 29 and later.

<service
    android:name=".MyForegroundService"
    android:exported="false" 
    android:foregroundServiceType="connectedDevice"
    android:label="@string/my_foreground_service_name">
</service>

You now have a service implementation that fulfills the requirements for running as a foreground service. Let's look at when we can start this service and when we cannot.

Foreground service restrictions

Starting with API level 31 (Android 12), starting services became even more restricted. From that version, you're no longer able to call Context.startForegroundService() from the background (like when receiving a broadcast when your app doesn't have an Activity in the foreground), except for a few special cases.

Note: if you are doing anything regarding foreground services on Android, you must have a full understanding of these exceptions. Read the page linked above!!

I won't cover all these "special cases" in this post but instead, focus on the most common cases.

Activity pausing

The most common case is described as "Your app transitions from a user-visible state, such as an activity.". This usually means the user is switching to a different app or going back to the launcher. Let's assume that your app is an audiobook player. In this case, you would start your foreground service when onPause() is called on your Activity.

You could start your foreground service before this point, but this all depends on how you wish the notifications for your app to work. I recommend that you start your foreground service as soon as possible, so for apps playing audio, it could be done when you start the app or at least when starting the playback.

In fact, starting your foreground service when your activity is in the foreground is probably the best choice for most apps that need a service like this. If you have an app for tracking your running activities, you start the service when the user starts the tracking in the app.

High-priority FCM message

The second common case is when your app receives a high-priority messagefrom Firebase Cloud Messaging. A typical use case for this is messaging apps where the user receives a message that they should be notified about, even when the device is sleeping. If you believe that you need to run a foreground service at this point to keep your app running, this is a situation where that is possible.

However, if the user doesn't interact with the notification from the FCM message, the system will deprioritize future messages to a normal priority and you're no longer allowed to start a foreground service from these. You can tell if this is the case by checking RemoteMessage.getPriority() on the received message and comparing it with RemoteMessage.getOriginalPriority(). If you're no longer a high-priority message, you know your user isn't checking these notifications and you can't start a foreground service anymore.

For messaging apps, I would not recommend a foreground service in any case, since you already have FCM to deliver messages when you're app isn't running (which will launch your app). Keeping a network connection to your messaging service alive indefinitely is a waste of battery, so please be a good citizen and design your messaging app to rely on FCM instead.

Remember that your app won't be killed immediately when the user puts your app in the background, so you can still retain a network connection for a while to allow fast message delivery when the user switches between apps.

Companion apps

The third common case is when your app is a companion app to an external peripheral, like a smartwatch, bike, or smart home device. This is also the most common cause of errors regarding foreground services. You want your app to keep running while it is connected to the device. Also, when not connected you want to be woken up when the device is detected nearby by the phone.

The short answer is that you must use the CompanionDeviceManager for all cases where you want to start a foreground service from a background state, and you're acting as a companion app to a peripheral. I've seen all sorts of efforts to get around this restriction, but the harsh truth is that you cannot do this reliably from Android 12 without the CompanionDeviceManager.

Let's assume it's a Bluetooth LE peripheral that your app is connecting to. When onboarding your users, you should use the CompanionDeviceManager to pair with the device. If you already have an app but aren't using the CompanionDeviceManager you must still perform this pairing. To make that situation easier in these situations, you simply use a ScanFilter with the device address that you already know and call setSingleDevice() when creating the AssociationRequest. The latter will tell the CompanionDeviceManager to search among already bonded devices and is specifically there to handle these cases.

When using the CompanionDeviceManager, you should also add the related permissions to your manifest. At least, you probably want to include the following.

<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" />

The two common permissions to add when using the CompanionDeviceManager

Their purpose is fairly self-explanatory so I won't go into the details, but it is important to understand that these permissions won't be applied unless you have an association with a peripheral created with the CompanionDeviceManager. You can confirm this by fetching the current associations. If that list is empty, you need to perform the association step.

Once you have the device pairing created, you have two methods for being woken when the peripheral is detected by the phone. You either use the BluetoothLeScanner and register a PendingIntent that gets triggered when the device is detected, or you use a CompanionDeviceService together with CompanionDeviceManager.startObservingDevicePresence(). The latter is only available from API level 31 (Android 12). While you still can use BluetoothLeScanner on Android 12 and above to trigger the foreground service, I do recommend using the CompanionDeviceService when it is available.

This will effectively mean that your app will have two different foreground services, one that extends the regular Service class and one that extends CompanionDeviceService. I recommend keeping two bools  in your resources that let you toggle the enabled attribute in the manifest depending on the API level. The following code snippets show how this could look.

<resources>
    <bool name="enable_legacy_service">false</bool>
    <bool name="enable_cdm_service">true</bool>
</resources>

Bools in values-v31 resource folder

<resources>
    <bool name="enable_legacy_service">true</bool>
    <bool name="enable_cdm_service">false</bool>
</resources>

Bools in default values resource folder

<service
    android:name=".peripherals.SampleCompanionService"
    android:exported="true"
    android:enabled="@bool/enable_cdm_service"
    android:foregroundServiceType="connectedDevice|"
    android:label="@string/peripheral_device_service_name"
    android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">
    <intent-filter>
        <action android:name="android.companion.CompanionDeviceService" />
    </intent-filter>
</service>

<service
    android:name=".peripherals.BackgroundScanPeripheralService"
    android:exported="true"
    android:foregroundServiceType="connectedDevice"
    android:enabled="@bool/enable_legacy_service"
    android:label="@string/peripheral_device_service_name">
    <intent-filter>
        <action android:name="se.hellsoft.foregroundservices.peripherals.START_SERVICE" />
    </intent-filter>
</service>

The service definitions in the manifest where the enabled attribute is toggled based on the API level.

Let's sum up the case for companion devices and foreground services: You must use the CompanionDeviceManager to be able to start a foreground service when the device is detected and your app isn't running in the foreground. Even when you have an app with existing pairing to a device, you still need to create a companion pairing in order to be granted the necessary permission.

Use the BluetoothLeScanner with a PendingIntent before API level 31 to wake up your app and start the foreground service when the device is detected. From API level 31, you instead should use the CompanionDeviceService to detect the device presence.

The device rebooted or the app updated

There are some other exceptions that might apply to your app as well and which would let you start a foreground service from the background. These are usually complementary to one of the cases described above.

If your app gets updated, a special broadcast ACTION_MY_PACKAGE_REPLACED will be sent to your app (if you have a receiver registered in the manifest for this action) and you will be allowed to start your foreground service from there. Also, if your app listens for ACTION_BOOT_COMPLETED or ACTION_LOCKED_BOOT_COMPLETED you can also start a foreground service from the receiver.

These three broadcasts are useful for companion apps where you need to check if you can connect to the device immediately, or otherwise resume background scanning after a reboot or when your app was updated. It can also be useful for messaging apps to reconnect to the service after reboot to see if there are any new messages since any FCM message might have been lost during the reboot.

Asking the user to turn off battery optimizations (Please don't!)

You can also ask the user to disable battery optimizations for your app by directing them to the system's settings through the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS intent action. I strongly advise against this practice as you rarely need to do this if you follow the guide above. It will probably also result in bad battery performance for the user and they will be able to see that your app is a likely cause for this, resulting in bad reviews and lost users.

Since you usually don't need to do this, I recommend that you try to apply one of the patterns above to your app. Most likely, a creative interpretation of the use of foreground services will make for a better user experience overall.

Use the Work Manager instead

While there are legitimate reasons for starting foreground services from the background, most developers haven't properly explored the option of using the Work Manager instead.

Any periodic work that doesn't need exact timing should be done with the Work Manager instead of a foreground service. If your app needs to transfer a large amount of data, then use the option for long-running workers to do so instead of a foreground service.

Conclusions

Getting foreground services to work correctly on Android is a difficult task, and it keeps getting more complicated with each new Android version as Google keeps adding and modifying the restrictions.

We can already now see what the new restrictions for foreground services are coming with Android 14, so make sure you're already updated with what is necessary for earlier versions and your life as an Android developer will be easier once Android 14 is released.

The most common mistake I see developers do is to try to bypass the restrictions or to simply yield and ask the user to disable battery optimizations. I hope this post will help more developers to overcome the challenges and get their foreground services working correctly.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK