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")); + } +}