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
This commit is contained in:
bachinger 2024-04-04 08:59:24 -07:00 committed by Copybara-Service
parent f9ed303bf1
commit 617f9898c3
3 changed files with 67 additions and 9 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
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 @@ public abstract class MediaSessionService extends Service {
/* 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()}.
*
* <p>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<MediaSession> sessionList = getSessions();
for (int i = 0; i < sessionList.size(); i++) {
sessionList.get(i).getPlayer().setPlayWhenReady(false);
}
stopSelf();
}
/**
* {@inheritDoc}
*
* <p>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.
*
* <p>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.
*
* <p>Note: The service <a
* href="https://developer.android.com/develop/background-work/services/bound-services#Lifecycle">can't
* be stopped</a> 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.
*