Android Services for beginners

Abhishek Pathak
9 min readSep 19, 2022

--

The Android Service class is designed specifically to allow applications to initiate and perform background tasks. services are designed to perform tasks that take a long time to complete (such as downloading a file over an internet connection or streaming music to the user) but do not require a user interface.

Types of Service

  1. Started Services / Background Services

Started Service also called background service performs an operation that isn’t directly noticed by the user. For example, if an app used a service to compact its storage, that would usually be a background service.

2. Bound Services

A service is bound when an application component binds to it by calling bindService(). A bound service offers a client-server interface that allows components to interact with the service, send requests, receive results, and even do so across processes with IPC (interprocess communication).

3. Foreground Services

Foreground service performs operations that are noticeable to the user. It shows a status bar notification, so that users are actively aware that your app is performing a task in the foreground and is consuming system resources. While this notification is shown, the Android system will be gentle to the application “owning” the notification, giving it the required priority and trying not to stop it, because it’s probably doing some important work.

LifeCycle of Started and Bound Service

Managing destroyed Service restart options

The onStartCommand() callback method is required to return an integer value to define what should happen with regard to the service in the event that it is destroyed by the Android runtime system. Possible return values for these methods are as follows:

  • START_NOT_STICKY — Indicates to the system that the service should not be restarted in the event that it is destroyed unless there are pending intents awaiting delivery.

It’s the best option to avoid running a service in case if it is not necessary.

  • START_STICKY — Indicates that the service should be restarted as soon as possible after it has been destroyed if the destruction occurred after the onStartCommand() method returned. In the event that no pending intents are waiting to be delivered, the onStartCommand() callback method is called with a NULL intent value. The intent being processed at the time that the service was destroyed is discarded.

This is suitable for the service which are not executing commands but running independently and waiting for the job.

  • START_REDELIVER_INTENT — Indicates that, if the service was destroyed after returning from the onStartCommand() callback method, the service should be restarted with the current intent redelivered to the onStartCommand() method followed by any pending intents.

This is useful for services that are receiving commands of work to do, and want to make sure they do eventually complete the work for each command sent.

Implementation of Started Services

class MusicService : Service() {    private lateinit var player: MediaPlayer    override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { player = MediaPlayer.create(this, Settings.System.DEFAULT_RINGTONE_URI)
player.isLooping = true
player.start()
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
player.stop()
}
}

Implementation of Bound Service

import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.SystemClock
import android.util.Log
import android.widget.Chronometer

class BoundService : Service() {
lateinit var mBinder: IBinder
private lateinit var mChronometer: Chronometer

override fun onCreate() {
super.onCreate()
Log.v(LOG_TAG, "in onCreate")
mBinder = MyBinder()
mChronometer = Chronometer(this)
mChronometer.base = SystemClock.elapsedRealtime()
mChronometer.start()
}

override fun onBind(intent: Intent): IBinder? {
Log.v(LOG_TAG, "in onBind")
return mBinder
}

override fun onRebind(intent: Intent) {
Log.v(LOG_TAG, "in onRebind")
super.onRebind(intent)
}

override fun onUnbind(intent: Intent): Boolean {
Log.v(LOG_TAG, "in onUnbind")
return true
}

override fun onDestroy() {
super.onDestroy()
Log.v(LOG_TAG, "in onDestroy")
if (this::mChronometer.isInitialized) {
mChronometer.stop()
}
}

val timestamp: String
get() {
val elapsedMillis = (SystemClock.elapsedRealtime() - mChronometer.base)
val hours = (elapsedMillis / 3600000).toInt()
val minutes = (elapsedMillis - hours * 3600000).toInt() / 60000
val seconds = (elapsedMillis - hours * 3600000 - minutes * 60000).toInt() / 1000
val millis = (elapsedMillis - hours * 3600000 - minutes * 60000 - seconds * 1000).toInt()
return "$hours:$minutes:$seconds:$millis"
}

// Start the Chronometer
fun play() {
if (!mChronometer.isRunning) {
mChronometer.base = SystemClock.elapsedRealtime() - mChronometer.elapsedRealtime()
mChronometer.start()
}
}

// Pause the Chronometer
fun pause() {
if (mChronometer.isRunning) {
mChronometer.stop()
}
}

inner class MyBinder : Binder() {
val service: BoundService
get() = this@BoundService
}

companion object {
private const val LOG_TAG = "BoundService"
}
}

How to use above service?

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
private var boundService: BoundService? = null
private var isBound = false

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as BoundService.MyBinder
boundService = binder.service
isBound = true
}

override fun onServiceDisconnected(name: ComponentName?) {
boundService = null
isBound = false
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val serviceIntent = Intent(this, BoundService::class.java)
bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE)

val playButton = findViewById<Button>(R.id.play_button)
val pauseButton = findViewById<Button>(R.id.pause_button)

playButton.setOnClickListener {
// Call the play method in the service
boundService?.play()
}

pauseButton.setOnClickListener {
// Call the pause method in the service
boundService?.pause()
}
}

// Call this method to get the timestamp from the bound service
private fun getTimestamp() {
val timestamp = boundService?.timestamp
// Do something with the timestamp
}

override fun onDestroy() {
super.onDestroy()
if (isBound) {
unbindService(serviceConnection)
}
}
}

This Android project demonstrates the implementation of a Bound Service that utilizes a Chronometer to keep track of time. It includes Play and Pause functionality to control the Chronometer. The Bound Service allows an Android activity to bind to it, retrieve timestamps, and control the Chronometer.

Implementation of Foreground Service

import android.app.*
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.NotificationCompat

class MusicPlayerService : Service() {
private var isPlaying = false

inner class MusicPlayerBinder : Binder() {
fun getService(): MusicPlayerService = this@MusicPlayerService
}

private val binder = MusicPlayerBinder()

override fun onBind(intent: Intent?): IBinder? {
return binder
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_NOT_STICKY
}

fun playOrPause() {
if (isPlaying) {
pause()
} else {
play()
}
}

private fun play() {
// Implement your music playback logic here
isPlaying = true
// Start playback
Toast.makeText(this, "Music started", Toast.LENGTH_SHORT).show()
// Update notification
startForeground(1, createNotification("Music is playing"))
}

private fun pause() {
// Implement your music pause logic here
isPlaying = false
// Pause playback
Toast.makeText(this, "Music paused", Toast.LENGTH_SHORT).show()
// Update notification
startForeground(1, createNotification("Music is paused"))
}

private fun createNotification(contentText: String): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)

val playPauseIntent = Intent(this, MusicPlayerService::class.java)
playPauseIntent.action = "PLAY_PAUSE"
val playPausePendingIntent = PendingIntent.getService(
this,
0,
playPauseIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)

val notification = NotificationCompat.Builder(this, "ChannelId")
.setContentTitle("Music Player")
.setContentText(contentText)
.setSmallIcon(R.drawable.ic_music_note)
.setContentIntent(pendingIntent)
.addAction(
R.drawable.ic_play,
"Play/Pause",
playPausePendingIntent
)
.build()

return notification
}
}

How to use this?

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.yourappname.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private var musicPlayerService: MusicPlayerService? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

createNotificationChannel()

binding.startServiceButton.setOnClickListener {
startMusicService()
}

binding.stopServiceButton.setOnClickListener {
stopMusicService()
}

binding.playPauseButton.setOnClickListener {
musicPlayerService?.playOrPause()
}
}

private fun startMusicService() {
val message = "Foreground service message"
MusicPlayerService.startService(this, message)
musicPlayerService = MusicPlayerService()
}

private fun stopMusicService() {
musicPlayerService?.stopSelf()
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
"ChannelId", "Music Player",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager!!.createNotificationChannel(serviceChannel)
}
}
}

Understanding IntentService in Android

In Android development, handling background tasks efficiently is crucial for creating responsive and smooth user experiences. One of the tools available for this purpose is the IntentService.

What is IntentService?

IntentService is a class provided by the Android framework that simplifies the creation of background services for performing asynchronous tasks off the main thread. It is a subclass of Service and is designed to handle one-off tasks in a queue.

How does IntentService work?

When you start an IntentService, it creates a new worker thread and places the incoming Intents into a queue. It processes each Intent sequentially on the worker thread, ensuring that only one task is executed at a time. Once all the Intents in the queue are processed, the service stops itself automatically, making it a convenient choice for tasks that need to be performed asynchronously and then cleaned up afterward.

Key Features of IntentService:

  1. Automatic Threading: IntentService manages the worker thread automatically, allowing developers to focus on implementing the task logic without worrying about thread management.
  2. Queueing: Intents sent to the IntentService are placed in a queue and processed sequentially, ensuring that tasks are executed in the order they were received.
  3. Self-Stopping: After completing all queued tasks, IntentService stops itself, saving system resources and avoiding memory leaks.
  4. Integration with other Android components: IntentService can easily be integrated with other Android components such as activities, fragments, and broadcast receivers, allowing for seamless communication between different parts of the application.

Intent Service Implementation

class MyIntentService : IntentService("MyIntentService") {

override fun onHandleIntent(p0: Intent?) {
if (p0 != null) {
Log.i("tag", "data is ${p0.getStringExtra("data")}")
}
if(Looper.myLooper() == Looper.getMainLooper()) {
Log.i("tag", "You are in UI thread")
}else{
Log.i("tag", "You are not in UI thread")
}
Log.i("tag", "Intent service started and processing background task")
}

override fun onDestroy() {
super.onDestroy()
Log.i("tag", "Intent service got destroyed")
}
}

How to call it from activity

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.btnStartService.setOnClickListener {
if(Looper.myLooper() == Looper.getMainLooper()) {
Log.i("tag", "You are in UI thread onCreate of activity")
}else{
Log.i("tag", "You are not in UI thread")
}

val intent = Intent(this, MyIntentService::class.java)
intent.putExtra("data", "hey i am a service")
startService(intent)
}
}
}

Service Vs Intent service

How to Use Foreground Service?

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.yourappname.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.startServiceButton.setOnClickListener {
startForegroundService()
}

binding.stopServiceButton.setOnClickListener {
stopForegroundService()
}
}

private fun startForegroundService() {
val message = "Foreground service message"
ForegroundService.startService(this, message)
}

private fun stopForegroundService() {
ForegroundService.stopService(this)
}
}

Service Vs Intent service

  • The Service can be used in tasks with no UI, but shouldn’t be too long. If you need to perform long tasks, you must use threads within Service.
  • The IntentService can be used in long tasks usually with no communication to Main Thread. If communication is required, can use Main Thread handler or broadcast intents. Another case of use is when callbacks are needed (Intent triggered tasks).

Runs On which thread?

  • The Service runs in background but it runs on the Main Thread of the application.
  • The IntentService runs on a separate worker thread.

How to start?

  • The Service is triggered by calling method startService().
  • The IntentService is triggered using an Intent, it spawns a new worker thread and the method onHandleIntent() is called on this thread.

Triggered From which thread?

  • The Service and IntentService may be triggered from any thread, activity or other application component.

Disadvantages of using these?

  • The Service may block the Main Thread of the application.
  • The IntentService cannot run tasks in parallel. Hence all the consecutive intents will go into the message queue for the worker thread and will execute sequentially.

Job Schedulers In android

Job Scheduler is introduced in android to set limitations on Background executions? why????

Because background services consumer battery a lot and ultimately reduces the performance of device and drain out the battery.

Job Schedulers are kind of modern Alarm manager. If you have some operations that is based on some time then you should go with Alarm manager but when you need do some functionality on some conditions like may be if Bluetooth enabled or WIFI/mobile data enabled or once battery charged fully then you should go for this concept called Job Scheduler.

Simple Implementation for Job Scheduler

class MyJobScheduler : JobService() {

override fun onStartJob(p0: JobParameters?): Boolean {
var value = 0
for (value in 0..25) {
Thread.sleep((2000))
Log.i("tag", "Running job $value")
}
Toast.makeText(applicationContext, "Job done!!", Toast.LENGTH_SHORT).show()
return false
}

override fun onStopJob(p0: JobParameters?): Boolean {
Toast.makeText(applicationContext, "Job got cancelled!!", Toast.LENGTH_SHORT).show()
return false
}

override fun onLowMemory() {
super.onLowMemory()
Log.i("tag", "your device is running out of memory")
}
}

And then use-case of Job scheduler

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var jobScheduler: JobScheduler

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.btnStart.setOnClickListener {
jobScheduler = jobSchedulerWork()
jobScheduler.schedule(getJob())
}

binding.btnStop.setOnClickListener {
if (this::jobScheduler.isInitialized) {
jobScheduler.cancelAll()
}
}
}

private fun jobSchedulerWork(): JobScheduler {
return getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler
}

private fun getJob(): JobInfo {
val component = ComponentName(this, MyJobScheduler::class.java)
return JobInfo.Builder(1, component)
.setMinimumLatency(3000)
.build()
}
}

--

--