4

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()

}
MH. Abdi
  • 298
  • 5
  • 22
  • Looks to me like this question could also be phrased as, "how do I close all my Activities from a Service?" Could you explain how this is different from [this question](https://stackoverflow.com/questions/25754554/go-to-home-screen-from-android-activity) or using `System.exit(0)`? – greeble31 Feb 02 '19 at 16:54
  • I dont nessessary need to close my Activities. I just simply need to stop my service, but because activities are bound to it, they need to "unbind" , not close – MH. Abdi Feb 02 '19 at 17:04
  • OK, so why do you need the `Service` to stop? Are you trying to get it to `onDestroy()` for some reason? If your Activities don't need to reference the `Service` while they're visible, why did you bind them in the first place? – greeble31 Feb 02 '19 at 17:35
  • Its a music player. I want to close because user wants to stop music and close the app, and binding is for controlling the music. – MH. Abdi Feb 02 '19 at 17:37
  • You lost me a little bit -- it looks from your code like play is controlled via `Intents` in `onStartCommand()`. – greeble31 Feb 02 '19 at 17:38
  • These intents are for controlling the service from notification. When the user clicks on close, the ACTION_CANCEL will be passed to it and now I want to close my service, but the activities may still be bound to it and prevent it from closing – MH. Abdi Feb 02 '19 at 17:41

2 Answers2

1

Try something like this

fun stopServices() {
    val serviceIntent = Intent(this, NotificationService::class.java)
    serviceIntent.action = Constants.ACTION.STOPTFOREGROUND_ACTION
    stopService(serviceIntent)
}

in your activity Call this method

in your Service start command

 ACTION.STOPFOREGROUND_ACTION -> {
            //Toast.makeText(this, "Service Stoped", Toast.LENGTH_SHORT).show();
            stopForeground(true);
            stopSelf();
            Intent intents = new Intent("com.app.package.ACTION_STOP");
            LocalBroadcastManager.getInstance(this).sendBroadcast(intents);
            //startActivity(new Intent(this,MainActivity.class));

        }



val notificationLayout= new RemoteViews(getPackageName(), R.layout.custom_layout);


 Intent closeIntent = new Intent(this, NotificationService.class);
    closeIntent.setAction(Constants.ACTION.STOPFOREGROUND_ACTION);
    PendingIntent cancelPendingIntent= PendingIntent.getService(this, 0, closeIntent, 0);

close button click

    notificationLayout.setOnClickPendingIntent(R.id.btn_cancel, cancelPendingIntent);
Praveen
  • 946
  • 6
  • 14
  • It doesn't work, you are just sending an intent with action when receiving it and I don't really understand what the code is supposed to do. What I want is unbinding all activities so the service can stop. – MH. Abdi Feb 02 '19 at 10:43
  • are you using a custom notification layout for Music player? – Praveen Feb 02 '19 at 10:48
  • Yes. thanks for reminding, I'll add it to the question – MH. Abdi Feb 02 '19 at 10:49
  • the problem is not intent for closing the notification,its already done. the problem is how to unbind all activities so the service can stop – MH. Abdi Feb 02 '19 at 11:15
1

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).

greeble31
  • 4,894
  • 2
  • 16
  • 30
  • I am binding them on `onStart` and unbinding them on `onStop` but that still leaves the question of how to stop the service when I'm still in one of the activities.If I call `stopSelf` the service will NOT stop.I need to detach all the activities first. The reason that im binding to the service is that I need the iBinder to call the functions on the service and get some information at the task at hand. maybe I didn't understand your answer. thank you for your detailed answer – MH. Abdi Feb 02 '19 at 18:09
  • "how to stop the service when I'm still in one of the activities" <-- this is where you're going wrong. You need to stop the music, not the `Service`. Consider that for a moment, let me know your thoughts. ("...a service is not a means itself to do work off the main thread...") – greeble31 Feb 02 '19 at 18:18
  • The typical approach is, if your `Activity` needs the `Service`, then it has a "dependency" on that `Service`, and it should need it all the time - from `onStart()` to `onStop()`. I don't see any IBinder code in your question, but I find it hard to envision a use case where an `Activity` needs to talk to the `Service/Binder` (in order to arrange a playlist or whatever), but then _can tolerate the early loss of that `Binder` b/c you intentionally destroyed the `Service` before the `Activity` was complete_. – greeble31 Feb 02 '19 at 18:21
  • so what you are saying is I should stop the music, and the service will stop whenever all activities are closed. thanks, I didn't think of it this way! I'll try it – MH. Abdi Feb 02 '19 at 18:24
  • 1
    Yes, or put another way, you always want your `Service` to be around when your `Activities` are open, b/c the `Service` contains the player, and the `Activities` need to talk to that player. And the `Service` can keep itself around even longer, so it can play in the "background", using `startService()`, as long as you follow the `startForeground()` rules (post-Oreo). Good luck. – greeble31 Feb 02 '19 at 18:29