Add session URI to Intent used with the notification

The service handles three different types of `Intents`. Custom command and media
command Intents created by the library and media button event Intents from other
sources.

Media commands from the library as well as from external sources have the action
set to `android.intent.action.MEDIA_BUTTON`. If the data URI is set and can be
used to identify a session then it is a library Intent. If the Intent is coming
from an external KeyEvent, the service implementation is asked which session to use
by calling `onGetSession(controllerInfo)` with the controller info being an
anonymous legacy controller info.

Intents representing a custom command are always coming from the library and hence
always have a data URI.

Issue: androidx/media#82
PiperOrigin-RevId: 453932972
(cherry picked from commit 8b592fc77aeead345adac999eda27da55df0ae01)
This commit is contained in:
bachinger 2022-06-09 15:36:13 +00:00 committed by Marc Baechinger
parent 9a73ae90a0
commit 7c0b787bdb
6 changed files with 123 additions and 42 deletions

View File

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

View File

@ -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<CommandButton> 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));

View File

@ -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);
}
/**

View File

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

View File

@ -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. */

View File

@ -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<NotificationCompat.Action> 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<NotificationCompat.Action> actionCaptor =
ArgumentCaptor.forClass(NotificationCompat.Action.class);
verify(mockNotificationBuilder).addAction(actionCaptor.capture());
verifyNoMoreInteractions(mockNotificationBuilder);
verify(mockMediaSessionImpl).getUri();
verifyNoMoreInteractions(mockMediaSessionImpl);
List<NotificationCompat.Action> actions = actionCaptor.getAllValues();
assertThat(actions).hasSize(1);
assertThat(String.valueOf(actions.get(0).title)).isEqualTo("displayName1");