Lesson 55 of 83 intermediate

BroadcastReceiver, Services, Binder & Bound Services

Android background components that every senior dev must know cold

Open interactive version (quiz + challenge)

Real-world analogy

A BroadcastReceiver is like a smoke detector — it sits dormant until it hears a specific signal, then springs to action. A Service is like a kitchen appliance that keeps running even when you leave the room. A Binder is the intercom system that lets two rooms talk directly.

What is it?

BroadcastReceiver, Service, and Binder are three of Android's four core application components. Together they handle background work, event listening, and inter-process communication. Though WorkManager has replaced many Service use cases, these components remain foundational for audio players, location trackers, system integrations, and any app that must react to OS-level events.

Real-world relevance

A music player app uses a Foreground Service to keep playback alive when the user switches apps. It uses a Bound Service so the UI Activity can call play(), pause(), seekTo() directly on the player object. A BroadcastReceiver listens for ACTION_AUDIO_BECOMING_NOISY (headphones unplugged) and pauses playback automatically.

Key points

Code example

// Foreground Service example (music player)
class MusicService : Service() {

    private val binder = MusicBinder()
    private var mediaPlayer: MediaPlayer? = null

    inner class MusicBinder : Binder() {
        fun getService(): MusicService = this@MusicService
    }

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

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = buildNotification()
        startForeground(NOTIF_ID, notification)
        return START_STICKY
    }

    fun play(url: String) {
        mediaPlayer = MediaPlayer().apply {
            setDataSource(url)
            prepareAsync()
            setOnPreparedListener { start() }
        }
    }

    fun pause() { mediaPlayer?.pause() }

    fun stop() {
        mediaPlayer?.stop()
        mediaPlayer?.release()
        mediaPlayer = null
        stopForeground(true)
        stopSelf()
    }

    override fun onDestroy() {
        super.onDestroy()
        mediaPlayer?.release()
    }

    private fun buildNotification(): Notification {
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Now Playing")
            .setSmallIcon(R.drawable.ic_music)
            .setOngoing(true)
            .build()
    }
}

// Activity binding
class PlayerActivity : AppCompatActivity() {
    private var musicService: MusicService? = null
    private var bound = false

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, binder: IBinder) {
            musicService = (binder as MusicService.MusicBinder).getService()
            bound = true
        }
        override fun onServiceDisconnected(name: ComponentName) {
            bound = false
        }
    }

    override fun onStart() {
        super.onStart()
        Intent(this, MusicService::class.java).also { intent ->
            bindService(intent, connection, Context.BIND_AUTO_CREATE)
        }
    }

    override fun onStop() {
        super.onStop()
        if (bound) {
            unbindService(connection)
            bound = false
        }
    }
}

// Dynamic BroadcastReceiver for headphones unplugged
class NoisyReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
            // pause playback
        }
    }
}

// Register dynamically in service
val filter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
registerReceiver(noisyReceiver, filter)

Line-by-line walkthrough

  1. 1. MusicService extends Service and defines an inner MusicBinder class that extends Binder — this is the same-process Binder pattern.
  2. 2. getService() returns the outer Service instance so the bound client gets a direct reference — no serialization, no IPC overhead.
  3. 3. onBind() returns the binder instance; Android delivers this to the client's ServiceConnection.onServiceConnected().
  4. 4. onStartCommand() calls startForeground() immediately with a notification — this is mandatory within 5 seconds of startForegroundService().
  5. 5. START_STICKY ensures the system restarts the service after killing it to reclaim memory, which is correct for a music player.
  6. 6. play() creates a MediaPlayer asynchronously using prepareAsync() to avoid blocking the service thread.
  7. 7. The ServiceConnection in the Activity captures the IBinder, casts it to MusicBinder, and calls getService() — now the Activity has a direct reference to the running Service.
  8. 8. bindService() in onStart() and unbindService() in onStop() is the correct lifecycle pairing — ensures the service is not kept alive unnecessarily.
  9. 9. The NoisyReceiver's onReceive() triggers on headphone unplug — the ACTION_AUDIO_BECOMING_NOISY broadcast tells the app the audio route changed.
  10. 10. Registering the receiver dynamically (in code) instead of the Manifest means it only fires when our service is running — correct scoping.

Spot the bug

class UploadService : Service() {

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val fileUri = intent?.getStringExtra("file_uri")
        uploadFile(fileUri)
        return START_STICKY
    }

    private fun uploadFile(uri: String?) {
        // simulated upload on calling thread
        Thread.sleep(30000) // 30 second upload
        stopSelf()
    }

    override fun onBind(intent: Intent): IBinder? = null
}
Need a hint?
There are two bugs: one causes an ANR, the other causes a silent failure on Android 8+ when the app is in the background.
Show answer
Bug 1: uploadFile() calls Thread.sleep(30000) on the main thread. Service methods run on the main thread by default. Sleeping for 30 seconds here will cause an Application Not Responding (ANR) error after 5 seconds. Fix: move the upload to a background thread (coroutine, Thread, or ExecutorService). Bug 2: This is a background service started via startService(). On Android 8+ (API 26+), the system will kill a background service shortly after the app goes to the background. The upload will be silently cancelled. Fix: use startForegroundService() and call startForeground() with a notification in onStartCommand() before the upload begins, OR use WorkManager for the upload task which handles background execution constraints correctly.

Explain like I'm 5

A BroadcastReceiver is like your phone's alarm app — it waits quietly and wakes up when the exact right signal arrives. A Service is like a TV that keeps playing even after you leave the room. A Bound Service is a TV that also has a remote control you can hold — you press buttons and it responds directly to you.

Fun fact

The Binder IPC mechanism in Android is not just for app developers — it is the backbone of every system service call your app makes. When you call getSystemService(LOCATION_SERVICE), under the hood Android uses Binder to cross the process boundary into the system server. Every Activity launch, every permission check, every notification post — all Binder calls.

Hands-on challenge

Design a podcast player service: (1) Write the Service class with Binder, implementing play(url), pause(), resume(), stop(), and getCurrentPosition(). (2) Show how an Activity binds and unbinds correctly across onStart/onStop. (3) Register a BroadcastReceiver for ACTION_AUDIO_BECOMING_NOISY and handle it. (4) Ensure the foreground notification is shown immediately on startForegroundService() call. Identify the exact lifecycle method ordering when the user navigates away from the app.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery