diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java index cf63e6cc4b..699f9cb991 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java @@ -66,20 +66,25 @@ import androidx.media3.common.util.Util; @Override public NotificationCompat.Action createMediaAction( - IconCompat icon, CharSequence title, @Player.Command int command) { - return new NotificationCompat.Action(icon, title, createMediaActionPendingIntent(command)); + MediaSession mediaSession, IconCompat icon, CharSequence title, @Player.Command int command) { + return new NotificationCompat.Action( + icon, title, createMediaActionPendingIntent(mediaSession, command)); } @Override public NotificationCompat.Action createCustomAction( - IconCompat icon, CharSequence title, String customAction, Bundle extras) { + MediaSession mediaSession, + IconCompat icon, + CharSequence title, + String customAction, + Bundle extras) { return new NotificationCompat.Action( - icon, title, createCustomActionPendingIntent(customAction, extras)); + icon, title, createCustomActionPendingIntent(mediaSession, customAction, extras)); } @Override public NotificationCompat.Action createCustomActionFromCustomCommandButton( - CommandButton customCommandButton) { + MediaSession mediaSession, CommandButton customCommandButton) { checkArgument( customCommandButton.sessionCommand != null && customCommandButton.sessionCommand.commandCode @@ -88,13 +93,16 @@ import androidx.media3.common.util.Util; return new NotificationCompat.Action( IconCompat.createWithResource(service, customCommandButton.iconResId), customCommandButton.displayName, - createCustomActionPendingIntent(customCommand.customAction, customCommand.customExtras)); + createCustomActionPendingIntent( + mediaSession, customCommand.customAction, customCommand.customExtras)); } @Override - public PendingIntent createMediaActionPendingIntent(@Player.Command long command) { + public PendingIntent createMediaActionPendingIntent( + MediaSession mediaSession, @Player.Command long command) { int keyCode = toKeyCode(command); Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setData(mediaSession.getImpl().getUri()); intent.setComponent(new ComponentName(service, service.getClass())); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); if (Util.SDK_INT >= 26 && command == COMMAND_PLAY_PAUSE) { @@ -126,8 +134,10 @@ import androidx.media3.common.util.Util; return KEYCODE_UNKNOWN; } - private PendingIntent createCustomActionPendingIntent(String action, Bundle extras) { + private PendingIntent createCustomActionPendingIntent( + MediaSession mediaSession, String action, Bundle extras) { Intent intent = new Intent(ACTION_CUSTOM); + intent.setData(mediaSession.getImpl().getUri()); intent.setComponent(new ComponentName(service, service.getClass())); intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM, action); intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS, extras); 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 dc88c45510..992a6fdad0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -140,6 +140,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi MediaStyle mediaStyle = new MediaStyle(); int[] compactViewIndices = addNotificationActions( + mediaSession, getMediaButtons(player.getAvailableCommands(), customLayout, player.getPlayWhenReady()), builder, actionFactory); @@ -173,7 +174,8 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi 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)); + mediaStyle.setCancelButtonIntent( + actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP)); } long playbackStartTimeMs = getPlaybackStartTimeEpochMs(player); @@ -186,7 +188,8 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi Notification notification = builder .setContentIntent(mediaSession.getSessionActivity()) - .setDeleteIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)) + .setDeleteIntent( + actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP)) .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.media3_notification_small_icon) .setStyle(mediaStyle) @@ -292,6 +295,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi * buttons are marked with {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX} * to declare the index in compact view of the given command button in the button extras. * + * @param mediaSession The media session to which the actions will be sent. * @param mediaButtons The command buttons to be included in the notification. * @param builder The builder to add the actions to. * @param actionFactory The actions factory to be used to build notifications. @@ -300,6 +304,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi * notification}. */ protected int[] addNotificationActions( + MediaSession mediaSession, List mediaButtons, NotificationCompat.Builder builder, MediaNotification.ActionFactory actionFactory) { @@ -309,11 +314,13 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi for (int i = 0; i < mediaButtons.size(); i++) { CommandButton commandButton = mediaButtons.get(i); if (commandButton.sessionCommand != null) { - builder.addAction(actionFactory.createCustomActionFromCustomCommandButton(commandButton)); + builder.addAction( + actionFactory.createCustomActionFromCustomCommandButton(mediaSession, commandButton)); } else { checkState(commandButton.playerCommand != COMMAND_INVALID); builder.addAction( actionFactory.createMediaAction( + mediaSession, IconCompat.createWithResource(context, commandButton.iconResId), commandButton.displayName, commandButton.playerCommand)); 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 c9c39af9bc..14064cfc55 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java @@ -43,12 +43,16 @@ public final class MediaNotification { * Creates a {@link NotificationCompat.Action} for a notification. These actions will be handled * by the library. * + * @param mediaSession The media session to which the action will be sent. * @param icon The icon to show for this action. * @param title The title of the action. * @param command A command to send when users trigger this action. */ NotificationCompat.Action createMediaAction( - IconCompat icon, CharSequence title, @Player.Command int command); + MediaSession mediaSession, + IconCompat icon, + CharSequence title, + @Player.Command int command); /** * Creates a {@link NotificationCompat.Action} for a notification with a custom action. Actions @@ -56,6 +60,7 @@ public final class MediaNotification { * to the {@linkplain MediaNotification.Provider#handleCustomCommand notification provider} that * provided them. * + * @param mediaSession The media session to which the action will be sent. * @param icon The icon to show for this action. * @param title The title of the action. * @param customAction The custom action set. @@ -63,7 +68,11 @@ public final class MediaNotification { * @see MediaNotification.Provider#handleCustomCommand */ NotificationCompat.Action createCustomAction( - IconCompat icon, CharSequence title, String customAction, Bundle extras); + MediaSession mediaSession, + IconCompat icon, + CharSequence title, + String customAction, + Bundle extras); /** * Creates a {@link NotificationCompat.Action} for a notification from a custom command button. @@ -76,18 +85,21 @@ public final class MediaNotification { * SessionCommand#customExtras command's extras} will be passed to {@link * Provider#handleCustomCommand(MediaSession, String, Bundle)} when the action is executed. * + * @param mediaSession The media session to which the action will be sent. * @param customCommandButton A {@linkplain CommandButton custom command button}. * @see MediaNotification.Provider#handleCustomCommand */ NotificationCompat.Action createCustomActionFromCustomCommandButton( - CommandButton customCommandButton); + MediaSession mediaSession, CommandButton customCommandButton); /** * Creates a {@link PendingIntent} for a media action that will be handled by the library. * + * @param mediaSession The media session to which the action will be sent. * @param command The intent's command. */ - PendingIntent createMediaActionPendingIntent(@Player.Command long command); + PendingIntent createMediaActionPendingIntent( + MediaSession mediaSession, @Player.Command long command); } /** 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 d44c3d0da1..680c8834b9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -354,19 +354,11 @@ public abstract class MediaSessionService extends Service { if (keyEvent != null) { session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); } - } else if (actionFactory.isCustomAction(intent)) { + } else if (session != null && actionFactory.isCustomAction(intent)) { @Nullable String customAction = actionFactory.getCustomAction(intent); if (customAction == null) { return START_STICKY; } - if (session == null) { - ControllerInfo controllerInfo = ControllerInfo.createLegacyControllerInfo(); - session = onGetSession(controllerInfo); - if (session == null) { - return START_STICKY; - } - addSession(session); - } Bundle customExtras = actionFactory.getCustomActionExtras(intent); getMediaNotificationManager().onCustomAction(session, customAction, customExtras); } diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java index fc7f5b1402..73c68ce21f 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java @@ -16,10 +16,13 @@ package androidx.media3.session; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.app.PendingIntent; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -39,12 +42,18 @@ public class DefaultActionFactoryTest { public void createMediaPendingIntent_intentIsMediaAction() { DefaultActionFactory actionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); + MediaSession mockMediaSession = mock(MediaSession.class); + MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); + when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); + Uri dataUri = Uri.parse("http://example.com"); + when(mockMediaSessionImpl.getUri()).thenReturn(dataUri); PendingIntent pendingIntent = - actionFactory.createMediaActionPendingIntent(Player.COMMAND_PLAY_PAUSE); + actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_PLAY_PAUSE); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue(); + assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(dataUri); } @Test @@ -71,7 +80,11 @@ public class DefaultActionFactoryTest { public void createCustomActionFromCustomCommandButton() { DefaultActionFactory actionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - + MediaSession mockMediaSession = mock(MediaSession.class); + MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); + when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); + Uri dataUri = Uri.parse("http://example.com"); + when(mockMediaSessionImpl.getUri()).thenReturn(dataUri); Bundle commandBundle = new Bundle(); commandBundle.putString("command-key", "command-value"); Bundle buttonBundle = new Bundle(); @@ -85,8 +98,11 @@ public class DefaultActionFactoryTest { .build(); NotificationCompat.Action notificationAction = - actionFactory.createCustomActionFromCustomCommandButton(customSessionCommand); + actionFactory.createCustomActionFromCustomCommandButton( + mockMediaSession, customSessionCommand); + ShadowPendingIntent shadowPendingIntent = shadowOf(notificationAction.actionIntent); + assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(dataUri); assertThat(String.valueOf(notificationAction.title)).isEqualTo("name"); assertThat(notificationAction.getIconCompat().getResId()) .isEqualTo(R.drawable.media3_notification_pause); @@ -99,7 +115,6 @@ public class DefaultActionFactoryTest { createCustomActionFromCustomCommandButton_notACustomAction_throwsIllegalArgumentException() { DefaultActionFactory actionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - CommandButton customSessionCommand = new CommandButton.Builder() .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) @@ -109,7 +124,9 @@ public class DefaultActionFactoryTest { Assert.assertThrows( IllegalArgumentException.class, - () -> actionFactory.createCustomActionFromCustomCommandButton(customSessionCommand)); + () -> + actionFactory.createCustomActionFromCustomCommandButton( + mock(MediaSession.class), customSessionCommand)); } /** A test service for unit tests. */ diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index de56133863..4cca95092b 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -22,7 +22,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -119,6 +121,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) @@ -155,6 +158,7 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( + mockMediaSession, ImmutableList.of(commandButton1, commandButton2, commandButton3, commandButton4), mockNotificationBuilder, mockActionFactory); @@ -163,10 +167,17 @@ public class DefaultMediaNotificationProviderTest { InOrder inOrder = Mockito.inOrder(mockActionFactory); inOrder .verify(mockActionFactory) - .createMediaAction(any(), eq("displayName"), eq(commandButton1.playerCommand)); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton2); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton3); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton4); + .createMediaAction( + eq(mockMediaSession), any(), eq("displayName"), eq(commandButton1.playerCommand)); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton3); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton4); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().containsExactly(1, 3, 2).inOrder(); } @@ -177,6 +188,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setDisplayName("displayName") @@ -192,6 +204,7 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( + mockMediaSession, ImmutableList.of(commandButton1, commandButton2), mockNotificationBuilder, mockActionFactory); @@ -202,10 +215,13 @@ public class DefaultMediaNotificationProviderTest { List actions = actionCaptor.getAllValues(); assertThat(actions).hasSize(2); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); inOrder .verify(mockActionFactory) - .createMediaAction(any(), eq("displayName"), eq(commandButton2.playerCommand)); + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + inOrder + .verify(mockActionFactory) + .createMediaAction( + eq(mockMediaSession), any(), eq("displayName"), eq(commandButton2.playerCommand)); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().containsExactly(1); } @@ -217,6 +233,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setDisplayName("displayName") @@ -226,10 +243,15 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - ImmutableList.of(commandButton1), mockNotificationBuilder, mockActionFactory); + mockMediaSession, + ImmutableList.of(commandButton1), + mockNotificationBuilder, + mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().isEmpty(); } @@ -240,6 +262,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); Bundle commandButtonBundle1 = new Bundle(); commandButtonBundle1.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 2); CommandButton commandButton1 = @@ -262,13 +285,18 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( + mockMediaSession, ImmutableList.of(commandButton1, commandButton2), mockNotificationBuilder, mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton2); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().isEmpty(); } @@ -279,6 +307,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); Bundle commandButtonBundle = new Bundle(); commandButtonBundle.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1); CommandButton commandButton1 = @@ -291,10 +320,15 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - ImmutableList.of(commandButton1), mockNotificationBuilder, mockActionFactory); + mockMediaSession, + ImmutableList.of(commandButton1), + mockNotificationBuilder, + mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); verifyNoMoreInteractions(mockActionFactory); // [INDEX_UNSET, 1, INDEX_UNSET] cropped up to the first INDEX_UNSET value assertThat(compactViewIndices).asList().isEmpty(); @@ -307,6 +341,10 @@ public class DefaultMediaNotificationProviderTest { NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); DefaultActionFactory defaultActionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); + MediaSession mockMediaSession = mock(MediaSession.class); + MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); + when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); + when(mockMediaSessionImpl.getUri()).thenReturn(Uri.parse("http://example.com")); Bundle commandButtonBundle = new Bundle(); commandButtonBundle.putString("testKey", "testValue"); CommandButton commandButton1 = @@ -318,12 +356,17 @@ public class DefaultMediaNotificationProviderTest { .build(); defaultMediaNotificationProvider.addNotificationActions( - ImmutableList.of(commandButton1), mockNotificationBuilder, defaultActionFactory); + mockMediaSession, + ImmutableList.of(commandButton1), + mockNotificationBuilder, + defaultActionFactory); ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(NotificationCompat.Action.class); verify(mockNotificationBuilder).addAction(actionCaptor.capture()); verifyNoMoreInteractions(mockNotificationBuilder); + verify(mockMediaSessionImpl).getUri(); + verifyNoMoreInteractions(mockMediaSessionImpl); List actions = actionCaptor.getAllValues(); assertThat(actions).hasSize(1); assertThat(String.valueOf(actions.get(0).title)).isEqualTo("displayName1");