Android Services: Getting Started

Learn about Android Services and the differences between foreground, background and bound services. By Gabriela Kordić.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Updating the Notification When the Timer Changes

TimerService contains a coroutine that ticks every second while the level is running. You’ll use the timer value to update the notification view.

Look for broadcastUpdate() inside the TimerService.kt, and add this code inside the if:

// 1
sendBroadcast(
  Intent(TIMER_ACTION)
    .putExtra(NOTIFICATION_TEXT, elapsedTime)
)

// 2
helper.updateNotification(
  getString(R.string.time_is_running, elapsedTime.secondsToTime())
)

Here is what this code block does:

  1. Here, you send a broadcast with the elapsed time to MainActivity. With it, MainActivity can update the time in the TextView below the card’s view.
  2. This helper method updates the status bar notification you posted above.

At this point, you’ve implemented everything the user sees while playing the game. But what happens if the user kills the app while the timer is running?

To handle that, add the following line to broadcastUpdate() in the else if:

helper.updateNotification(getString(R.string.get_back))

This line updates the notification text to call the user back to the game.

Awesome work! Build and run to test the timer. Start a level and notice how the timer changes at the bottom. Pull down the status bar to see how the timer changes within the notification. Try exiting the application to test the message text update in the notification.

Time ticks are visible on the screen

Using Background Processing for Complex Work

Sometimes, the user doesn’t need to know about certain actions happening, e.g. if you store the user’s activity on the server for developing purposes, you don’t have to display a notification. Place this kind of continuous work and similar in the background.

To achieve that, you can use:

  1. Custom Background Service – by default, the service runs on the UI thread. To avoid blocking it, create a Service with a job processing on the background thread.
  2. IntentService – a subclass of Service that executes requests sequentially by using worker thread. Since Android 8, it’s usage is not recommended. Also, IntentService is deprecated from Android 11.
  3. JobIntentService – a replacement for IntentService. Instead of service, it uses JobScheduler for executing jobs. In earlier versions than Android 8, it will act just like IntentService. For more info, read the official JobIntentService documentation.
  4. Background work with WorkManager – this is a general concept for doing background work in Android. Use it when you want to execute a periodical job in the future, or when you have some job constraints.

Explaining Background Execution Limits

Android 8 and newer versions have restrictions when using Android Services on the background thread:

  1. The system will destroy the service after some time if the app runs the service in the background. Once it goes to the background, it has a window of a few minutes in which it can interact with services. After that, the system kills all running services.
  2. The system will throw an exception if the service was started when the app is in the background and outside of the time window.

Find more in Background execution limits documentation.

Because of all these restrictions, Background Services are not used frequently. As mentioned above, as well as recommended by official Android documentation, the best solution for Background work is using Work Manager.Read in details about this at Scheduling Tasks With Android WorkManager.

Now, it’s time to learn about the last kind of service.

Creating a Bound Service

A bound service is an implementation of Service. It’s a component that other components bind to, interacting with it and performing interprocess communication (IPC). The bound component can be an Activity, another service, a broadcast receiver or a content provider.

The lifecycle of the bound service depends on another application component. When the component unbinds, the bound service destroys itself. Consequently, it can’t run in the background forever.

However, it doesn’t necessarily need a bound component — it can start and stop itself as well. In that case, there’s an option to run indefinitely.

To try this out, you’ll give Memo the ability to play audio.

Converting MusicService into actual Service

To implement the audio playing feature, you’ll use MusicService. Firstly, modify it to have access to basic service methods.

Open MusicService.kt and make it extend Service:

class MusicService : Service() {

As its type says, another component will use this service to bind to it. You already learned about the mandatory method you need for binding to service, so add it to MusicService:

override fun onBind(intent: Intent?): IBinder = binder

The difference between the last implementation of onBind() and this one is, obviously, the return value. Since you’re binding to service here, you need to provide the IBinder value.

IBinder is a programming interface of Binder that clients use to interact with the service. Its methods allow you to send a call to an IBinder object and receive a call coming in to a Binder object. To learn more, check out IBinder’s documentation.

Android Studio shows an error because you haven’t defined binder variable. To fix this, add the following to the top of the class:

private val binder by lazy { MusicBinder() }

Here, you’re doing a lazy initialization of binder, which is a type of MusicBinder. Implementation of MusicBinder is your next step.

Defining a Binder

This service can’t be accessed outside of this application and doesn’t have its own process. Therefore, you have to create an interface that will have access to the service’s context. Do that by extending the Binder class. This way all components can use all public methods from Binder or the Service.

Now, to define the interface, add these lines at the bottom of the class:

inner class MusicBinder : Binder() {

  fun getService(): MusicService = this@MusicService
}

MusicBinder is nested inside another class and it can access all methods from it. It can use all of Binder’s methods as well. Inside of it, you create a method for retrieving a service context.

Defining Service Methods for Managing Audio

This service allows the user to interact with the audio icons for playing, pausing, stopping and shuffling the music. For simplicity, the methods for managing audio are already defined. However, a music player isn’t.

To instantiate the music player, use this code inside initializeMediaPlayer() inside MusicService.kt:

  musicMediaPlayer = MediaPlayer.create(this, randomSongs.first()).apply {
    isLooping = true
  }

Here, you use MediaPlayer to run a continuously repeating audio of the first song in randomSongs.

Another fun feature is that this bound service can provide the name of the track that’s playing. Inside MusicService, add:

fun getNameOfSong(): String =
    resources.getResourceEntryName(randomSongs.first())
        .replaceFirstChar { 
             if (it.isLowerCase()) it.titlecase(Locale.ENGLISH) 
             else it.toString() 
        }.replace("_", " ")

In this method, you’re reading a track name from the resources and changing the result String to be more readable to the user.

All done! Now, you’ll use these methods from another component.