Commit ff919fe7 by tonihei Committed by Marc Baechinger

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
parent 28aa5e84
......@@ -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<androidx.media3.session.MediaSession.ControllerInfo> 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<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 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 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<androidx.media3.session.MediaSession.ControllerInfo> 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<androidx.media3.session.SessionResult> 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<androidx.media3.session.SessionResult> setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List<androidx.media3.session.CommandButton>);
method public final void setCustomLayout(java.util.List<androidx.media3.session.CommandButton>);
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 {
......
......@@ -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
}
......
......@@ -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<ControllerInfo> getConnectedControllers() {
public final List<ControllerInfo> 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<SessionResult> setCustomLayout(
public final ListenableFuture<SessionResult> setCustomLayout(
ControllerInfo controller, List<CommandButton> 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<CommandButton> layout) {
public final void setCustomLayout(List<CommandButton> 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<SessionResult> sendCustomCommand(
public final ListenableFuture<SessionResult> 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();
}
......
......@@ -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. */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment