Ad

Stop A Service By Unbinding All Bounded Activities

I have a started and bounded service (music player) and I want to stop it when the notification's close button is clicked. however, the service will not be stopped if there are activities bounded to it. How can I stop service when the button is clicked?

I have tried gathering a list of activities in service and calling the callback to them to unbind (actually finish() which then on onStop() will call unbind()) and then I stop the service by calling stopSelf() but I don't think it a good idea because lifecycle issues and some activities being added multiple times and maintaining the list is hard . There SHOULD be a better way! though I haven't found anything after searching for hours.

here is my PlayerService's onStartCommand()

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    info("Starting Player Service ...")
    started=true

    if (intent == null) {
        error("intent was null")
        pause()
        return START_STICKY
    }

    if (intent.hasExtra(KEY_SONGS)) {
        if(intent.hasExtra(KEY_PLAYLIST_ID)) playlistId=intent.getStringExtra(KEY_PLAYLIST_ID)
        initExoPlayer(intent)
    } else {
        info("No new song information found for intent, intent's action: ${intent.action}")
    }
    if (intent.action == null) intent.action = ""
    when (intent.action) {
        ACTION_PLAY -> if (isPlaying()) pause() else play()
        ACTION_CANCEL -> {
            //TODO: implement a better and the correct way
            for (client in clients) {
                client.onStop()
            }
            stopSelf()
        }
        else -> showNotification()
    }

    return START_STICKY
}

and my activity:

 playerService.clients.add(object : PlayerService.Client {
        override fun onStop() {
            finish()
        }
    })

And here is my custom notification:

private fun getCustomNotification(name: String, showMainActivity: Intent): Notification {
    /*
        some codes to prepare notificationLayout
     */
    notificationLayout.setTextViewText(R.id.tv_name, name)
    notificationLayout.setImageViewResource(R.id.btn_play, playDrawableID)
    notificationLayout.setOnClickPendingIntent(R.id.btn_play, playPendingIntent)
    notificationLayout.setOnClickPendingIntent(R.id.btn_cancel, cancelPendingIntent)

    // Apply the layouts to the notification
    return NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.logo_mouj)
            .setCustomContentView(notificationLayout)
            .setContentIntent(PendingIntent.getActivity(this, 0, showMainActivity, 0))
            .setOngoing(true)
            .build()

}
Ad

Answer

If this task seems frustrating, it's probably because this is an unusual use case, and somewhat outside what the Android designers had in mind. You may want to ask yourself if there is a more "Android-like" way of accomplishing your goals (that is, without needing to enumerate bound clients).

Consider your requirements:

  1. The player should be available while the user is interacting with the app (via an Activity)
  2. The player should continue to exist while the user has the app in the background
  3. The player should stop when the user taps the notification
  4. The app should get cleaned up when the user is done with it

I think you may have chosen to start playback by binding to the Service, and to end playback by onDestroy()-ing it. This is design error, and it's going to cause you lots of problems. After all, according to the docs, a local, in-process Service really just represents "an application's desire to perform a longer-running operation while not interacting with the user" and it "is not a means itself to do work off of the main thread."

Instead, by keeping each Activity bound in-between onStart() and onStop(), by using startService() when play begins, and stopSelf() when play stops, you will automatically accomplish goals 1, 2, and 4. When the app is in a state where all Activities are offscreen, it will have 0 bound clients. If, in addition to that, no startService() are outstanding, it is a candidate for immediate termination by the OS. In any other state, the OS will keep it around.

This is without any extra fancy logic to track the bound Activities. Of course, you'll probably want to startForeground() while the player is playing, and stopForeground() when it's done (presumably you've already handled that detail).

This leaves the question of how to accomplish goal #3. To effect a state change in the Service, from either the Activities or the notification, you can just use Intents (much like you already are). In this case, the Service would manage its own lifecycle via stopSelf(). This assumes, once again, that you are using startForeground() at the proper times (Oreo+), or else Android may terminate your app due to the background execution limits.

In fact, if you choose to use Intents, you may find that you don't even need to bind your Activities to the Service at all (IOW, goal #1 is irrelevant in this case). You only need to bind to a Service if you need a direct reference to that Service (via the Binder).

Ad
source: stackoverflow.com
Ad