MediaSessionStub.java

/*
 * Copyright 2019 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.common.Player.COMMAND_ADJUST_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;
import static androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE;
import static androidx.media3.common.Player.COMMAND_SET_VOLUME;
import static androidx.media3.common.Player.COMMAND_STOP;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.common.util.Util.postOrRunWithCompletion;
import static androidx.media3.common.util.Util.transformFutureAsync;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SEARCH;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING;

import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
import androidx.media.MediaSessionManager;
import androidx.media3.common.BundleListRetriever;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Rating;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Consumer;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition;
import androidx.media3.session.SessionCommand.CommandCode;
import com.google.common.collect.ImmutableList;
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.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;

/**
 * Class that handles incoming commands from {@link MediaController} and {@link MediaBrowser} to
 * both {@link MediaSession} and {@link MediaLibrarySession}.
 */
// We cannot create a subclass for library service specific function because AIDL doesn't support
// subclassing and it's generated stub class is an abstract class.
/* package */ final class MediaSessionStub extends IMediaSession.Stub {

  private static final String TAG = "MediaSessionStub";

  /** The version of the IMediaSession interface. */
  public static final int VERSION_INT = 1;

  private final WeakReference<MediaSessionImpl> sessionImpl;
  private final MediaSessionManager sessionManager;
  private final ConnectedControllersManager<IBinder> connectedControllersManager;
  private final Set<ControllerInfo> pendingControllers;

  public MediaSessionStub(MediaSessionImpl sessionImpl) {
    // Initialize members with params.
    this.sessionImpl = new WeakReference<>(sessionImpl);
    sessionManager = MediaSessionManager.getSessionManager(sessionImpl.getContext());
    connectedControllersManager = new ConnectedControllersManager<>(sessionImpl);
    // ConcurrentHashMap has a bug in APIs 21-22 that can result in lost updates.
    pendingControllers = Collections.synchronizedSet(new HashSet<>());
  }

  public ConnectedControllersManager<IBinder> getConnectedControllersManager() {
    return connectedControllersManager;
  }

  private static void sendSessionResult(
      ControllerInfo controller, int sequenceNumber, SessionResult result) {
    try {
      checkStateNotNull(controller.getControllerCb()).onSessionResult(sequenceNumber, result);
    } catch (RemoteException e) {
      Log.w(TAG, "Failed to send result to controller " + controller, e);
    }
  }

  private static <K extends MediaSessionImpl>
      SessionTask<ListenableFuture<Void>, K> sendSessionResultSuccess(
          Consumer<PlayerWrapper> task) {
    return sendSessionResultSuccess((player, controller) -> task.accept(player));
  }

  private static <K extends MediaSessionImpl>
      SessionTask<ListenableFuture<Void>, K> sendSessionResultSuccess(ControllerPlayerTask task) {
    return (sessionImpl, controller, sequenceNumber) -> {
      if (sessionImpl.isReleased()) {
        return Futures.immediateVoidFuture();
      }
      task.run(sessionImpl.getPlayerWrapper(), controller);
      sendSessionResult(
          controller, sequenceNumber, new SessionResult(SessionResult.RESULT_SUCCESS));
      return Futures.immediateVoidFuture();
    };
  }

  private static <K extends MediaSessionImpl>
      SessionTask<ListenableFuture<Void>, K> sendSessionResultWhenReady(
          SessionTask<ListenableFuture<SessionResult>, K> task) {
    return (sessionImpl, controller, sequenceNumber) ->
        handleSessionTaskWhenReady(
            sessionImpl,
            controller,
            sequenceNumber,
            task,
            future -> {
              SessionResult result;
              try {
                result = checkNotNull(future.get(), "SessionResult must not be null");
              } catch (CancellationException unused) {
                result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED);
              } catch (ExecutionException | InterruptedException exception) {
                result =
                    new SessionResult(
                        exception.getCause() instanceof UnsupportedOperationException
                            ? SessionResult.RESULT_ERROR_NOT_SUPPORTED
                            : SessionResult.RESULT_ERROR_UNKNOWN);
              }
              sendSessionResult(controller, sequenceNumber, result);
            });
  }

  private static <K extends MediaSessionImpl>
      SessionTask<ListenableFuture<SessionResult>, K> handleMediaItemsWhenReady(
          SessionTask<ListenableFuture<List<MediaItem>>, K> mediaItemsTask,
          MediaItemPlayerTask mediaItemPlayerTask) {
    return (sessionImpl, controller, sequenceNumber) -> {
      if (sessionImpl.isReleased()) {
        return Futures.immediateFuture(
            new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
      }
      return transformFutureAsync(
          mediaItemsTask.run(sessionImpl, controller, sequenceNumber),
          mediaItems ->
              postOrRunWithCompletion(
                  sessionImpl.getApplicationHandler(),
                  () -> {
                    if (!sessionImpl.isReleased()) {
                      mediaItemPlayerTask.run(
                          sessionImpl.getPlayerWrapper(), controller, mediaItems);
                    }
                  },
                  new SessionResult(SessionResult.RESULT_SUCCESS)));
    };
  }

  private static <K extends MediaSessionImpl>
      SessionTask<ListenableFuture<SessionResult>, K> handleMediaItemsWithStartPositionWhenReady(
          SessionTask<ListenableFuture<MediaItemsWithStartPosition>, K> mediaItemsTask,
          MediaItemsWithStartPositionPlayerTask mediaItemPlayerTask) {
    return (sessionImpl, controller, sequenceNumber) -> {
      if (sessionImpl.isReleased()) {
        return Futures.immediateFuture(
            new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
      }
      return transformFutureAsync(
          mediaItemsTask.run(sessionImpl, controller, sequenceNumber),
          mediaItemsWithStartPosition ->
              postOrRunWithCompletion(
                  sessionImpl.getApplicationHandler(),
                  () -> {
                    if (!sessionImpl.isReleased()) {
                      mediaItemPlayerTask.run(
                          sessionImpl.getPlayerWrapper(), mediaItemsWithStartPosition);
                    }
                  },
                  new SessionResult(SessionResult.RESULT_SUCCESS)));
    };
  }

  private static void sendLibraryResult(
      ControllerInfo controller, int sequenceNumber, LibraryResult<?> result) {
    try {
      checkStateNotNull(controller.getControllerCb()).onLibraryResult(sequenceNumber, result);
    } catch (RemoteException e) {
      Log.w(TAG, "Failed to send result to browser " + controller, e);
    }
  }

  private static <V, K extends MediaLibrarySessionImpl>
      SessionTask<ListenableFuture<Void>, K> sendLibraryResultWhenReady(
          SessionTask<ListenableFuture<LibraryResult<V>>, K> task) {
    return (sessionImpl, controller, sequenceNumber) ->
        handleSessionTaskWhenReady(
            sessionImpl,
            controller,
            sequenceNumber,
            task,
            future -> {
              LibraryResult<V> result;
              try {
                result = checkNotNull(future.get(), "LibraryResult must not be null");
              } catch (CancellationException unused) {
                result = LibraryResult.ofError(LibraryResult.RESULT_INFO_SKIPPED);
              } catch (ExecutionException | InterruptedException unused) {
                result = LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN);
              }
              sendLibraryResult(controller, sequenceNumber, result);
            });
  }

  private <K extends MediaSessionImpl> void queueSessionTaskWithPlayerCommand(
      IMediaController caller,
      int sequenceNumber,
      @Player.Command int command,
      SessionTask<ListenableFuture<Void>, K> task) {
    long token = Binder.clearCallingIdentity();
    try {
      @SuppressWarnings({"unchecked", "cast.unsafe"})
      @Nullable
      K sessionImpl = (K) this.sessionImpl.get();
      if (sessionImpl == null || sessionImpl.isReleased()) {
        return;
      }
      @Nullable
      ControllerInfo controller = connectedControllersManager.getController(caller.asBinder());
      if (controller == null) {
        return;
      }
      postOrRun(
          sessionImpl.getApplicationHandler(),
          () -> {
            if (!connectedControllersManager.isPlayerCommandAvailable(controller, command)) {
              sendSessionResult(
                  controller,
                  sequenceNumber,
                  new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED));
              return;
            }
            @SessionResult.Code
            int resultCode = sessionImpl.onPlayerCommandRequestOnHandler(controller, command);
            if (resultCode != SessionResult.RESULT_SUCCESS) {
              // Don't run rejected command.
              sendSessionResult(controller, sequenceNumber, new SessionResult(resultCode));
              return;
            }
            if (command == COMMAND_SET_VIDEO_SURFACE) {
              task.run(sessionImpl, controller, sequenceNumber);
            } else {
              connectedControllersManager.addToCommandQueue(
                  controller, () -> task.run(sessionImpl, controller, sequenceNumber));
            }
          });
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  private <K extends MediaSessionImpl> void dispatchSessionTaskWithSessionCommand(
      IMediaController caller,
      int sequenceNumber,
      @CommandCode int commandCode,
      SessionTask<ListenableFuture<Void>, K> task) {
    dispatchSessionTaskWithSessionCommand(
        caller, sequenceNumber, /* sessionCommand= */ null, commandCode, task);
  }

  private <K extends MediaSessionImpl> void dispatchSessionTaskWithSessionCommand(
      IMediaController caller,
      int sequenceNumber,
      SessionCommand sessionCommand,
      SessionTask<ListenableFuture<Void>, K> task) {
    dispatchSessionTaskWithSessionCommand(
        caller, sequenceNumber, sessionCommand, COMMAND_CODE_CUSTOM, task);
  }

  private <K extends MediaSessionImpl> void dispatchSessionTaskWithSessionCommand(
      IMediaController caller,
      int sequenceNumber,
      @Nullable SessionCommand sessionCommand,
      @CommandCode int commandCode,
      SessionTask<ListenableFuture<Void>, K> task) {
    long token = Binder.clearCallingIdentity();
    try {
      @SuppressWarnings({"unchecked", "cast.unsafe"})
      @Nullable
      K sessionImpl = (K) this.sessionImpl.get();
      if (sessionImpl == null || sessionImpl.isReleased()) {
        return;
      }
      @Nullable
      ControllerInfo controller = connectedControllersManager.getController(caller.asBinder());
      if (controller == null) {
        return;
      }
      postOrRun(
          sessionImpl.getApplicationHandler(),
          () -> {
            if (!connectedControllersManager.isConnected(controller)) {
              return;
            }
            if (sessionCommand != null) {
              if (!connectedControllersManager.isSessionCommandAvailable(
                  controller, sessionCommand)) {
                sendSessionResult(
                    controller,
                    sequenceNumber,
                    new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED));
                return;
              }
            } else {
              if (!connectedControllersManager.isSessionCommandAvailable(controller, commandCode)) {
                sendSessionResult(
                    controller,
                    sequenceNumber,
                    new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED));
                return;
              }
            }
            task.run(sessionImpl, controller, sequenceNumber);
          });
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  private static <T, K extends MediaSessionImpl> ListenableFuture<Void> handleSessionTaskWhenReady(
      K sessionImpl,
      ControllerInfo controller,
      int sequenceNumber,
      SessionTask<ListenableFuture<T>, K> task,
      Consumer<ListenableFuture<T>> futureResultHandler) {
    if (sessionImpl.isReleased()) {
      return Futures.immediateVoidFuture();
    }
    ListenableFuture<T> future = task.run(sessionImpl, controller, sequenceNumber);
    SettableFuture<Void> outputFuture = SettableFuture.create();
    future.addListener(
        () -> {
          if (sessionImpl.isReleased()) {
            outputFuture.set(null);
            return;
          }
          try {
            futureResultHandler.accept(future);
            outputFuture.set(null);
          } catch (Throwable error) {
            outputFuture.setException(error);
          }
        },
        MoreExecutors.directExecutor());
    return outputFuture;
  }

  private int maybeCorrectMediaItemIndex(
      ControllerInfo controllerInfo, PlayerWrapper player, int mediaItemIndex) {
    if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)
        && !connectedControllersManager.isPlayerCommandAvailable(
            controllerInfo, Player.COMMAND_GET_TIMELINE)
        && connectedControllersManager.isPlayerCommandAvailable(
            controllerInfo, Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
      // COMMAND_GET_TIMELINE was filtered out for this controller, so all indices are relative to
      // the current one.
      return mediaItemIndex + player.getCurrentMediaItemIndex();
    }
    return mediaItemIndex;
  }

  public void connect(
      IMediaController caller,
      int controllerVersion,
      int controllerInterfaceVersion,
      String callingPackage,
      int pid,
      int uid,
      Bundle connectionHints) {
    MediaSessionManager.RemoteUserInfo remoteUserInfo =
        new MediaSessionManager.RemoteUserInfo(callingPackage, pid, uid);
    ControllerInfo controllerInfo =
        new ControllerInfo(
            remoteUserInfo,
            controllerVersion,
            controllerInterfaceVersion,
            sessionManager.isTrustedForMediaControl(remoteUserInfo),
            new Controller2Cb(caller),
            connectionHints);
    @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
    if (sessionImpl == null || sessionImpl.isReleased()) {
      try {
        caller.onDisconnected(/* seq= */ 0);
      } catch (RemoteException e) {
        // Controller may be died prematurely.
        // Not an issue because we'll ignore it anyway.
      }
      return;
    }
    pendingControllers.add(controllerInfo);
    postOrRun(
        sessionImpl.getApplicationHandler(),
        () -> {
          boolean connected = false;
          try {
            pendingControllers.remove(controllerInfo);
            if (sessionImpl.isReleased()) {
              return;
            }
            IBinder callbackBinder =
                checkStateNotNull((Controller2Cb) controllerInfo.getControllerCb())
                    .getCallbackBinder();
            MediaSession.ConnectionResult connectionResult =
                sessionImpl.onConnectOnHandler(controllerInfo);
            // Don't reject connection for the request from trusted app.
            // Otherwise server will fail to retrieve session's information to dispatch
            // media keys to.
            if (!connectionResult.isAccepted && !controllerInfo.isTrusted()) {
              return;
            }
            if (!connectionResult.isAccepted) {
              // For the accepted controller, send non-null allowed commands to keep connection.
              connectionResult =
                  MediaSession.ConnectionResult.accept(
                      SessionCommands.EMPTY, Player.Commands.EMPTY);
            }
            SequencedFutureManager sequencedFutureManager;
            if (connectedControllersManager.isConnected(controllerInfo)) {
              Log.w(
                  TAG,
                  "Controller "
                      + controllerInfo
                      + " has sent connection"
                      + " request multiple times");
            }
            connectedControllersManager.addController(
                callbackBinder,
                controllerInfo,
                connectionResult.availableSessionCommands,
                connectionResult.availablePlayerCommands);
            sequencedFutureManager =
                checkStateNotNull(
                    connectedControllersManager.getSequencedFutureManager(controllerInfo));
            // If connection is accepted, notify the current state to the controller.
            // It's needed because we cannot call synchronous calls between
            // session/controller.
            PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
            PlayerInfo playerInfo = playerWrapper.createPlayerInfoForBundling();
            ConnectionState state =
                new ConnectionState(
                    MediaLibraryInfo.VERSION_INT,
                    MediaSessionStub.VERSION_INT,
                    MediaSessionStub.this,
                    sessionImpl.getSessionActivity(),
                    connectionResult.availableSessionCommands,
                    connectionResult.availablePlayerCommands,
                    playerWrapper.getAvailableCommands(),
                    sessionImpl.getToken().getExtras(),
                    playerInfo);

            // Double check if session is still there, because release() can be called in
            // another thread.
            if (sessionImpl.isReleased()) {
              return;
            }
            try {
              caller.onConnected(
                  sequencedFutureManager.obtainNextSequenceNumber(), state.toBundle());
              connected = true;
            } catch (RemoteException e) {
              // Controller may be died prematurely.
            }
            sessionImpl.onPostConnectOnHandler(controllerInfo);
          } finally {
            if (!connected) {
              try {
                caller.onDisconnected(/* seq= */ 0);
              } catch (RemoteException e) {
                // Controller may be died prematurely.
                // Not an issue because we'll ignore it anyway.
              }
            }
          }
        });
  }

  public void release() {
    List<ControllerInfo> controllers = connectedControllersManager.getConnectedControllers();
    for (ControllerInfo controller : controllers) {
      ControllerCb cb = controller.getControllerCb();
      if (cb != null) {
        try {
          cb.onDisconnected(/* seq= */ 0);
        } catch (RemoteException e) {
          // Ignore. We're releasing.
        }
      }
    }
    for (ControllerInfo controller : pendingControllers) {
      ControllerCb cb = controller.getControllerCb();
      if (cb != null) {
        try {
          cb.onDisconnected(/* seq= */ 0);
        } catch (RemoteException e) {
          // Ignore. We're releasing.
        }
      }
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////
  // AIDL methods for session overrides
  //////////////////////////////////////////////////////////////////////////////////////////////

  @Override
  public void connect(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable Bundle connectionRequestBundle)
      throws RuntimeException {
    if (caller == null || connectionRequestBundle == null) {
      return;
    }
    ConnectionRequest request;
    try {
      request = ConnectionRequest.CREATOR.fromBundle(connectionRequestBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for ConnectionRequest", e);
      return;
    }
    int uid = Binder.getCallingUid();
    int callingPid = Binder.getCallingPid();
    long token = Binder.clearCallingIdentity();
    // Binder.getCallingPid() can be 0 for an oneway call from the remote process.
    // If it's the case, use PID from the ConnectionRequest.
    int pid = (callingPid != 0) ? callingPid : request.pid;
    try {
      connect(
          caller,
          request.libraryVersion,
          request.controllerInterfaceVersion,
          request.packageName,
          pid,
          uid,
          request.connectionHints);
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  @Override
  public void stop(@Nullable IMediaController caller, int sequenceNumber) throws RemoteException {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(player -> player.stop()));
  }

  @Override
  public void release(@Nullable IMediaController caller, int sequenceNumber)
      throws RemoteException {
    if (caller == null) {
      return;
    }
    long token = Binder.clearCallingIdentity();
    try {
      @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
      if (sessionImpl == null || sessionImpl.isReleased()) {
        return;
      }
      postOrRun(
          sessionImpl.getApplicationHandler(),
          () -> connectedControllersManager.removeController(caller.asBinder()));
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  @Override
  public void onControllerResult(
      @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle sessionResultBundle) {
    if (caller == null || sessionResultBundle == null) {
      return;
    }
    SessionResult result;
    try {
      result = SessionResult.CREATOR.fromBundle(sessionResultBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for SessionResult", e);
      return;
    }
    long token = Binder.clearCallingIdentity();
    try {
      @Nullable
      SequencedFutureManager manager =
          connectedControllersManager.getSequencedFutureManager(caller.asBinder());
      if (manager == null) {
        return;
      }
      manager.setFutureResult(sequenceNumber, result);
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  @Override
  public void play(@Nullable IMediaController caller, int sequenceNumber) throws RuntimeException {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_PLAY_PAUSE,
        sendSessionResultSuccess(
            player -> {
              @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
              if (sessionImpl == null || sessionImpl.isReleased()) {
                return;
              }

              if (sessionImpl.onPlayRequested()) {
                player.play();
              }
            }));
  }

  @Override
  public void pause(@Nullable IMediaController caller, int sequenceNumber) throws RuntimeException {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller, sequenceNumber, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::pause));
  }

  @Override
  public void prepare(@Nullable IMediaController caller, int sequenceNumber)
      throws RuntimeException {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller, sequenceNumber, COMMAND_PREPARE, sendSessionResultSuccess(Player::prepare));
  }

  @Override
  public void seekToDefaultPosition(IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_TO_DEFAULT_POSITION,
        sendSessionResultSuccess(player -> player.seekToDefaultPosition()));
  }

  @Override
  public void seekToDefaultPositionWithMediaItemIndex(
      IMediaController caller, int sequenceNumber, int mediaItemIndex) throws RemoteException {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_TO_MEDIA_ITEM,
        sendSessionResultSuccess(
            (player, controller) ->
                player.seekToDefaultPosition(
                    maybeCorrectMediaItemIndex(controller, player, mediaItemIndex))));
  }

  @Override
  public void seekTo(@Nullable IMediaController caller, int sequenceNumber, long positionMs)
      throws RuntimeException {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
        sendSessionResultSuccess(player -> player.seekTo(positionMs)));
  }

  @Override
  public void seekToWithMediaItemIndex(
      IMediaController caller, int sequenceNumber, int mediaItemIndex, long positionMs)
      throws RemoteException {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_TO_MEDIA_ITEM,
        sendSessionResultSuccess(
            (player, controller) ->
                player.seekTo(
                    maybeCorrectMediaItemIndex(controller, player, mediaItemIndex), positionMs)));
  }

  @Override
  public void seekBack(IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller, sequenceNumber, COMMAND_SEEK_BACK, sendSessionResultSuccess(Player::seekBack));
  }

  @Override
  public void seekForward(IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_FORWARD,
        sendSessionResultSuccess(Player::seekForward));
  }

  @Override
  public void onCustomCommand(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable Bundle commandBundle,
      @Nullable Bundle args) {
    if (caller == null || commandBundle == null || args == null) {
      return;
    }
    SessionCommand command;
    try {
      command = SessionCommand.CREATOR.fromBundle(commandBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for SessionCommand", e);
      return;
    }
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        command,
        sendSessionResultWhenReady(
            (sessionImpl, controller, sequenceNum) ->
                sessionImpl.onCustomCommandOnHandler(controller, command, args)));
  }

  @Override
  public void setRatingWithMediaId(
      @Nullable IMediaController caller,
      int sequenceNumber,
      String mediaId,
      @Nullable Bundle ratingBundle) {
    if (caller == null || ratingBundle == null) {
      return;
    }
    if (TextUtils.isEmpty(mediaId)) {
      Log.w(TAG, "setRatingWithMediaId(): Ignoring empty mediaId");
      return;
    }
    Rating rating;
    try {
      rating = Rating.CREATOR.fromBundle(ratingBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for Rating", e);
      return;
    }
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_SESSION_SET_RATING,
        sendSessionResultWhenReady(
            (sessionImpl, controller, sequenceNum) ->
                sessionImpl.onSetRatingOnHandler(controller, mediaId, rating)));
  }

  @Override
  public void setRating(
      @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle ratingBundle) {
    if (caller == null || ratingBundle == null) {
      return;
    }
    Rating rating;
    try {
      rating = Rating.CREATOR.fromBundle(ratingBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for Rating", e);
      return;
    }
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_SESSION_SET_RATING,
        sendSessionResultWhenReady(
            (sessionImpl, controller, sequenceNum) ->
                sessionImpl.onSetRatingOnHandler(controller, rating)));
  }

  @Override
  public void setPlaybackSpeed(@Nullable IMediaController caller, int sequenceNumber, float speed) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_SPEED_AND_PITCH,
        sendSessionResultSuccess(player -> player.setPlaybackSpeed(speed)));
  }

  @Override
  public void setPlaybackParameters(
      @Nullable IMediaController caller, int sequenceNumber, Bundle playbackParametersBundle) {
    if (caller == null || playbackParametersBundle == null) {
      return;
    }
    PlaybackParameters playbackParameters =
        PlaybackParameters.CREATOR.fromBundle(playbackParametersBundle);
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_SPEED_AND_PITCH,
        sendSessionResultSuccess(player -> player.setPlaybackParameters(playbackParameters)));
  }

  @Override
  public void setMediaItem(
      @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle mediaItemBundle) {
    setMediaItemWithResetPosition(
        caller, sequenceNumber, mediaItemBundle, /* resetPosition= */ true);
  }

  @Override
  public void setMediaItemWithStartPosition(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable Bundle mediaItemBundle,
      long startPositionMs) {
    if (caller == null || mediaItemBundle == null) {
      return;
    }
    MediaItem mediaItem;
    try {
      mediaItem = MediaItem.CREATOR.fromBundle(mediaItemBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_MEDIA_ITEM,
        sendSessionResultWhenReady(
            handleMediaItemsWithStartPositionWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onSetMediaItemsOnHandler(
                        controller,
                        ImmutableList.of(mediaItem),
                        /* startIndex= */ 0,
                        startPositionMs),
                MediaUtils::setMediaItemsWithStartIndexAndPosition)));
  }

  @Override
  public void setMediaItemWithResetPosition(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable Bundle mediaItemBundle,
      boolean resetPosition) {
    if (caller == null || mediaItemBundle == null) {
      return;
    }
    MediaItem mediaItem;
    try {
      mediaItem = MediaItem.CREATOR.fromBundle(mediaItemBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_MEDIA_ITEM,
        sendSessionResultWhenReady(
            handleMediaItemsWithStartPositionWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onSetMediaItemsOnHandler(
                        controller,
                        ImmutableList.of(mediaItem),
                        resetPosition
                            ? C.INDEX_UNSET
                            : sessionImpl.getPlayerWrapper().getCurrentMediaItemIndex(),
                        resetPosition
                            ? C.TIME_UNSET
                            : sessionImpl.getPlayerWrapper().getCurrentPosition()),
                MediaUtils::setMediaItemsWithStartIndexAndPosition)));
  }

  @Override
  public void setMediaItems(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable IBinder mediaItemsRetriever) {
    setMediaItemsWithResetPosition(
        caller, sequenceNumber, mediaItemsRetriever, /* resetPosition= */ true);
  }

  @Override
  public void setMediaItemsWithResetPosition(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable IBinder mediaItemsRetriever,
      boolean resetPosition) {
    if (caller == null || mediaItemsRetriever == null) {
      return;
    }
    List<MediaItem> mediaItemList;
    try {
      mediaItemList =
          BundleableUtil.fromBundleList(
              MediaItem.CREATOR, BundleListRetriever.getList(mediaItemsRetriever));
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultWhenReady(
            handleMediaItemsWithStartPositionWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onSetMediaItemsOnHandler(
                        controller,
                        mediaItemList,
                        resetPosition
                            ? C.INDEX_UNSET
                            : sessionImpl.getPlayerWrapper().getCurrentMediaItemIndex(),
                        resetPosition
                            ? C.TIME_UNSET
                            : sessionImpl.getPlayerWrapper().getCurrentPosition()),
                MediaUtils::setMediaItemsWithStartIndexAndPosition)));
  }

  @Override
  public void setMediaItemsWithStartIndex(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable IBinder mediaItemsRetriever,
      int startIndex,
      long startPositionMs) {
    if (caller == null || mediaItemsRetriever == null) {
      return;
    }
    List<MediaItem> mediaItemList;
    try {
      mediaItemList =
          BundleableUtil.fromBundleList(
              MediaItem.CREATOR, BundleListRetriever.getList(mediaItemsRetriever));
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultWhenReady(
            handleMediaItemsWithStartPositionWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onSetMediaItemsOnHandler(
                        controller,
                        mediaItemList,
                        (startIndex == C.INDEX_UNSET)
                            ? sessionImpl.getPlayerWrapper().getCurrentMediaItemIndex()
                            : startIndex,
                        (startIndex == C.INDEX_UNSET)
                            ? sessionImpl.getPlayerWrapper().getCurrentPosition()
                            : startPositionMs),
                MediaUtils::setMediaItemsWithStartIndexAndPosition)));
  }

  @Override
  public void setPlaylistMetadata(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable Bundle playlistMetadataBundle) {
    if (caller == null || playlistMetadataBundle == null) {
      return;
    }
    MediaMetadata playlistMetadata;
    try {
      playlistMetadata = MediaMetadata.CREATOR.fromBundle(playlistMetadataBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaMetadata", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_MEDIA_ITEMS_METADATA,
        sendSessionResultSuccess(player -> player.setPlaylistMetadata(playlistMetadata)));
  }

  @Override
  public void addMediaItem(
      @Nullable IMediaController caller, int sequenceNumber, Bundle mediaItemBundle) {
    if (caller == null || mediaItemBundle == null) {
      return;
    }
    MediaItem mediaItem;
    try {
      mediaItem = MediaItem.CREATOR.fromBundle(mediaItemBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultWhenReady(
            handleMediaItemsWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
                (playerWrapper, controller, mediaItems) ->
                    playerWrapper.addMediaItems(mediaItems))));
  }

  @Override
  public void addMediaItemWithIndex(
      @Nullable IMediaController caller, int sequenceNumber, int index, Bundle mediaItemBundle) {
    if (caller == null || mediaItemBundle == null) {
      return;
    }
    MediaItem mediaItem;
    try {
      mediaItem = MediaItem.CREATOR.fromBundle(mediaItemBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultWhenReady(
            handleMediaItemsWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
                (player, controller, mediaItems) ->
                    player.addMediaItems(
                        maybeCorrectMediaItemIndex(controller, player, index), mediaItems))));
  }

  @Override
  public void addMediaItems(
      @Nullable IMediaController caller,
      int sequenceNumber,
      @Nullable IBinder mediaItemsRetriever) {
    if (caller == null || mediaItemsRetriever == null) {
      return;
    }
    List<MediaItem> mediaItems;
    try {
      mediaItems =
          BundleableUtil.fromBundleList(
              MediaItem.CREATOR, BundleListRetriever.getList(mediaItemsRetriever));
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultWhenReady(
            handleMediaItemsWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems),
                (playerWrapper, controller, items) -> playerWrapper.addMediaItems(items))));
  }

  @Override
  public void addMediaItemsWithIndex(
      @Nullable IMediaController caller,
      int sequenceNumber,
      int index,
      @Nullable IBinder mediaItemsRetriever) {
    if (caller == null || mediaItemsRetriever == null) {
      return;
    }
    List<MediaItem> mediaItems;
    try {
      mediaItems =
          BundleableUtil.fromBundleList(
              MediaItem.CREATOR, BundleListRetriever.getList(mediaItemsRetriever));
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultWhenReady(
            handleMediaItemsWhenReady(
                (sessionImpl, controller, sequenceNum) ->
                    sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems),
                (player, controller, items) ->
                    player.addMediaItems(
                        maybeCorrectMediaItemIndex(controller, player, index), items))));
  }

  @Override
  public void removeMediaItem(@Nullable IMediaController caller, int sequenceNumber, int index) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultSuccess(
            (player, controller) ->
                player.removeMediaItem(maybeCorrectMediaItemIndex(controller, player, index))));
  }

  @Override
  public void removeMediaItems(
      @Nullable IMediaController caller, int sequenceNumber, int fromIndex, int toIndex) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultSuccess(
            (player, controller) ->
                player.removeMediaItems(
                    maybeCorrectMediaItemIndex(controller, player, fromIndex),
                    maybeCorrectMediaItemIndex(controller, player, toIndex))));
  }

  @Override
  public void clearMediaItems(@Nullable IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultSuccess(Player::clearMediaItems));
  }

  @Override
  public void moveMediaItem(
      @Nullable IMediaController caller, int sequenceNumber, int currentIndex, int newIndex) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultSuccess(player -> player.moveMediaItem(currentIndex, newIndex)));
  }

  @Override
  public void moveMediaItems(
      @Nullable IMediaController caller,
      int sequenceNumber,
      int fromIndex,
      int toIndex,
      int newIndex) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_CHANGE_MEDIA_ITEMS,
        sendSessionResultSuccess(player -> player.moveMediaItems(fromIndex, toIndex, newIndex)));
  }

  @Override
  public void seekToPreviousMediaItem(@Nullable IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
        sendSessionResultSuccess(Player::seekToPreviousMediaItem));
  }

  @Override
  public void seekToNextMediaItem(@Nullable IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
        sendSessionResultSuccess(Player::seekToNextMediaItem));
  }

  @Override
  public void seekToPrevious(@Nullable IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SEEK_TO_PREVIOUS,
        sendSessionResultSuccess(Player::seekToPrevious));
  }

  @Override
  public void seekToNext(@Nullable IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller, sequenceNumber, COMMAND_SEEK_TO_NEXT, sendSessionResultSuccess(Player::seekToNext));
  }

  @Override
  public void setRepeatMode(
      @Nullable IMediaController caller, int sequenceNumber, @Player.RepeatMode int repeatMode) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_REPEAT_MODE,
        sendSessionResultSuccess(player -> player.setRepeatMode(repeatMode)));
  }

  @Override
  public void setShuffleModeEnabled(
      @Nullable IMediaController caller, int sequenceNumber, boolean shuffleModeEnabled) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_SHUFFLE_MODE,
        sendSessionResultSuccess(player -> player.setShuffleModeEnabled(shuffleModeEnabled)));
  }

  @Override
  public void setVideoSurface(
      @Nullable IMediaController caller, int sequenceNumber, @Nullable Surface surface) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_VIDEO_SURFACE,
        sendSessionResultSuccess(player -> player.setVideoSurface(surface)));
  }

  @Override
  public void setVolume(@Nullable IMediaController caller, int sequenceNumber, float volume) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_VOLUME,
        sendSessionResultSuccess(player -> player.setVolume(volume)));
  }

  @Override
  public void setDeviceVolume(@Nullable IMediaController caller, int sequenceNumber, int volume) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_DEVICE_VOLUME,
        sendSessionResultSuccess(player -> player.setDeviceVolume(volume)));
  }

  @Override
  public void increaseDeviceVolume(@Nullable IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_ADJUST_DEVICE_VOLUME,
        sendSessionResultSuccess(Player::increaseDeviceVolume));
  }

  @Override
  public void decreaseDeviceVolume(@Nullable IMediaController caller, int sequenceNumber) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_ADJUST_DEVICE_VOLUME,
        sendSessionResultSuccess(Player::decreaseDeviceVolume));
  }

  @Override
  public void setDeviceMuted(@Nullable IMediaController caller, int sequenceNumber, boolean muted) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_ADJUST_DEVICE_VOLUME,
        sendSessionResultSuccess(player -> player.setDeviceMuted(muted)));
  }

  @Override
  public void setPlayWhenReady(
      @Nullable IMediaController caller, int sequenceNumber, boolean playWhenReady) {
    if (caller == null) {
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_PLAY_PAUSE,
        sendSessionResultSuccess(player -> player.setPlayWhenReady(playWhenReady)));
  }

  @Override
  public void flushCommandQueue(@Nullable IMediaController caller) {
    if (caller == null) {
      return;
    }
    long token = Binder.clearCallingIdentity();
    try {
      @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
      if (sessionImpl == null || sessionImpl.isReleased()) {
        return;
      }
      ControllerInfo controllerInfo = connectedControllersManager.getController(caller.asBinder());
      if (controllerInfo != null) {
        postOrRun(
            sessionImpl.getApplicationHandler(),
            () -> connectedControllersManager.flushCommandQueue(controllerInfo));
      }
    } finally {
      Binder.restoreCallingIdentity(token);
    }
  }

  @Override
  public void setTrackSelectionParameters(
      @Nullable IMediaController caller, int sequenceNumber, Bundle trackSelectionParametersBundle)
      throws RemoteException {
    if (caller == null) {
      return;
    }
    TrackSelectionParameters trackSelectionParameters;
    try {
      trackSelectionParameters =
          TrackSelectionParameters.fromBundle(trackSelectionParametersBundle);
    } catch (RuntimeException e) {
      Log.w(TAG, "Ignoring malformed Bundle for TrackSelectionParameters", e);
      return;
    }
    queueSessionTaskWithPlayerCommand(
        caller,
        sequenceNumber,
        COMMAND_SET_TRACK_SELECTION_PARAMETERS,
        sendSessionResultSuccess(
            player -> player.setTrackSelectionParameters(trackSelectionParameters)));
  }

  //////////////////////////////////////////////////////////////////////////////////////////////
  // AIDL methods for LibrarySession overrides
  //////////////////////////////////////////////////////////////////////////////////////////////

  @Override
  public void getLibraryRoot(
      @Nullable IMediaController caller, int sequenceNumber, @Nullable Bundle libraryParamsBundle)
      throws RuntimeException {
    if (caller == null) {
      return;
    }
    @Nullable
    LibraryParams libraryParams =
        libraryParamsBundle == null ? null : LibraryParams.CREATOR.fromBundle(libraryParamsBundle);
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT,
        sendLibraryResultWhenReady(
            (librarySessionImpl, controller, sequenceNum) ->
                librarySessionImpl.onGetLibraryRootOnHandler(controller, libraryParams)));
  }

  @Override
  public void getItem(
      @Nullable IMediaController caller, int sequenceNumber, @Nullable String mediaId)
      throws RuntimeException {
    if (caller == null) {
      return;
    }
    if (TextUtils.isEmpty(mediaId)) {
      Log.w(TAG, "getItem(): Ignoring empty mediaId");
      return;
    }
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_LIBRARY_GET_ITEM,
        sendLibraryResultWhenReady(
            (librarySessionImpl, controller, sequenceNum) ->
                librarySessionImpl.onGetItemOnHandler(controller, mediaId)));
  }

  @Override
  public void getChildren(
      @Nullable IMediaController caller,
      int sequenceNumber,
      String parentId,
      int page,
      int pageSize,
      @Nullable Bundle libraryParamsBundle)
      throws RuntimeException {
    if (caller == null) {
      return;
    }
    if (TextUtils.isEmpty(parentId)) {
      Log.w(TAG, "getChildren(): Ignoring empty parentId");
      return;
    }
    if (page < 0) {
      Log.w(TAG, "getChildren(): Ignoring negative page");
      return;
    }
    if (pageSize < 1) {
      Log.w(TAG, "getChildren(): Ignoring pageSize less than 1");
      return;
    }
    @Nullable
    LibraryParams libraryParams =
        libraryParamsBundle == null ? null : LibraryParams.CREATOR.fromBundle(libraryParamsBundle);
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_LIBRARY_GET_CHILDREN,
        sendLibraryResultWhenReady(
            (librarySessionImpl, controller, sequenceNum) ->
                librarySessionImpl.onGetChildrenOnHandler(
                    controller, parentId, page, pageSize, libraryParams)));
  }

  @Override
  public void search(
      @Nullable IMediaController caller,
      int sequenceNumber,
      String query,
      @Nullable Bundle libraryParamsBundle) {
    if (caller == null) {
      return;
    }
    if (TextUtils.isEmpty(query)) {
      Log.w(TAG, "search(): Ignoring empty query");
      return;
    }
    @Nullable
    LibraryParams libraryParams =
        libraryParamsBundle == null ? null : LibraryParams.CREATOR.fromBundle(libraryParamsBundle);
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_LIBRARY_SEARCH,
        sendLibraryResultWhenReady(
            (librarySessionImpl, controller, sequenceNum) ->
                librarySessionImpl.onSearchOnHandler(controller, query, libraryParams)));
  }

  @Override
  public void getSearchResult(
      @Nullable IMediaController caller,
      int sequenceNumber,
      String query,
      int page,
      int pageSize,
      @Nullable Bundle libraryParamsBundle) {
    if (caller == null) {
      return;
    }
    if (TextUtils.isEmpty(query)) {
      Log.w(TAG, "getSearchResult(): Ignoring empty query");
      return;
    }
    if (page < 0) {
      Log.w(TAG, "getSearchResult(): Ignoring negative page");
      return;
    }
    if (pageSize < 1) {
      Log.w(TAG, "getSearchResult(): Ignoring pageSize less than 1");
      return;
    }
    @Nullable
    LibraryParams libraryParams =
        libraryParamsBundle == null ? null : LibraryParams.CREATOR.fromBundle(libraryParamsBundle);
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT,
        sendLibraryResultWhenReady(
            (librarySessionImpl, controller, sequenceNum) ->
                librarySessionImpl.onGetSearchResultOnHandler(
                    controller, query, page, pageSize, libraryParams)));
  }

  @Override
  public void subscribe(
      @Nullable IMediaController caller,
      int sequenceNumber,
      String parentId,
      @Nullable Bundle libraryParamsBundle) {
    if (caller == null) {
      return;
    }
    if (TextUtils.isEmpty(parentId)) {
      Log.w(TAG, "subscribe(): Ignoring empty parentId");
      return;
    }
    @Nullable
    LibraryParams libraryParams =
        libraryParamsBundle == null ? null : LibraryParams.CREATOR.fromBundle(libraryParamsBundle);
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_LIBRARY_SUBSCRIBE,
        sendLibraryResultWhenReady(
            (librarySessionImpl, controller, sequenceNum) ->
                librarySessionImpl.onSubscribeOnHandler(controller, parentId, libraryParams)));
  }

  @Override
  public void unsubscribe(@Nullable IMediaController caller, int sequenceNumber, String parentId) {
    if (caller == null) {
      return;
    }
    if (TextUtils.isEmpty(parentId)) {
      Log.w(TAG, "unsubscribe(): Ignoring empty parentId");
      return;
    }
    dispatchSessionTaskWithSessionCommand(
        caller,
        sequenceNumber,
        COMMAND_CODE_LIBRARY_UNSUBSCRIBE,
        sendLibraryResultWhenReady(
            (librarySessionImpl, controller, sequenceNum) ->
                librarySessionImpl.onUnsubscribeOnHandler(controller, parentId)));
  }

  /** Common interface for code snippets to handle all incoming commands from the controller. */
  private interface SessionTask<T, K extends MediaSessionImpl> {
    T run(K sessionImpl, ControllerInfo controller, int sequenceNumber);
  }

  private interface MediaItemPlayerTask {
    void run(PlayerWrapper player, ControllerInfo controller, List<MediaItem> mediaItems);
  }

  private interface ControllerPlayerTask {
    void run(PlayerWrapper player, ControllerInfo controller);
  }

  private interface MediaItemsWithStartPositionPlayerTask {
    void run(PlayerWrapper player, MediaItemsWithStartPosition mediaItemsWithStartPosition);
  }

  /* package */ static final class Controller2Cb implements ControllerCb {

    private final IMediaController iController;

    public Controller2Cb(IMediaController callback) {
      iController = callback;
    }

    public IBinder getCallbackBinder() {
      return iController.asBinder();
    }

    @Override
    public void onSessionResult(int sequenceNumber, SessionResult result) throws RemoteException {
      iController.onSessionResult(sequenceNumber, result.toBundle());
    }

    @Override
    public void onLibraryResult(int sequenceNumber, LibraryResult<?> result)
        throws RemoteException {
      iController.onLibraryResult(sequenceNumber, result.toBundle());
    }

    @Override
    public void onPlayerInfoChanged(
        int sequenceNumber,
        PlayerInfo playerInfo,
        Player.Commands availableCommands,
        boolean excludeTimeline,
        boolean excludeTracks,
        int controllerInterfaceVersion)
        throws RemoteException {
      Assertions.checkState(controllerInterfaceVersion != 0);
      // The bundling exclusions merge the performance overrides with the available commands.
      boolean bundlingExclusionsTimeline =
          excludeTimeline || !availableCommands.contains(Player.COMMAND_GET_TIMELINE);
      boolean bundlingExclusionsTracks =
          excludeTracks || !availableCommands.contains(Player.COMMAND_GET_TRACKS);
      if (controllerInterfaceVersion >= 2) {
        iController.onPlayerInfoChangedWithExclusions(
            sequenceNumber,
            playerInfo.toBundle(availableCommands, excludeTimeline, excludeTracks),
            new PlayerInfo.BundlingExclusions(bundlingExclusionsTimeline, bundlingExclusionsTracks)
                .toBundle());
      } else {
        //noinspection deprecation
        iController.onPlayerInfoChanged(
            sequenceNumber,
            playerInfo.toBundle(availableCommands, excludeTimeline, /* excludeTracks= */ true),
            bundlingExclusionsTimeline);
      }
    }

    @Override
    public void setCustomLayout(int sequenceNumber, List<CommandButton> layout)
        throws RemoteException {
      iController.onSetCustomLayout(sequenceNumber, BundleableUtil.toBundleList(layout));
    }

    @Override
    public void onAvailableCommandsChangedFromSession(
        int sequenceNumber, SessionCommands sessionCommands, Player.Commands playerCommands)
        throws RemoteException {
      iController.onAvailableCommandsChangedFromSession(
          sequenceNumber, sessionCommands.toBundle(), playerCommands.toBundle());
    }

    @Override
    public void onAvailableCommandsChangedFromPlayer(
        int sequenceNumber, Player.Commands availableCommands) throws RemoteException {
      iController.onAvailableCommandsChangedFromPlayer(
          sequenceNumber, availableCommands.toBundle());
    }

    @Override
    public void sendCustomCommand(int sequenceNumber, SessionCommand command, Bundle args)
        throws RemoteException {
      iController.onCustomCommand(sequenceNumber, command.toBundle(), args);
    }

    @SuppressWarnings("nullness:argument") // params can be null.
    @Override
    public void onChildrenChanged(
        int sequenceNumber, String parentId, int itemCount, @Nullable LibraryParams params)
        throws RemoteException {
      iController.onChildrenChanged(
          sequenceNumber, parentId, itemCount, params == null ? null : params.toBundle());
    }

    @SuppressWarnings("nullness:argument") // params can be null.
    @Override
    public void onSearchResultChanged(
        int sequenceNumber, String query, int itemCount, @Nullable LibraryParams params)
        throws RemoteException {
      iController.onSearchResultChanged(
          sequenceNumber, query, itemCount, params == null ? null : params.toBundle());
    }

    @Override
    public void onDisconnected(int sequenceNumber) throws RemoteException {
      iController.onDisconnected(sequenceNumber);
    }

    @Override
    public void onPeriodicSessionPositionInfoChanged(
        int sequenceNumber,
        SessionPositionInfo sessionPositionInfo,
        boolean canAccessCurrentMediaItem,
        boolean canAccessTimeline)
        throws RemoteException {
      iController.onPeriodicSessionPositionInfoChanged(
          sequenceNumber,
          sessionPositionInfo.toBundle(canAccessCurrentMediaItem, canAccessTimeline));
    }

    @Override
    public void onRenderedFirstFrame(int sequenceNumber) throws RemoteException {
      iController.onRenderedFirstFrame(sequenceNumber);
    }

    @Override
    public void onSessionExtrasChanged(int sequenceNumber, Bundle sessionExtras)
        throws RemoteException {
      iController.onExtrasChanged(sequenceNumber, sessionExtras);
    }

    @Override
    public int hashCode() {
      return ObjectsCompat.hash(getCallbackBinder());
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null || obj.getClass() != Controller2Cb.class) {
        return false;
      }
      Controller2Cb other = (Controller2Cb) obj;
      return Util.areEqual(getCallbackBinder(), other.getCallbackBinder());
    }
  }
}