MediaSessionService: allow apps to opt-out from notifications

Issue: androidx/media#50
PiperOrigin-RevId: 447435259
This commit is contained in:
christosts 2022-05-09 12:26:50 +01:00 committed by Ian Baker
parent 2e544224c2
commit 1b15d5c370
2 changed files with 73 additions and 29 deletions

View File

@ -73,7 +73,7 @@ import java.util.concurrent.TimeoutException;
if (controllerMap.containsKey(session)) {
return;
}
MediaControllerListener listener = new MediaControllerListener(session);
MediaControllerListener listener = new MediaControllerListener(mediaSessionService, session);
ListenableFuture<MediaController> controllerFuture =
new MediaController.Builder(mediaSessionService, session.getToken())
.setListener(listener)
@ -118,7 +118,7 @@ import java.util.concurrent.TimeoutException;
}
}
private void updateNotification(MediaSession session) {
public void updateNotification(MediaSession session) {
@Nullable ListenableFuture<MediaController> controllerFuture = controllerMap.get(session);
if (controllerFuture == null) {
return;
@ -132,6 +132,11 @@ import java.util.concurrent.TimeoutException;
throw new IllegalStateException(e);
}
if (!mediaSessionService.isSessionAdded(session) || !canStartPlayback(session.getPlayer())) {
maybeStopForegroundService(/* removeNotifications= */ true);
return;
}
int notificationSequence = ++totalNotificationCount;
MediaNotification.Provider.Callback callback =
notification ->
@ -140,17 +145,18 @@ import java.util.concurrent.TimeoutException;
MediaNotification mediaNotification =
this.mediaNotificationProvider.createNotification(mediaController, actionFactory, callback);
updateNotification(session, mediaNotification);
updateNotificationInternal(session, mediaNotification);
}
private void onNotificationUpdated(
int notificationSequence, MediaSession session, MediaNotification mediaNotification) {
if (notificationSequence == totalNotificationCount) {
updateNotification(session, mediaNotification);
updateNotificationInternal(session, mediaNotification);
}
}
private void updateNotification(MediaSession session, MediaNotification mediaNotification) {
private void updateNotificationInternal(
MediaSession session, MediaNotification mediaNotification) {
if (Util.SDK_INT >= 21) {
// Call Notification.MediaStyle#setMediaSession() indirectly.
android.media.session.MediaSession.Token fwkToken =
@ -188,7 +194,7 @@ import java.util.concurrent.TimeoutException;
}
}
// To hide the notification on all API levels, we need to call both Service.stopForeground(true)
// and notificationManagerCompat.cancelAll(). For pre-L devices, we must also call
// and notificationManagerCompat.cancel(notificationId). For pre-L devices, we must also call
// Service.stopForeground(true) anyway as a workaround that prevents the media notification from
// being undismissable.
mediaSessionService.stopForeground(removeNotifications || Util.SDK_INT < 21);
@ -209,40 +215,40 @@ import java.util.concurrent.TimeoutException;
return player.getPlaybackState() != Player.STATE_IDLE && !player.getCurrentTimeline().isEmpty();
}
private final class MediaControllerListener implements MediaController.Listener, Player.Listener {
private static final class MediaControllerListener
implements MediaController.Listener, Player.Listener {
private final MediaSessionService mediaSessionService;
private final MediaSession session;
public MediaControllerListener(MediaSession session) {
public MediaControllerListener(MediaSessionService mediaSessionService, MediaSession session) {
this.mediaSessionService = mediaSessionService;
this.session = session;
}
public void onConnected() {
if (canStartPlayback(session.getPlayer())) {
updateNotification(session);
// We need to present a notification.
mediaSessionService.onUpdateNotification(session);
}
}
@Override
public void onEvents(Player player, Player.Events events) {
if (!canStartPlayback(player)) {
maybeStopForegroundService(/* removeNotifications= */ true);
return;
}
// Limit the events on which we may update the notification to ensure we don't update the
// notification too frequently, otherwise the system may suppress notifications.
// We must limit the frequency of notification updates, otherwise the system may suppress
// them.
if (events.containsAny(
Player.EVENT_PLAYBACK_STATE_CHANGED,
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_MEDIA_METADATA_CHANGED)) {
updateNotification(session);
mediaSessionService.onUpdateNotification(session);
}
}
@Override
public void onDisconnected(MediaController controller) {
mediaSessionService.removeSession(session);
maybeStopForegroundService(/* removeNotifications= */ true);
// We may need to hide the notification.
mediaSessionService.onUpdateNotification(session);
}
}
}

View File

@ -94,14 +94,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* controller. If it's accepted, the controller will be available and keep the binding. If it's
* rejected, the controller will unbind.
*
* <p>When a playback is started on the service, the service will obtain a {@link MediaNotification}
* from the {@link MediaNotification.Provider} that's set with {@link #setMediaNotificationProvider}
* (or {@link DefaultMediaNotificationProvider}, if no provider is set), and the service will become
* a <a href="https://developer.android.com/guide/components/foreground-services">foreground
* service</a>. It's required to keep the playback after the controller is destroyed. The service
* will become a background service when all playbacks are stopped. Apps targeting {@code SDK_INT >=
* 28} must request the permission, {@link android.Manifest.permission#FOREGROUND_SERVICE}, in order
* to make the service foreground.
* <p>{@link #onUpdateNotification(MediaSession)} will be called whenever a notification needs to be
* shown, updated or cancelled. The default implementation will display notifications using a
* default UI or using a {@link MediaNotification.Provider} that's set with {@link
* #setMediaNotificationProvider}. In addition, when playback starts, the service will become a <a
* href="https://developer.android.com/guide/components/foreground-services">foreground service</a>.
* It's required to keep the playback after the controller is destroyed. The service will become a
* background service when all playbacks are stopped. Apps targeting {@code SDK_INT >= 28} must
* request the permission, {@link android.Manifest.permission#FOREGROUND_SERVICE}, in order to make
* the service foreground. You can control when to show or hide notifications by overriding {@link
* #onUpdateNotification(MediaSession)}. In this case, you must also start or stop the service from
* the foreground, when playback starts or stops respectively.
*
* <p>The service will be destroyed when all sessions are closed, or no controller is binding to the
* service while the service is in the background.
@ -263,6 +266,16 @@ public abstract class MediaSessionService extends Service {
}
}
/**
* Returns whether {@code session} has been added to this service via {@link #addSession} or
* {@link #onGetSession(ControllerInfo)}.
*/
public final boolean isSessionAdded(MediaSession session) {
synchronized (lock) {
return sessions.containsKey(session.getId());
}
}
/**
* Called when a component is about to bind to the service.
*
@ -373,12 +386,37 @@ public abstract class MediaSessionService extends Service {
}
}
/**
* Called when a notification needs to be updated. Override this method to show or cancel your own
* notifications.
*
* <p>This method is called whenever the service has detected a change that requires to show,
* update or cancel a notification. The method will be called on the application thread of the app
* that the service belongs to.
*
* <p>Override this method to create your own notification and customize the foreground handling
* of your service.
*
* <p>The default implementation will present a default notification or the notification provided
* by the {@link MediaNotification.Provider} that is {@link
* #setMediaNotificationProvider(MediaNotification.Provider) set} by the app. Further, the service
* is started in the <a
* href="https://developer.android.com/guide/components/foreground-services">foreground</a> when
* playback is ongoing and put back into background otherwise.
*
* <p>Apps targeting {@code SDK_INT >= 28} must request the permission, {@link
* android.Manifest.permission#FOREGROUND_SERVICE}.
*
* @param session A session that needs notification update.
*/
public void onUpdateNotification(MediaSession session) {
getMediaNotificationManager().updateNotification(session);
}
/**
* Sets the {@link MediaNotification.Provider} to customize notifications.
*
* <p>This should be called before any session is attached to this service via {@link
* #onGetSession(ControllerInfo)} or {@link #addSession(MediaSession)}. Otherwise a default UX
* will be shown with {@link DefaultMediaNotificationProvider}.
* <p>This should be called before {@link #onCreate()} returns.
*/
@UnstableApi
protected final void setMediaNotificationProvider(