diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 11f3841e0c..809ddc229c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -58,6 +58,9 @@ allow requested `MediaItems` to be passed onto `Player` if they have `LocalConfiguration` (e.g. URI) ([#282](https://github.com/androidx/media/issues/282)). + * Add "seek to previous" and "seek to next" command buttons on compact + media notification view by default for Android 12 and below + ([#410](https://github.com/androidx/media/issues/410)). * UI: * Downloads: * OkHttp Extension: 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 02488affc2..a6e85731cd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -401,9 +401,10 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi *
Override this method to customize the buttons on the notification. Commands of the buttons * returned by this method must be contained in {@link MediaController#getAvailableCommands()}. * - *
By default, the notification shows {@link Player#COMMAND_PLAY_PAUSE} in {@linkplain - * Notification.MediaStyle#setShowActionsInCompactView(int...) compact view}. This can be - * customized by defining the index of the command in compact view of up to 3 commands in their + *
By default, the notification shows buttons for {@link Player#seekToPreviousMediaItem()}, + * {@link Player#play()} or {@link Player#pause()}, {@link Player#seekToNextMediaItem()} in + * {@linkplain Notification.MediaStyle#setShowActionsInCompactView(int...) compact view}. This can + * be customized by defining the index of the command in compact view of up to 3 commands in their * extras with key {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX}. * *
To make the custom layout and commands work, you need to {@linkplain @@ -491,9 +492,11 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi * and define which actions are shown in compact view by returning the indices of the buttons to * be shown in compact view. * - *
By default, {@link Player#COMMAND_PLAY_PAUSE} is shown in compact view, unless some of the - * 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. + *
By default, the buttons for {@link Player#seekToPreviousMediaItem()}, {@link Player#play()}
+ * or {@link Player#pause()}, {@link Player#seekToNextMediaItem()} are shown in compact view,
+ * unless some of the 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.
@@ -509,7 +512,9 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
NotificationCompat.Builder builder,
MediaNotification.ActionFactory actionFactory) {
int[] compactViewIndices = new int[3];
+ int[] defaultCompactViewIndices = new int[3];
Arrays.fill(compactViewIndices, INDEX_UNSET);
+ Arrays.fill(defaultCompactViewIndices, INDEX_UNSET);
int compactViewCommandCount = 0;
for (int i = 0; i < mediaButtons.size(); i++) {
CommandButton commandButton = mediaButtons.get(i);
@@ -534,10 +539,26 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
if (compactViewIndex >= 0 && compactViewIndex < compactViewIndices.length) {
compactViewCommandCount++;
compactViewIndices[compactViewIndex] = i;
- } else if (commandButton.playerCommand == COMMAND_PLAY_PAUSE
- && compactViewCommandCount == 0) {
- // If there is no custom configuration we use the play/pause action in compact view.
- compactViewIndices[0] = i;
+ } else if (commandButton.playerCommand == COMMAND_SEEK_TO_PREVIOUS
+ || commandButton.playerCommand == COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) {
+ defaultCompactViewIndices[0] = i;
+ } else if (commandButton.playerCommand == COMMAND_PLAY_PAUSE) {
+ defaultCompactViewIndices[1] = i;
+ } else if (commandButton.playerCommand == COMMAND_SEEK_TO_NEXT
+ || commandButton.playerCommand == COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) {
+ defaultCompactViewIndices[2] = i;
+ }
+ }
+ if (compactViewCommandCount == 0) {
+ // If there is no custom configuration we use the seekPrev (if any), play/pause (if any),
+ // seekNext (if any) action in compact view.
+ int indexInCompactViewIndices = 0;
+ for (int i = 0; i < defaultCompactViewIndices.length; i++) {
+ if (defaultCompactViewIndices[i] == INDEX_UNSET) {
+ continue;
+ }
+ compactViewIndices[indexInCompactViewIndices] = defaultCompactViewIndices[i];
+ indexInCompactViewIndices++;
}
}
for (int i = 0; i < compactViewIndices.length; i++) {
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 8aa7484a56..935ef67d82 100644
--- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java
+++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java
@@ -20,15 +20,16 @@ import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_C
import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
+import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
@@ -49,10 +50,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.SettableFuture;
import java.util.List;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
+import org.mockito.Mock;
import org.mockito.Mockito;
import org.robolectric.Robolectric;
import org.robolectric.Shadows;
@@ -64,6 +66,39 @@ import org.robolectric.shadows.ShadowNotificationManager;
public class DefaultMediaNotificationProviderTest {
private final Context context = ApplicationProvider.getApplicationContext();
+ private static final String TEST_CHANNEL_ID = "test_channel_id";
+ private static final NotificationCompat.Action fakeAction =
+ new NotificationCompat.Action(0, null, null);
+ /**
+ * The key string is defined as
+ * {@code NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES}
+ */
+ private static final String EXTRA_ALLOW_GENERATED_REPLIES =
+ "android.support.allowGeneratedReplies";
+ /**
+ * The key string is defined as
+ * {@code NotificationCompat.EXTRA_SHOWS_USER_INTERFACE}
+ */
+ private static final String EXTRA_SHOWS_USER_INTERFACE =
+ "android.support.action.showsUserInterface";
+ /**
+ * The key string is defined as
+ * {@code NotificationCompat.EXTRA_SEMANTIC_ACTION}
+ */
+ private static final String EXTRA_SEMANTIC_ACTION = "android.support.action.semanticAction";
+
+ @Mock private MediaNotification.ActionFactory mockActionFactory;
+
+ @Before
+ public void setUp() {
+ mockActionFactory = mock(MediaNotification.ActionFactory.class);
+ when(mockActionFactory.createCustomActionFromCustomCommandButton(any(), any()))
+ .thenReturn(fakeAction);
+ when(mockActionFactory.createMediaAction(any(), any(), any(), anyInt())).thenReturn(fakeAction);
+ }
@Test
public void getMediaButtons_playWhenReadyTrueOrFalse_correctPlayPauseResources() {
@@ -167,8 +202,9 @@ public class DefaultMediaNotificationProviderTest {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
- NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
- MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
+ NotificationCompat.Builder notificationBuilder =
+ new NotificationCompat.Builder(
+ ApplicationProvider.getApplicationContext(), TEST_CHANNEL_ID);
CommandButton commandButton1 =
new CommandButton.Builder()
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
@@ -209,12 +245,12 @@ public class DefaultMediaNotificationProviderTest {
defaultMediaNotificationProvider.addNotificationActions(
mediaSession,
ImmutableList.of(commandButton1, commandButton2, commandButton3, commandButton4),
- mockNotificationBuilder,
+ notificationBuilder,
mockActionFactory);
mediaSession.release();
player.release();
- verify(mockNotificationBuilder, times(4)).addAction(any());
+ assertThat(notificationBuilder.build().actions).hasLength(4);
InOrder inOrder = Mockito.inOrder(mockActionFactory);
inOrder
.verify(mockActionFactory)
@@ -234,12 +270,14 @@ public class DefaultMediaNotificationProviderTest {
}
@Test
- public void addNotificationActions_playPauseCommandNoCustomDeclaration_playPauseInCompactView() {
+ public void
+ addNotificationActions_playPauseSeekPrevSeekNextCommands_noCustomDeclaration_seekPrevPlayPauseSeekNextInCompactView() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
- NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
- MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
+ NotificationCompat.Builder notificationBuilder =
+ new NotificationCompat.Builder(
+ ApplicationProvider.getApplicationContext(), TEST_CHANNEL_ID);
CommandButton commandButton1 =
new CommandButton.Builder()
.setDisplayName("displayName")
@@ -252,23 +290,31 @@ public class DefaultMediaNotificationProviderTest {
.setDisplayName("displayName")
.setIconResId(R.drawable.media3_icon_circular_play)
.build();
+ CommandButton commandButton3 =
+ new CommandButton.Builder()
+ .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
+ .setDisplayName("displayName")
+ .setIconResId(R.drawable.media3_icon_circular_play)
+ .build();
+ CommandButton commandButton4 =
+ new CommandButton.Builder()
+ .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
+ .setDisplayName("displayName")
+ .setIconResId(R.drawable.media3_icon_circular_play)
+ .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
int[] compactViewIndices =
defaultMediaNotificationProvider.addNotificationActions(
mediaSession,
- ImmutableList.of(commandButton1, commandButton2),
- mockNotificationBuilder,
+ ImmutableList.of(commandButton1, commandButton2, commandButton3, commandButton4),
+ notificationBuilder,
mockActionFactory);
mediaSession.release();
player.release();
- ArgumentCaptor