From ff919fe74d9a8a6a2c446033b6ee2a9eb0c64934 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 4 Apr 2023 16:50:17 +0100 Subject: [PATCH] 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 --- api.txt | 30 +-- libraries/session/build.gradle | 1 + .../androidx/media3/session/MediaSession.java | 53 ++-- .../session/DefaultActionFactoryTest.java | 68 +++-- .../DefaultMediaNotificationProviderTest.java | 237 +++++++++++------- 5 files changed, 218 insertions(+), 171 deletions(-) diff --git a/api.txt b/api.txt index a733063d74..64fe17bf88 100644 --- a/api.txt +++ b/api.txt @@ -1625,21 +1625,21 @@ package androidx.media3.session { field @IntRange(from=1) public final int notificationId; } - public class MediaSession { - method public void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); - method public java.util.List getConnectedControllers(); - method public String getId(); - method public androidx.media3.common.Player getPlayer(); - method @Nullable public android.app.PendingIntent getSessionActivity(); - method public androidx.media3.session.SessionToken getToken(); - method public void release(); - method public com.google.common.util.concurrent.ListenableFuture 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 com.google.common.util.concurrent.ListenableFuture setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List); - method public void setCustomLayout(java.util.List); - method public void setPlayer(androidx.media3.common.Player); - method public void setSessionExtras(android.os.Bundle); - method public void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle); + @com.google.errorprone.annotations.DoNotMock public class MediaSession { + method public final void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); + method public final java.util.List getConnectedControllers(); + method public final String getId(); + method public final androidx.media3.common.Player getPlayer(); + method @Nullable public final android.app.PendingIntent getSessionActivity(); + method public final androidx.media3.session.SessionToken getToken(); + method public final void release(); + method public final com.google.common.util.concurrent.ListenableFuture sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle); + method public final void setAvailableCommands(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands); + method public final com.google.common.util.concurrent.ListenableFuture setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List); + method public final void setCustomLayout(java.util.List); + method public final void setPlayer(androidx.media3.common.Player); + method public final void setSessionExtras(android.os.Bundle); + method public final void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle); } public static final class MediaSession.Builder { diff --git a/libraries/session/build.gradle b/libraries/session/build.gradle index 74e341395f..b64f025b9c 100644 --- a/libraries/session/build.gradle +++ b/libraries/session/build.gradle @@ -43,6 +43,7 @@ dependencies { androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion testImplementation project(modulePrefix + 'test-utils') + testImplementation project(modulePrefix + 'lib-exoplayer') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index e491ff96e3..b59a672f12 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -63,6 +63,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.primitives.Longs; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.DoNotMock; import java.util.HashMap; import java.util.List; 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 * connect and an app can accept such controllers in the same way as with legacy sessions. */ +@DoNotMock public class MediaSession { static { @@ -592,7 +594,7 @@ public class MediaSession { * @return The {@link PendingIntent} to launch an activity belonging to the session. */ @Nullable - public PendingIntent getSessionActivity() { + public final PendingIntent getSessionActivity() { return impl.getSessionActivity(); } @@ -605,7 +607,7 @@ public class MediaSession { * @throws IllegalStateException if the new player's application looper differs from the current * looper. */ - public void setPlayer(Player player) { + public final void setPlayer(Player player) { checkNotNull(player); checkArgument(player.canAdvertiseSession()); 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 * release the player. */ - public void release() { + public final void release() { try { synchronized (STATIC_LOCK) { 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(); } /** Returns the underlying {@link Player}. */ - public Player getPlayer() { + public final Player getPlayer() { return impl.getPlayerWrapper().getWrappedPlayer(); } /** Returns the session ID. */ - public String getId() { + public final String getId() { return impl.getId(); } /** Returns the {@link SessionToken} for creating {@link MediaController}. */ - public SessionToken getToken() { + public final SessionToken getToken() { return impl.getToken(); } /** Returns the list of connected controllers. */ - public List getConnectedControllers() { + public final List getConnectedControllers() { return impl.getConnectedControllers(); } @@ -705,7 +707,7 @@ public class MediaSession { * @param controller The controller to specify layout. * @param layout The ordered list of {@link CommandButton}. */ - public ListenableFuture setCustomLayout( + public final ListenableFuture setCustomLayout( ControllerInfo controller, List layout) { checkNotNull(controller, "controller 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}. */ - public void setCustomLayout(List layout) { + public final void setCustomLayout(List layout) { checkNotNull(layout, "layout must not be null"); impl.setCustomLayout(layout); } @@ -749,7 +751,7 @@ public class MediaSession { * @param sessionCommands The new available session commands. * @param playerCommands The new available player commands. */ - public void setAvailableCommands( + public final void setAvailableCommands( ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) { checkNotNull(controller, "controller 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. * @see #sendCustomCommand(ControllerInfo, SessionCommand, Bundle) */ - public void broadcastCustomCommand(SessionCommand command, Bundle args) { + public final void broadcastCustomCommand(SessionCommand command, Bundle args) { checkNotNull(command); checkNotNull(args); checkArgument( @@ -784,7 +786,7 @@ public class MediaSession { * * @param sessionExtras The session extras. */ - public void setSessionExtras(Bundle sessionExtras) { + public final void setSessionExtras(Bundle sessionExtras) { checkNotNull(sessionExtras); impl.setSessionExtras(sessionExtras); } @@ -797,7 +799,7 @@ public class MediaSession { * @param controller The controller to send the extras to. * @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(sessionExtras); impl.setSessionExtras(controller, sessionExtras); @@ -805,7 +807,7 @@ public class MediaSession { /** Returns the {@link BitmapLoader}. */ @UnstableApi - public BitmapLoader getBitmapLoader() { + public final BitmapLoader getBitmapLoader() { return impl.getBitmapLoader(); } @@ -823,7 +825,7 @@ public class MediaSession { * @return A {@link ListenableFuture} of {@link SessionResult} from the controller. * @see #broadcastCustomCommand(SessionCommand, Bundle) */ - public ListenableFuture sendCustomCommand( + public final ListenableFuture sendCustomCommand( ControllerInfo controller, SessionCommand command, Bundle args) { checkNotNull(controller); checkNotNull(command); @@ -834,7 +836,7 @@ public class MediaSession { return impl.sendCustomCommand(controller, command, args); } - /* package */ MediaSessionCompat getSessionCompat() { + /* package */ final MediaSessionCompat getSessionCompat() { return impl.getSessionCompat(); } @@ -843,7 +845,7 @@ public class MediaSession { * internally by this session. */ @UnstableApi - public MediaSessionCompat.Token getSessionCompatToken() { + public final MediaSessionCompat.Token getSessionCompatToken() { return impl.getSessionCompat().getSessionToken(); } @@ -852,12 +854,12 @@ public class MediaSession { * * @param timeoutMs The timeout in milliseconds. */ - /* package */ void setLegacyControllerConnectionTimeoutMs(long timeoutMs) { + /* package */ final void setLegacyControllerConnectionTimeoutMs(long timeoutMs) { impl.setLegacyControllerConnectionTimeoutMs(timeoutMs); } /** Handles the controller's connection request from {@link MediaSessionService}. */ - /* package */ void handleControllerConnectionFromService( + /* package */ final void handleControllerConnectionFromService( IMediaController controller, int controllerVersion, int controllerInterfaceVersion, @@ -875,7 +877,7 @@ public class MediaSession { connectionHints); } - /* package */ IBinder getLegacyBrowserServiceBinder() { + /* package */ final IBinder getLegacyBrowserServiceBinder() { return impl.getLegacyBrowserServiceBinder(); } @@ -887,21 +889,22 @@ public class MediaSession { * after an immediate one-time update. */ @VisibleForTesting - /* package */ void setSessionPositionUpdateDelayMs(long updateDelayMs) { + /* package */ final void setSessionPositionUpdateDelayMs(long updateDelayMs) { impl.setSessionPositionUpdateDelayMsOnHandler(updateDelayMs); } /** Sets the {@linkplain Listener listener}. */ - /* package */ void setListener(Listener listener) { + /* package */ final void setListener(Listener listener) { impl.setMediaSessionListener(listener); } /** Clears the {@linkplain Listener listener}. */ - /* package */ void clearListener() { + /* package */ final void clearListener() { impl.clearMediaSessionListener(); } - private Uri getUri() { + @VisibleForTesting + /* package */ final Uri getUri() { return impl.getUri(); } diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java index d83ad4d74b..d4e904ccb7 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java @@ -16,19 +16,21 @@ package androidx.media3.session; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; 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 org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -38,41 +40,42 @@ import org.robolectric.shadows.ShadowPendingIntent; @RunWith(AndroidJUnit4.class) 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 public void createMediaPendingIntent_intentIsMediaAction() { DefaultActionFactory actionFactory = 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 = - actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_SEEK_FORWARD); + actionFactory.createMediaActionPendingIntent(mediaSession, Player.COMMAND_SEEK_FORWARD); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue(); - assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(dataUri); + assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(mediaSession.getUri()); } @Test public void createMediaPendingIntent_commandPlayPauseWhenNotPlayWhenReady_isForegroundService() { DefaultActionFactory actionFactory = 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 = - actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_PLAY_PAUSE); + actionFactory.createMediaActionPendingIntent(mediaSession, Player.COMMAND_PLAY_PAUSE); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); assertThat(shadowPendingIntent.isForegroundService()).isTrue(); @@ -82,17 +85,10 @@ public class DefaultActionFactoryTest { public void createMediaPendingIntent_commandPlayPauseWhenPlayWhenReady_notAForegroundService() { DefaultActionFactory actionFactory = 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 = - actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_PLAY_PAUSE); + actionFactory.createMediaActionPendingIntent(mediaSession, Player.COMMAND_PLAY_PAUSE); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue(); @@ -123,11 +119,6 @@ public class DefaultActionFactoryTest { public void createCustomActionFromCustomCommandButton() { DefaultActionFactory actionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - MediaSession mockMediaSession = mock(MediaSession.class); - MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); - when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); - Uri dataUri = Uri.parse("http://example.com"); - when(mockMediaSessionImpl.getUri()).thenReturn(dataUri); Bundle commandBundle = new Bundle(); commandBundle.putString("command-key", "command-value"); Bundle buttonBundle = new Bundle(); @@ -141,11 +132,10 @@ public class DefaultActionFactoryTest { .build(); NotificationCompat.Action notificationAction = - actionFactory.createCustomActionFromCustomCommandButton( - mockMediaSession, customSessionCommand); + actionFactory.createCustomActionFromCustomCommandButton(mediaSession, customSessionCommand); 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(notificationAction.getIconCompat().getResId()) .isEqualTo(R.drawable.media3_notification_pause); @@ -169,7 +159,7 @@ public class DefaultActionFactoryTest { IllegalArgumentException.class, () -> actionFactory.createCustomActionFromCustomCommandButton( - mock(MediaSession.class), customSessionCommand)); + mediaSession, customSessionCommand)); } /** A test service for unit tests. */ diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index d11dff27f6..6866f25fc4 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -20,7 +20,6 @@ import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_C import static androidx.media3.session.DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -39,10 +38,12 @@ import android.os.Bundle; import android.os.Looper; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; +import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.Player.Commands; import androidx.media3.common.util.BitmapLoader; +import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; @@ -62,26 +63,31 @@ import org.robolectric.shadows.ShadowNotificationManager; @RunWith(AndroidJUnit4.class) public class DefaultMediaNotificationProviderTest { + private final Context context = ApplicationProvider.getApplicationContext(); + @Test public void getMediaButtons_playWhenReadyTrueOrFalse_correctPlayPauseResources() { DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) .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 mediaButtonsWhenPlaying = defaultMediaNotificationProvider.getMediaButtons( - mockMediaSession, + mediaSession, commands, /* customLayout= */ ImmutableList.of(), /* showPauseButton= */ true); List mediaButtonWhenPaused = defaultMediaNotificationProvider.getMediaButtons( - mockMediaSession, + mediaSession, commands, /* customLayout= */ ImmutableList.of(), /* showPauseButton= */ false); + mediaSession.release(); + player.release(); assertThat(mediaButtonsWhenPlaying).hasSize(3); assertThat(mediaButtonsWhenPlaying.get(1).playerCommand).isEqualTo(Player.COMMAND_PLAY_PAUSE); @@ -100,7 +106,6 @@ public class DefaultMediaNotificationProviderTest { DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) .build(); - MediaSession mockMediaSession = mock(MediaSession.class); Commands commands = new Commands.Builder().addAllCommands().build(); SessionCommand customSessionCommand = new SessionCommand("", Bundle.EMPTY); CommandButton customCommandButton = @@ -109,13 +114,17 @@ public class DefaultMediaNotificationProviderTest { .setIconResId(R.drawable.media3_icon_circular_play) .setSessionCommand(customSessionCommand) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); List mediaButtons = defaultMediaNotificationProvider.getMediaButtons( - mockMediaSession, + mediaSession, commands, ImmutableList.of(customCommandButton), /* showPauseButton= */ true); + mediaSession.release(); + player.release(); assertThat(mediaButtons).hasSize(4); assertThat(mediaButtons.get(0).playerCommand) @@ -130,7 +139,6 @@ public class DefaultMediaNotificationProviderTest { DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) .build(); - MediaSession mockMediaSession = mock(MediaSession.class); Commands commands = new Commands.Builder().build(); SessionCommand customSessionCommand = new SessionCommand("action1", Bundle.EMPTY); CommandButton customCommandButton = @@ -139,13 +147,17 @@ public class DefaultMediaNotificationProviderTest { .setIconResId(R.drawable.media3_icon_circular_play) .setSessionCommand(customSessionCommand) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); List mediaButtons = defaultMediaNotificationProvider.getMediaButtons( - mockMediaSession, + mediaSession, commands, ImmutableList.of(customCommandButton), /* showPauseButton= */ true); + mediaSession.release(); + player.release(); assertThat(mediaButtons).containsExactly(customCommandButton); } @@ -157,7 +169,6 @@ public class DefaultMediaNotificationProviderTest { .build(); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); - MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) @@ -191,29 +202,33 @@ public class DefaultMediaNotificationProviderTest { .setSessionCommand(new SessionCommand("action4", Bundle.EMPTY)) .setExtras(commandButton4Bundle) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - mockMediaSession, + mediaSession, ImmutableList.of(commandButton1, commandButton2, commandButton3, commandButton4), mockNotificationBuilder, mockActionFactory); + mediaSession.release(); + player.release(); verify(mockNotificationBuilder, times(4)).addAction(any()); InOrder inOrder = Mockito.inOrder(mockActionFactory); inOrder .verify(mockActionFactory) .createMediaAction( - eq(mockMediaSession), any(), eq("displayName"), eq(commandButton1.playerCommand)); + eq(mediaSession), any(), eq("displayName"), eq(commandButton1.playerCommand)); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton2); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton3); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton3); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton4); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton4); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().containsExactly(1, 3, 2).inOrder(); } @@ -225,7 +240,6 @@ public class DefaultMediaNotificationProviderTest { .build(); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); - MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setDisplayName("displayName") @@ -238,13 +252,17 @@ public class DefaultMediaNotificationProviderTest { .setDisplayName("displayName") .setIconResId(R.drawable.media3_icon_circular_play) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - mockMediaSession, + mediaSession, ImmutableList.of(commandButton1, commandButton2), mockNotificationBuilder, mockActionFactory); + mediaSession.release(); + player.release(); ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(NotificationCompat.Action.class); @@ -254,11 +272,11 @@ public class DefaultMediaNotificationProviderTest { InOrder inOrder = Mockito.inOrder(mockActionFactory); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton1); inOrder .verify(mockActionFactory) .createMediaAction( - eq(mockMediaSession), any(), eq("displayName"), eq(commandButton2.playerCommand)); + eq(mediaSession), any(), eq("displayName"), eq(commandButton2.playerCommand)); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().containsExactly(1); } @@ -271,25 +289,28 @@ public class DefaultMediaNotificationProviderTest { .build(); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); - MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setDisplayName("displayName") .setIconResId(R.drawable.media3_icon_circular_play) .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - mockMediaSession, + mediaSession, ImmutableList.of(commandButton1), mockNotificationBuilder, mockActionFactory); + mediaSession.release(); + player.release(); InOrder inOrder = Mockito.inOrder(mockActionFactory); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton1); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().isEmpty(); } @@ -301,7 +322,6 @@ public class DefaultMediaNotificationProviderTest { .build(); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); - MediaSession mockMediaSession = mock(MediaSession.class); Bundle commandButtonBundle1 = new Bundle(); commandButtonBundle1.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 2); CommandButton commandButton1 = @@ -321,21 +341,25 @@ public class DefaultMediaNotificationProviderTest { .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .setExtras(commandButtonBundle2) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - mockMediaSession, + mediaSession, ImmutableList.of(commandButton1, commandButton2), mockNotificationBuilder, mockActionFactory); + mediaSession.release(); + player.release(); InOrder inOrder = Mockito.inOrder(mockActionFactory); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton1); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton2); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().isEmpty(); } @@ -347,7 +371,6 @@ public class DefaultMediaNotificationProviderTest { .build(); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); - MediaSession mockMediaSession = mock(MediaSession.class); Bundle commandButtonBundle = new Bundle(); commandButtonBundle.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1); CommandButton commandButton1 = @@ -357,18 +380,22 @@ public class DefaultMediaNotificationProviderTest { .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .setExtras(commandButtonBundle) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - mockMediaSession, + mediaSession, ImmutableList.of(commandButton1), mockNotificationBuilder, mockActionFactory); + mediaSession.release(); + player.release(); InOrder inOrder = Mockito.inOrder(mockActionFactory); inOrder .verify(mockActionFactory) - .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + .createCustomActionFromCustomCommandButton(mediaSession, commandButton1); verifyNoMoreInteractions(mockActionFactory); // [INDEX_UNSET, 1, INDEX_UNSET] cropped up to the first INDEX_UNSET value assertThat(compactViewIndices).asList().isEmpty(); @@ -382,10 +409,6 @@ public class DefaultMediaNotificationProviderTest { NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); DefaultActionFactory defaultActionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - MediaSession mockMediaSession = mock(MediaSession.class); - MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); - when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); - when(mockMediaSessionImpl.getUri()).thenReturn(Uri.parse("http://example.com")); Bundle commandButtonBundle = new Bundle(); commandButtonBundle.putString("testKey", "testValue"); CommandButton commandButton1 = @@ -395,19 +418,21 @@ public class DefaultMediaNotificationProviderTest { .setSessionCommand(new SessionCommand("action1", Bundle.EMPTY)) .setExtras(commandButtonBundle) .build(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession mediaSession = new MediaSession.Builder(context, player).build(); defaultMediaNotificationProvider.addNotificationActions( - mockMediaSession, + mediaSession, ImmutableList.of(commandButton1), mockNotificationBuilder, defaultActionFactory); + mediaSession.release(); + player.release(); ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(NotificationCompat.Action.class); verify(mockNotificationBuilder).addAction(actionCaptor.capture()); verifyNoMoreInteractions(mockNotificationBuilder); - verify(mockMediaSessionImpl).getUri(); - verifyNoMoreInteractions(mockMediaSessionImpl); List actions = actionCaptor.getAllValues(); assertThat(actions).hasSize(1); assertThat(String.valueOf(actions.get(0).title)).isEqualTo("displayName1"); @@ -426,8 +451,8 @@ public class DefaultMediaNotificationProviderTest { shadowOf(Looper.getMainLooper()).pause(); // Create a MediaSession whose player returns non-null media metadata so that the // notification provider will request to load artwork bitmaps. - MediaSession mockMediaSession = - createMockMediaSessionForNotification( + Player player = + createPlayerWithMetadata( new MediaMetadata.Builder() .setArtworkUri(Uri.parse("http://example.test/image.jpg")) .build()); @@ -436,7 +461,8 @@ public class DefaultMediaNotificationProviderTest { BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); SettableFuture bitmapFuture = SettableFuture.create(); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(bitmapFuture); - when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); + MediaSession mediaSession = + new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build(); DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext()) .build(); @@ -447,7 +473,7 @@ public class DefaultMediaNotificationProviderTest { MediaNotification.Provider.Callback mockOnNotificationChangedCallback1 = mock(MediaNotification.Provider.Callback.class); defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, /* customLayout= */ ImmutableList.of(), defaultActionFactory, mockOnNotificationChangedCallback1); @@ -456,13 +482,16 @@ public class DefaultMediaNotificationProviderTest { MediaNotification.Provider.Callback mockOnNotificationChangedCallback2 = mock(MediaNotification.Provider.Callback.class); defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, /* customLayout= */ ImmutableList.of(), defaultActionFactory, mockOnNotificationChangedCallback2); // The bitmap has arrived. bitmapFuture.set(Bitmap.createBitmap(/* width= */ 8, /* height= */ 8, Bitmap.Config.RGB_565)); ShadowLooper.idleMainLooper(); + mediaSession.release(); + player.release(); + verify(mockOnNotificationChangedCallback2).onNotificationChanged(any()); verifyNoInteractions(mockOnNotificationChangedCallback1); } @@ -472,19 +501,22 @@ public class DefaultMediaNotificationProviderTest { Context context = ApplicationProvider.getApplicationContext(); DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider.Builder(context).build(); - MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); 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 = new DefaultActionFactory(Robolectric.setupService(TestService.class)); MediaNotification notification = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); assertThat(notification.notificationId).isEqualTo(DEFAULT_NOTIFICATION_ID); assertThat(notification.notification.getChannelId()).isEqualTo(DEFAULT_CHANNEL_ID); @@ -506,19 +538,22 @@ public class DefaultMediaNotificationProviderTest { .setChannelId(/* channelId= */ "customChannelId") .setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description) .build(); - MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); 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 = new DefaultActionFactory(Robolectric.setupService(TestService.class)); MediaNotification notification = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); assertThat(notification.notificationId).isEqualTo(2); assertThat(notification.notification.getChannelId()).isEqualTo("customChannelId"); @@ -543,19 +578,22 @@ public class DefaultMediaNotificationProviderTest { }) .setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description) .build(); - MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); 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 = new DefaultActionFactory(Robolectric.setupService(TestService.class)); MediaNotification notification = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); assertThat(notification.notificationId).isEqualTo(3); } @@ -567,14 +605,15 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider.Builder(context).build(); DefaultActionFactory defaultActionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); 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 = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); @@ -582,10 +621,12 @@ public class DefaultMediaNotificationProviderTest { defaultMediaNotificationProvider.setSmallIcon(R.drawable.media3_icon_circular_play); MediaNotification notificationWithSmallIcon = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); assertThat(notification.notification.getSmallIcon().getResId()) .isEqualTo(R.drawable.media3_notification_small_icon); @@ -600,19 +641,20 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider.Builder(context).build(); DefaultActionFactory defaultActionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - MediaSession mockMediaSession = - createMockMediaSessionForNotification( - new MediaMetadata.Builder().setTitle("title").build()); + Player player = createPlayerWithMetadata(new MediaMetadata.Builder().setTitle("title").build()); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); - when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); + MediaSession mediaSession = + new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build(); MediaNotification notification = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); boolean isMediaMetadataTitleEqualToNotificationContentTitle = "title".contentEquals(NotificationCompat.getContentTitle(notification.notification)); @@ -626,19 +668,21 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider.Builder(context).build(); DefaultActionFactory defaultActionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - MediaSession mockMediaSession = - createMockMediaSessionForNotification( - new MediaMetadata.Builder().setArtist("artist").build()); + Player player = + createPlayerWithMetadata(new MediaMetadata.Builder().setArtist("artist").build()); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); - when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); + MediaSession mediaSession = + new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build(); MediaNotification notification = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); boolean isMediaMetadataArtistEqualToNotificationContentText = "artist".contentEquals(NotificationCompat.getContentText(notification.notification)); @@ -653,20 +697,23 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider.Builder(context).build(); DefaultActionFactory defaultActionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - MediaSession mockMediaSession = - createMockMediaSessionForNotification( + Player player = + createPlayerWithMetadata( new MediaMetadata.Builder().setArtist("artist").setTitle("title").build(), - /* getMetadataCommandAvailable= */ false); + /* isMetadataCommandAvailable= */ false); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null); - when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader); + MediaSession mediaSession = + new MediaSession.Builder(context, player).setBitmapLoader(mockBitmapLoader).build(); MediaNotification notification = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, ImmutableList.of(), defaultActionFactory, mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); assertThat(NotificationCompat.getContentText(notification.notification)).isNull(); assertThat(NotificationCompat.getContentTitle(notification.notification)).isNull(); @@ -681,19 +728,22 @@ public class DefaultMediaNotificationProviderTest { Context context = ApplicationProvider.getApplicationContext(); DefaultMediaNotificationProvider defaultMediaNotificationProvider = new DefaultMediaNotificationProvider(context); - MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY); BitmapLoader mockBitmapLoader = mock(BitmapLoader.class); 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 = new DefaultActionFactory(Robolectric.setupService(TestService.class)); MediaNotification notification = defaultMediaNotificationProvider.createNotification( - mockMediaSession, + mediaSession, /* customLayout= */ ImmutableList.of(), defaultActionFactory, /* onNotificationChangedCallback= */ mock(MediaNotification.Provider.Callback.class)); + mediaSession.release(); + player.release(); assertThat(notification.notificationId).isEqualTo(DEFAULT_NOTIFICATION_ID); assertThat(notification.notification.getChannelId()).isEqualTo(DEFAULT_CHANNEL_ID); @@ -765,29 +815,32 @@ public class DefaultMediaNotificationProviderTest { assertThat(found).isTrue(); } - private static MediaSession createMockMediaSessionForNotification(MediaMetadata mediaMetadata) { - return createMockMediaSessionForNotification( - mediaMetadata, /* getMetadataCommandAvailable= */ true); + private Player createPlayerWithMetadata(MediaMetadata mediaMetadata) { + return createPlayerWithMetadata(mediaMetadata, /* isMetadataCommandAvailable= */ true); } - private static MediaSession createMockMediaSessionForNotification( - MediaMetadata mediaMetadata, boolean getMetadataCommandAvailable) { - Player mockPlayer = mock(Player.class); - when(mockPlayer.isCommandAvailable(anyInt())).thenReturn(false); - if (getMetadataCommandAvailable) { - when(mockPlayer.getAvailableCommands()) - .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); - MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); - when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); - when(mockMediaSessionImpl.getUri()).thenReturn(Uri.parse("https://example.test")); - return mockMediaSession; + private Player createPlayerWithMetadata( + MediaMetadata mediaMetadata, boolean isMetadataCommandAvailable) { + return new ForwardingPlayer(new TestExoPlayerBuilder(context).build()) { + @Override + public boolean isCommandAvailable(int command) { + return isMetadataCommandAvailable || command != Player.COMMAND_GET_MEDIA_ITEMS_METADATA; + } + + @Override + public Commands getAvailableCommands() { + Commands.Builder commandsBuilder = new Commands.Builder().addAllCommands(); + if (!isMetadataCommandAvailable) { + 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. */