From 617f9898c3f23608f644ab2c345f017305199c55 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 4 Apr 2024 08:59:24 -0700 Subject: [PATCH] Add isPlaybackOngoing and stopMediaSessionService This API additions help an app to implement the lifecycle of a MediaSessionService properly and in consistency with the `MediaSessionService` being in the foreground or not. Not properly implementing `onTaskRemoved` is the main reason for crashes and confusion. This change provides `MediaSessionService` with a default implementation that avoids crashes of the service. This default implementation uses the new API provided with this change just as an app can do. Issue: androidx/media#1219 PiperOrigin-RevId: 621874838 --- RELEASENOTES.md | 9 +++ .../demo/session/DemoPlaybackService.kt | 10 +--- .../media3/session/MediaSessionService.java | 57 +++++++++++++++++++ 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b37dcdc5c35..32e79a970be 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -121,6 +121,15 @@ * Fix issue where `MediaMetadata` with just non-null `extras` is not transmitted between media controllers and sessions ([#1176](https://github.com/androidx/media/issues/1176)). + * Add `MediaSessionService.isPlaybackOngoing()` to let apps query whether + the service needs to be stopped in `onTaskRemoved()` + ([#1219](https://github.com/androidx/media/issues/1219)). + * Add `MediaSessionService.pauseAllPlayersAndStopSelf()` that conveniently + allows to pause playback of all sessions and call `stopSelf` to + terminate the lifecyce of the `MediaSessionService`. + * Override `MediaSessionService.onTaskRemoved(Intent)` to provide a safe + default implementation that keeps the service running in the foreground + if playback is ongoing or stops the service otherwise. * UI: * Fallback to include audio track language name if `Locale` cannot identify a display name diff --git a/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt b/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt index 3bd5440bddc..f34d864658b 100644 --- a/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt +++ b/demos/session_service/src/main/java/androidx/media3/demo/session/DemoPlaybackService.kt @@ -19,7 +19,6 @@ import android.Manifest import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent -import android.content.Intent import android.content.pm.PackageManager import android.os.Build import androidx.annotation.OptIn @@ -90,13 +89,6 @@ open class DemoPlaybackService : MediaLibraryService() { return mediaLibrarySession } - override fun onTaskRemoved(rootIntent: Intent?) { - val player = mediaLibrarySession.player - if (!player.playWhenReady || player.mediaItemCount == 0) { - stopSelf() - } - } - // MediaSession.setSessionActivity // MediaSessionService.clearListener @OptIn(UnstableApi::class) @@ -166,7 +158,7 @@ open class DemoPlaybackService : MediaLibraryService() { NotificationChannel( CHANNEL_ID, getString(R.string.notification_channel_name), - NotificationManager.IMPORTANCE_DEFAULT + NotificationManager.IMPORTANCE_DEFAULT, ) notificationManagerCompat.createNotificationChannel(channel) } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index beb6e71e8b5..120a5a55015 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.postOrRun; +import android.app.Activity; import android.app.ForegroundServiceStartNotAllowedException; import android.app.Service; import android.content.ComponentName; @@ -469,6 +470,62 @@ private static ControllerInfo createFallbackMediaButtonCaller(Intent mediaButton /* connectionHints= */ Bundle.EMPTY); } + /** + * Returns whether there is a session with ongoing playback that must be paused or stopped before + * being able to terminate the service by calling {@link #stopSelf()}. + */ + @UnstableApi + public boolean isPlaybackOngoing() { + return getMediaNotificationManager().isStartedInForeground(); + } + + /** + * Pauses the player of each session managed by the service and calls {@link #stopSelf()}. + * + *

This terminates the service lifecycle and triggers {@link #onDestroy()} that an app can + * override to release the sessions and other resources. + */ + @UnstableApi + public void pauseAllPlayersAndStopSelf() { + List sessionList = getSessions(); + for (int i = 0; i < sessionList.size(); i++) { + sessionList.get(i).getPlayer().setPlayWhenReady(false); + } + stopSelf(); + } + + /** + * {@inheritDoc} + * + *

If {@linkplain #isPlaybackOngoing() playback is ongoing}, the service continues running in + * the foreground when the app is dismissed from the recent apps. Otherwise, the service is + * stopped by calling {@link #stopSelf()} which terminates the service lifecycle and triggers + * {@link #onDestroy()} that an app can override to release the sessions and other resources. + * + *

An app can safely override this method without calling super to implement a different + * behaviour, for instance unconditionally calling {@link #pauseAllPlayersAndStopSelf()} to stop + * the service even when playing. However, if {@linkplain #isPlaybackOngoing() playback is not + * ongoing}, the service must be terminated otherwise the service will be crashed and restarted by + * the system. + * + *

Note: The service can't + * be stopped until all media controllers have been unbound. Hence, an app needs to release + * all internal controllers that have connected to the service (for instance from an activity in + * {@link Activity#onStop()}). If an app allows external apps to connect a {@link MediaController} + * to the service, these controllers also need to be disconnected. In such a scenario of external + * bound clients, an app needs to override this method to release the session before calling + * {@link #stopSelf()}. + */ + @Override + public void onTaskRemoved(@Nullable Intent rootIntent) { + if (!isPlaybackOngoing()) { + // The service needs to be stopped when playback is not ongoing and the service is not in the + // foreground. + stopSelf(); + } + } + /** * Called when the service is no longer used and is being removed. *