Notes: 19. Create Foreground Services
The student materials have been reviewed and are updated as of SEPTEMBER 2022.
As you’ve learned so far, services in Android Oreo and higher are restricted.
The whitelist of what apps can do, when they are in the background, is the following: they can handle high-priority Firebase Cloud Messaging messages. They can receive broadcasts, for example SMS/MMS messages. They can execute pending intents from notifications. And they can start a VpnService.
Other than that, apps are forbidden to work while in the background, if they are not using Foreground Services with notifications.
A foreground service is specific because it has to display a notification, which tells the user that the service is running, and it may be draining battery, or using mobile data.
It’s a good way to tell the user that something is still running, so they can kill the app if they don’t like it. Let’s see how to implement a foreground service! :]
Open the SynchronizeImagesService.kt file first. Add the following base code changes, to start implementing the Foreground service:
const val NOTIFICATION_CHANNEL_NAME = "Synchronize service channel"
const val NOTIFICATION_CHANNEL_ID = "Synchronize ID"
class SynchronizeImagesService : Service() {
private val remoteApi by lazy { App.remoteApi }
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
showNotification()
clearStorage()
fetchImages()
return START_NOT_STICKY
}
}
Notice you’ve gone back to the original Android Service, and not the JobIntentService. Also notice the notification constants. You’ll use these to display the notification for the foreground service in a moment.
The clearStorage() and fetchImages() functions exist, all you need to build now is the showNotification() function. Do so the following way:
const val NOTIFICATION_CHANNEL_NAME = "Synchronize service channel"
const val NOTIFICATION_CHANNEL_ID = "Synchronize ID"
class SynchronizeImagesService : Service() {
...
private fun showNotification() {
createNotificationChannel()
val notificationIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, 0)
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Synchronization service")
.setContentText("Downloading images")
.setContentIntent(pendingIntent)
.build()
startForeground(1, notification)
}
...
}
This is a pretty big snippet of code. You first create a notification channel for the notification, in case the API level is Android Oreo or higher. You’ll build that function next.
Then you prepare the notification and pending intents, which will update the MainActivity with a new intent, if clicked.
You also use the Notification Builder from Android, to create a new notification with the text as in the code.
And finally, you call startForeground(), with an arbitrary ID and the notification. Once you start the notification, it’ll be displayed in the notification area, until you stop the foreground service, or the user kills the app. Finally, add the code to build the notification channel:
const val NOTIFICATION_CHANNEL_NAME = "Synchronize service channel"
const val NOTIFICATION_CHANNEL_ID = "Synchronize ID"
class SynchronizeImagesService : Service() {
...
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(serviceChannel)
}
}
...
}
Notification channels exist only in Android Oreo and above, which is why building them before that does not make sense. You build the service channel, with the channel ID, and the channel name. You also set the default importance. The importance governs how much visibility the notification will have.
Lower importance means it won’t make any sound, and the notification won’t be as visible. Higher importance means it will make sound, and it will be visible to the user. Two small things you need to change before running the app.
Head over to FileUtils, and change the queueImagesForDownload(), to immediately download images, rather than using the DownloadManager:
fun queueImagesForDownload(context: Context, images: Array<String>) {
if (images.isNotEmpty()) {
images.forEach { imageUrl ->
val file = File(context.externalMediaDirs.first(), imageUrl)
downloadImage(file, imageUrl)
}
}
}
This is because it doesn’t make sense to display two notifications, or use two different services to do the same thing. And finally, head over to the SettingsFragment, and change the synchronizeImages() function, to start this service:
private fun synchronizeImages() {
val intent = Intent(requireContext(), SynchronizeImagesService::class.java)
activity?.startService(intent)
}
Wow, that’s quite the overall change, just to support foreground services! But it’s worth it, as you now have a nice personalized way to display to the user, that you’re downloading images! Run the project now, and synchronize the images!
Remember that you can always slow down the network speed of the emulator, to be able to catch that notification!