Mark MediaSession methods final to prevent accidental overrides

It's currently not possible to even subclass MediaSession because
the constructor is package-private. To avoid any accidental usage or
future indirect subclassing, all methods can be marked as final.

PiperOrigin-RevId: 521775373
This commit is contained in:
tonihei 2023-04-04 16:50:17 +01:00 committed by Marc Baechinger
parent 28aa5e847b
commit ff919fe74d
5 changed files with 218 additions and 171 deletions

30
api.txt
View File

@ -1625,21 +1625,21 @@ package androidx.media3.session {
field @IntRange(from=1) public final int notificationId; field @IntRange(from=1) public final int notificationId;
} }
public class MediaSession { @com.google.errorprone.annotations.DoNotMock public class MediaSession {
method public void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); method public final void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle);
method public java.util.List<androidx.media3.session.MediaSession.ControllerInfo> getConnectedControllers(); method public final java.util.List<androidx.media3.session.MediaSession.ControllerInfo> getConnectedControllers();
method public String getId(); method public final String getId();
method public androidx.media3.common.Player getPlayer(); method public final androidx.media3.common.Player getPlayer();
method @Nullable public android.app.PendingIntent getSessionActivity(); method @Nullable public final android.app.PendingIntent getSessionActivity();
method public androidx.media3.session.SessionToken getToken(); method public final androidx.media3.session.SessionToken getToken();
method public void release(); method public final void release();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle); method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle);
method public void setAvailableCommands(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands); method public final void setAvailableCommands(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List<androidx.media3.session.CommandButton>); method public final com.google.common.util.concurrent.ListenableFuture<androidx.media3.session.SessionResult> setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List<androidx.media3.session.CommandButton>);
method public void setCustomLayout(java.util.List<androidx.media3.session.CommandButton>); method public final void setCustomLayout(java.util.List<androidx.media3.session.CommandButton>);
method public void setPlayer(androidx.media3.common.Player); method public final void setPlayer(androidx.media3.common.Player);
method public void setSessionExtras(android.os.Bundle); method public final void setSessionExtras(android.os.Bundle);
method public void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle); method public final void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle);
} }
public static final class MediaSession.Builder { public static final class MediaSession.Builder {

View File

@ -43,6 +43,7 @@ dependencies {
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
testImplementation project(modulePrefix + 'test-utils') testImplementation project(modulePrefix + 'test-utils')
testImplementation project(modulePrefix + 'lib-exoplayer')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }

View File

@ -63,6 +63,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.DoNotMock;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -220,6 +221,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* by trusted controllers (e.g. Bluetooth, Auto, ...). This means only trusted controllers can * by trusted controllers (e.g. Bluetooth, Auto, ...). This means only trusted controllers can
* connect and an app can accept such controllers in the same way as with legacy sessions. * connect and an app can accept such controllers in the same way as with legacy sessions.
*/ */
@DoNotMock
public class MediaSession { public class MediaSession {
static { static {
@ -592,7 +594,7 @@ public class MediaSession {
* @return The {@link PendingIntent} to launch an activity belonging to the session. * @return The {@link PendingIntent} to launch an activity belonging to the session.
*/ */
@Nullable @Nullable
public PendingIntent getSessionActivity() { public final PendingIntent getSessionActivity() {
return impl.getSessionActivity(); return impl.getSessionActivity();
} }
@ -605,7 +607,7 @@ public class MediaSession {
* @throws IllegalStateException if the new player's application looper differs from the current * @throws IllegalStateException if the new player's application looper differs from the current
* looper. * looper.
*/ */
public void setPlayer(Player player) { public final void setPlayer(Player player) {
checkNotNull(player); checkNotNull(player);
checkArgument(player.canAdvertiseSession()); checkArgument(player.canAdvertiseSession());
checkArgument(player.getApplicationLooper() == getPlayer().getApplicationLooper()); checkArgument(player.getApplicationLooper() == getPlayer().getApplicationLooper());
@ -623,7 +625,7 @@ public class MediaSession {
* further use the player after the session is released and needs to make sure to eventually * further use the player after the session is released and needs to make sure to eventually
* release the player. * release the player.
*/ */
public void release() { public final void release() {
try { try {
synchronized (STATIC_LOCK) { synchronized (STATIC_LOCK) {
SESSION_ID_TO_SESSION_MAP.remove(impl.getId()); SESSION_ID_TO_SESSION_MAP.remove(impl.getId());
@ -634,27 +636,27 @@ public class MediaSession {
} }
} }
/* package */ boolean isReleased() { /* package */ final boolean isReleased() {
return impl.isReleased(); return impl.isReleased();
} }
/** Returns the underlying {@link Player}. */ /** Returns the underlying {@link Player}. */
public Player getPlayer() { public final Player getPlayer() {
return impl.getPlayerWrapper().getWrappedPlayer(); return impl.getPlayerWrapper().getWrappedPlayer();
} }
/** Returns the session ID. */ /** Returns the session ID. */
public String getId() { public final String getId() {
return impl.getId(); return impl.getId();
} }
/** Returns the {@link SessionToken} for creating {@link MediaController}. */ /** Returns the {@link SessionToken} for creating {@link MediaController}. */
public SessionToken getToken() { public final SessionToken getToken() {
return impl.getToken(); return impl.getToken();
} }
/** Returns the list of connected controllers. */ /** Returns the list of connected controllers. */
public List<ControllerInfo> getConnectedControllers() { public final List<ControllerInfo> getConnectedControllers() {
return impl.getConnectedControllers(); return impl.getConnectedControllers();
} }
@ -705,7 +707,7 @@ public class MediaSession {
* @param controller The controller to specify layout. * @param controller The controller to specify layout.
* @param layout The ordered list of {@link CommandButton}. * @param layout The ordered list of {@link CommandButton}.
*/ */
public ListenableFuture<SessionResult> setCustomLayout( public final ListenableFuture<SessionResult> setCustomLayout(
ControllerInfo controller, List<CommandButton> layout) { ControllerInfo controller, List<CommandButton> layout) {
checkNotNull(controller, "controller must not be null"); checkNotNull(controller, "controller must not be null");
checkNotNull(layout, "layout must not be null"); checkNotNull(layout, "layout must not be null");
@ -728,7 +730,7 @@ public class MediaSession {
* *
* @param layout The ordered list of {@link CommandButton}. * @param layout The ordered list of {@link CommandButton}.
*/ */
public void setCustomLayout(List<CommandButton> layout) { public final void setCustomLayout(List<CommandButton> layout) {
checkNotNull(layout, "layout must not be null"); checkNotNull(layout, "layout must not be null");
impl.setCustomLayout(layout); impl.setCustomLayout(layout);
} }
@ -749,7 +751,7 @@ public class MediaSession {
* @param sessionCommands The new available session commands. * @param sessionCommands The new available session commands.
* @param playerCommands The new available player commands. * @param playerCommands The new available player commands.
*/ */
public void setAvailableCommands( public final void setAvailableCommands(
ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) { ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) {
checkNotNull(controller, "controller must not be null"); checkNotNull(controller, "controller must not be null");
checkNotNull(sessionCommands, "sessionCommands must not be null"); checkNotNull(sessionCommands, "sessionCommands must not be null");
@ -768,7 +770,7 @@ public class MediaSession {
* @param args A {@link Bundle} for additional arguments. May be empty. * @param args A {@link Bundle} for additional arguments. May be empty.
* @see #sendCustomCommand(ControllerInfo, SessionCommand, Bundle) * @see #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)
*/ */
public void broadcastCustomCommand(SessionCommand command, Bundle args) { public final void broadcastCustomCommand(SessionCommand command, Bundle args) {
checkNotNull(command); checkNotNull(command);
checkNotNull(args); checkNotNull(args);
checkArgument( checkArgument(
@ -784,7 +786,7 @@ public class MediaSession {
* *
* @param sessionExtras The session extras. * @param sessionExtras The session extras.
*/ */
public void setSessionExtras(Bundle sessionExtras) { public final void setSessionExtras(Bundle sessionExtras) {
checkNotNull(sessionExtras); checkNotNull(sessionExtras);
impl.setSessionExtras(sessionExtras); impl.setSessionExtras(sessionExtras);
} }
@ -797,7 +799,7 @@ public class MediaSession {
* @param controller The controller to send the extras to. * @param controller The controller to send the extras to.
* @param sessionExtras The session extras. * @param sessionExtras The session extras.
*/ */
public void setSessionExtras(ControllerInfo controller, Bundle sessionExtras) { public final void setSessionExtras(ControllerInfo controller, Bundle sessionExtras) {
checkNotNull(controller, "controller must not be null"); checkNotNull(controller, "controller must not be null");
checkNotNull(sessionExtras); checkNotNull(sessionExtras);
impl.setSessionExtras(controller, sessionExtras); impl.setSessionExtras(controller, sessionExtras);
@ -805,7 +807,7 @@ public class MediaSession {
/** Returns the {@link BitmapLoader}. */ /** Returns the {@link BitmapLoader}. */
@UnstableApi @UnstableApi
public BitmapLoader getBitmapLoader() { public final BitmapLoader getBitmapLoader() {
return impl.getBitmapLoader(); return impl.getBitmapLoader();
} }
@ -823,7 +825,7 @@ public class MediaSession {
* @return A {@link ListenableFuture} of {@link SessionResult} from the controller. * @return A {@link ListenableFuture} of {@link SessionResult} from the controller.
* @see #broadcastCustomCommand(SessionCommand, Bundle) * @see #broadcastCustomCommand(SessionCommand, Bundle)
*/ */
public ListenableFuture<SessionResult> sendCustomCommand( public final ListenableFuture<SessionResult> sendCustomCommand(
ControllerInfo controller, SessionCommand command, Bundle args) { ControllerInfo controller, SessionCommand command, Bundle args) {
checkNotNull(controller); checkNotNull(controller);
checkNotNull(command); checkNotNull(command);
@ -834,7 +836,7 @@ public class MediaSession {
return impl.sendCustomCommand(controller, command, args); return impl.sendCustomCommand(controller, command, args);
} }
/* package */ MediaSessionCompat getSessionCompat() { /* package */ final MediaSessionCompat getSessionCompat() {
return impl.getSessionCompat(); return impl.getSessionCompat();
} }
@ -843,7 +845,7 @@ public class MediaSession {
* internally by this session. * internally by this session.
*/ */
@UnstableApi @UnstableApi
public MediaSessionCompat.Token getSessionCompatToken() { public final MediaSessionCompat.Token getSessionCompatToken() {
return impl.getSessionCompat().getSessionToken(); return impl.getSessionCompat().getSessionToken();
} }
@ -852,12 +854,12 @@ public class MediaSession {
* *
* @param timeoutMs The timeout in milliseconds. * @param timeoutMs The timeout in milliseconds.
*/ */
/* package */ void setLegacyControllerConnectionTimeoutMs(long timeoutMs) { /* package */ final void setLegacyControllerConnectionTimeoutMs(long timeoutMs) {
impl.setLegacyControllerConnectionTimeoutMs(timeoutMs); impl.setLegacyControllerConnectionTimeoutMs(timeoutMs);
} }
/** Handles the controller's connection request from {@link MediaSessionService}. */ /** Handles the controller's connection request from {@link MediaSessionService}. */
/* package */ void handleControllerConnectionFromService( /* package */ final void handleControllerConnectionFromService(
IMediaController controller, IMediaController controller,
int controllerVersion, int controllerVersion,
int controllerInterfaceVersion, int controllerInterfaceVersion,
@ -875,7 +877,7 @@ public class MediaSession {
connectionHints); connectionHints);
} }
/* package */ IBinder getLegacyBrowserServiceBinder() { /* package */ final IBinder getLegacyBrowserServiceBinder() {
return impl.getLegacyBrowserServiceBinder(); return impl.getLegacyBrowserServiceBinder();
} }
@ -887,21 +889,22 @@ public class MediaSession {
* after an immediate one-time update. * after an immediate one-time update.
*/ */
@VisibleForTesting @VisibleForTesting
/* package */ void setSessionPositionUpdateDelayMs(long updateDelayMs) { /* package */ final void setSessionPositionUpdateDelayMs(long updateDelayMs) {
impl.setSessionPositionUpdateDelayMsOnHandler(updateDelayMs); impl.setSessionPositionUpdateDelayMsOnHandler(updateDelayMs);
} }
/** Sets the {@linkplain Listener listener}. */ /** Sets the {@linkplain Listener listener}. */
/* package */ void setListener(Listener listener) { /* package */ final void setListener(Listener listener) {
impl.setMediaSessionListener(listener); impl.setMediaSessionListener(listener);
} }
/** Clears the {@linkplain Listener listener}. */ /** Clears the {@linkplain Listener listener}. */
/* package */ void clearListener() { /* package */ final void clearListener() {
impl.clearMediaSessionListener(); impl.clearMediaSessionListener();
} }
private Uri getUri() { @VisibleForTesting
/* package */ final Uri getUri() {
return impl.getUri(); return impl.getUri();
} }

View File

@ -16,19 +16,21 @@
package androidx.media3.session; package androidx.media3.session;
import static com.google.common.truth.Truth.assertThat; 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 static org.robolectric.Shadows.shadowOf;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
@ -38,41 +40,42 @@ import org.robolectric.shadows.ShadowPendingIntent;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class DefaultActionFactoryTest { public class DefaultActionFactoryTest {
private Player player;
private MediaSession mediaSession;
@Before
public void setUp() {
Context context = ApplicationProvider.getApplicationContext();
player = new TestExoPlayerBuilder(context).build();
mediaSession = new MediaSession.Builder(context, player).build();
}
@After
public void tearDown() {
mediaSession.release();
player.release();
}
@Test @Test
public void createMediaPendingIntent_intentIsMediaAction() { public void createMediaPendingIntent_intentIsMediaAction() {
DefaultActionFactory actionFactory = DefaultActionFactory actionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
Uri dataUri = Uri.parse("http://example.com");
MediaSession mockMediaSession = mock(MediaSession.class);
Player mockPlayer = mock(Player.class);
MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class);
when(mockMediaSession.getPlayer()).thenReturn(mockPlayer);
when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl);
when(mockMediaSessionImpl.getUri()).thenReturn(dataUri);
PendingIntent pendingIntent = PendingIntent pendingIntent =
actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_SEEK_FORWARD); actionFactory.createMediaActionPendingIntent(mediaSession, Player.COMMAND_SEEK_FORWARD);
ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent);
assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue(); assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue();
assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(dataUri); assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(mediaSession.getUri());
} }
@Test @Test
public void createMediaPendingIntent_commandPlayPauseWhenNotPlayWhenReady_isForegroundService() { public void createMediaPendingIntent_commandPlayPauseWhenNotPlayWhenReady_isForegroundService() {
DefaultActionFactory actionFactory = DefaultActionFactory actionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
Uri dataUri = Uri.parse("http://example.com");
MediaSession mockMediaSession = mock(MediaSession.class);
Player mockPlayer = mock(Player.class);
MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class);
when(mockMediaSession.getPlayer()).thenReturn(mockPlayer);
when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl);
when(mockMediaSessionImpl.getUri()).thenReturn(dataUri);
when(mockPlayer.getPlayWhenReady()).thenReturn(false);
PendingIntent pendingIntent = PendingIntent pendingIntent =
actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_PLAY_PAUSE); actionFactory.createMediaActionPendingIntent(mediaSession, Player.COMMAND_PLAY_PAUSE);
ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent);
assertThat(shadowPendingIntent.isForegroundService()).isTrue(); assertThat(shadowPendingIntent.isForegroundService()).isTrue();
@ -82,17 +85,10 @@ public class DefaultActionFactoryTest {
public void createMediaPendingIntent_commandPlayPauseWhenPlayWhenReady_notAForegroundService() { public void createMediaPendingIntent_commandPlayPauseWhenPlayWhenReady_notAForegroundService() {
DefaultActionFactory actionFactory = DefaultActionFactory actionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
Uri dataUri = Uri.parse("http://example.com");
MediaSession mockMediaSession = mock(MediaSession.class);
Player mockPlayer = mock(Player.class);
MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class);
when(mockMediaSession.getPlayer()).thenReturn(mockPlayer);
when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl);
when(mockMediaSessionImpl.getUri()).thenReturn(dataUri);
when(mockPlayer.getPlayWhenReady()).thenReturn(true);
player.play();
PendingIntent pendingIntent = PendingIntent pendingIntent =
actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_PLAY_PAUSE); actionFactory.createMediaActionPendingIntent(mediaSession, Player.COMMAND_PLAY_PAUSE);
ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent);
assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue(); assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue();
@ -123,11 +119,6 @@ public class DefaultActionFactoryTest {
public void createCustomActionFromCustomCommandButton() { public void createCustomActionFromCustomCommandButton() {
DefaultActionFactory actionFactory = DefaultActionFactory actionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); 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(); Bundle commandBundle = new Bundle();
commandBundle.putString("command-key", "command-value"); commandBundle.putString("command-key", "command-value");
Bundle buttonBundle = new Bundle(); Bundle buttonBundle = new Bundle();
@ -141,11 +132,10 @@ public class DefaultActionFactoryTest {
.build(); .build();
NotificationCompat.Action notificationAction = NotificationCompat.Action notificationAction =
actionFactory.createCustomActionFromCustomCommandButton( actionFactory.createCustomActionFromCustomCommandButton(mediaSession, customSessionCommand);
mockMediaSession, customSessionCommand);
ShadowPendingIntent shadowPendingIntent = shadowOf(notificationAction.actionIntent); ShadowPendingIntent shadowPendingIntent = shadowOf(notificationAction.actionIntent);
assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(dataUri); assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(mediaSession.getUri());
assertThat(String.valueOf(notificationAction.title)).isEqualTo("name"); assertThat(String.valueOf(notificationAction.title)).isEqualTo("name");
assertThat(notificationAction.getIconCompat().getResId()) assertThat(notificationAction.getIconCompat().getResId())
.isEqualTo(R.drawable.media3_notification_pause); .isEqualTo(R.drawable.media3_notification_pause);
@ -169,7 +159,7 @@ public class DefaultActionFactoryTest {
IllegalArgumentException.class, IllegalArgumentException.class,
() -> () ->
actionFactory.createCustomActionFromCustomCommandButton( actionFactory.createCustomActionFromCustomCommandButton(
mock(MediaSession.class), customSessionCommand)); mediaSession, customSessionCommand));
} }
/** A test service for unit tests. */ /** A test service for unit tests. */

View File

@ -20,7 +20,6 @@ import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_C
import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID; import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -39,10 +38,12 @@ import android.os.Bundle;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.media3.common.ForwardingPlayer;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Player.Commands; import androidx.media3.common.Player.Commands;
import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.BitmapLoader;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -62,26 +63,31 @@ import org.robolectric.shadows.ShadowNotificationManager;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class DefaultMediaNotificationProviderTest { public class DefaultMediaNotificationProviderTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test @Test
public void getMediaButtons_playWhenReadyTrueOrFalse_correctPlayPauseResources() { public void getMediaButtons_playWhenReadyTrueOrFalse_correctPlayPauseResources() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider = DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build(); .build();
Commands commands = new Commands.Builder().addAllCommands().build(); Commands commands = new Commands.Builder().addAllCommands().build();
MediaSession mockMediaSession = mock(MediaSession.class); Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
List<CommandButton> mediaButtonsWhenPlaying = List<CommandButton> mediaButtonsWhenPlaying =
defaultMediaNotificationProvider.getMediaButtons( defaultMediaNotificationProvider.getMediaButtons(
mockMediaSession, mediaSession,
commands, commands,
/* customLayout= */ ImmutableList.of(), /* customLayout= */ ImmutableList.of(),
/* showPauseButton= */ true); /* showPauseButton= */ true);
List<CommandButton> mediaButtonWhenPaused = List<CommandButton> mediaButtonWhenPaused =
defaultMediaNotificationProvider.getMediaButtons( defaultMediaNotificationProvider.getMediaButtons(
mockMediaSession, mediaSession,
commands, commands,
/* customLayout= */ ImmutableList.of(), /* customLayout= */ ImmutableList.of(),
/* showPauseButton= */ false); /* showPauseButton= */ false);
mediaSession.release();
player.release();
assertThat(mediaButtonsWhenPlaying).hasSize(3); assertThat(mediaButtonsWhenPlaying).hasSize(3);
assertThat(mediaButtonsWhenPlaying.get(1).playerCommand).isEqualTo(Player.COMMAND_PLAY_PAUSE); assertThat(mediaButtonsWhenPlaying.get(1).playerCommand).isEqualTo(Player.COMMAND_PLAY_PAUSE);
@ -100,7 +106,6 @@ public class DefaultMediaNotificationProviderTest {
DefaultMediaNotificationProvider defaultMediaNotificationProvider = DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build(); .build();
MediaSession mockMediaSession = mock(MediaSession.class);
Commands commands = new Commands.Builder().addAllCommands().build(); Commands commands = new Commands.Builder().addAllCommands().build();
SessionCommand customSessionCommand = new SessionCommand("", Bundle.EMPTY); SessionCommand customSessionCommand = new SessionCommand("", Bundle.EMPTY);
CommandButton customCommandButton = CommandButton customCommandButton =
@ -109,13 +114,17 @@ public class DefaultMediaNotificationProviderTest {
.setIconResId(R.drawable.media3_icon_circular_play) .setIconResId(R.drawable.media3_icon_circular_play)
.setSessionCommand(customSessionCommand) .setSessionCommand(customSessionCommand)
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
List<CommandButton> mediaButtons = List<CommandButton> mediaButtons =
defaultMediaNotificationProvider.getMediaButtons( defaultMediaNotificationProvider.getMediaButtons(
mockMediaSession, mediaSession,
commands, commands,
ImmutableList.of(customCommandButton), ImmutableList.of(customCommandButton),
/* showPauseButton= */ true); /* showPauseButton= */ true);
mediaSession.release();
player.release();
assertThat(mediaButtons).hasSize(4); assertThat(mediaButtons).hasSize(4);
assertThat(mediaButtons.get(0).playerCommand) assertThat(mediaButtons.get(0).playerCommand)
@ -130,7 +139,6 @@ public class DefaultMediaNotificationProviderTest {
DefaultMediaNotificationProvider defaultMediaNotificationProvider = DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build(); .build();
MediaSession mockMediaSession = mock(MediaSession.class);
Commands commands = new Commands.Builder().build(); Commands commands = new Commands.Builder().build();
SessionCommand customSessionCommand = new SessionCommand("action1", Bundle.EMPTY); SessionCommand customSessionCommand = new SessionCommand("action1", Bundle.EMPTY);
CommandButton customCommandButton = CommandButton customCommandButton =
@ -139,13 +147,17 @@ public class DefaultMediaNotificationProviderTest {
.setIconResId(R.drawable.media3_icon_circular_play) .setIconResId(R.drawable.media3_icon_circular_play)
.setSessionCommand(customSessionCommand) .setSessionCommand(customSessionCommand)
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
List<CommandButton> mediaButtons = List<CommandButton> mediaButtons =
defaultMediaNotificationProvider.getMediaButtons( defaultMediaNotificationProvider.getMediaButtons(
mockMediaSession, mediaSession,
commands, commands,
ImmutableList.of(customCommandButton), ImmutableList.of(customCommandButton),
/* showPauseButton= */ true); /* showPauseButton= */ true);
mediaSession.release();
player.release();
assertThat(mediaButtons).containsExactly(customCommandButton); assertThat(mediaButtons).containsExactly(customCommandButton);
} }
@ -157,7 +169,6 @@ public class DefaultMediaNotificationProviderTest {
.build(); .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
CommandButton commandButton1 = CommandButton commandButton1 =
new CommandButton.Builder() new CommandButton.Builder()
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE) .setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
@ -191,29 +202,33 @@ public class DefaultMediaNotificationProviderTest {
.setSessionCommand(new SessionCommand("action4", Bundle.EMPTY)) .setSessionCommand(new SessionCommand("action4", Bundle.EMPTY))
.setExtras(commandButton4Bundle) .setExtras(commandButton4Bundle)
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
int[] compactViewIndices = int[] compactViewIndices =
defaultMediaNotificationProvider.addNotificationActions( defaultMediaNotificationProvider.addNotificationActions(
mockMediaSession, mediaSession,
ImmutableList.of(commandButton1, commandButton2, commandButton3, commandButton4), ImmutableList.of(commandButton1, commandButton2, commandButton3, commandButton4),
mockNotificationBuilder, mockNotificationBuilder,
mockActionFactory); mockActionFactory);
mediaSession.release();
player.release();
verify(mockNotificationBuilder, times(4)).addAction(any()); verify(mockNotificationBuilder, times(4)).addAction(any());
InOrder inOrder = Mockito.inOrder(mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createMediaAction( .createMediaAction(
eq(mockMediaSession), any(), eq("displayName"), eq(commandButton1.playerCommand)); eq(mediaSession), any(), eq("displayName"), eq(commandButton1.playerCommand));
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); .createCustomActionFromCustomCommandButton(mediaSession, commandButton2);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton3); .createCustomActionFromCustomCommandButton(mediaSession, commandButton3);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton4); .createCustomActionFromCustomCommandButton(mediaSession, commandButton4);
verifyNoMoreInteractions(mockActionFactory); verifyNoMoreInteractions(mockActionFactory);
assertThat(compactViewIndices).asList().containsExactly(1, 3, 2).inOrder(); assertThat(compactViewIndices).asList().containsExactly(1, 3, 2).inOrder();
} }
@ -225,7 +240,6 @@ public class DefaultMediaNotificationProviderTest {
.build(); .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
CommandButton commandButton1 = CommandButton commandButton1 =
new CommandButton.Builder() new CommandButton.Builder()
.setDisplayName("displayName") .setDisplayName("displayName")
@ -238,13 +252,17 @@ public class DefaultMediaNotificationProviderTest {
.setDisplayName("displayName") .setDisplayName("displayName")
.setIconResId(R.drawable.media3_icon_circular_play) .setIconResId(R.drawable.media3_icon_circular_play)
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
int[] compactViewIndices = int[] compactViewIndices =
defaultMediaNotificationProvider.addNotificationActions( defaultMediaNotificationProvider.addNotificationActions(
mockMediaSession, mediaSession,
ImmutableList.of(commandButton1, commandButton2), ImmutableList.of(commandButton1, commandButton2),
mockNotificationBuilder, mockNotificationBuilder,
mockActionFactory); mockActionFactory);
mediaSession.release();
player.release();
ArgumentCaptor<NotificationCompat.Action> actionCaptor = ArgumentCaptor<NotificationCompat.Action> actionCaptor =
ArgumentCaptor.forClass(NotificationCompat.Action.class); ArgumentCaptor.forClass(NotificationCompat.Action.class);
@ -254,11 +272,11 @@ public class DefaultMediaNotificationProviderTest {
InOrder inOrder = Mockito.inOrder(mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); .createCustomActionFromCustomCommandButton(mediaSession, commandButton1);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createMediaAction( .createMediaAction(
eq(mockMediaSession), any(), eq("displayName"), eq(commandButton2.playerCommand)); eq(mediaSession), any(), eq("displayName"), eq(commandButton2.playerCommand));
verifyNoMoreInteractions(mockActionFactory); verifyNoMoreInteractions(mockActionFactory);
assertThat(compactViewIndices).asList().containsExactly(1); assertThat(compactViewIndices).asList().containsExactly(1);
} }
@ -271,25 +289,28 @@ public class DefaultMediaNotificationProviderTest {
.build(); .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
CommandButton commandButton1 = CommandButton commandButton1 =
new CommandButton.Builder() new CommandButton.Builder()
.setDisplayName("displayName") .setDisplayName("displayName")
.setIconResId(R.drawable.media3_icon_circular_play) .setIconResId(R.drawable.media3_icon_circular_play)
.setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY))
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
int[] compactViewIndices = int[] compactViewIndices =
defaultMediaNotificationProvider.addNotificationActions( defaultMediaNotificationProvider.addNotificationActions(
mockMediaSession, mediaSession,
ImmutableList.of(commandButton1), ImmutableList.of(commandButton1),
mockNotificationBuilder, mockNotificationBuilder,
mockActionFactory); mockActionFactory);
mediaSession.release();
player.release();
InOrder inOrder = Mockito.inOrder(mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); .createCustomActionFromCustomCommandButton(mediaSession, commandButton1);
verifyNoMoreInteractions(mockActionFactory); verifyNoMoreInteractions(mockActionFactory);
assertThat(compactViewIndices).asList().isEmpty(); assertThat(compactViewIndices).asList().isEmpty();
} }
@ -301,7 +322,6 @@ public class DefaultMediaNotificationProviderTest {
.build(); .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
Bundle commandButtonBundle1 = new Bundle(); Bundle commandButtonBundle1 = new Bundle();
commandButtonBundle1.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 2); commandButtonBundle1.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 2);
CommandButton commandButton1 = CommandButton commandButton1 =
@ -321,21 +341,25 @@ public class DefaultMediaNotificationProviderTest {
.setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY))
.setExtras(commandButtonBundle2) .setExtras(commandButtonBundle2)
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
int[] compactViewIndices = int[] compactViewIndices =
defaultMediaNotificationProvider.addNotificationActions( defaultMediaNotificationProvider.addNotificationActions(
mockMediaSession, mediaSession,
ImmutableList.of(commandButton1, commandButton2), ImmutableList.of(commandButton1, commandButton2),
mockNotificationBuilder, mockNotificationBuilder,
mockActionFactory); mockActionFactory);
mediaSession.release();
player.release();
InOrder inOrder = Mockito.inOrder(mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); .createCustomActionFromCustomCommandButton(mediaSession, commandButton1);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); .createCustomActionFromCustomCommandButton(mediaSession, commandButton2);
verifyNoMoreInteractions(mockActionFactory); verifyNoMoreInteractions(mockActionFactory);
assertThat(compactViewIndices).asList().isEmpty(); assertThat(compactViewIndices).asList().isEmpty();
} }
@ -347,7 +371,6 @@ public class DefaultMediaNotificationProviderTest {
.build(); .build();
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class);
MediaSession mockMediaSession = mock(MediaSession.class);
Bundle commandButtonBundle = new Bundle(); Bundle commandButtonBundle = new Bundle();
commandButtonBundle.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1); commandButtonBundle.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1);
CommandButton commandButton1 = CommandButton commandButton1 =
@ -357,18 +380,22 @@ public class DefaultMediaNotificationProviderTest {
.setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY))
.setExtras(commandButtonBundle) .setExtras(commandButtonBundle)
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
int[] compactViewIndices = int[] compactViewIndices =
defaultMediaNotificationProvider.addNotificationActions( defaultMediaNotificationProvider.addNotificationActions(
mockMediaSession, mediaSession,
ImmutableList.of(commandButton1), ImmutableList.of(commandButton1),
mockNotificationBuilder, mockNotificationBuilder,
mockActionFactory); mockActionFactory);
mediaSession.release();
player.release();
InOrder inOrder = Mockito.inOrder(mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory);
inOrder inOrder
.verify(mockActionFactory) .verify(mockActionFactory)
.createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); .createCustomActionFromCustomCommandButton(mediaSession, commandButton1);
verifyNoMoreInteractions(mockActionFactory); verifyNoMoreInteractions(mockActionFactory);
// [INDEX_UNSET, 1, INDEX_UNSET] cropped up to the first INDEX_UNSET value // [INDEX_UNSET, 1, INDEX_UNSET] cropped up to the first INDEX_UNSET value
assertThat(compactViewIndices).asList().isEmpty(); assertThat(compactViewIndices).asList().isEmpty();
@ -382,10 +409,6 @@ public class DefaultMediaNotificationProviderTest {
NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class);
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); 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(); Bundle commandButtonBundle = new Bundle();
commandButtonBundle.putString("testKey", "testValue"); commandButtonBundle.putString("testKey", "testValue");
CommandButton commandButton1 = CommandButton commandButton1 =
@ -395,19 +418,21 @@ public class DefaultMediaNotificationProviderTest {
.setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY))
.setExtras(commandButtonBundle) .setExtras(commandButtonBundle)
.build(); .build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
defaultMediaNotificationProvider.addNotificationActions( defaultMediaNotificationProvider.addNotificationActions(
mockMediaSession, mediaSession,
ImmutableList.of(commandButton1), ImmutableList.of(commandButton1),
mockNotificationBuilder, mockNotificationBuilder,
defaultActionFactory); defaultActionFactory);
mediaSession.release();
player.release();
ArgumentCaptor<NotificationCompat.Action> actionCaptor = ArgumentCaptor<NotificationCompat.Action> actionCaptor =
ArgumentCaptor.forClass(NotificationCompat.Action.class); ArgumentCaptor.forClass(NotificationCompat.Action.class);
verify(mockNotificationBuilder).addAction(actionCaptor.capture()); verify(mockNotificationBuilder).addAction(actionCaptor.capture());
verifyNoMoreInteractions(mockNotificationBuilder); verifyNoMoreInteractions(mockNotificationBuilder);
verify(mockMediaSessionImpl).getUri();
verifyNoMoreInteractions(mockMediaSessionImpl);
List<NotificationCompat.Action> actions = actionCaptor.getAllValues(); List<NotificationCompat.Action> actions = actionCaptor.getAllValues();
assertThat(actions).hasSize(1); assertThat(actions).hasSize(1);
assertThat(String.valueOf(actions.get(0).title)).isEqualTo("displayName1"); assertThat(String.valueOf(actions.get(0).title)).isEqualTo("displayName1");
@ -426,8 +451,8 @@ public class DefaultMediaNotificationProviderTest {
shadowOf(Looper.getMainLooper()).pause(); shadowOf(Looper.getMainLooper()).pause();
// Create a MediaSession whose player returns non-null media metadata so that the // Create a MediaSession whose player returns non-null media metadata so that the
// notification provider will request to load artwork bitmaps. // notification provider will request to load artwork bitmaps.
MediaSession mockMediaSession = Player player =
createMockMediaSessionForNotification( createPlayerWithMetadata(
new MediaMetadata.Builder() new MediaMetadata.Builder()
.setArtworkUri(Uri.parse("http://example.test/image.jpg")) .setArtworkUri(Uri.parse("http://example.test/image.jpg"))
.build()); .build());
@ -436,7 +461,8 @@ public class DefaultMediaNotificationProviderTest {
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
SettableFuture<Bitmap> bitmapFuture = SettableFuture.create(); SettableFuture<Bitmap> bitmapFuture = SettableFuture.create();
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(bitmapFuture); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(bitmapFuture);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider = DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build(); .build();
@ -447,7 +473,7 @@ public class DefaultMediaNotificationProviderTest {
MediaNotification.Provider.Callback mockOnNotificationChangedCallback1 = MediaNotification.Provider.Callback mockOnNotificationChangedCallback1 =
mock(MediaNotification.Provider.Callback.class); mock(MediaNotification.Provider.Callback.class);
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
/* customLayout= */ ImmutableList.of(), /* customLayout= */ ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mockOnNotificationChangedCallback1); mockOnNotificationChangedCallback1);
@ -456,13 +482,16 @@ public class DefaultMediaNotificationProviderTest {
MediaNotification.Provider.Callback mockOnNotificationChangedCallback2 = MediaNotification.Provider.Callback mockOnNotificationChangedCallback2 =
mock(MediaNotification.Provider.Callback.class); mock(MediaNotification.Provider.Callback.class);
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
/* customLayout= */ ImmutableList.of(), /* customLayout= */ ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mockOnNotificationChangedCallback2); mockOnNotificationChangedCallback2);
// The bitmap has arrived. // The bitmap has arrived.
bitmapFuture.set(Bitmap.createBitmap(/* width= */ 8, /* height= */ 8, Bitmap.Config.RGB_565)); bitmapFuture.set(Bitmap.createBitmap(/* width= */ 8, /* height= */ 8, Bitmap.Config.RGB_565));
ShadowLooper.idleMainLooper(); ShadowLooper.idleMainLooper();
mediaSession.release();
player.release();
verify(mockOnNotificationChangedCallback2).onNotificationChanged(any()); verify(mockOnNotificationChangedCallback2).onNotificationChanged(any());
verifyNoInteractions(mockOnNotificationChangedCallback1); verifyNoInteractions(mockOnNotificationChangedCallback1);
} }
@ -472,19 +501,22 @@ public class DefaultMediaNotificationProviderTest {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
DefaultMediaNotificationProvider defaultMediaNotificationProvider = DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(context).build(); new DefaultMediaNotificationProvider.Builder(context).build();
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
assertThat(notification.notificationId).isEqualTo(DEFAULT_NOTIFICATION_ID); assertThat(notification.notificationId).isEqualTo(DEFAULT_NOTIFICATION_ID);
assertThat(notification.notification.getChannelId()).isEqualTo(DEFAULT_CHANNEL_ID); assertThat(notification.notification.getChannelId()).isEqualTo(DEFAULT_CHANNEL_ID);
@ -506,19 +538,22 @@ public class DefaultMediaNotificationProviderTest {
.setChannelId(/* channelId= */ "customChannelId") .setChannelId(/* channelId= */ "customChannelId")
.setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description) .setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description)
.build(); .build();
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
assertThat(notification.notificationId).isEqualTo(2); assertThat(notification.notificationId).isEqualTo(2);
assertThat(notification.notification.getChannelId()).isEqualTo("customChannelId"); assertThat(notification.notification.getChannelId()).isEqualTo("customChannelId");
@ -543,19 +578,22 @@ public class DefaultMediaNotificationProviderTest {
}) })
.setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description) .setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description)
.build(); .build();
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
assertThat(notification.notificationId).isEqualTo(3); assertThat(notification.notificationId).isEqualTo(3);
} }
@ -567,14 +605,15 @@ public class DefaultMediaNotificationProviderTest {
new DefaultMediaNotificationProvider.Builder(context).build(); new DefaultMediaNotificationProvider.Builder(context).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
@ -582,10 +621,12 @@ public class DefaultMediaNotificationProviderTest {
defaultMediaNotificationProvider.setSmallIcon(R.drawable.media3_icon_circular_play); defaultMediaNotificationProvider.setSmallIcon(R.drawable.media3_icon_circular_play);
MediaNotification notificationWithSmallIcon = MediaNotification notificationWithSmallIcon =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
assertThat(notification.notification.getSmallIcon().getResId()) assertThat(notification.notification.getSmallIcon().getResId())
.isEqualTo(R.drawable.media3_notification_small_icon); .isEqualTo(R.drawable.media3_notification_small_icon);
@ -600,19 +641,20 @@ public class DefaultMediaNotificationProviderTest {
new DefaultMediaNotificationProvider.Builder(context).build(); new DefaultMediaNotificationProvider.Builder(context).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mockMediaSession = Player player = createPlayerWithMetadata(new MediaMetadata.Builder().setTitle("title").build());
createMockMediaSessionForNotification(
new MediaMetadata.Builder().setTitle("title").build());
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
boolean isMediaMetadataTitleEqualToNotificationContentTitle = boolean isMediaMetadataTitleEqualToNotificationContentTitle =
"title".contentEquals(NotificationCompat.getContentTitle(notification.notification)); "title".contentEquals(NotificationCompat.getContentTitle(notification.notification));
@ -626,19 +668,21 @@ public class DefaultMediaNotificationProviderTest {
new DefaultMediaNotificationProvider.Builder(context).build(); new DefaultMediaNotificationProvider.Builder(context).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mockMediaSession = Player player =
createMockMediaSessionForNotification( createPlayerWithMetadata(new MediaMetadata.Builder().setArtist("artist").build());
new MediaMetadata.Builder().setArtist("artist").build());
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
boolean isMediaMetadataArtistEqualToNotificationContentText = boolean isMediaMetadataArtistEqualToNotificationContentText =
"artist".contentEquals(NotificationCompat.getContentText(notification.notification)); "artist".contentEquals(NotificationCompat.getContentText(notification.notification));
@ -653,20 +697,23 @@ public class DefaultMediaNotificationProviderTest {
new DefaultMediaNotificationProvider.Builder(context).build(); new DefaultMediaNotificationProvider.Builder(context).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mockMediaSession = Player player =
createMockMediaSessionForNotification( createPlayerWithMetadata(
new MediaMetadata.Builder().setArtist("artist").setTitle("title").build(), new MediaMetadata.Builder().setArtist("artist").setTitle("title").build(),
/* getMetadataCommandAvailable= */ false); /* isMetadataCommandAvailable= */ false);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
ImmutableList.of(), ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
mock(MediaNotification.Provider.Callback.class)); mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
assertThat(NotificationCompat.getContentText(notification.notification)).isNull(); assertThat(NotificationCompat.getContentText(notification.notification)).isNull();
assertThat(NotificationCompat.getContentTitle(notification.notification)).isNull(); assertThat(NotificationCompat.getContentTitle(notification.notification)).isNull();
@ -681,19 +728,22 @@ public class DefaultMediaNotificationProviderTest {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
DefaultMediaNotificationProvider defaultMediaNotificationProvider = DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider(context); new DefaultMediaNotificationProvider(context);
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession =
new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build();
DefaultActionFactory defaultActionFactory = DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class)); new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaNotification notification = MediaNotification notification =
defaultMediaNotificationProvider.createNotification( defaultMediaNotificationProvider.createNotification(
mockMediaSession, mediaSession,
/* customLayout= */ ImmutableList.of(), /* customLayout= */ ImmutableList.of(),
defaultActionFactory, defaultActionFactory,
/* onNotificationChangedCallback= */ mock(MediaNotification.Provider.Callback.class)); /* onNotificationChangedCallback= */ mock(MediaNotification.Provider.Callback.class));
mediaSession.release();
player.release();
assertThat(notification.notificationId).isEqualTo(DEFAULT_NOTIFICATION_ID); assertThat(notification.notificationId).isEqualTo(DEFAULT_NOTIFICATION_ID);
assertThat(notification.notification.getChannelId()).isEqualTo(DEFAULT_CHANNEL_ID); assertThat(notification.notification.getChannelId()).isEqualTo(DEFAULT_CHANNEL_ID);
@ -765,29 +815,32 @@ public class DefaultMediaNotificationProviderTest {
assertThat(found).isTrue(); assertThat(found).isTrue();
} }
private static MediaSession createMockMediaSessionForNotification(MediaMetadata mediaMetadata) { private Player createPlayerWithMetadata(MediaMetadata mediaMetadata) {
return createMockMediaSessionForNotification( return createPlayerWithMetadata(mediaMetadata, /* isMetadataCommandAvailable= */ true);
mediaMetadata, /* getMetadataCommandAvailable= */ true);
} }
private static MediaSession createMockMediaSessionForNotification( private Player createPlayerWithMetadata(
MediaMetadata mediaMetadata, boolean getMetadataCommandAvailable) { MediaMetadata mediaMetadata, boolean isMetadataCommandAvailable) {
Player mockPlayer = mock(Player.class); return new ForwardingPlayer(new TestExoPlayerBuilder(context).build()) {
when(mockPlayer.isCommandAvailable(anyInt())).thenReturn(false); @Override
if (getMetadataCommandAvailable) { public boolean isCommandAvailable(int command) {
when(mockPlayer.getAvailableCommands()) return isMetadataCommandAvailable || command != Player.COMMAND_GET_MEDIA_ITEMS_METADATA;
.thenReturn(new Commands.Builder().add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA).build());
when(mockPlayer.isCommandAvailable(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)).thenReturn(true);
when(mockPlayer.getMediaMetadata()).thenReturn(mediaMetadata);
} else {
when(mockPlayer.getAvailableCommands()).thenReturn(Commands.EMPTY);
} }
MediaSession mockMediaSession = mock(MediaSession.class);
when(mockMediaSession.getPlayer()).thenReturn(mockPlayer); @Override
MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); public Commands getAvailableCommands() {
when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); Commands.Builder commandsBuilder = new Commands.Builder().addAllCommands();
when(mockMediaSessionImpl.getUri()).thenReturn(Uri.parse("https://example.test")); if (!isMetadataCommandAvailable) {
return mockMediaSession; commandsBuilder.remove(Player.COMMAND_GET_MEDIA_ITEMS_METADATA);
}
return commandsBuilder.build();
}
@Override
public MediaMetadata getMediaMetadata() {
return isMetadataCommandAvailable ? mediaMetadata : MediaMetadata.EMPTY;
}
};
} }
/** A test service for unit tests. */ /** A test service for unit tests. */