diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java
index a81e5825a1..9c75c30283 100644
--- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java
+++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java
@@ -1466,7 +1466,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
* playback, in microseconds.
*
*
The default position must be less or equal to the {@linkplain #setDurationUs duration},
- * is set.
+ * if set.
*
* @param defaultPositionUs The default position relative to the start of the media item at
* which to begin playback, in microseconds.
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java
index 361834849e..be700a5de3 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java
@@ -1970,6 +1970,13 @@ public class MediaController implements Player {
connectionCallback.onAccepted();
}
+ /** Returns the binder object used to connect to the session. */
+ @Nullable
+ @VisibleForTesting(otherwise = NONE)
+ /* package */ final IMediaController getBinder() {
+ return impl.getBinder();
+ }
+
private void verifyApplicationThread() {
checkState(Looper.myLooper() == getApplicationLooper(), WRONG_THREAD_ERROR_MESSAGE);
}
@@ -2215,5 +2222,8 @@ public class MediaController implements Player {
@Nullable
MediaBrowserCompat getBrowserCompat();
+
+ @Nullable
+ IMediaController getBinder();
}
}
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
index 98f8ffdb12..7c94e1ee29 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
@@ -1929,6 +1929,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
return null;
}
+ @Override
+ public IMediaController getBinder() {
+ return controllerStub;
+ }
+
private static Timeline createMaskingTimeline(List windows, List periods) {
return new RemotableTimeline(
new ImmutableList.Builder().addAll(windows).build(),
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java
index 98924351f3..e48fac3ad5 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java
@@ -1316,6 +1316,12 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
return browserCompat;
}
+ @Nullable
+ @Override
+ public IMediaController getBinder() {
+ return null;
+ }
+
void onConnected() {
if (released || connected) {
return;
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java
index 0d09930ac1..e382cbd74c 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java
@@ -47,7 +47,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onSessionResult(int sequenceNum, Bundle sessionResultBundle) {
+ public void onSessionResult(int sequenceNum, @Nullable Bundle sessionResultBundle) {
+ if (sessionResultBundle == null) {
+ return;
+ }
SessionResult result;
try {
result = SessionResult.fromBundle(sessionResultBundle);
@@ -62,7 +65,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onLibraryResult(int sequenceNum, Bundle libraryResultBundle) {
+ public void onLibraryResult(int sequenceNum, @Nullable Bundle libraryResultBundle) {
+ if (libraryResultBundle == null) {
+ return;
+ }
LibraryResult> result;
try {
result = LibraryResult.fromUnknownBundle(libraryResultBundle);
@@ -77,7 +83,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onConnected(int seq, Bundle connectionResultBundle) {
+ public void onConnected(int seq, @Nullable Bundle connectionResultBundle) {
+ if (connectionResultBundle == null) {
+ return;
+ }
ConnectionState connectionState;
try {
connectionState = ConnectionState.fromBundle(connectionResultBundle);
@@ -97,7 +106,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onSetCustomLayout(int seq, List commandButtonBundleList) {
+ public void onSetCustomLayout(int seq, @Nullable List commandButtonBundleList) {
+ if (commandButtonBundleList == null) {
+ return;
+ }
List layout;
try {
layout =
@@ -111,7 +123,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
@Override
public void onAvailableCommandsChangedFromSession(
- int seq, Bundle sessionCommandsBundle, Bundle playerCommandsBundle) {
+ int seq, @Nullable Bundle sessionCommandsBundle, @Nullable Bundle playerCommandsBundle) {
+ if (sessionCommandsBundle == null || playerCommandsBundle == null) {
+ return;
+ }
SessionCommands sessionCommands;
try {
sessionCommands = SessionCommands.fromBundle(sessionCommandsBundle);
@@ -132,7 +147,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onAvailableCommandsChangedFromPlayer(int seq, Bundle commandsBundle) {
+ public void onAvailableCommandsChangedFromPlayer(int seq, @Nullable Bundle commandsBundle) {
+ if (commandsBundle == null) {
+ return;
+ }
Commands commandsFromPlayer;
try {
commandsFromPlayer = Commands.fromBundle(commandsBundle);
@@ -145,8 +163,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onCustomCommand(int seq, Bundle commandBundle, Bundle args) {
- if (args == null) {
+ public void onCustomCommand(int seq, @Nullable Bundle commandBundle, @Nullable Bundle args) {
+ if (commandBundle == null || args == null) {
Log.w(TAG, "Ignoring custom command with null args.");
return;
}
@@ -161,14 +179,22 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onSessionActivityChanged(int seq, PendingIntent sessionActivity)
+ public void onSessionActivityChanged(int seq, @Nullable PendingIntent sessionActivity)
throws RemoteException {
+ if (sessionActivity == null) {
+ Log.w(TAG, "Ignoring null session activity intent");
+ return;
+ }
dispatchControllerTaskOnHandler(
controller -> controller.onSetSessionActivity(seq, sessionActivity));
}
@Override
- public void onPeriodicSessionPositionInfoChanged(int seq, Bundle sessionPositionInfoBundle) {
+ public void onPeriodicSessionPositionInfoChanged(
+ int seq, @Nullable Bundle sessionPositionInfoBundle) {
+ if (sessionPositionInfoBundle == null) {
+ return;
+ }
SessionPositionInfo sessionPositionInfo;
try {
sessionPositionInfo = SessionPositionInfo.fromBundle(sessionPositionInfoBundle);
@@ -185,7 +211,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
*/
@Override
@Deprecated
- public void onPlayerInfoChanged(int seq, Bundle playerInfoBundle, boolean isTimelineExcluded) {
+ public void onPlayerInfoChanged(
+ int seq, @Nullable Bundle playerInfoBundle, boolean isTimelineExcluded) {
onPlayerInfoChangedWithExclusions(
seq,
playerInfoBundle,
@@ -196,7 +223,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
/** Added in {@link #VERSION_INT} 2. */
@Override
public void onPlayerInfoChangedWithExclusions(
- int seq, Bundle playerInfoBundle, Bundle playerInfoExclusions) {
+ int seq, @Nullable Bundle playerInfoBundle, @Nullable Bundle playerInfoExclusions) {
+ if (playerInfoBundle == null || playerInfoExclusions == null) {
+ return;
+ }
PlayerInfo playerInfo;
try {
playerInfo = PlayerInfo.fromBundle(playerInfoBundle);
@@ -216,7 +246,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
@Override
- public void onExtrasChanged(int seq, Bundle extras) {
+ public void onExtrasChanged(int seq, @Nullable Bundle extras) {
+ if (extras == null) {
+ Log.w(TAG, "Ignoring null Bundle for extras");
+ return;
+ }
dispatchControllerTaskOnHandler(controller -> controller.onExtrasChanged(extras));
}
@@ -230,7 +264,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onSearchResultChanged(
- int seq, String query, int itemCount, @Nullable Bundle libraryParams)
+ int seq, @Nullable String query, int itemCount, @Nullable Bundle libraryParamsBundle)
throws RuntimeException {
if (TextUtils.isEmpty(query)) {
Log.w(TAG, "onSearchResultChanged(): Ignoring empty query");
@@ -240,18 +274,22 @@ import org.checkerframework.checker.nullness.qual.NonNull;
Log.w(TAG, "onSearchResultChanged(): Ignoring negative itemCount: " + itemCount);
return;
}
+ @Nullable LibraryParams libraryParams;
+ try {
+ libraryParams =
+ libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for LibraryParams", e);
+ return;
+ }
dispatchControllerTaskOnHandler(
(ControllerTask)
- browser ->
- browser.notifySearchResultChanged(
- query,
- itemCount,
- libraryParams == null ? null : LibraryParams.fromBundle(libraryParams)));
+ browser -> browser.notifySearchResultChanged(query, itemCount, libraryParams));
}
@Override
public void onChildrenChanged(
- int seq, String parentId, int itemCount, @Nullable Bundle libraryParams) {
+ int seq, @Nullable String parentId, int itemCount, @Nullable Bundle libraryParamsBundle) {
if (TextUtils.isEmpty(parentId)) {
Log.w(TAG, "onChildrenChanged(): Ignoring empty parentId");
return;
@@ -260,13 +298,17 @@ import org.checkerframework.checker.nullness.qual.NonNull;
Log.w(TAG, "onChildrenChanged(): Ignoring negative itemCount: " + itemCount);
return;
}
+ @Nullable LibraryParams libraryParams;
+ try {
+ libraryParams =
+ libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for LibraryParams", e);
+ return;
+ }
dispatchControllerTaskOnHandler(
(ControllerTask)
- browser ->
- browser.notifyChildrenChanged(
- parentId,
- itemCount,
- libraryParams == null ? null : LibraryParams.fromBundle(libraryParams)));
+ browser -> browser.notifyChildrenChanged(parentId, itemCount, libraryParams));
}
public void destroy() {
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
index 8f00442c97..cd2cd3a902 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
@@ -454,7 +454,10 @@ import java.util.concurrent.ExecutionException;
}
@SuppressWarnings("UngroupedOverloads") // Overload belongs to AIDL interface that is separated.
- public void connect(IMediaController caller, ControllerInfo controllerInfo) {
+ public void connect(@Nullable IMediaController caller, @Nullable ControllerInfo controllerInfo) {
+ if (caller == null || controllerInfo == null) {
+ return;
+ }
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
if (sessionImpl == null || sessionImpl.isReleased()) {
try {
@@ -507,8 +510,11 @@ import java.util.concurrent.ExecutionException;
connectionResult.availableSessionCommands,
connectionResult.availablePlayerCommands);
sequencedFutureManager =
- checkStateNotNull(
- connectedControllersManager.getSequencedFutureManager(controllerInfo));
+ connectedControllersManager.getSequencedFutureManager(controllerInfo);
+ if (sequencedFutureManager == null) {
+ Log.w(TAG, "Ignoring connection request from unknown controller info");
+ return;
+ }
// If connection is accepted, notify the current state to the controller.
// It's needed because we cannot call synchronous calls between
// session/controller.
@@ -596,8 +602,7 @@ import java.util.concurrent.ExecutionException;
public void connect(
@Nullable IMediaController caller,
int sequenceNumber,
- @Nullable Bundle connectionRequestBundle)
- throws RuntimeException {
+ @Nullable Bundle connectionRequestBundle) {
if (caller == null || connectionRequestBundle == null) {
return;
}
@@ -633,7 +638,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void stop(@Nullable IMediaController caller, int sequenceNumber) throws RemoteException {
+ public void stop(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {
return;
}
@@ -653,8 +658,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void release(@Nullable IMediaController caller, int sequenceNumber)
- throws RemoteException {
+ public void release(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {
return;
}
@@ -700,7 +704,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void play(@Nullable IMediaController caller, int sequenceNumber) throws RuntimeException {
+ public void play(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {
return;
}
@@ -727,7 +731,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void pause(@Nullable IMediaController caller, int sequenceNumber) throws RuntimeException {
+ public void pause(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {
return;
}
@@ -744,8 +748,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void prepare(@Nullable IMediaController caller, int sequenceNumber)
- throws RuntimeException {
+ public void prepare(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {
return;
}
@@ -754,7 +757,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void seekToDefaultPosition(IMediaController caller, int sequenceNumber) {
+ public void seekToDefaultPosition(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {
return;
}
@@ -767,7 +770,7 @@ import java.util.concurrent.ExecutionException;
@Override
public void seekToDefaultPositionWithMediaItemIndex(
- IMediaController caller, int sequenceNumber, int mediaItemIndex) throws RemoteException {
+ @Nullable IMediaController caller, int sequenceNumber, int mediaItemIndex) {
if (caller == null) {
return;
}
@@ -782,8 +785,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void seekTo(@Nullable IMediaController caller, int sequenceNumber, long positionMs)
- throws RuntimeException {
+ public void seekTo(@Nullable IMediaController caller, int sequenceNumber, long positionMs) {
if (caller == null) {
return;
}
@@ -796,8 +798,7 @@ import java.util.concurrent.ExecutionException;
@Override
public void seekToWithMediaItemIndex(
- IMediaController caller, int sequenceNumber, int mediaItemIndex, long positionMs)
- throws RemoteException {
+ @Nullable IMediaController caller, int sequenceNumber, int mediaItemIndex, long positionMs) {
if (caller == null) {
return;
}
@@ -812,7 +813,7 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void seekBack(IMediaController caller, int sequenceNumber) {
+ public void seekBack(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {
return;
}
@@ -880,9 +881,9 @@ import java.util.concurrent.ExecutionException;
public void setRatingWithMediaId(
@Nullable IMediaController caller,
int sequenceNumber,
- String mediaId,
+ @Nullable String mediaId,
@Nullable Bundle ratingBundle) {
- if (caller == null || ratingBundle == null) {
+ if (caller == null || mediaId == null || ratingBundle == null) {
return;
}
if (TextUtils.isEmpty(mediaId)) {
@@ -941,11 +942,19 @@ import java.util.concurrent.ExecutionException;
@Override
public void setPlaybackParameters(
- @Nullable IMediaController caller, int sequenceNumber, Bundle playbackParametersBundle) {
+ @Nullable IMediaController caller,
+ int sequenceNumber,
+ @Nullable Bundle playbackParametersBundle) {
if (caller == null || playbackParametersBundle == null) {
return;
}
- PlaybackParameters playbackParameters = PlaybackParameters.fromBundle(playbackParametersBundle);
+ PlaybackParameters playbackParameters;
+ try {
+ playbackParameters = PlaybackParameters.fromBundle(playbackParametersBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for PlaybackParameters", e);
+ return;
+ }
queueSessionTaskWithPlayerCommand(
caller,
sequenceNumber,
@@ -1134,7 +1143,7 @@ import java.util.concurrent.ExecutionException;
@Override
public void addMediaItem(
- @Nullable IMediaController caller, int sequenceNumber, Bundle mediaItemBundle) {
+ @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle mediaItemBundle) {
if (caller == null || mediaItemBundle == null) {
return;
}
@@ -1159,7 +1168,10 @@ import java.util.concurrent.ExecutionException;
@Override
public void addMediaItemWithIndex(
- @Nullable IMediaController caller, int sequenceNumber, int index, Bundle mediaItemBundle) {
+ @Nullable IMediaController caller,
+ int sequenceNumber,
+ int index,
+ @Nullable Bundle mediaItemBundle) {
if (caller == null || mediaItemBundle == null) {
return;
}
@@ -1317,7 +1329,10 @@ import java.util.concurrent.ExecutionException;
@Override
public void replaceMediaItem(
- IMediaController caller, int sequenceNumber, int index, Bundle mediaItemBundle) {
+ @Nullable IMediaController caller,
+ int sequenceNumber,
+ int index,
+ @Nullable Bundle mediaItemBundle) {
if (caller == null || mediaItemBundle == null) {
return;
}
@@ -1351,11 +1366,11 @@ import java.util.concurrent.ExecutionException;
@Override
public void replaceMediaItems(
- IMediaController caller,
+ @Nullable IMediaController caller,
int sequenceNumber,
int fromIndex,
int toIndex,
- IBinder mediaItemsRetriever) {
+ @Nullable IBinder mediaItemsRetriever) {
if (caller == null || mediaItemsRetriever == null) {
return;
}
@@ -1606,9 +1621,16 @@ import java.util.concurrent.ExecutionException;
public void setAudioAttributes(
@Nullable IMediaController caller,
int sequenceNumber,
- Bundle audioAttributes,
+ @Nullable Bundle audioAttributes,
boolean handleAudioFocus) {
- if (caller == null) {
+ if (caller == null || audioAttributes == null) {
+ return;
+ }
+ AudioAttributes attributes;
+ try {
+ attributes = AudioAttributes.fromBundle(audioAttributes);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for AudioAttributes", e);
return;
}
queueSessionTaskWithPlayerCommand(
@@ -1616,9 +1638,7 @@ import java.util.concurrent.ExecutionException;
sequenceNumber,
COMMAND_SET_AUDIO_ATTRIBUTES,
sendSessionResultSuccess(
- player ->
- player.setAudioAttributes(
- AudioAttributes.fromBundle(audioAttributes), handleAudioFocus)));
+ player -> player.setAudioAttributes(attributes, handleAudioFocus)));
}
@Override
@@ -1658,9 +1678,10 @@ import java.util.concurrent.ExecutionException;
@Override
public void setTrackSelectionParameters(
- @Nullable IMediaController caller, int sequenceNumber, Bundle trackSelectionParametersBundle)
- throws RemoteException {
- if (caller == null) {
+ @Nullable IMediaController caller,
+ int sequenceNumber,
+ @Nullable Bundle trackSelectionParametersBundle) {
+ if (caller == null || trackSelectionParametersBundle == null) {
return;
}
TrackSelectionParameters trackSelectionParameters;
@@ -1689,14 +1710,18 @@ import java.util.concurrent.ExecutionException;
@Override
public void getLibraryRoot(
- @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle libraryParamsBundle)
- throws RuntimeException {
+ @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle libraryParamsBundle) {
if (caller == null) {
return;
}
- @Nullable
- LibraryParams libraryParams =
- libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ @Nullable LibraryParams libraryParams;
+ try {
+ libraryParams =
+ libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for LibraryParams", e);
+ return;
+ }
dispatchSessionTaskWithSessionCommand(
caller,
sequenceNumber,
@@ -1708,8 +1733,7 @@ import java.util.concurrent.ExecutionException;
@Override
public void getItem(
- @Nullable IMediaController caller, int sequenceNumber, @Nullable String mediaId)
- throws RuntimeException {
+ @Nullable IMediaController caller, int sequenceNumber, @Nullable String mediaId) {
if (caller == null) {
return;
}
@@ -1730,11 +1754,10 @@ import java.util.concurrent.ExecutionException;
public void getChildren(
@Nullable IMediaController caller,
int sequenceNumber,
- String parentId,
+ @Nullable String parentId,
int page,
int pageSize,
- @Nullable Bundle libraryParamsBundle)
- throws RuntimeException {
+ @Nullable Bundle libraryParamsBundle) {
if (caller == null) {
return;
}
@@ -1750,9 +1773,14 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "getChildren(): Ignoring pageSize less than 1");
return;
}
- @Nullable
- LibraryParams libraryParams =
- libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ @Nullable LibraryParams libraryParams;
+ try {
+ libraryParams =
+ libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for LibraryParams", e);
+ return;
+ }
dispatchSessionTaskWithSessionCommand(
caller,
sequenceNumber,
@@ -1767,7 +1795,7 @@ import java.util.concurrent.ExecutionException;
public void search(
@Nullable IMediaController caller,
int sequenceNumber,
- String query,
+ @Nullable String query,
@Nullable Bundle libraryParamsBundle) {
if (caller == null) {
return;
@@ -1776,9 +1804,14 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "search(): Ignoring empty query");
return;
}
- @Nullable
- LibraryParams libraryParams =
- libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ @Nullable LibraryParams libraryParams;
+ try {
+ libraryParams =
+ libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for LibraryParams", e);
+ return;
+ }
dispatchSessionTaskWithSessionCommand(
caller,
sequenceNumber,
@@ -1792,7 +1825,7 @@ import java.util.concurrent.ExecutionException;
public void getSearchResult(
@Nullable IMediaController caller,
int sequenceNumber,
- String query,
+ @Nullable String query,
int page,
int pageSize,
@Nullable Bundle libraryParamsBundle) {
@@ -1811,9 +1844,14 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "getSearchResult(): Ignoring pageSize less than 1");
return;
}
- @Nullable
- LibraryParams libraryParams =
- libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ @Nullable LibraryParams libraryParams;
+ try {
+ libraryParams =
+ libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for LibraryParams", e);
+ return;
+ }
dispatchSessionTaskWithSessionCommand(
caller,
sequenceNumber,
@@ -1828,7 +1866,7 @@ import java.util.concurrent.ExecutionException;
public void subscribe(
@Nullable IMediaController caller,
int sequenceNumber,
- String parentId,
+ @Nullable String parentId,
@Nullable Bundle libraryParamsBundle) {
if (caller == null) {
return;
@@ -1837,9 +1875,14 @@ import java.util.concurrent.ExecutionException;
Log.w(TAG, "subscribe(): Ignoring empty parentId");
return;
}
- @Nullable
- LibraryParams libraryParams =
- libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ @Nullable LibraryParams libraryParams;
+ try {
+ libraryParams =
+ libraryParamsBundle == null ? null : LibraryParams.fromBundle(libraryParamsBundle);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Ignoring malformed Bundle for LibraryParams", e);
+ return;
+ }
dispatchSessionTaskWithSessionCommand(
caller,
sequenceNumber,
@@ -1850,7 +1893,8 @@ import java.util.concurrent.ExecutionException;
}
@Override
- public void unsubscribe(@Nullable IMediaController caller, int sequenceNumber, String parentId) {
+ public void unsubscribe(
+ @Nullable IMediaController caller, int sequenceNumber, @Nullable String parentId) {
if (caller == null) {
return;
}
diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaControllerStubTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaControllerStubTest.java
new file mode 100644
index 0000000000..31b6ace097
--- /dev/null
+++ b/libraries/session/src/test/java/androidx/media3/session/MediaControllerStubTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.session;
+
+import static androidx.media3.test.utils.TestUtil.getThrowingBundle;
+
+import android.content.Context;
+import android.os.Bundle;
+import androidx.media3.common.Player;
+import androidx.media3.exoplayer.ExoPlayer;
+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;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link MediaControllerStub}. */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerStubTest {
+
+ @Test
+ public void invalidBinderArguments_doNotCrashController() throws Exception {
+ // Access controller stub directly and then send invalid arguments. None of them should crash
+ // the controller and this test asserts this by running through without throwing an exception.
+ Context context = ApplicationProvider.getApplicationContext();
+ ExoPlayer player = new TestExoPlayerBuilder(context).build();
+ MediaSession session = new MediaSession.Builder(context, player).build();
+ MediaController controller =
+ new MediaController.Builder(context, session.getToken()).buildAsync().get();
+ IMediaController binder = controller.getBinder();
+
+ // Call methods with non-primitive parameters set to null.
+ binder.onConnected(/* seq= */ 0, /* connectionResult= */ null);
+ binder.onSessionResult(/* seq= */ 0, /* sessionResult= */ null);
+ binder.onLibraryResult(/* seq= */ 0, /* libraryResult= */ null);
+ binder.onSetCustomLayout(/* seq= */ 0, /* commandButtonList= */ null);
+ binder.onCustomCommand(
+ /* seq= */ 0,
+ /* command= */ new SessionCommand(SessionCommand.COMMAND_CODE_LIBRARY_SEARCH).toBundle(),
+ /* args= */ null);
+ binder.onCustomCommand(/* seq= */ 0, /* command= */ null, /* args= */ new Bundle());
+ binder.onPlayerInfoChanged(
+ /* seq= */ 0, /* playerInfoBundle= */ null, /* isTimelineExcluded= */ false);
+ binder.onPlayerInfoChangedWithExclusions(
+ /* seq= */ 0,
+ /* playerInfoBundle= */ null,
+ /* playerInfoExclusions= */ new PlayerInfo.BundlingExclusions(
+ /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ false)
+ .toBundle());
+ binder.onPlayerInfoChangedWithExclusions(
+ /* seq= */ 0,
+ /* playerInfoBundle= */ PlayerInfo.DEFAULT.toBundle(),
+ /* playerInfoExclusions= */ null);
+ binder.onPeriodicSessionPositionInfoChanged(/* seq= */ 0, /* sessionPositionInfo= */ null);
+ binder.onAvailableCommandsChangedFromPlayer(/* seq= */ 0, /* commandsBundle= */ null);
+ binder.onAvailableCommandsChangedFromSession(
+ /* seq= */ 0,
+ /* sessionCommandsBundle= */ new SessionCommands.Builder().build().toBundle(),
+ /* playerCommandsBundle= */ null);
+ binder.onAvailableCommandsChangedFromSession(
+ /* seq= */ 0,
+ /* sessionCommandsBundle= */ null,
+ /* playerCommandsBundle= */ new Player.Commands.Builder().build().toBundle());
+ binder.onExtrasChanged(/* seq= */ 0, /* extras= */ null);
+ binder.onSessionActivityChanged(/* seq= */ 0, /* pendingIntent= */ null);
+ binder.onChildrenChanged(
+ /* seq= */ 0,
+ /* parentId= */ null,
+ /* itemCount= */ 1,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.onChildrenChanged(
+ /* seq= */ 0, /* parentId= */ "", /* itemCount= */ 1, /* libraryParams= */ null);
+ binder.onSearchResultChanged(
+ /* seq= */ 0,
+ /* query= */ null,
+ /* itemCount= */ 1,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.onSearchResultChanged(
+ /* seq= */ 0, /* query= */ "", /* itemCount= */ 1, /* libraryParams= */ null);
+
+ // Call methods with non-null arguments, but invalid Bundles.
+ binder.onConnected(/* seq= */ 0, /* connectionResult= */ getThrowingBundle());
+ binder.onSessionResult(/* seq= */ 0, /* sessionResult= */ getThrowingBundle());
+ binder.onLibraryResult(/* seq= */ 0, /* libraryResult= */ getThrowingBundle());
+ binder.onSetCustomLayout(
+ /* seq= */ 0, /* commandButtonList= */ ImmutableList.of(getThrowingBundle()));
+ binder.onCustomCommand(
+ /* seq= */ 0,
+ /* command= */ new SessionCommand(SessionCommand.COMMAND_CODE_LIBRARY_SEARCH).toBundle(),
+ /* args= */ getThrowingBundle());
+ binder.onCustomCommand(
+ /* seq= */ 0, /* command= */ getThrowingBundle(), /* args= */ new Bundle());
+ binder.onPlayerInfoChanged(
+ /* seq= */ 0, /* playerInfoBundle= */ getThrowingBundle(), /* isTimelineExcluded= */ false);
+ binder.onPlayerInfoChangedWithExclusions(
+ /* seq= */ 0,
+ /* playerInfoBundle= */ getThrowingBundle(),
+ /* playerInfoExclusions= */ new PlayerInfo.BundlingExclusions(
+ /* isTimelineExcluded= */ true, /* areCurrentTracksExcluded= */ false)
+ .toBundle());
+ binder.onPlayerInfoChangedWithExclusions(
+ /* seq= */ 0,
+ /* playerInfoBundle= */ PlayerInfo.DEFAULT.toBundle(),
+ /* playerInfoExclusions= */ getThrowingBundle());
+ binder.onPeriodicSessionPositionInfoChanged(
+ /* seq= */ 0, /* sessionPositionInfo= */ getThrowingBundle());
+ binder.onAvailableCommandsChangedFromPlayer(
+ /* seq= */ 0, /* commandsBundle= */ getThrowingBundle());
+ binder.onAvailableCommandsChangedFromSession(
+ /* seq= */ 0,
+ /* sessionCommandsBundle= */ new SessionCommands.Builder().build().toBundle(),
+ /* playerCommandsBundle= */ getThrowingBundle());
+ binder.onAvailableCommandsChangedFromSession(
+ /* seq= */ 0,
+ /* sessionCommandsBundle= */ getThrowingBundle(),
+ /* playerCommandsBundle= */ new Player.Commands.Builder().build().toBundle());
+ binder.onExtrasChanged(/* seq= */ 0, /* extras= */ getThrowingBundle());
+ binder.onChildrenChanged(
+ /* seq= */ 0,
+ /* parentId= */ "",
+ /* itemCount= */ 1,
+ /* libraryParams= */ getThrowingBundle());
+ binder.onSearchResultChanged(
+ /* seq= */ 0,
+ /* query= */ "",
+ /* itemCount= */ 1,
+ /* libraryParams= */ getThrowingBundle());
+ }
+}
diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaSessionStubTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaSessionStubTest.java
new file mode 100644
index 0000000000..c15e6fb938
--- /dev/null
+++ b/libraries/session/src/test/java/androidx/media3/session/MediaSessionStubTest.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.session;
+
+import static androidx.media3.test.utils.TestUtil.getThrowingBundle;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import androidx.media3.common.AudioAttributes;
+import androidx.media3.common.BundleListRetriever;
+import androidx.media3.common.HeartRating;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.MediaMetadata;
+import androidx.media3.common.PlaybackParameters;
+import androidx.media3.common.Player;
+import androidx.media3.common.TrackSelectionParameters;
+import androidx.media3.exoplayer.ExoPlayer;
+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;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link MediaSessionStub}. */
+@RunWith(AndroidJUnit4.class)
+public class MediaSessionStubTest {
+
+ @Test
+ public void invalidBinderArguments_doNotCrashSession() throws Exception {
+ // Access session stub directly and then send invalid arguments. None of them should crash the
+ // session app and this test asserts this by running through without throwing an exception.
+ Context context = ApplicationProvider.getApplicationContext();
+ ExoPlayer player = new TestExoPlayerBuilder(context).build();
+ MediaSession session = new MediaSession.Builder(context, player).setId("invalidArgs").build();
+ IMediaSession binder = (IMediaSession) session.getToken().getBinder();
+
+ // Call methods with caller==null and valid additional parameters.
+ binder.setVolume(/* caller= */ null, /* seq= */ 0, /* volume= */ 0);
+ binder.setDeviceVolume(/* caller= */ null, /* seq= */ 0, /* volume= */ 0);
+ binder.setDeviceVolumeWithFlags(
+ /* caller= */ null, /* seq= */ 0, /* volume= */ 0, /* flags= */ 0);
+ binder.increaseDeviceVolume(/* caller= */ null, /* seq= */ 0);
+ binder.increaseDeviceVolumeWithFlags(/* caller= */ null, /* seq= */ 0, /* flags= */ 0);
+ binder.decreaseDeviceVolume(/* caller= */ null, /* seq= */ 0);
+ binder.decreaseDeviceVolumeWithFlags(/* caller= */ null, /* seq= */ 0, /* flags= */ 0);
+ binder.setDeviceMuted(/* caller= */ null, /* seq= */ 0, /* muted= */ false);
+ binder.setDeviceMutedWithFlags(
+ /* caller= */ null, /* seq= */ 0, /* muted= */ false, /* flags= */ 0);
+ binder.setAudioAttributes(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* audioAttributes= */ AudioAttributes.DEFAULT.toBundle(),
+ /* handleAudioFocus= */ false);
+ binder.setMediaItem(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItemBundle= */ new MediaItem.Builder().build().toBundle());
+ binder.setMediaItemWithStartPosition(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItemBundle= */ new MediaItem.Builder().build().toBundle(),
+ /* startPositionMs= */ 0);
+ binder.setMediaItemWithResetPosition(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItemBundle= */ new MediaItem.Builder().build().toBundle(),
+ /* resetPosition= */ false);
+ binder.setMediaItems(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(
+ ImmutableList.of(new MediaItem.Builder().build().toBundle())));
+ binder.setMediaItemsWithResetPosition(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(
+ ImmutableList.of(new MediaItem.Builder().build().toBundle())),
+ /* resetPosition= */ false);
+ binder.setMediaItemsWithStartIndex(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(
+ ImmutableList.of(new MediaItem.Builder().build().toBundle())),
+ /* startIndex= */ 0,
+ /* startPositionMs= */ 0);
+ binder.setPlayWhenReady(/* caller= */ null, /* seq= */ 0, /* playWhenReady= */ false);
+ binder.onControllerResult(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* controllerResult= */ new SessionResult(SessionResult.RESULT_SUCCESS).toBundle());
+ binder.connect(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* connectionRequest= */ new ConnectionRequest(
+ "pkg", /* pid= */ 0, /* connectionHints= */ new Bundle())
+ .toBundle());
+ binder.onCustomCommand(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* sessionCommand= */ new SessionCommand(SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM)
+ .toBundle(),
+ /* args= */ new Bundle());
+ binder.setRepeatMode(
+ /* caller= */ null, /* seq= */ 0, /* repeatMode= */ Player.REPEAT_MODE_OFF);
+ binder.setShuffleModeEnabled(/* caller= */ null, /* seq= */ 0, /* shuffleModeEnabled= */ false);
+ binder.removeMediaItem(/* caller= */ null, /* seq= */ 0, /* index= */ 0);
+ binder.removeMediaItems(/* caller= */ null, /* seq= */ 0, /* fromIndex= */ 0, /* toIndex= */ 0);
+ binder.clearMediaItems(/* caller= */ null, /* seq= */ 0);
+ binder.moveMediaItem(
+ /* caller= */ null, /* seq= */ 0, /* currentIndex= */ 0, /* newIndex= */ 0);
+ binder.moveMediaItems(
+ /* caller= */ null, /* seq= */ 0, /* fromIndex= */ 0, /* toIndex= */ 0, /* newIndex= */ 0);
+ binder.replaceMediaItem(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* index= */ 0,
+ /* mediaItemBundle= */ new MediaItem.Builder().build().toBundle());
+ binder.replaceMediaItems(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* fromIndex= */ 0,
+ /* toIndex= */ 0,
+ /* mediaItems= */ new BundleListRetriever(
+ ImmutableList.of(new MediaItem.Builder().build().toBundle())));
+ binder.play(/* caller= */ null, /* seq= */ 0);
+ binder.pause(/* caller= */ null, /* seq= */ 0);
+ binder.prepare(/* caller= */ null, /* seq= */ 0);
+ binder.setPlaybackParameters(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* playbackParametersBundle= */ new PlaybackParameters(/* speed= */ 1f).toBundle());
+ binder.setPlaybackSpeed(/* caller= */ null, /* seq= */ 0, /* speed= */ 0);
+ binder.addMediaItem(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItemBundle= */ new MediaItem.Builder().build().toBundle());
+ binder.addMediaItemWithIndex(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* index= */ 0,
+ /* mediaItemBundle= */ new MediaItem.Builder().build().toBundle());
+ binder.addMediaItems(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(
+ ImmutableList.of(new MediaItem.Builder().build().toBundle())));
+ binder.addMediaItemsWithIndex(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* index= */ 0,
+ /* mediaItems= */ new BundleListRetriever(
+ ImmutableList.of(new MediaItem.Builder().build().toBundle())));
+ binder.setPlaylistMetadata(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* playlistMetadata= */ new MediaMetadata.Builder().build().toBundle());
+ binder.stop(/* caller= */ null, /* seq= */ 0);
+ binder.release(/* caller= */ null, /* seq= */ 0);
+ binder.seekToDefaultPosition(/* caller= */ null, /* seq= */ 0);
+ binder.seekToDefaultPositionWithMediaItemIndex(
+ /* caller= */ null, /* seq= */ 0, /* mediaItemIndex= */ 0);
+ binder.seekTo(/* caller= */ null, /* seq= */ 0, /* positionMs= */ 0);
+ binder.seekToWithMediaItemIndex(
+ /* caller= */ null, /* seq= */ 0, /* mediaItemIndex= */ 0, /* positionMs= */ 0);
+ binder.seekBack(/* caller= */ null, /* seq= */ 0);
+ binder.seekForward(/* caller= */ null, /* seq= */ 0);
+ binder.seekToPreviousMediaItem(/* caller= */ null, /* seq= */ 0);
+ binder.seekToNextMediaItem(/* caller= */ null, /* seq= */ 0);
+ binder.setVideoSurface(/* caller= */ null, /* seq= */ 0, /* surface= */ null);
+ binder.flushCommandQueue(/* caller= */ null);
+ binder.seekToPrevious(/* caller= */ null, /* seq= */ 0);
+ binder.seekToNext(/* caller= */ null, /* seq= */ 0);
+ binder.setTrackSelectionParameters(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* trackSelectionParametersBundle= */ new TrackSelectionParameters.Builder(context)
+ .build()
+ .toBundle());
+ binder.setRatingWithMediaId(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* mediaId= */ "",
+ /* rating= */ new HeartRating().toBundle());
+ binder.setRating(/* caller= */ null, /* seq= */ 0, /* rating= */ new HeartRating().toBundle());
+ binder.getLibraryRoot(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.getItem(/* caller= */ null, /* seq= */ 0, /* mediaId= */ "");
+ binder.getChildren(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* parentId= */ "",
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.search(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* query= */ "",
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.getSearchResult(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* query= */ "",
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.subscribe(
+ /* caller= */ null,
+ /* seq= */ 0,
+ /* parentId= */ "",
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.unsubscribe(/* caller= */ null, /* seq= */ 0, /* parentId= */ "");
+
+ // Call methods with non-null caller, but other non-primitive parameters set to null.
+ MediaController controller =
+ new MediaController.Builder(context, session.getToken()).buildAsync().get();
+ IMediaController caller = controller.getBinder();
+ binder.setAudioAttributes(
+ caller, /* seq= */ 0, /* audioAttributes= */ null, /* handleAudioFocus= */ false);
+ binder.setMediaItem(caller, /* seq= */ 0, /* mediaItemBundle= */ null);
+ binder.setMediaItemWithStartPosition(
+ caller, /* seq= */ 0, /* mediaItemBundle= */ null, /* startPositionMs= */ 0);
+ binder.setMediaItemWithResetPosition(
+ caller, /* seq= */ 0, /* mediaItemBundle= */ null, /* resetPosition= */ false);
+ binder.setMediaItems(caller, /* seq= */ 0, /* mediaItems= */ null);
+ binder.setMediaItemsWithResetPosition(
+ caller, /* seq= */ 0, /* mediaItems= */ null, /* resetPosition= */ false);
+ binder.setMediaItemsWithStartIndex(
+ caller,
+ /* seq= */ 0,
+ /* mediaItems= */ null,
+ /* startIndex= */ 0,
+ /* startPositionMs= */ 0);
+ binder.onControllerResult(caller, /* seq= */ 0, /* controllerResult= */ null);
+ binder.connect(caller, /* seq= */ 0, /* connectionRequest= */ null);
+ binder.onCustomCommand(
+ caller, /* seq= */ 0, /* sessionCommand= */ null, /* args= */ new Bundle());
+ binder.onCustomCommand(
+ caller,
+ /* seq= */ 0,
+ /* sessionCommand= */ new SessionCommand(SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM)
+ .toBundle(),
+ /* args= */ null);
+ binder.replaceMediaItem(caller, /* seq= */ 0, /* index= */ 0, /* mediaItemBundle= */ null);
+ binder.replaceMediaItems(
+ caller, /* seq= */ 0, /* fromIndex= */ 0, /* toIndex= */ 0, /* mediaItems= */ null);
+ binder.setPlaybackParameters(caller, /* seq= */ 0, /* playbackParametersBundle= */ null);
+ binder.addMediaItem(caller, /* seq= */ 0, /* mediaItemBundle= */ null);
+ binder.addMediaItemWithIndex(caller, /* seq= */ 0, /* index= */ 0, /* mediaItemBundle= */ null);
+ binder.addMediaItems(caller, /* seq= */ 0, /* mediaItems= */ null);
+ binder.addMediaItemsWithIndex(caller, /* seq= */ 0, /* index= */ 0, /* mediaItems= */ null);
+ binder.setPlaylistMetadata(caller, /* seq= */ 0, /* playlistMetadata= */ null);
+ binder.setTrackSelectionParameters(
+ caller, /* seq= */ 0, /* trackSelectionParametersBundle= */ null);
+ binder.setRatingWithMediaId(
+ caller, /* seq= */ 0, /* mediaId= */ null, /* rating= */ new HeartRating().toBundle());
+ binder.setRatingWithMediaId(caller, /* seq= */ 0, /* mediaId= */ "", /* rating= */ null);
+ binder.setRating(caller, /* seq= */ 0, /* rating= */ null);
+ binder.getLibraryRoot(caller, /* seq= */ 0, /* libraryParams= */ null);
+ binder.getItem(caller, /* seq= */ 0, /* mediaId= */ null);
+ binder.getChildren(
+ caller,
+ /* seq= */ 0,
+ /* parentId= */ null,
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.getChildren(
+ caller,
+ /* seq= */ 0,
+ /* parentId= */ "",
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ null);
+ binder.search(
+ caller,
+ /* seq= */ 0,
+ /* query= */ null,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.search(caller, /* seq= */ 0, /* query= */ "", /* libraryParams= */ null);
+ binder.getSearchResult(
+ caller,
+ /* seq= */ 0,
+ /* query= */ null,
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.getSearchResult(
+ caller,
+ /* seq= */ 0,
+ /* query= */ "",
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ null);
+ binder.subscribe(
+ caller,
+ /* seq= */ 0,
+ /* parentId= */ null,
+ /* libraryParams= */ new MediaLibraryService.LibraryParams.Builder().build().toBundle());
+ binder.subscribe(caller, /* seq= */ 0, /* parentId= */ "", /* libraryParams= */ null);
+ binder.unsubscribe(caller, /* seq= */ 0, /* parentId= */ null);
+
+ // Call methods with non-null arguments, but invalid Bundles.
+ IBinder noopBinder = new Binder() {};
+ binder.setAudioAttributes(
+ caller,
+ /* seq= */ 0,
+ /* audioAttributes= */ getThrowingBundle(),
+ /* handleAudioFocus= */ false);
+ binder.setMediaItem(caller, /* seq= */ 0, /* mediaItemBundle= */ getThrowingBundle());
+ binder.setMediaItemWithStartPosition(
+ caller, /* seq= */ 0, /* mediaItemBundle= */ getThrowingBundle(), /* startPositionMs= */ 0);
+ binder.setMediaItemWithResetPosition(
+ caller,
+ /* seq= */ 0,
+ /* mediaItemBundle= */ getThrowingBundle(),
+ /* resetPosition= */ false);
+ binder.setMediaItems(caller, /* seq= */ 0, /* mediaItems= */ noopBinder);
+ binder.setMediaItems(
+ caller,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(ImmutableList.of(getThrowingBundle())));
+ binder.setMediaItemsWithResetPosition(
+ caller, /* seq= */ 0, /* mediaItems= */ noopBinder, /* resetPosition= */ false);
+ binder.setMediaItemsWithResetPosition(
+ caller,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(ImmutableList.of(getThrowingBundle())),
+ /* resetPosition= */ false);
+ binder.setMediaItemsWithStartIndex(
+ caller,
+ /* seq= */ 0,
+ /* mediaItems= */ noopBinder,
+ /* startIndex= */ 0,
+ /* startPositionMs= */ 0);
+ binder.setMediaItemsWithStartIndex(
+ caller,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(ImmutableList.of(getThrowingBundle())),
+ /* startIndex= */ 0,
+ /* startPositionMs= */ 0);
+ binder.onControllerResult(caller, /* seq= */ 0, /* controllerResult= */ getThrowingBundle());
+ binder.onCustomCommand(
+ caller, /* seq= */ 0, /* sessionCommand= */ getThrowingBundle(), /* args= */ new Bundle());
+ binder.replaceMediaItem(
+ caller, /* seq= */ 0, /* index= */ 0, /* mediaItemBundle= */ getThrowingBundle());
+ binder.replaceMediaItems(
+ caller, /* seq= */ 0, /* fromIndex= */ 0, /* toIndex= */ 0, /* mediaItems= */ noopBinder);
+ binder.replaceMediaItems(
+ caller,
+ /* seq= */ 0,
+ /* fromIndex= */ 0,
+ /* toIndex= */ 0,
+ /* mediaItems= */ new BundleListRetriever(ImmutableList.of(getThrowingBundle())));
+ binder.setPlaybackParameters(
+ caller, /* seq= */ 0, /* playbackParametersBundle= */ getThrowingBundle());
+ binder.addMediaItem(caller, /* seq= */ 0, /* mediaItemBundle= */ getThrowingBundle());
+ binder.addMediaItemWithIndex(
+ caller, /* seq= */ 0, /* index= */ 0, /* mediaItemBundle= */ getThrowingBundle());
+ binder.addMediaItems(caller, /* seq= */ 0, /* mediaItems= */ noopBinder);
+ binder.addMediaItems(
+ caller,
+ /* seq= */ 0,
+ /* mediaItems= */ new BundleListRetriever(ImmutableList.of(getThrowingBundle())));
+ binder.addMediaItemsWithIndex(
+ caller, /* seq= */ 0, /* index= */ 0, /* mediaItems= */ noopBinder);
+ binder.addMediaItemsWithIndex(
+ caller,
+ /* seq= */ 0,
+ /* index= */ 0,
+ /* mediaItems= */ new BundleListRetriever(ImmutableList.of(getThrowingBundle())));
+ binder.setPlaylistMetadata(caller, /* seq= */ 0, /* playlistMetadata= */ getThrowingBundle());
+ binder.setTrackSelectionParameters(
+ caller, /* seq= */ 0, /* trackSelectionParametersBundle= */ getThrowingBundle());
+ binder.setRatingWithMediaId(
+ caller, /* seq= */ 0, /* mediaId= */ "", /* rating= */ getThrowingBundle());
+ binder.setRating(caller, /* seq= */ 0, /* rating= */ getThrowingBundle());
+ binder.getLibraryRoot(caller, /* seq= */ 0, /* libraryParams= */ getThrowingBundle());
+ binder.getChildren(
+ caller,
+ /* seq= */ 0,
+ /* parentId= */ "",
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ getThrowingBundle());
+ binder.search(caller, /* seq= */ 0, /* query= */ "", /* libraryParams= */ getThrowingBundle());
+ binder.getSearchResult(
+ caller,
+ /* seq= */ 0,
+ /* query= */ "",
+ /* page= */ 0,
+ /* pageSize= */ 0,
+ /* libraryParams= */ getThrowingBundle());
+ binder.subscribe(
+ caller, /* seq= */ 0, /* parentId= */ "", /* libraryParams= */ getThrowingBundle());
+ }
+
+ @Test
+ public void binderConnectRequest_withInvalidController_doesNotCrashSession() throws Exception {
+ // Obtain a direct reference to binder and attempt to connect with an invalid controller. This
+ // should not crash the session app and this test asserts this by running through without
+ // throwing an exception.
+ Context context = ApplicationProvider.getApplicationContext();
+ ExoPlayer player = new TestExoPlayerBuilder(context).build();
+ MediaSession session = new MediaSession.Builder(context, player).setId("connect").build();
+ IMediaSession binder = (IMediaSession) session.getToken().getBinder();
+ MediaController controller =
+ new MediaController.Builder(context, session.getToken()).buildAsync().get();
+ IMediaController caller = controller.getBinder();
+
+ binder.connect(
+ caller,
+ /* seq= */ 0,
+ /* connectionRequest= */ new ConnectionRequest(
+ /* packageName= */ "invalud", /* pid= */ 9999, /* connectionHints= */ new Bundle())
+ .toBundle());
+ }
+}
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java
index a016a10e48..7a0157b1b4 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java
@@ -15,6 +15,7 @@
*/
package androidx.media3.test.utils;
+import static androidx.media3.common.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
@@ -25,6 +26,8 @@ import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.MediaCodec;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
@@ -593,6 +596,26 @@ public class TestUtil {
.build();
}
+ /** Returns a {@link Bundle} that will throw an exception at the first attempt to read a value. */
+ public static Bundle getThrowingBundle() {
+ // Create Bundle containing a Parcelable class that will require a ClassLoader.
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("0", new StreamKey(0, 0));
+ // Serialize this Bundle to a Parcel to remove the direct object reference.
+ Parcel parcel = Parcel.obtain();
+ parcel.writeBundle(bundle);
+ // Read the same Bundle from the Parcel again, but with a ClassLoader that can't load the class.
+ parcel.setDataPosition(0);
+ ClassLoader throwingClassLoader =
+ new ClassLoader() {
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException();
+ }
+ };
+ return checkNotNull(parcel.readBundle(throwingClassLoader));
+ }
+
private static final class NoUidOrShufflingTimeline extends Timeline {
private final Timeline delegate;
diff --git a/libraries/test_utils/src/test/java/androidx/media3/test/utils/TestUtilTest.java b/libraries/test_utils/src/test/java/androidx/media3/test/utils/TestUtilTest.java
new file mode 100644
index 0000000000..9084d71dc7
--- /dev/null
+++ b/libraries/test_utils/src/test/java/androidx/media3/test/utils/TestUtilTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.test.utils;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link TestUtil}. */
+@RunWith(AndroidJUnit4.class)
+public class TestUtilTest {
+
+ @Test
+ public void getThrowingBundle_throwsWhenUsed() {
+ Bundle bundle = TestUtil.getThrowingBundle();
+
+ assertThrows(RuntimeException.class, () -> bundle.getInt("0"));
+ }
+}