Adds androidStopForegroundOnCompleted to AudioServiceConfig which will stop foreground service when AudioProcessingState == AudioProcessingState.completed
This PR adds a AudioServiceConfig parameter androidStopForegroundOnCompleted which will stop foreground service when AudioProcessingState == AudioProcessingState.completed. I added this as a Feature Request as an app we are working on requires a solution like this.
We have an app that uses multiple audio files to play to the user in one stream of audio. Throughout this process we pause/unpause so the androidStopForegroundOnPause causes issues when playing in background as we do not have permissions to startForeground from the background. If we turn set androidStopForegroundOnPause to false, the notification is not hidden when the audio is completed or the user removes from the task list.
Using the feature added in this PR would allow the notification to be dismissed when the audio is completed.
See the feature request issue here: https://github.com/ryanheise/audio_service/issues/1053
Pre-launch Checklist
- [x] I read the [CONTRIBUTING.md] and followed the process outlined there for submitting PRs.
- [x] My change is not breaking and lands in
minorbranch OR my change is breaking and lands inmajorbranch. - [x] If I'm the first to contribute to the next version, I incremented the version number in
pubspec.yamlaccording to the [pub versioning philosophy]. - [x] I updated CHANGELOG.md to add a description of the change (format:
* DESCRIPTION OF YOUR CHANGE (@your-git-username)). - [x] I updated/added relevant documentation (doc comments with
///). - [x] I ran
dart analyze. - [x] I ran
dart format. - [x] I ran
flutter testand all tests are passing.
as we do not have permissions to startForeground from the background.
Have you had a look at https://github.com/ryanheise/audio_service/issues/932#issuecomment-1382729296 >
Hi Ryan, yes thanks. I have looked at all of the threads regarding this issue (I believe). We do not think the battery optimization workaround is the best approach for us though we considered it. For our application, it seems better to control when stopForeground being called when the audio is complete or stopped rather than changing the battery optimization level so we can start/stop the foreground service whenever we want. We do not really need to start/stop foreground service during playback, we just have audio process with multiple pauses though for the user, it is one continual audio stream. Hope this makes sense.
It would also be possible to use the current implementation as is and just emit a !playing state from your app to trigger when you want to stop foreground, but I'll have a think about the various combinations that need to be supported. It may turn out that we don't need to support all combinations since some of them are meaningless. E.g. Perhaps stopForeground should always be called from the completed state.
Thanks, Ryan. Yes, I agree that would be another solution. I added the parameter option in this PR in case others do not want stopForeground when audio is completed. Thanks for taking the time to think through this.
I may have misunderstood your response. In the current implementation emitting a !playing state does NOT trigger stopForeground. We would need adding that code to a non-playing state, such as the completed state. The PR implements it in the completed state with a parameter to turn that off/on.
I may have misunderstood your response. In the current implementation emitting a !playing state does NOT trigger stopForeground.
It should if you use the default config parameter stopForegroundOnPause.
Right. That is what I was explaining above. Our audio pauses/unpauses throughout playback. We don't want to keep stopping foreground for each but only when it is complete. We run into exceptions for not having permissions to start foreground while in background.
Could be just implementing stopForeground when state is completed.
Yes, it would be nice if it turns out that it always makes sense to stopForeground on completed.
The question is whether anyone has any use case where, for some reason, they don't want to stopForeground on completed, although it feels right that logically the pause state is similar to the completed state in this context.
Hi @ryanheise, if you have a specific reservation about this PR, I'd be happy to address it. I believe these changes will help others who are running into this same issue where the app requires stopForeground on audio complete (not on pause). I'm open to making any necessary adjustments.
Hey @skiluk, I was looking for a solution to this problem and came across your PR. I tried your patch on a phone with Android 8 and the result was positive: The media notification disappeared on completed state. However nothing changed on a phone with Android 14. Maybe @ryanheise was (https://github.com/ryanheise/audio_service/issues/462#issuecomment-691633624) right: One does not simply dismiss a media notification on Android.
Thank you for this. The ability to control the lifecycle of the audio service is a must, given that we can no longer resume the service without direct user input.
I made a fork of this branch with a few minor modifications that I believe better align with best practices: https://github.com/Colton127/audio_service
AudioProcessingState.idle: Stop foreground service and remove notification. It successfully removes the notification on Android 14 and Android 8, but not 11, which I think may not be possible.
AudioProcessingState.completed: Stop foreground service, but keep notification active, allowing users to restart playback at a later time through the notification.
I believe the lifecycle of the service should mirror that of Spotify and YouTube: The service remains active until the notification or app is swiped away by the user.
Hi @Colton127 , I would be interested to review the diffs of what changed, although I see your diff contains a lot of formatting changes which unfortunately makes it difficult to review.
In any case, the problem with all of the various proposals is that they are all different. This means that each person may have their own preferred way that it would work. So what I'm thinking is that the plugin should provide a sensible default, but then should also provide direct access to the underlying stopForeground API so that anyone who needs flexibility can call it exactly when they want, and disable the androidStopForegroundOnPause option.
By the way @skiluk , I think you can already emulate this today without any code changes by setting androidStopForegroundOnPause: false and then whenever you've finished playing the last media item in your queue, set the processing state to idle which will effectively call stopForeground. So the only difference between this and my potentially exposing the stopForeground method for you to call directly when you want is that perhaps the latter would allow also exposing the different flag parameter options. I suspect you don't need those, however, and so setting the processing state to idle should suffice.
Regarding the androidStopForegroundOnPause option, this is supposed to be the sensible default, and there is an opportunity with the next major release to make some changes to this if we think it's current behaviour is not enough. I think it is sensible to automatically call stopForeground on pause and idle, as it currently does, but also on complete and on error, which it does not currently do. So if I were to make a change to the default behaviour, it would be to rename this option to something more inclusive, and make it stopForeground on pause/idle/complete/error.
Thoughts?
@ryanheise Apologies for the inconvenience -- I have updated my repo to align with standard Dart formatting.
In my testing, setting AudioProcessingState to idle while using androidStopForegroundOnPause: false does not stop the audio service. My branch fixed this by calling legacyStopForeground(true) upon the processing state being set to idle. My understanding is that the stop() function, calling stopSelf(), does not immediately destroy the service. It just puts it in a state where it is eligible to be destroyed by the system.
To reiterate, I think the best practice is to align the service lifecycle with AudioProcessingState. AudioProcessingState.idle indicates that the service is not currently needed, and it must be started within the app itself. AudioProcessingState.completed only differs in that the notification is still present, allowing audio playback to be resumed through the media notification by the user. The other AudioProcessingState's indicate that the app is actively using the service. This only differs from @skiluk PR here in how the idle state is managed.
In my testing, setting AudioProcessingState to idle while using androidStopForegroundOnPause: false does not stop the audio service.
Hmm, I wonder why that would be. On idle, the service should be stopped and onDestroy should also stop foreground.
In my testing, setting AudioProcessingState to idle while using androidStopForegroundOnPause: false does not stop the audio service.
Hmm, I wonder why that would be. On
idle, the service should be stopped andonDestroyshould also stop foreground.
This was my understanding as well, but is not the case in practice:
Using the latest audio_service example project, I changed AudioServiceConfig to:
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.ryanheise.myapp.channel.audio',
androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: false,
androidStopForegroundOnPause: false,
),
and adding the current processing state to the UI:
StreamBuilder<AudioProcessingState>(
stream: _audioHandler.playbackState.map((state) => state.processingState).distinct(),
builder: (context, snapshot) {
final processingState = snapshot.data;
return Text('Processing state: ${processingState?.name}');
},
),
Hitting "stop" does set the processing state to idle, but does not stop the service, nor the notification.
I tested on:
- Galaxy S22U (Android 14, OneUI 6.1)
- Android Emulator (Android 15) (Flutter 3.27.1)
From my research, stopSelf notifies Android that the service needs to stop, but it is up to Android to call onDestroy. Since there's a million variants of Android, the end behavior is ultimately unpredictable. The workaround is to call stopForeground before stopSelf, guaranteeing the service is stopped.
@ryanheise You mentioned somewhere that you would be okay with exposing stopForeground() directly. I believe that would help as this could be called when needed. I would be happy to create a PR for this if this would help.
Yes, I mentioned that above here: https://github.com/ryanheise/audio_service/pull/1054#issuecomment-2543149262
Thanks for your feedback on that, as I do think this is probably the way forward, but was waiting for some sort of feedback before I proceeded with it. There are still some questions about the API, though. We can't just add this as a new method, we also need a way of disabling the automatic management of stopForeground that is currently happening. One thought is that androidStopForegroundOnPause could serve that purpose, but even when this is false, it will still call stopForeground on dispose without giving the app the opportunity to choose the parameters for stopForeground. The next thought is that we can simply move the automatic management of stopForeground into the app's stop() implementation, so that the app can override it. But this is not strictly backward compatible because there may be apps that already override this method without calling super.stop() and those apps will find that stopForeground is no longer automatically being called. OK, there may be a hack we can do here such as have the default implementation of stop signal that it is still being called so that the plugin knows whether the app overrode it or not, and will fallback to automatically calling stopForeground if necessary... The other option, then, is to add an additional config option to allow full manual management of stopForeground.
I'm not sure which of these options is best.
any update on this PR ?