diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 9d6aa87817..0a2d83b742 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -19,6 +19,12 @@
* Ensure commands are always executed in the correct order even if some
require asynchronous resolution
([#85](https://github.com/androidx/media/issues/85)).
+ * Add `DefaultNotificationProvider.Builder` to build
+ `DefaultNotificationProvider` instances. The builder can configure the
+ notification ID, the notification channel ID and the notification
+ channel name used by the provider. Also, add method
+ `DefaultNotificationProvider.setSmallIcon(int)` to set the notifications
+ small icon ([#104](https://github.com/androidx/media/issues/104)).
### 1.0.0-beta02 (2022-07-15)
diff --git a/libraries/session/build.gradle b/libraries/session/build.gradle
index 114a98bed6..ec89bc41df 100644
--- a/libraries/session/build.gradle
+++ b/libraries/session/build.gradle
@@ -32,6 +32,7 @@ android {
}
dependencies {
api project(modulePrefix + 'lib-common')
+ compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
implementation 'androidx.collection:collection:' + androidxCollectionVersion
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 0059a9d99a..4b588cfda7 100644
--- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java
+++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java
@@ -36,7 +36,9 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
@@ -51,6 +53,7 @@ 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.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -89,12 +92,115 @@ import java.util.concurrent.ExecutionException;
*
{@code media3_notification_seek_to_previous} - The previous icon.
*
{@code media3_notification_seek_to_next} - The next icon.
*
{@code media3_notification_small_icon} - The {@link
- * NotificationCompat.Builder#setSmallIcon(int) small icon}.
+ * NotificationCompat.Builder#setSmallIcon(int) small icon}. A different icon can be set with
+ * {@link #setSmallIcon(int)}.
+ *
+ *
+ *
String resources
+ *
+ * String resources used can be overridden by resources with the same names defined the application.
+ * These are:
+ *
+ *
+ *
{@code media3_controls_play_description} - The description of the play icon.
+ *
{@code media3_controls_pause_description} - The description of the pause icon.
+ *
{@code media3_controls_seek_to_previous_description} - The description of the
+ * previous icon.
+ *
{@code media3_controls_seek_to_next_description} - The description of the next icon.
+ *
{@code default_notification_channel_name} The name of the {@link
+ * NotificationChannel} on which created notifications are posted. A different string resource
+ * can be set when constructing the provider with {@link
+ * DefaultMediaNotificationProvider.Builder#setChannelName(int)}.
*
*/
@UnstableApi
public class DefaultMediaNotificationProvider implements MediaNotification.Provider {
+ /** A builder for {@link DefaultMediaNotificationProvider} instances. */
+ public static final class Builder {
+ private final Context context;
+ private int notificationId;
+ private String channelId;
+ @StringRes private int channelNameResourceId;
+ private BitmapLoader bitmapLoader;
+ private boolean built;
+
+ /**
+ * Creates a builder.
+ *
+ * @param context Any {@link Context}.
+ */
+ public Builder(Context context) {
+ this.context = context;
+ notificationId = DEFAULT_NOTIFICATION_ID;
+ channelId = DEFAULT_CHANNEL_ID;
+ channelNameResourceId = DEFAULT_CHANNEL_NAME_RESOURCE_ID;
+ bitmapLoader = new SimpleBitmapLoader();
+ }
+
+ /**
+ * Sets the {@link MediaNotification#notificationId} used for the created notifications. By
+ * default this is set to {@link #DEFAULT_NOTIFICATION_ID}.
+ *
+ * @param notificationId The notification ID.
+ * @return This builder.
+ */
+ @CanIgnoreReturnValue
+ public Builder setNotificationId(int notificationId) {
+ this.notificationId = notificationId;
+ return this;
+ }
+
+ /**
+ * Sets the ID of the {@link NotificationChannel} on which created notifications are posted on.
+ * By default this is set to {@link #DEFAULT_CHANNEL_ID}.
+ *
+ * @param channelId The channel ID.
+ * @return This builder.
+ */
+ @CanIgnoreReturnValue
+ public Builder setChannelId(String channelId) {
+ this.channelId = channelId;
+ return this;
+ }
+
+ /**
+ * Sets the name of the {@link NotificationChannel} on which created notifications are posted
+ * on. By default this is set to {@link #DEFAULT_CHANNEL_NAME_RESOURCE_ID}.
+ *
+ * @param channelNameResourceId The string resource ID with the channel name.
+ * @return This builder.
+ */
+ @CanIgnoreReturnValue
+ public Builder setChannelName(@StringRes int channelNameResourceId) {
+ this.channelNameResourceId = channelNameResourceId;
+ return this;
+ }
+
+ /**
+ * Sets the {@link BitmapLoader} used load artwork. By default, a {@link SimpleBitmapLoader}
+ * will be used.
+ *
+ * @param bitmapLoader The bitmap loader.
+ * @return This builder.
+ */
+ @CanIgnoreReturnValue
+ public Builder setBitmapLoader(BitmapLoader bitmapLoader) {
+ this.bitmapLoader = bitmapLoader;
+ return this;
+ }
+
+ /**
+ * Builds the {@link DefaultMediaNotificationProvider}. The method can be called at most once.
+ */
+ public DefaultMediaNotificationProvider build() {
+ checkState(!built);
+ DefaultMediaNotificationProvider provider = new DefaultMediaNotificationProvider(this);
+ built = true;
+ return provider;
+ }
+ }
+
/**
* An extras key that can be used to define the index of a {@link CommandButton} in {@linkplain
* Notification.MediaStyle#setShowActionsInCompactView(int...) compact view}.
@@ -102,12 +208,27 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
public static final String COMMAND_KEY_COMPACT_VIEW_INDEX =
"androidx.media3.session.command.COMPACT_VIEW_INDEX";
+ /** The default ID used for the {@link MediaNotification#notificationId}. */
+ public static final int DEFAULT_NOTIFICATION_ID = 1001;
+ /**
+ * The default ID used for the {@link NotificationChannel} on which created notifications are
+ * posted on.
+ */
+ public static final String DEFAULT_CHANNEL_ID = "default_channel_id";
+ /**
+ * The default name used for the {@link NotificationChannel} on which created notifications are
+ * posted on.
+ */
+ @StringRes
+ public static final int DEFAULT_CHANNEL_NAME_RESOURCE_ID =
+ R.string.default_notification_channel_name;
+
private static final String TAG = "NotificationProvider";
- private static final int NOTIFICATION_ID = 1001;
- private static final String NOTIFICATION_CHANNEL_ID = "default_channel_id";
- private static final String NOTIFICATION_CHANNEL_NAME = "Now playing";
private final Context context;
+ private final int notificationId;
+ private final String channelId;
+ @StringRes private final int channelNameResourceId;
private final NotificationManager notificationManager;
private final BitmapLoader bitmapLoader;
// Cache the last loaded bitmap to avoid reloading the bitmap again, particularly useful when
@@ -116,24 +237,25 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
private final Handler mainHandler;
private OnBitmapLoadedFutureCallback pendingOnBitmapLoadedFutureCallback;
+ @DrawableRes private int smallIconResourceId;
- /** Creates an instance that uses a {@link SimpleBitmapLoader} for loading artwork images. */
- public DefaultMediaNotificationProvider(Context context) {
- this(context, new SimpleBitmapLoader());
- }
-
- /** Creates an instance that uses the {@code bitmapLoader} for loading artwork images. */
- public DefaultMediaNotificationProvider(Context context, BitmapLoader bitmapLoader) {
- this.context = context.getApplicationContext();
- this.bitmapLoader = bitmapLoader;
- lastLoadedBitmapInfo = new LoadedBitmapInfo();
- mainHandler = new Handler(Looper.getMainLooper());
+ private DefaultMediaNotificationProvider(Builder builder) {
+ this.context = builder.context;
+ this.notificationId = builder.notificationId;
+ this.channelId = builder.channelId;
+ this.channelNameResourceId = builder.channelNameResourceId;
+ this.bitmapLoader = builder.bitmapLoader;
notificationManager =
checkStateNotNull(
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
+ lastLoadedBitmapInfo = new LoadedBitmapInfo();
+ mainHandler = new Handler(Looper.getMainLooper());
pendingOnBitmapLoadedFutureCallback = new OnBitmapLoadedFutureCallback(bitmap -> {});
+ smallIconResourceId = R.drawable.media3_notification_small_icon;
}
+ // MediaNotification.Provider implementation
+
@Override
public final MediaNotification createNotification(
MediaSession mediaSession,
@@ -143,8 +265,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
ensureNotificationChannel();
Player player = mediaSession.getPlayer();
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
MediaStyle mediaStyle = new MediaStyle();
int[] compactViewIndices =
@@ -173,7 +294,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
bitmap -> {
builder.setLargeIcon(bitmap);
onNotificationChangedCallback.onNotificationChanged(
- new MediaNotification(NOTIFICATION_ID, builder.build()));
+ new MediaNotification(notificationId, builder.build()));
}),
// This callback must be executed on the next looper iteration, after this method has
// returned a media notification.
@@ -200,12 +321,12 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
.setDeleteIntent(
actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP))
.setOnlyAlertOnce(true)
- .setSmallIcon(R.drawable.media3_notification_small_icon)
+ .setSmallIcon(smallIconResourceId)
.setStyle(mediaStyle)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(false)
.build();
- return new MediaNotification(NOTIFICATION_ID, notification);
+ return new MediaNotification(notificationId, notification);
}
@Override
@@ -214,6 +335,18 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
return false;
}
+ // Other methods
+
+ /**
+ * Sets the small icon of the notification which is also shown in the system status bar.
+ *
+ * @see NotificationCompat.Builder#setSmallIcon(int)
+ * @param smallIconResourceId The resource id of the small icon.
+ */
+ public final void setSmallIcon(@DrawableRes int smallIconResourceId) {
+ this.smallIconResourceId = smallIconResourceId;
+ }
+
/**
* Returns the ordered list of {@linkplain CommandButton command buttons} to be used to build the
* notification.
@@ -367,13 +500,14 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
}
private void ensureNotificationChannel() {
- if (Util.SDK_INT < 26
- || notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null) {
+ if (Util.SDK_INT < 26 || notificationManager.getNotificationChannel(channelId) != null) {
return;
}
NotificationChannel channel =
new NotificationChannel(
- NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
+ channelId,
+ context.getString(channelNameResourceId),
+ NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
}
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 680c8834b9..c5ac3fbddc 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java
@@ -433,7 +433,8 @@ public abstract class MediaSessionService extends Service {
synchronized (lock) {
if (mediaNotificationManager == null) {
if (mediaNotificationProvider == null) {
- mediaNotificationProvider = new DefaultMediaNotificationProvider(getApplicationContext());
+ mediaNotificationProvider =
+ new DefaultMediaNotificationProvider.Builder(getApplicationContext()).build();
}
mediaNotificationManager =
new MediaNotificationManager(
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 4cca95092b..b6555098a7 100644
--- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java
+++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java
@@ -24,10 +24,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
+import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Commands;
import androidx.test.core.app.ApplicationProvider;
@@ -40,6 +44,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.robolectric.Robolectric;
+import org.robolectric.Shadows;
+import org.robolectric.shadows.ShadowNotificationManager;
/** Tests for {@link DefaultMediaNotificationProvider}. */
@RunWith(AndroidJUnit4.class)
@@ -48,7 +54,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void getMediaButtons_playWhenReadyTrueOrFalse_correctPlayPauseResources() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
Commands commands = new Commands.Builder().addAllCommands().build();
List mediaButtonsWhenPlaying =
@@ -73,7 +80,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void getMediaButtons_allCommandsAvailable_createsPauseSkipNextSkipPreviousButtons() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
Commands commands = new Commands.Builder().addAllCommands().build();
SessionCommand customSessionCommand = new SessionCommand("", Bundle.EMPTY);
CommandButton customCommandButton =
@@ -98,7 +106,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void getMediaButtons_noPlayerCommandsAvailable_onlyCustomLayoutButtons() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
Commands commands = new Commands.Builder().build();
SessionCommand customSessionCommand = new SessionCommand("action1", Bundle.EMPTY);
CommandButton customCommandButton =
@@ -118,7 +127,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void addNotificationActions_customCompactViewDeclarations_correctCompactViewIndices() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
@@ -185,7 +195,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void addNotificationActions_playPauseCommandNoCustomDeclaration_playPauseInCompactView() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
@@ -230,7 +241,8 @@ public class DefaultMediaNotificationProviderTest {
public void
addNotificationActions_noPlayPauseCommandNoCustomDeclaration_emptyCompactViewIndices() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
@@ -259,7 +271,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void addNotificationActions_outOfBoundsCompactViewIndices_ignored() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
@@ -304,7 +317,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void addNotificationActions_unsetLeadingArrayFields_cropped() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
@@ -337,7 +351,8 @@ public class DefaultMediaNotificationProviderTest {
@Test
public void addNotificationActions_correctNotificationActionAttributes() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
- new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext());
+ new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
+ .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
@@ -374,6 +389,96 @@ public class DefaultMediaNotificationProviderTest {
assertThat(actions.get(0).getExtras().size()).isEqualTo(0);
}
+ @Test
+ public void provider_withCustomIds_notificationsUseCustomIds() {
+ Context context = ApplicationProvider.getApplicationContext();
+ DefaultMediaNotificationProvider defaultMediaNotificationProvider =
+ new DefaultMediaNotificationProvider.Builder(context)
+ .setNotificationId(/* notificationId= */ 2)
+ .setChannelId(/* channelId= */ "customChannelId")
+ .setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description)
+ .build();
+ MediaSession mockMediaSession = createMockMediaSessionForNotification();
+ DefaultActionFactory defaultActionFactory =
+ new DefaultActionFactory(Robolectric.setupService(TestService.class));
+
+ MediaNotification notification =
+ defaultMediaNotificationProvider.createNotification(
+ mockMediaSession,
+ ImmutableList.of(),
+ defaultActionFactory,
+ mock(MediaNotification.Provider.Callback.class));
+
+ assertThat(notification.notificationId).isEqualTo(2);
+ assertThat(notification.notification.getChannelId()).isEqualTo("customChannelId");
+ ShadowNotificationManager shadowNotificationManager =
+ Shadows.shadowOf(
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
+ assertHasNotificationChannel(
+ shadowNotificationManager.getNotificationChannels(),
+ /* channelId= */ "customChannelId",
+ /* channelName= */ context.getString(R.string.media3_controls_play_description));
+ }
+
+ @Test
+ public void setCustomSmallIcon_notificationUsesCustomSmallIcon() {
+ Context context = ApplicationProvider.getApplicationContext();
+ DefaultMediaNotificationProvider defaultMediaNotificationProvider =
+ new DefaultMediaNotificationProvider.Builder(context).build();
+ DefaultActionFactory defaultActionFactory =
+ new DefaultActionFactory(Robolectric.setupService(TestService.class));
+ MediaSession mockMediaSession = createMockMediaSessionForNotification();
+
+ MediaNotification notification =
+ defaultMediaNotificationProvider.createNotification(
+ mockMediaSession,
+ ImmutableList.of(),
+ defaultActionFactory,
+ mock(MediaNotification.Provider.Callback.class));
+ // Change the small icon.
+ defaultMediaNotificationProvider.setSmallIcon(R.drawable.media3_icon_circular_play);
+ MediaNotification notificationWithSmallIcon =
+ defaultMediaNotificationProvider.createNotification(
+ mockMediaSession,
+ ImmutableList.of(),
+ defaultActionFactory,
+ mock(MediaNotification.Provider.Callback.class));
+
+ assertThat(notification.notification.getSmallIcon().getResId())
+ .isEqualTo(R.drawable.media3_notification_small_icon);
+ assertThat(notificationWithSmallIcon.notification.getSmallIcon().getResId())
+ .isEqualTo(R.drawable.media3_icon_circular_play);
+ }
+
+ private static void assertHasNotificationChannel(
+ List