diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index e452bead1f..710070b2ef 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -51,7 +51,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -126,16 +125,17 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi @Override public final MediaNotification createNotification( - MediaController mediaController, + MediaSession mediaSession, ImmutableList customLayout, MediaNotification.ActionFactory actionFactory, Callback onNotificationChangedCallback) { ensureNotificationChannel(); + Player player = mediaSession.getPlayer(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); // Set metadata info in the notification. - MediaMetadata metadata = mediaController.getMediaMetadata(); + MediaMetadata metadata = player.getMediaMetadata(); builder.setContentTitle(metadata.title).setContentText(metadata.artist); @Nullable ListenableFuture bitmapFuture = loadArtworkBitmap(metadata); @@ -164,19 +164,16 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi MediaStyle mediaStyle = new MediaStyle(); int[] compactViewIndices = addNotificationActions( - getMediaButtons( - mediaController.getAvailableCommands(), - customLayout, - mediaController.getPlayWhenReady()), + getMediaButtons(player.getAvailableCommands(), customLayout, player.getPlayWhenReady()), builder, actionFactory); mediaStyle.setShowActionsInCompactView(compactViewIndices); - if (mediaController.isCommandAvailable(COMMAND_STOP) || Util.SDK_INT < 21) { + if (player.isCommandAvailable(COMMAND_STOP) || Util.SDK_INT < 21) { // We must include a cancel intent for pre-L devices. mediaStyle.setCancelButtonIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)); } - long playbackStartTimeMs = getPlaybackStartTimeEpochMs(mediaController); + long playbackStartTimeMs = getPlaybackStartTimeEpochMs(player); boolean displayElapsedTimeWithChronometer = playbackStartTimeMs != C.TIME_UNSET; builder .setWhen(playbackStartTimeMs) @@ -185,7 +182,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi Notification notification = builder - .setContentIntent(mediaController.getSessionActivity()) + .setContentIntent(mediaSession.getSessionActivity()) .setDeleteIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)) .setOnlyAlertOnce(true) .setSmallIcon(getSmallIconResId(context)) @@ -197,34 +194,9 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi } @Override - public final void handleCustomCommand( - MediaController mediaController, String action, Bundle extras) { - @Nullable SessionCommand customCommand = null; - for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) { - if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM - && command.customAction.equals(action)) { - customCommand = command; - break; - } - } - if (customCommand != null) { - ListenableFuture future = - mediaController.sendCustomCommand(customCommand, extras); - Futures.addCallback( - future, - new FutureCallback() { - @Override - public void onSuccess(SessionResult result) { - // Do nothing. - } - - @Override - public void onFailure(Throwable t) { - Log.w(TAG, "custom command " + action + " produced an error: " + t.getMessage(), t); - } - }, - MoreExecutors.directExecutor()); - } + public final boolean handleCustomCommand(MediaSession session, String action, Bundle extras) { + // Make the custom action being delegated to the session as a custom session command. + return false; } /** @@ -422,14 +394,14 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi } } - private static long getPlaybackStartTimeEpochMs(MediaController controller) { + private static long getPlaybackStartTimeEpochMs(Player player) { // Changing "showWhen" causes notification flicker if SDK_INT < 21. if (Util.SDK_INT >= 21 - && controller.isPlaying() - && !controller.isPlayingAd() - && !controller.isCurrentMediaItemDynamic() - && controller.getPlaybackParameters().speed == 1f) { - return System.currentTimeMillis() - controller.getContentPosition(); + && player.isPlaying() + && !player.isPlayingAd() + && !player.isCurrentMediaItemDynamic() + && player.getPlaybackParameters().speed == 1f) { + return System.currentTimeMillis() - player.getContentPosition(); } else { return C.TIME_UNSET; } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java index 0632c80c48..996cd51702 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java @@ -74,7 +74,7 @@ public final class MediaNotification { *

The returned {@link NotificationCompat.Action} will have a {@link PendingIntent} with the * extras from {@link SessionCommand#customExtras}. Accordingly the {@linkplain * SessionCommand#customExtras command's extras} will be passed to {@link - * Provider#handleCustomCommand(MediaController, String, Bundle)} when the action is executed. + * Provider#handleCustomCommand(MediaSession, String, Bundle)} when the action is executed. * * @param customCommandButton A {@linkplain CommandButton custom command button}. * @see MediaNotification.Provider#handleCustomCommand @@ -116,8 +116,8 @@ public final class MediaNotification { /** * Creates a new {@link MediaNotification}. * - * @param mediaController The controller of the session. - * @param actionFactory The {@link ActionFactory} for creating notification {@linkplain + * @param session The media session. + * @param actionFactory The {@link ActionFactory} for creating notification {@link * NotificationCompat.Action actions}. * @param customLayout The custom layout {@linkplain MediaSession#setCustomLayout(List) set by * the session}. @@ -126,7 +126,7 @@ public final class MediaNotification { * been loaded asynchronously. */ MediaNotification createNotification( - MediaController mediaController, + MediaSession session, ImmutableList customLayout, ActionFactory actionFactory, Callback onNotificationChangedCallback); @@ -134,13 +134,15 @@ public final class MediaNotification { /** * Handles a notification's custom command. * - * @param mediaController The controller of the session. + * @param session The media session. * @param action The custom command action. * @param extras A bundle {@linkplain SessionCommand#customExtras set in the custom command}, * otherwise {@link Bundle#EMPTY}. + * @return {@code false} if the action should be delivered to the session as a custom command or + * {@code true} if the action has been handled completely by the provider. * @see ActionFactory#createCustomAction */ - void handleCustomCommand(MediaController mediaController, String action, Bundle extras); + boolean handleCustomCommand(MediaSession session, String action, Bundle extras); } /** The notification id. */ diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index 001ef42cbb..d6385d725b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -32,8 +32,10 @@ import androidx.media3.common.Player; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -122,28 +124,44 @@ import java.util.concurrent.TimeoutException; return; } try { - MediaController mediaController = controllerFuture.get(0, TimeUnit.MILLISECONDS); - mediaNotificationProvider.handleCustomCommand(mediaController, action, extras); - } catch (InterruptedException | ExecutionException | TimeoutException e) { + MediaController mediaController = checkStateNotNull(Futures.getDone(controllerFuture)); + if (!mediaNotificationProvider.handleCustomCommand(session, action, extras)) { + @Nullable SessionCommand customCommand = null; + for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) { + if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM + && command.customAction.equals(action)) { + customCommand = command; + break; + } + } + if (customCommand != null + && mediaController.getAvailableSessionCommands().contains(customCommand)) { + ListenableFuture future = + mediaController.sendCustomCommand(customCommand, Bundle.EMPTY); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(SessionResult result) { + // Do nothing. + } + + @Override + public void onFailure(Throwable t) { + Log.w( + TAG, "custom command " + action + " produced an error: " + t.getMessage(), t); + } + }, + MoreExecutors.directExecutor()); + } + } + } catch (ExecutionException e) { // We should never reach this. throw new IllegalStateException(e); } } public void updateNotification(MediaSession session) { - @Nullable ListenableFuture controllerFuture = controllerMap.get(session); - if (controllerFuture == null) { - return; - } - - MediaController mediaController; - try { - mediaController = checkStateNotNull(Futures.getDone(controllerFuture)); - } catch (ExecutionException e) { - // We should never reach this point. - throw new IllegalStateException(e); - } - if (!mediaSessionService.isSessionAdded(session) || !canStartPlayback(session.getPlayer())) { maybeStopForegroundService(/* removeNotifications= */ true); return; @@ -157,10 +175,7 @@ import java.util.concurrent.TimeoutException; MediaNotification mediaNotification = this.mediaNotificationProvider.createNotification( - mediaController, - checkStateNotNull(customLayoutMap.get(session)), - actionFactory, - callback); + session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback); updateNotificationInternal(session, mediaNotification); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index db269b2689..cc91b1ae3e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -35,6 +35,7 @@ import android.view.KeyEvent; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.app.NotificationCompat; import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media3.common.AudioAttributes; import androidx.media3.common.DeviceInfo; @@ -247,6 +248,14 @@ public class MediaSession { * Sets a {@link PendingIntent} to launch an {@link android.app.Activity} for the {@link * MediaSession}. This can be used as a quick link to an ongoing media screen. * + *

A client can use this pending intent to start an activity belonging to this session. When + * this pending intent is for instance included in the notification {@linkplain + * NotificationCompat.Builder#setContentIntent(PendingIntent) as the content intent}, tapping + * the notification will open this activity. + * + *

See 'Start an + * Activity from a Notification' also. + * * @param pendingIntent The pending intent. * @return The builder to allow chaining. */ @@ -514,6 +523,17 @@ public class MediaSession { return null; } + /** + * Returns the {@link PendingIntent} to launch {@linkplain + * Builder#setSessionActivity(PendingIntent) the session activity} or null if not set. + * + * @return The {@link PendingIntent} to launch an activity belonging to the session. + */ + @Nullable + public PendingIntent getSessionActivity() { + return impl.getSessionActivity(); + } + /** * Sets the underlying {@link Player} for this session to dispatch incoming events to. *