diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index fde7447f508..0332f124306 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -27,7 +27,6 @@ import android.app.PendingIntent; import android.content.Context; import android.os.Bundle; -import android.os.RemoteException; import androidx.annotation.Nullable; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; @@ -359,19 +358,6 @@ protected MediaSessionServiceLegacyStub createLegacyBrowserService( return stub; } - @Override - protected void dispatchRemoteControllerTaskWithoutReturn(RemoteControllerTask task) { - super.dispatchRemoteControllerTaskWithoutReturn(task); - @Nullable MediaLibraryServiceLegacyStub legacyStub = getLegacyBrowserService(); - if (legacyStub != null) { - try { - task.run(legacyStub.getBrowserLegacyCbForBroadcast(), /* seq= */ 0); - } catch (RemoteException e) { - Log.e(TAG, "Exception in using media1 API", e); - } - } - } - private void maybeUpdateLegacyErrorState(ControllerInfo browser, LibraryResult result) { if (libraryErrorReplicationMode == MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_NONE || browser.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index ac88c9cdbad..f589d2ff09b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -406,20 +406,6 @@ public void onConnected(boolean shouldShowNotification) { } } - @Override - public void onMediaButtonPreferencesChanged( - MediaController controller, List mediaButtonPreferences) { - mediaSessionService.onUpdateNotificationInternal( - session, /* startInForegroundWhenPaused= */ false); - } - - @Override - public void onAvailableSessionCommandsChanged( - MediaController controller, SessionCommands commands) { - mediaSessionService.onUpdateNotificationInternal( - session, /* startInForegroundWhenPaused= */ false); - } - @Override public ListenableFuture onCustomCommand( MediaController controller, SessionCommand command, Bundle args) { @@ -440,20 +426,6 @@ public void onDisconnected(MediaController controller) { mediaSessionService.onUpdateNotificationInternal( session, /* startInForegroundWhenPaused= */ false); } - - @Override - public void onEvents(Player player, Player.Events events) { - // We must limit the frequency of notification updates, otherwise the system may suppress - // them. - if (events.containsAny( - Player.EVENT_PLAYBACK_STATE_CHANGED, - Player.EVENT_PLAY_WHEN_READY_CHANGED, - Player.EVENT_MEDIA_METADATA_CHANGED, - Player.EVENT_TIMELINE_CHANGED)) { - mediaSessionService.onUpdateNotificationInternal( - session, /* startInForegroundWhenPaused= */ false); - } - } } @SuppressLint("InlinedApi") // Using compile time constant FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index c9c2226b2fd..cae0e4d3b21 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -231,7 +231,6 @@ public MediaSessionImpl( new MediaSessionLegacyStub( /* session= */ thisRef, sessionUri, - applicationHandler, tokenExtras, playIfSuppressed, customLayout, @@ -240,7 +239,7 @@ public MediaSessionImpl( connectionResult.availablePlayerCommands, sessionExtras); - Token platformToken = sessionLegacyStub.getSessionCompat().getSessionToken().getToken(); + Token platformToken = sessionLegacyStub.getSessionToken().getToken(); sessionToken = new SessionToken( Process.myUid(), @@ -504,7 +503,7 @@ public ListenableFuture setCustomLayout( ControllerInfo controller, ImmutableList customLayout) { if (isMediaNotificationController(controller)) { sessionLegacyStub.setPlatformCustomLayout(customLayout); - sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper); + sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper, true); } return dispatchRemoteControllerTask( controller, (controller1, seq) -> controller1.setCustomLayout(seq, customLayout)); @@ -529,7 +528,7 @@ public ListenableFuture setMediaButtonPreferences( ControllerInfo controller, ImmutableList mediaButtonPreferences) { if (isMediaNotificationController(controller)) { sessionLegacyStub.setPlatformMediaButtonPreferences(mediaButtonPreferences); - sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper); + sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper, true); } return dispatchRemoteControllerTask( controller, @@ -980,7 +979,7 @@ public void connectFromService(IMediaController caller, ControllerInfo controlle @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding confusion by just using "Token" public android.media.session.MediaSession.Token getPlatformToken() { - return sessionLegacyStub.getSessionCompat().getSessionToken().getToken(); + return sessionLegacyStub.getSessionToken().getToken(); } public void setLegacyControllerConnectionTimeoutMs(long timeoutMs) { @@ -1049,8 +1048,7 @@ protected IBinder getLegacyBrowserServiceBinder() { MediaSessionServiceLegacyStub legacyStub; synchronized (lock) { if (browserServiceLegacyStub == null) { - browserServiceLegacyStub = - createLegacyBrowserService(sessionLegacyStub.getSessionCompat().getSessionToken()); + browserServiceLegacyStub = createLegacyBrowserService(sessionLegacyStub.getSessionToken()); } legacyStub = browserServiceLegacyStub; } @@ -1460,7 +1458,7 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma sessionLegacyStub.onSkipToNext(); return true; } else if (callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) { - sessionLegacyStub.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); + sessionLegacyStub.getControllerCompat().dispatchMediaButtonEvent(keyEvent); return true; } // This is an unhandled framework event. Return false to let the framework resolve by calling diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 1b5bd9b0c09..0d3d8e462d0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -61,6 +61,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -91,6 +92,7 @@ import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SessionCommand.CommandCode; +import androidx.media3.session.legacy.MediaControllerCompat; import androidx.media3.session.legacy.MediaDescriptionCompat; import androidx.media3.session.legacy.MediaMetadataCompat; import androidx.media3.session.legacy.MediaSessionCompat; @@ -105,6 +107,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -136,11 +139,13 @@ private final MediaSessionCompat sessionCompat; @Nullable private final MediaButtonReceiver runtimeBroadcastReceiver; @Nullable private final ComponentName broadcastReceiverComponentName; - @Nullable private VolumeProviderCompat volumeProviderCompat; private final boolean playIfSuppressed; + private final HandlerThread compatSessionInteractionThread; + private final Handler compatSessionInteractionHandler; private volatile long connectionTimeoutMs; @Nullable private FutureCallback pendingBitmapLoadCallback; + @Nullable private VolumeProviderCompat volumeProviderCompat; private int sessionFlags; @Nullable private LegacyError legacyError; private Bundle legacyExtras; @@ -158,7 +163,6 @@ public MediaSessionLegacyStub( MediaSessionImpl session, Uri sessionUri, - Handler handler, Bundle tokenExtras, boolean playIfSuppressed, ImmutableList customLayout, @@ -181,6 +185,9 @@ public MediaSessionLegacyStub( connectionTimeoutHandler = new ConnectionTimeoutHandler( session.getApplicationHandler().getLooper(), connectedControllersManager); + compatSessionInteractionThread = new HandlerThread("MSLegacyStub:CompatSIT"); + compatSessionInteractionThread.start(); + compatSessionInteractionHandler = new Handler(compatSessionInteractionThread.getLooper()); if (!mediaButtonPreferences.isEmpty()) { updateCustomLayoutAndLegacyExtrasForMediaButtonPreferences(); @@ -245,7 +252,8 @@ public MediaSessionLegacyStub( sessionCompatId, SDK_INT < 31 ? receiverComponentName : null, SDK_INT < 31 ? mediaButtonIntent : null, - /* sessionInfo= */ tokenExtras); + /* sessionInfo= */ tokenExtras, + compatSessionInteractionHandler.getLooper()); if (SDK_INT >= 31 && broadcastReceiverComponentName != null) { Api31.setMediaButtonBroadcastReceiver(sessionCompat, broadcastReceiverComponentName); } @@ -258,7 +266,11 @@ public MediaSessionLegacyStub( @SuppressWarnings("nullness:assignment") @Initialized MediaSessionLegacyStub thisRef = this; - sessionCompat.setCallback(thisRef, handler); + sessionCompat.setCallback(thisRef, compatSessionInteractionHandler); + } + + private void postOrRunForCompatSession(Runnable r) { + postOrRun(compatSessionInteractionHandler, r); } /** @@ -297,14 +309,14 @@ public void setAvailableCommands( /* defaultValue= */ false) != hadNextReservation); if (extrasChanged) { - getSessionCompat().setExtras(legacyExtras); + postOrRunForCompatSession(() -> sessionCompat.setExtras(legacyExtras)); } } if (commandGetTimelineChanged) { - updateLegacySessionPlaybackStateAndQueue(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackStateAndQueue(sessionImpl.getPlayerWrapper(), true); } else { - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), true); } } @@ -356,7 +368,7 @@ public void setPlatformMediaButtonPreferences( /* defaultValue= */ false) != hadNextReservation); if (extrasChanged) { - getSessionCompat().setExtras(legacyExtras); + postOrRunForCompatSession(() -> sessionCompat.setExtras(legacyExtras)); } } @@ -376,8 +388,8 @@ public void setPlaybackException( customPlaybackException = playbackException; this.playerCommandsForErrorState = playerCommandsForErrorState; if (playbackException != null) { - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); maybeUpdateFlags(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } } @@ -418,7 +430,7 @@ public void setLegacyError(LibraryResult result, boolean isFatal) { bundle = result.sessionError.extras; } legacyError = new LegacyError(isFatal, legacyErrorCode, errorMessage, bundle); - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } /** @@ -428,7 +440,7 @@ public void setLegacyError(LibraryResult result, boolean isFatal) { public void clearLegacyErrorStatus() { if (legacyError != null) { legacyError = null; - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } } @@ -455,37 +467,45 @@ private static ComponentName queryPackageManagerForMediaButtonReceiver(Context c /** Starts to receive commands. */ public void start() { - sessionCompat.setActive(true); + postOrRunForCompatSession(() -> sessionCompat.setActive(true)); } @SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent. public void release() { - if (SDK_INT < 31) { - if (broadcastReceiverComponentName == null) { - // No broadcast receiver available. Playback resumption not supported. - setMediaButtonReceiver(sessionCompat, /* mediaButtonReceiverIntent= */ null); - } else { - // Override the runtime receiver with the broadcast receiver for playback resumption. - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionImpl.getUri()); - intent.setComponent(broadcastReceiverComponentName); - PendingIntent mediaButtonReceiverIntent = - PendingIntent.getBroadcast( - sessionImpl.getContext(), - /* requestCode= */ 0, - intent, - PENDING_INTENT_FLAG_MUTABLE); - setMediaButtonReceiver(sessionCompat, mediaButtonReceiverIntent); - } - } if (runtimeBroadcastReceiver != null) { sessionImpl.getContext().unregisterReceiver(runtimeBroadcastReceiver); } - // No check for COMMAND_RELEASE needed as MediaControllers can always be released. - sessionCompat.release(); + postOrRunForCompatSession( + () -> { + if (SDK_INT < 31) { + if (broadcastReceiverComponentName == null) { + // No broadcast receiver available. Playback resumption not supported. + setMediaButtonReceiver(/* mediaButtonReceiverIntent= */ null); + } else { + // Override the runtime receiver with the broadcast receiver for playback resumption. + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionImpl.getUri()); + intent.setComponent(broadcastReceiverComponentName); + PendingIntent mediaButtonReceiverIntent = + PendingIntent.getBroadcast( + sessionImpl.getContext(), + /* requestCode= */ 0, + intent, + PENDING_INTENT_FLAG_MUTABLE); + setMediaButtonReceiver(mediaButtonReceiverIntent); + } + } + // No check for COMMAND_RELEASE needed as MediaControllers can always be released. + sessionCompat.release(); + }); + compatSessionInteractionThread.quitSafely(); + } + + public MediaSessionCompat.Token getSessionToken() { + return sessionCompat.getSessionToken(); // Safe to call on any thread. } - public MediaSessionCompat getSessionCompat() { - return sessionCompat; + public MediaControllerCompat getControllerCompat() { + return sessionCompat.getController(); // Safe to call on any thread. } @Override @@ -538,6 +558,18 @@ public void onCustomAction(String action, @Nullable Bundle args) { @Override public boolean onMediaButtonEvent(Intent intent) { + if (Looper.myLooper() != sessionImpl.getApplicationHandler().getLooper()) { + SettableFuture settableFuture = SettableFuture.create(); + sessionImpl + .getApplicationHandler() + .post(() -> settableFuture.set(onMediaButtonEvent(intent))); + // Block compatSessionInteractionThread until we have a decision. + try { + return settableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } return sessionImpl.onMediaButtonEvent( new ControllerInfo( checkNotNull(sessionCompat.getCurrentControllerInfo()), @@ -557,7 +589,7 @@ public boolean onMediaButtonEvent(Intent intent) { : 0; if (sessionFlags != newFlags) { sessionFlags = newFlags; - sessionCompat.setFlags(sessionFlags); + postOrRunForCompatSession(() -> sessionCompat.setFlags(sessionFlags)); } } @@ -666,36 +698,46 @@ public void onSeekTo(long pos) { @Override public void onSkipToNext() { - if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_NEXT, - controller -> sessionImpl.getPlayerWrapper().seekToNext(), - sessionCompat.getCurrentControllerInfo(), - /* callOnPlayerInteractionFinished= */ true); - } else { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, - controller -> sessionImpl.getPlayerWrapper().seekToNextMediaItem(), - sessionCompat.getCurrentControllerInfo(), - /* callOnPlayerInteractionFinished= */ true); - } + RemoteUserInfo controllerInfo = sessionCompat.getCurrentControllerInfo(); + postOrRun( + sessionImpl.getApplicationHandler(), + () -> { + if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_NEXT)) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_NEXT, + controller -> sessionImpl.getPlayerWrapper().seekToNext(), + controllerInfo, + /* callOnPlayerInteractionFinished= */ true); + } else { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, + controller -> sessionImpl.getPlayerWrapper().seekToNextMediaItem(), + controllerInfo, + /* callOnPlayerInteractionFinished= */ true); + } + }); } @Override public void onSkipToPrevious() { - if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_PREVIOUS, - controller -> sessionImpl.getPlayerWrapper().seekToPrevious(), - sessionCompat.getCurrentControllerInfo(), - /* callOnPlayerInteractionFinished= */ true); - } else { - dispatchSessionTaskWithPlayerCommand( - COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, - controller -> sessionImpl.getPlayerWrapper().seekToPreviousMediaItem(), - sessionCompat.getCurrentControllerInfo(), - /* callOnPlayerInteractionFinished= */ true); - } + RemoteUserInfo controllerInfo = sessionCompat.getCurrentControllerInfo(); + postOrRun( + sessionImpl.getApplicationHandler(), + () -> { + if (sessionImpl.getPlayerWrapper().isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_PREVIOUS, + controller -> sessionImpl.getPlayerWrapper().seekToPrevious(), + controllerInfo, + /* callOnPlayerInteractionFinished= */ true); + } else { + dispatchSessionTaskWithPlayerCommand( + COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, + controller -> sessionImpl.getPlayerWrapper().seekToPreviousMediaItem(), + controllerInfo, + /* callOnPlayerInteractionFinished= */ true); + } + }); } @Override @@ -1032,21 +1074,33 @@ public void setLegacyControllerDisconnectTimeoutMs(long timeoutMs) { connectionTimeoutMs = timeoutMs; } - public void updateLegacySessionPlaybackState(PlayerWrapper playerWrapper) { + public void updateLegacySessionPlaybackState(PlayerWrapper playerWrapper, boolean notify) { postOrRun( sessionImpl.getApplicationHandler(), - () -> sessionCompat.setPlaybackState(createPlaybackStateCompat(playerWrapper))); + () -> { + PlaybackStateCompat playbackStateCompat = createPlaybackStateCompat(playerWrapper); + postOrRunForCompatSession( + () -> { + sessionCompat.setPlaybackState(playbackStateCompat); + if (notify) { + sessionImpl.onNotificationRefreshRequired(); + } + }); + }); } - public void updateLegacySessionPlaybackStateAndQueue(PlayerWrapper playerWrapper) { + public void updateLegacySessionPlaybackStateAndQueue( + PlayerWrapper playerWrapper, boolean notify) { postOrRun( sessionImpl.getApplicationHandler(), () -> { - sessionCompat.setPlaybackState(createPlaybackStateCompat(playerWrapper)); + PlaybackStateCompat playbackStateCompat = createPlaybackStateCompat(playerWrapper); + postOrRunForCompatSession(() -> sessionCompat.setPlaybackState(playbackStateCompat)); controllerLegacyCbForBroadcast.updateQueue( playerWrapper.getAvailableCommands().contains(Player.COMMAND_GET_TIMELINE) ? playerWrapper.getCurrentTimeline() - : Timeline.EMPTY); + : Timeline.EMPTY, + notify); }); } @@ -1172,25 +1226,23 @@ private static void ignoreFuture(Future unused) { } @SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable. - private static void setMetadata( - MediaSessionCompat sessionCompat, @Nullable MediaMetadataCompat metadataCompat) { + private void setMetadata(@Nullable MediaMetadataCompat metadataCompat) { sessionCompat.setMetadata(metadataCompat); } @SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable. - private static void setMediaButtonReceiver( - MediaSessionCompat sessionCompat, @Nullable PendingIntent mediaButtonReceiverIntent) { + private void setMediaButtonReceiver(@Nullable PendingIntent mediaButtonReceiverIntent) { sessionCompat.setMediaButtonReceiver(mediaButtonReceiverIntent); } @SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable. - private static void setQueue(MediaSessionCompat sessionCompat, @Nullable List queue) { + private void setQueue(@Nullable List queue) { sessionCompat.setQueue(queue); } @SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable. - private void setQueueTitle(MediaSessionCompat sessionCompat, @Nullable CharSequence title) { - sessionCompat.setQueueTitle(isQueueEnabled() ? title : null); + private void setQueueTitle(boolean isQueueEnabled, @Nullable CharSequence title) { + sessionCompat.setQueueTitle(isQueueEnabled ? title : null); } private boolean isQueueEnabled() { @@ -1298,7 +1350,7 @@ public void onAvailableCommandsChangedFromPlayer(int seq, Player.Commands availa } PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); maybeUpdateFlags(playerWrapper); - updateLegacySessionPlaybackState(playerWrapper); + updateLegacySessionPlaybackState(playerWrapper, false); } @Override @@ -1354,7 +1406,7 @@ public void onPlayerChanged( // If PlaybackStateCompat isn't updated by above if-statement, forcefully update // PlaybackStateCompat to tell the latest position and its event // time. This would also update playback speed, buffering state, player state, and error. - updateLegacySessionPlaybackState(newPlayerWrapper); + updateLegacySessionPlaybackState(newPlayerWrapper, true); } } @@ -1363,17 +1415,17 @@ public void onPlayerError(int seq, @Nullable PlaybackException playerError) { if (skipLegacySessionPlaybackStateUpdates()) { return; } - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } @Override public void setCustomLayout(int seq, List layout) { - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), true); } @Override public void setMediaButtonPreferences(int seq, List mediaButtonPreferences) { - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), true); } @Override @@ -1385,13 +1437,13 @@ public void onSessionExtrasChanged(int seq, Bundle sessionExtras) { // Re-calculate custom layout in case we have to set any additional extras. updateCustomLayoutAndLegacyExtrasForMediaButtonPreferences(); } - sessionCompat.setExtras(legacyExtras); - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + postOrRunForCompatSession(() -> sessionCompat.setExtras(legacyExtras)); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } @Override public void onSessionActivityChanged(int seq, @Nullable PendingIntent sessionActivity) { - sessionCompat.setSessionActivity(sessionActivity); + postOrRunForCompatSession(() -> sessionCompat.setSessionActivity(sessionActivity)); } @Override @@ -1404,15 +1456,16 @@ public void onError(int seq, SessionError sessionError) { sessionError.message, sessionError.extras); if (!skipLegacySessionPlaybackStateUpdates()) { - sessionCompat.setPlaybackState(createPlaybackStateCompat(playerWrapper)); + PlaybackStateCompat playbackStateCompat = createPlaybackStateCompat(playerWrapper); + postOrRunForCompatSession(() -> sessionCompat.setPlaybackState(playbackStateCompat)); legacyError = null; - sessionCompat.setPlaybackState(createPlaybackStateCompat(playerWrapper)); + updateLegacySessionPlaybackState(playerWrapper, false); } } @Override public void sendCustomCommand(int seq, SessionCommand command, Bundle args) { - sessionCompat.sendSessionEvent(command.customAction, args); + postOrRunForCompatSession(() -> sessionCompat.sendSessionEvent(command.customAction, args)); } @Override @@ -1422,7 +1475,7 @@ public void onPlayWhenReadyChanged( return; } // Note: This method does not use any of the given arguments. - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), true); } @Override @@ -1431,7 +1484,7 @@ public void onPlaybackSuppressionReasonChanged( if (skipLegacySessionPlaybackStateUpdates()) { return; } - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } @Override @@ -1441,7 +1494,7 @@ public void onPlaybackStateChanged( return; } // Note: This method does not use any of the given arguments. - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), true); } @Override @@ -1449,7 +1502,7 @@ public void onIsPlayingChanged(int seq, boolean isPlaying) { if (skipLegacySessionPlaybackStateUpdates()) { return; } - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } @Override @@ -1462,7 +1515,7 @@ public void onPositionDiscontinuity( return; } // Note: This method does not use any of the given arguments. - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } @Override @@ -1471,7 +1524,7 @@ public void onPlaybackParametersChanged(int seq, PlaybackParameters playbackPara return; } // Note: This method does not use any of the given arguments. - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } @Override @@ -1481,14 +1534,20 @@ public void onMediaItemTransition( return; } // MediaMetadataCompat needs to be updated when the media ID or URI of the media item changes. - updateMetadataIfChanged(); - if (mediaItem == null) { - sessionCompat.setRatingType(RatingCompat.RATING_NONE); - } else { - sessionCompat.setRatingType( - LegacyConversions.getRatingCompatStyle(mediaItem.mediaMetadata.userRating)); - } - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateMetadataIfChanged(false); + PlaybackStateCompat playbackStateCompat = + createPlaybackStateCompat(sessionImpl.getPlayerWrapper()); + postOrRunForCompatSession( + () -> { + if (mediaItem == null) { + sessionCompat.setRatingType(RatingCompat.RATING_NONE); + } else { + sessionCompat.setRatingType( + LegacyConversions.getRatingCompatStyle(mediaItem.mediaMetadata.userRating)); + } + sessionCompat.setPlaybackState(playbackStateCompat); + sessionImpl.onNotificationRefreshRequired(); + }); } @Override @@ -1496,7 +1555,7 @@ public void onMediaMetadataChanged(int seq, MediaMetadata mediaMetadata) { if (skipLegacySessionPlaybackStateUpdates()) { return; } - updateMetadataIfChanged(); + updateMetadataIfChanged(true); } @Override @@ -1505,14 +1564,20 @@ public void onTimelineChanged( if (skipLegacySessionPlaybackStateUpdates()) { return; } - updateQueue(timeline); + updateQueue(timeline, false); // Duration might be unknown at onMediaItemTransition and become available afterward. - updateMetadataIfChanged(); + updateMetadataIfChanged(true); } - private void updateQueue(Timeline timeline) { + private void updateQueue(Timeline timeline, boolean notify) { if (!isQueueEnabled() || timeline.isEmpty()) { - setQueue(sessionCompat, /* queue= */ null); + postOrRunForCompatSession( + () -> { + setQueue(/* queue= */ null); + if (notify) { + sessionImpl.onNotificationRefreshRequired(); + } + }); return; } List mediaItemList = LegacyConversions.convertToMediaItemList(timeline); @@ -1522,7 +1587,7 @@ private void updateQueue(Timeline timeline) { () -> { int completedBitmapFutureCount = resultCount.incrementAndGet(); if (completedBitmapFutureCount == mediaItemList.size()) { - handleBitmapFuturesAllCompletedAndSetQueue(bitmapFutures, mediaItemList); + handleBitmapFuturesAllCompletedAndSetQueue(bitmapFutures, mediaItemList, notify); } }; @@ -1531,19 +1596,21 @@ private void updateQueue(Timeline timeline) { MediaMetadata metadata = mediaItem.mediaMetadata; if (metadata.artworkData == null) { bitmapFutures.add(null); - handleBitmapFuturesTask.run(); + postOrRunForCompatSession(handleBitmapFuturesTask); } else { ListenableFuture bitmapFuture = sessionImpl.getBitmapLoader().decodeBitmap(metadata.artworkData); bitmapFutures.add(bitmapFuture); bitmapFuture.addListener( - handleBitmapFuturesTask, sessionImpl.getApplicationHandler()::post); + handleBitmapFuturesTask, MediaSessionLegacyStub.this::postOrRunForCompatSession); } } } private void handleBitmapFuturesAllCompletedAndSetQueue( - List<@NullableType ListenableFuture> bitmapFutures, List mediaItems) { + List<@NullableType ListenableFuture> bitmapFutures, + List mediaItems, + boolean notify) { List queueItemList = new ArrayList<>(); for (int i = 0; i < bitmapFutures.size(); i++) { @Nullable ListenableFuture future = bitmapFutures.get(i); @@ -1560,7 +1627,10 @@ private void handleBitmapFuturesAllCompletedAndSetQueue( // Framework MediaSession#setQueue() uses ParceledListSlice, // which means we can safely send long lists. - setQueue(sessionCompat, queueItemList); + setQueue(queueItemList); + if (notify) { + sessionImpl.onNotificationRefreshRequired(); + } } @Override @@ -1570,23 +1640,31 @@ public void onPlaylistMetadataChanged(int seq, MediaMetadata playlistMetadata) { return; } // Since there is no 'queue metadata', only set title of the queue. - @Nullable CharSequence queueTitle = sessionCompat.getController().getQueueTitle(); - @Nullable CharSequence newTitle = playlistMetadata.title; - if (!TextUtils.equals(queueTitle, newTitle)) { - setQueueTitle(sessionCompat, newTitle); - } + final boolean isQueueEnabled = isQueueEnabled(); + postOrRunForCompatSession( + () -> { + @Nullable CharSequence queueTitle = sessionCompat.getController().getQueueTitle(); + @Nullable CharSequence newTitle = playlistMetadata.title; + if (!TextUtils.equals(queueTitle, newTitle)) { + setQueueTitle(isQueueEnabled, newTitle); + } + }); } @Override public void onShuffleModeEnabledChanged(int seq, boolean shuffleModeEnabled) { - sessionCompat.setShuffleMode( - LegacyConversions.convertToPlaybackStateCompatShuffleMode(shuffleModeEnabled)); + postOrRunForCompatSession( + () -> + sessionCompat.setShuffleMode( + LegacyConversions.convertToPlaybackStateCompatShuffleMode(shuffleModeEnabled))); } @Override public void onRepeatModeChanged(int seq, @RepeatMode int repeatMode) throws RemoteException { - sessionCompat.setRepeatMode( - LegacyConversions.convertToPlaybackStateCompatRepeatMode(repeatMode)); + postOrRunForCompatSession( + () -> + sessionCompat.setRepeatMode( + LegacyConversions.convertToPlaybackStateCompatRepeatMode(repeatMode))); } @Override @@ -1594,7 +1672,15 @@ public void onAudioAttributesChanged(int seq, AudioAttributes audioAttributes) { @DeviceInfo.PlaybackType int playbackType = sessionImpl.getPlayerWrapper().getDeviceInfo().playbackType; if (playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) { - sessionCompat.setPlaybackToLocal(audioAttributes.getStreamType()); + postOrRunForCompatSession( + () -> { + if (volumeProviderCompat != null) { + // Stale event. + return; + } + sessionCompat.setPlaybackToLocal(audioAttributes.getStreamType()); + sessionImpl.onNotificationRefreshRequired(); + }); } } @@ -1604,9 +1690,23 @@ public void onDeviceInfoChanged(int seq, DeviceInfo deviceInfo) { volumeProviderCompat = createVolumeProviderCompat(player); if (volumeProviderCompat == null) { int streamType = player.getAudioAttributesWithCommandCheck().getStreamType(); - sessionCompat.setPlaybackToLocal(streamType); + postOrRunForCompatSession(() -> { + if (volumeProviderCompat != null) { + // Stale event. + return; + } + sessionCompat.setPlaybackToLocal(streamType); + sessionImpl.onNotificationRefreshRequired(); + }); } else { - sessionCompat.setPlaybackToRemote(volumeProviderCompat); + postOrRunForCompatSession(() -> { + if (volumeProviderCompat == null) { + // Stale event. + return; + } + sessionCompat.setPlaybackToRemote(volumeProviderCompat); + sessionImpl.onNotificationRefreshRequired(); + }); } } @@ -1625,11 +1725,11 @@ public void onPeriodicSessionPositionInfoChanged( boolean unusedCanAccessTimeline, int controllerInterfaceVersion) { if (!skipLegacySessionPlaybackStateUpdates()) { - updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper(), false); } } - private void updateMetadataIfChanged() { + private void updateMetadataIfChanged(boolean notify) { PlayerWrapper player = sessionImpl.getPlayerWrapper(); @Nullable MediaItem currentMediaItem = player.getCurrentMediaItemWithCommandCheck(); MediaMetadata newMediaMetadata = player.getMediaMetadataWithCommandCheck(); @@ -1649,6 +1749,9 @@ private void updateMetadataIfChanged() { && Objects.equals(lastMediaId, newMediaId) && Objects.equals(lastMediaUri, newMediaUri) && lastDurationMs == newDurationMs) { + if (notify) { + sessionImpl.onNotificationRefreshRequired(); + } return; } @@ -1676,15 +1779,17 @@ public void onSuccess(Bitmap result) { if (this != pendingBitmapLoadCallback) { return; } - setMetadata( - sessionCompat, - LegacyConversions.convertToMediaMetadataCompat( - newMediaMetadata, - newMediaId, - newMediaUri, - newDurationMs, - /* artworkBitmap= */ result)); - sessionImpl.onNotificationRefreshRequired(); + postOrRunForCompatSession( + () -> { + setMetadata( + LegacyConversions.convertToMediaMetadataCompat( + newMediaMetadata, + newMediaId, + newMediaUri, + newDurationMs, + /* artworkBitmap= */ result)); + sessionImpl.onNotificationRefreshRequired(); + }); } @Override @@ -1701,10 +1806,16 @@ public void onFailure(Throwable t) { /* executor= */ sessionImpl.getApplicationHandler()::post); } } - setMetadata( - sessionCompat, - LegacyConversions.convertToMediaMetadataCompat( - newMediaMetadata, newMediaId, newMediaUri, newDurationMs, artworkBitmap)); + @Nullable Bitmap artworkBitmapFinal = artworkBitmap; + postOrRunForCompatSession( + () -> { + setMetadata( + LegacyConversions.convertToMediaMetadataCompat( + newMediaMetadata, newMediaId, newMediaUri, newDurationMs, artworkBitmapFinal)); + if (notify) { + sessionImpl.onNotificationRefreshRequired(); + } + }); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java index ee242179198..a0743622c35 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java @@ -327,7 +327,8 @@ public MediaSessionCompat( String tag, @Nullable ComponentName mbrComponent, @Nullable PendingIntent mbrIntent, - @Nullable Bundle sessionInfo) { + @Nullable Bundle sessionInfo, + @Nullable Looper callbackLooper) { if (TextUtils.isEmpty(tag)) { throw new IllegalArgumentException("tag must not be null or empty"); } @@ -361,7 +362,7 @@ public MediaSessionCompat( impl = new MediaSessionImplApi21(context, tag, sessionInfo); } // Set default callback to respond to controllers' extra binder requests. - Looper myLooper = Looper.myLooper(); + Looper myLooper = callbackLooper != null ? callbackLooper : Looper.myLooper(); Handler handler = new Handler(myLooper != null ? myLooper : Looper.getMainLooper()); setCallback(new Callback() {}, handler); impl.setMediaButtonReceiver(mbrIntent);