content: /*
   * Copyright (C) 2014 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 com.android.server.media;
  
  import android.app.PendingIntent;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.ParceledListSlice;
  import android.media.AudioAttributes;
  import android.media.AudioManager;
  import android.media.AudioManagerInternal;
  import android.media.AudioSystem;
  import android.media.MediaMetadata;
  import android.media.Rating;
  import android.media.VolumeProvider;
  import android.media.session.ISession;
  import android.media.session.ISessionCallback;
  import android.media.session.ISessionController;
  import android.media.session.ISessionControllerCallback;
  import android.media.session.MediaController;
  import android.media.session.MediaController.PlaybackInfo;
  import android.media.session.MediaSession;
  import android.media.session.MediaSession.QueueItem;
  import android.media.session.PlaybackState;
  import android.net.Uri;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.DeadObjectException;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Process;
  import android.os.RemoteException;
  import android.os.ResultReceiver;
  import android.os.SystemClock;
  import android.util.Log;
  import android.util.Slog;
  import android.view.KeyEvent;
  
  import com.android.server.LocalServices;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * This is the system implementation of a Session. Apps will interact with the
   * MediaSession wrapper class instead.
   */
  public class MediaSessionRecord implements IBinder.DeathRecipient {
      private static final String TAG = "MediaSessionRecord";
      private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
  
      /**
       * The amount of time we'll send an assumed volume after the last volume
       * command before reverting to the last reported volume.
       */
      private static final int OPTIMISTIC_VOLUME_TIMEOUT = 1000;
  
      private final MessageHandler mHandler;
  
      private final int mOwnerPid;
      private final int mOwnerUid;
      private final int mUserId;
      private final String mPackageName;
      private final String mTag;
      private final Bundle mSessionInfo;
      private final ControllerStub mController;
      private final MediaSession.Token mSessionToken;
      private final SessionStub mSession;
      private final SessionCb mSessionCb;
      private final MediaSessionService mService;
      private final Context mContext;
  
      private final Object mLock = new Object();
      private final ArrayList<ISessionControllerCallbackHolder> mControllerCallbackHolders =
              new ArrayList<>();
  
      private long mFlags;
      private PendingIntent mMediaButtonReceiver;
      private PendingIntent mLaunchIntent;
  
      // TransportPerformer fields
      private Bundle mExtras;
      // Note: Avoid unparceling the bundle inside MediaMetadata since unparceling in system process
      // may result in throwing an exception.
      private MediaMetadata mMetadata;
      private PlaybackState mPlaybackState;
      private List<QueueItem> mQueue;
      private CharSequence mQueueTitle;
      private int mRatingType;
      // End TransportPerformer fields
  
      // Volume handling fields
      private AudioAttributes mAudioAttrs;
      private AudioManager mAudioManager;
      private AudioManagerInternal mAudioManagerInternal;
      private int mVolumeType = PlaybackInfo.PLAYBACK_TYPE_LOCAL;
      private int mVolumeControlType = VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
      private int mMaxVolume = 0;
      private int mCurrentVolume = 0;
      private int mOptimisticVolume = -1;
      // End volume handling fields
  
      private boolean mIsActive = false;
      private boolean mDestroyed = false;
  
      private long mDuration = -1;
      private String mMetadataDescription;
  
      public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName,
              ISessionCallback cb, String tag, Bundle sessionInfo,
              MediaSessionService service, Looper handlerLooper) {
          mOwnerPid = ownerPid;
          mOwnerUid = ownerUid;
          mUserId = userId;
          mPackageName = ownerPackageName;
          mTag = tag;
          mSessionInfo = sessionInfo;
          mController = new ControllerStub();
          mSessionToken = new MediaSession.Token(mController);
          mSession = new SessionStub();
          mSessionCb = new SessionCb(cb);
          mService = service;
          mContext = mService.getContext();
          mHandler = new MessageHandler(handlerLooper);
          mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
          mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
          mAudioAttrs = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
      }
  
      /**
       * Get the session binder for the {@link MediaSession}.
       *
       * @return The session binder apps talk to.
       */
      public ISession getSessionBinder() {
          return mSession;
      }
  
      /**
       * Get the controller binder for the {@link MediaController}.
       *
       * @return The controller binder apps talk to.
       */
      public ISessionController getControllerBinder() {
          return mController;
      }
  
      /**
       * Get the session token for creating {@link MediaController}.
       *
       * @return The session token.
       */
      public MediaSession.Token getSessionToken() {
          return mSessionToken;
      }
  
      /**
       * Get the info for this session.
       *
       * @return Info that identifies this session.
       */
      public String getPackageName() {
          return mPackageName;
      }
  
      /**
       * Get the tag for the session.
       *
       * @return The session's tag.
       */
      public String getTag() {
          return mTag;
      }
  
      /**
       * Get the intent the app set for their media button receiver.
       *
       * @return The pending intent set by the app or null.
       */
      public PendingIntent getMediaButtonReceiver() {
          return mMediaButtonReceiver;
      }
  
      /**
       * Get this session's flags.
       *
       * @return The flags for this session.
       */
      public long getFlags() {
          return mFlags;
      }
  
      /**
       * Check if this session has the specified flag.
       *
       * @param flag The flag to check.
       * @return True if this session has that flag set, false otherwise.
       */
      public boolean hasFlag(int flag) {
          return (mFlags & flag) != 0;
      }
  
      /**
       * Get the UID this session was created for.
       *
       * @return The UID for this session.
       */
      public int getUid() {
          return mOwnerUid;
      }
  
      /**
       * Get the user id this session was created for.
       *
       * @return The user id for this session.
       */
      public int getUserId() {
          return mUserId;
      }
  
      /**
       * Check if this session has system priorty and should receive media buttons
       * before any other sessions.
       *
       * @return True if this is a system priority session, false otherwise
       */
      public boolean isSystemPriority() {
          return (mFlags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0;
      }
  
      /**
       * Send a volume adjustment to the session owner. Direction must be one of
       * {@link AudioManager#ADJUST_LOWER}, {@link AudioManager#ADJUST_RAISE},
       * {@link AudioManager#ADJUST_SAME}.
       *
       * @param packageName The package that made the original volume request.
       * @param opPackageName The op package that made the original volume request.
       * @param pid The pid that made the original volume request.
       * @param uid The uid that made the original volume request.
       * @param caller caller binder. can be {@code null} if it's from the volume key.
       * @param asSystemService {@code true} if the event sent to the session as if it was come from
       *          the system service instead of the app process. This helps sessions to distinguish
       *          between the key injection by the app and key events from the hardware devices.
       *          Should be used only when the volume key events aren't handled by foreground
       *          activity. {@code false} otherwise to tell session about the real caller.
       * @param direction The direction to adjust volume in.
       * @param flags Any of the flags from {@link AudioManager}.
       * @param useSuggested True to use adjustSuggestedStreamVolume instead of
       */
      public void adjustVolume(String packageName, String opPackageName, int pid, int uid,
              ISessionControllerCallback caller, boolean asSystemService, int direction, int flags,
              boolean useSuggested) {
          int previousFlagPlaySound = flags & AudioManager.FLAG_PLAY_SOUND;
          if (isPlaybackActive() || hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) {
              flags &= ~AudioManager.FLAG_PLAY_SOUND;
          }
          if (mVolumeType == PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
              // Adjust the volume with a handler not to be blocked by other system service.
              int stream = AudioAttributes.toLegacyStreamType(mAudioAttrs);
              postAdjustLocalVolume(stream, direction, flags, opPackageName, pid, uid,
                      asSystemService, useSuggested, previousFlagPlaySound);
          } else {
              if (mVolumeControlType == VolumeProvider.VOLUME_CONTROL_FIXED) {
                  // Nothing to do, the volume cannot be changed
                  return;
              }
              if (direction == AudioManager.ADJUST_TOGGLE_MUTE
                      || direction == AudioManager.ADJUST_MUTE
                      || direction == AudioManager.ADJUST_UNMUTE) {
                  Log.w(TAG, "Muting remote playback is not supported");
                  return;
              }
              if (DEBUG) {
                  Log.w(TAG, "adjusting volume, pkg=" + packageName + ", asSystemService="
                          + asSystemService + ", dir=" + direction);
              }
              mSessionCb.adjustVolume(packageName, pid, uid, caller, asSystemService, direction);
  
              int volumeBefore = (mOptimisticVolume < 0 ? mCurrentVolume : mOptimisticVolume);
              mOptimisticVolume = volumeBefore + direction;
              mOptimisticVolume = Math.max(0, Math.min(mOptimisticVolume, mMaxVolume));
              mHandler.removeCallbacks(mClearOptimisticVolumeRunnable);
              mHandler.postDelayed(mClearOptimisticVolumeRunnable, OPTIMISTIC_VOLUME_TIMEOUT);
              if (volumeBefore != mOptimisticVolume) {
                  pushVolumeUpdate();
              }
              mService.notifyRemoteVolumeChanged(flags, this);
  
              if (DEBUG) {
                  Log.d(TAG, "Adjusted optimistic volume to " + mOptimisticVolume + " max is "
                          + mMaxVolume);
              }
          }
      }
  
      private void setVolumeTo(String packageName, String opPackageName, int pid, int uid,
              ISessionControllerCallback caller, int value, int flags) {
          if (mVolumeType == PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
              int stream = AudioAttributes.toLegacyStreamType(mAudioAttrs);
              final int volumeValue = value;
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      try {
                          mAudioManagerInternal.setStreamVolumeForUid(stream, volumeValue, flags,
                                  opPackageName, uid);
                      } catch (IllegalArgumentException | SecurityException e) {
                          Log.e(TAG, "Cannot set volume: stream=" + stream + ", value=" + volumeValue
                                  + ", flags=" + flags, e);
                      }
                  }
              });
          } else {
              if (mVolumeControlType != VolumeProvider.VOLUME_CONTROL_ABSOLUTE) {
                  // Nothing to do. The volume can't be set directly.
                  return;
              }
              value = Math.max(0, Math.min(value, mMaxVolume));
              mSessionCb.setVolumeTo(packageName, pid, uid, caller, value);
  
              int volumeBefore = (mOptimisticVolume < 0 ? mCurrentVolume : mOptimisticVolume);
              mOptimisticVolume = Math.max(0, Math.min(value, mMaxVolume));
              mHandler.removeCallbacks(mClearOptimisticVolumeRunnable);
              mHandler.postDelayed(mClearOptimisticVolumeRunnable, OPTIMISTIC_VOLUME_TIMEOUT);
              if (volumeBefore != mOptimisticVolume) {
                  pushVolumeUpdate();
              }
              mService.notifyRemoteVolumeChanged(flags, this);
  
              if (DEBUG) {
                  Log.d(TAG, "Set optimistic volume to " + mOptimisticVolume + " max is "
                          + mMaxVolume);
              }
          }
      }
  
      /**
       * Check if this session has been set to active by the app.
       *
       * @return True if the session is active, false otherwise.
       */
      public boolean isActive() {
          return mIsActive && !mDestroyed;
      }
  
      /**
       * Get the playback state.
       *
       * @return The current playback state.
       */
      public PlaybackState getPlaybackState() {
          return mPlaybackState;
      }
  
      /**
       * Check if the session is currently performing playback.
       *
       * @return True if the session is performing playback, false otherwise.
       */
      public boolean isPlaybackActive() {
          int state = mPlaybackState == null ? PlaybackState.STATE_NONE : mPlaybackState.getState();
          return MediaSession.isActiveState(state);
      }
  
      /**
       * Get the type of playback, either local or remote.
       *
       * @return The current type of playback.
       */
      public int getPlaybackType() {
          return mVolumeType;
      }
  
      /**
       * Get the local audio stream being used. Only valid if playback type is
       * local.
       *
       * @return The audio stream the session is using.
       */
      public AudioAttributes getAudioAttributes() {
          return mAudioAttrs;
      }
  
      /**
       * Get the type of volume control. Only valid if playback type is remote.
       *
       * @return The volume control type being used.
       */
      public int getVolumeControl() {
          return mVolumeControlType;
      }
  
      /**
       * Get the max volume that can be set. Only valid if playback type is
       * remote.
       *
       * @return The max volume that can be set.
       */
      public int getMaxVolume() {
          return mMaxVolume;
      }
  
      /**
       * Get the current volume for this session. Only valid if playback type is
       * remote.
       *
       * @return The current volume of the remote playback.
       */
      public int getCurrentVolume() {
          return mCurrentVolume;
      }
  
      /**
       * Get the volume we'd like it to be set to. This is only valid for a short
       * while after a call to adjust or set volume.
       *
       * @return The current optimistic volume or -1.
       */
      public int getOptimisticVolume() {
          return mOptimisticVolume;
      }
  
      public boolean isTransportControlEnabled() {
          return hasFlag(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
      }
  
      @Override
      public void binderDied() {
          mService.sessionDied(this);
      }
  
      /**
       * Finish cleaning up this session, including disconnecting if connected and
       * removing the death observer from the callback binder.
       */
      public void onDestroy() {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              mDestroyed = true;
              mHandler.post(MessageHandler.MSG_DESTROYED);
          }
      }
  
      public ISessionCallback getCallback() {
          return mSessionCb.mCb;
      }
  
      /**
       * Sends media button.
       *
       * @param packageName caller package name
       * @param pid caller pid
       * @param uid caller uid
       * @param asSystemService {@code true} if the event sent to the session as if it was come from
       *          the system service instead of the app process.
       * @param ke key events
       * @param sequenceId (optional) sequence id. Use this only when a wake lock is needed.
       * @param cb (optional) result receiver to receive callback. Use this only when a wake lock is
       *           needed.
       * @return {@code true} if the attempt to send media button was successfuly.
       *         {@code false} otherwise.
       */
      public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService,
              KeyEvent ke, int sequenceId, ResultReceiver cb) {
          return mSessionCb.sendMediaButton(packageName, pid, uid, asSystemService, ke, sequenceId,
                  cb);
      }
  
      public void dump(PrintWriter pw, String prefix) {
          pw.println(prefix + mTag + " " + this);
  
          final String indent = prefix + "  ";
          pw.println(indent + "ownerPid=" + mOwnerPid + ", ownerUid=" + mOwnerUid
                  + ", userId=" + mUserId);
          pw.println(indent + "package=" + mPackageName);
          pw.println(indent + "launchIntent=" + mLaunchIntent);
          pw.println(indent + "mediaButtonReceiver=" + mMediaButtonReceiver);
          pw.println(indent + "active=" + mIsActive);
          pw.println(indent + "flags=" + mFlags);
          pw.println(indent + "rating type=" + mRatingType);
          pw.println(indent + "controllers: " + mControllerCallbackHolders.size());
          pw.println(indent + "state=" + (mPlaybackState == null ? null : mPlaybackState.toString()));
          pw.println(indent + "audioAttrs=" + mAudioAttrs);
          pw.println(indent + "volumeType=" + mVolumeType + ", controlType=" + mVolumeControlType
                  + ", max=" + mMaxVolume + ", current=" + mCurrentVolume);
          pw.println(indent + "metadata: " + mMetadataDescription);
          pw.println(indent + "queueTitle=" + mQueueTitle + ", size="
                  + (mQueue == null ? 0 : mQueue.size()));
      }
  
      @Override
      public String toString() {
          return mPackageName + "/" + mTag + " (userId=" + mUserId + ")";
      }
  
      private void postAdjustLocalVolume(final int stream, final int direction, final int flags,
              final String callingOpPackageName, final int callingPid, final int callingUid,
              final boolean asSystemService, final boolean useSuggested,
              final int previousFlagPlaySound) {
          if (DEBUG) {
              Log.w(TAG, "adjusting local volume, stream=" + stream + ", dir=" + direction
                      + ", asSystemService=" + asSystemService + ", useSuggested=" + useSuggested);
          }
          // Must use opPackageName for adjusting volumes with UID.
          final String opPackageName;
          final int uid;
          if (asSystemService) {
              opPackageName = mContext.getOpPackageName();
              uid = Process.SYSTEM_UID;
          } else {
              opPackageName = callingOpPackageName;
              uid = callingUid;
          }
          mHandler.post(new Runnable() {
              @Override
              public void run() {
                  try {
                      if (useSuggested) {
                          if (AudioSystem.isStreamActive(stream, 0)) {
                              mAudioManagerInternal.adjustSuggestedStreamVolumeForUid(stream,
                                      direction, flags, opPackageName, uid);
                          } else {
                              mAudioManagerInternal.adjustSuggestedStreamVolumeForUid(
                                      AudioManager.USE_DEFAULT_STREAM_TYPE, direction,
                                      flags | previousFlagPlaySound, opPackageName, uid);
                          }
                      } else {
                          mAudioManagerInternal.adjustStreamVolumeForUid(stream, direction, flags,
                                  opPackageName, uid);
                      }
                  } catch (IllegalArgumentException | SecurityException e) {
                      Log.e(TAG, "Cannot adjust volume: direction=" + direction + ", stream="
                              + stream + ", flags=" + flags + ", opPackageName=" + opPackageName
                              + ", uid=" + uid + ", useSuggested=" + useSuggested
                              + ", previousFlagPlaySound=" + previousFlagPlaySound, e);
                  }
              }
          });
      }
  
      private void logCallbackException(
              String msg, ISessionControllerCallbackHolder holder, Exception e) {
          Log.v(TAG, msg + ", this=" + this + ", callback package=" + holder.mPackageName
                  + ", exception=" + e);
      }
  
      private void pushPlaybackStateUpdate() {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onPlaybackStateChanged(mPlaybackState);
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushPlaybackStateUpdate",
                              holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushPlaybackStateUpdate",
                              holder, e);
                  }
              }
          }
      }
  
      private void pushMetadataUpdate() {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onMetadataChanged(mMetadata);
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushMetadataUpdate", holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushMetadataUpdate", holder, e);
                  }
              }
          }
      }
  
      private void pushQueueUpdate() {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onQueueChanged(mQueue == null ? null :
                              new ParceledListSlice<>(mQueue));
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushQueueUpdate", holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushQueueUpdate", holder, e);
                  }
              }
          }
      }
  
      private void pushQueueTitleUpdate() {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onQueueTitleChanged(mQueueTitle);
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushQueueTitleUpdate",
                              holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushQueueTitleUpdate", holder, e);
                  }
              }
          }
      }
  
      private void pushExtrasUpdate() {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onExtrasChanged(mExtras);
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushExtrasUpdate", holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushExtrasUpdate", holder, e);
                  }
              }
          }
      }
  
      private void pushVolumeUpdate() {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              PlaybackInfo info = getVolumeAttributes();
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onVolumeInfoChanged(info);
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushVolumeUpdate", holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushVolumeUpdate", holder, e);
                  }
              }
          }
      }
  
      private void pushEvent(String event, Bundle data) {
          synchronized (mLock) {
              if (mDestroyed) {
                  return;
              }
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onEvent(event, data);
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushEvent", holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushEvent", holder, e);
                  }
              }
          }
      }
  
      private void pushSessionDestroyed() {
          synchronized (mLock) {
              // This is the only method that may be (and can only be) called
              // after the session is destroyed.
              if (!mDestroyed) {
                  return;
              }
              for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
                  ISessionControllerCallbackHolder holder = mControllerCallbackHolders.get(i);
                  try {
                      holder.mCallback.onSessionDestroyed();
                  } catch (DeadObjectException e) {
                      mControllerCallbackHolders.remove(i);
                      logCallbackException("Removing dead callback in pushSessionDestroyed",
                              holder, e);
                  } catch (RemoteException e) {
                      logCallbackException("unexpected exception in pushSessionDestroyed", holder, e);
                  }
              }
              // After notifying clear all listeners
              mControllerCallbackHolders.clear();
          }
      }
  
      private PlaybackState getStateWithUpdatedPosition() {
          PlaybackState state;
          long duration;
          synchronized (mLock) {
              state = mPlaybackState;
              duration = mDuration;
          }
          PlaybackState result = null;
          if (state != null) {
              if (state.getState() == PlaybackState.STATE_PLAYING
                      || state.getState() == PlaybackState.STATE_FAST_FORWARDING
                      || state.getState() == PlaybackState.STATE_REWINDING) {
                  long updateTime = state.getLastPositionUpdateTime();
                  long currentTime = SystemClock.elapsedRealtime();
                  if (updateTime > 0) {
                      long position = (long) (state.getPlaybackSpeed()
                              * (currentTime - updateTime)) + state.getPosition();
                      if (duration >= 0 && position > duration) {
                          position = duration;
                      } else if (position < 0) {
                          position = 0;
                      }
                      PlaybackState.Builder builder = new PlaybackState.Builder(state);
                      builder.setState(state.getState(), position, state.getPlaybackSpeed(),
                              currentTime);
                      result = builder.build();
                  }
              }
          }
          return result == null ? state : result;
      }
  
      private int getControllerHolderIndexForCb(ISessionControllerCallback cb) {
          IBinder binder = cb.asBinder();
          for (int i = mControllerCallbackHolders.size() - 1; i >= 0; i--) {
              if (binder.equals(mControllerCallbackHolders.get(i).mCallback.asBinder())) {
                  return i;
              }
          }
          return -1;
      }
  
      private PlaybackInfo getVolumeAttributes() {
          int volumeType;
          AudioAttributes attributes;
          synchronized (mLock) {
              if (mVolumeType == PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
                  int current = mOptimisticVolume != -1 ? mOptimisticVolume : mCurrentVolume;
                  return new PlaybackInfo(mVolumeType, mVolumeControlType, mMaxVolume, current,
                          mAudioAttrs);
              }
              volumeType = mVolumeType;
              attributes = mAudioAttrs;
          }
          int stream = AudioAttributes.toLegacyStreamType(attributes);
          int max = mAudioManager.getStreamMaxVolume(stream);
          int current = mAudioManager.getStreamVolume(stream);
          return new PlaybackInfo(volumeType, VolumeProvider.VOLUME_CONTROL_ABSOLUTE, max,
                  current, attributes);
      }
  
      private final Runnable mClearOptimisticVolumeRunnable = new Runnable() {
          @Override
          public void run() {
              boolean needUpdate = (mOptimisticVolume != mCurrentVolume);
              mOptimisticVolume = -1;
              if (needUpdate) {
                  pushVolumeUpdate();
              }
          }
      };
  
      private final class SessionStub extends ISession.Stub {
          @Override
          public void destroySession() throws RemoteException {
              final long token = Binder.clearCallingIdentity();
              try {
                  mService.destroySession(MediaSessionRecord.this);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
          }
  
          @Override
          public void sendEvent(String event, Bundle data) throws RemoteException {
              mHandler.post(MessageHandler.MSG_SEND_EVENT, event,
                      data == null ? null : new Bundle(data));
          }
  
          @Override
          public ISessionController getController() throws RemoteException {
              return mController;
          }
  
          @Override
          public void setActive(boolean active) throws RemoteException {
              mIsActive = active;
              final long token = Binder.clearCallingIdentity();
              try {
                  mService.updateSession(MediaSessionRecord.this);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
              mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
          }
  
          @Override
          public void setFlags(int flags) throws RemoteException {
              if ((flags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
                  int pid = Binder.getCallingPid();
                  int uid = Binder.getCallingUid();
                  mService.enforcePhoneStatePermission(pid, uid);
              }
              mFlags = flags;
              if ((flags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
                  final long token = Binder.clearCallingIdentity();
                  try {
                      mService.setGlobalPrioritySession(MediaSessionRecord.this);
                  } finally {
                      Binder.restoreCallingIdentity(token);
                  }
              }
              mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
          }
  
          @Override
          public void setMediaButtonReceiver(PendingIntent pi) throws RemoteException {
              mMediaButtonReceiver = pi;
              final long token = Binder.clearCallingIdentity();
              try {
                  mService.onMediaButtonReceiverChanged(MediaSessionRecord.this);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
          }
  
          @Override
          public void setLaunchPendingIntent(PendingIntent pi) throws RemoteException {
              mLaunchIntent = pi;
          }
  
          @Override
          public void setMetadata(MediaMetadata metadata, long duration, String metadataDescription)
                  throws RemoteException {
              synchronized (mLock) {
                  MediaMetadata temp = metadata == null ? null : new MediaMetadata.Builder(metadata)
                          .build();
                  // This is to guarantee that the underlying bundle is unparceled
                  // before we set it to prevent concurrent reads from throwing an
                  // exception
                  if (temp != null) {
                      temp.size();
                  }
                  mMetadata = temp;
                  mDuration = duration;
                  mMetadataDescription = metadataDescription;
              }
              mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
          }
  
          @Override
          public void setPlaybackState(PlaybackState state) throws RemoteException {
              int oldState = mPlaybackState == null
                      ? PlaybackState.STATE_NONE : mPlaybackState.getState();
              int newState = state == null
                      ? PlaybackState.STATE_NONE : state.getState();
              synchronized (mLock) {
                  mPlaybackState = state;
              }
              final long token = Binder.clearCallingIdentity();
              try {
                  mService.onSessionPlaystateChanged(MediaSessionRecord.this, oldState, newState);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
              mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
          }
  
          @Override
          public void setQueue(ParceledListSlice queue) throws RemoteException {
              synchronized (mLock) {
                  mQueue = queue == null ? null : (List<QueueItem>) queue.getList();
              }
              mHandler.post(MessageHandler.MSG_UPDATE_QUEUE);
          }
  
          @Override
          public void setQueueTitle(CharSequence title) throws RemoteException {
              mQueueTitle = title;
              mHandler.post(MessageHandler.MSG_UPDATE_QUEUE_TITLE);
          }
  
          @Override
          public void setExtras(Bundle extras) throws RemoteException {
              synchronized (mLock) {
                  mExtras = extras == null ? null : new Bundle(extras);
              }
              mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS);
          }
  
          @Override
          public void setRatingType(int type) throws RemoteException {
              mRatingType = type;
          }
  
          @Override
          public void setCurrentVolume(int volume) throws RemoteException {
              mCurrentVolume = volume;
              mHandler.post(MessageHandler.MSG_UPDATE_VOLUME);
          }
  
          @Override
          public void setPlaybackToLocal(AudioAttributes attributes) throws RemoteException {
              boolean typeChanged;
              synchronized (mLock) {
                  typeChanged = mVolumeType == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
                  mVolumeType = PlaybackInfo.PLAYBACK_TYPE_LOCAL;
                  if (attributes != null) {
                      mAudioAttrs = attributes;
                  } else {
                      Log.e(TAG, "Received null audio attributes, using existing attributes");
                  }
              }
              if (typeChanged) {
                  final long token = Binder.clearCallingIdentity();
                  try {
                      mService.onSessionPlaybackTypeChanged(MediaSessionRecord.this);
                  } finally {
                      Binder.restoreCallingIdentity(token);
                  }
                  mHandler.post(MessageHandler.MSG_UPDATE_VOLUME);
              }
          }
  
          @Override
          public void setPlaybackToRemote(int control, int max) throws RemoteException {
              boolean typeChanged;
              synchronized (mLock) {
                  typeChanged = mVolumeType == PlaybackInfo.PLAYBACK_TYPE_LOCAL;
                  mVolumeType = PlaybackInfo.PLAYBACK_TYPE_REMOTE;
                  mVolumeControlType = control;
                  mMaxVolume = max;
              }
              if (typeChanged) {
                  final long token = Binder.clearCallingIdentity();
                  try {
                      mService.onSessionPlaybackTypeChanged(MediaSessionRecord.this);
                  } finally {
                      Binder.restoreCallingIdentity(token);
                  }
                  mHandler.post(MessageHandler.MSG_UPDATE_VOLUME);
              }
          }
      }
  
      class SessionCb {
          private final ISessionCallback mCb;
  
          SessionCb(ISessionCallback cb) {
              mCb = cb;
          }
  
          public boolean sendMediaButton(String packageName, int pid, int uid,
                  boolean asSystemService, KeyEvent keyEvent, int sequenceId, ResultReceiver cb) {
              try {
                  if (asSystemService) {
                      mCb.onMediaButton(mContext.getPackageName(), Process.myPid(),
                              Process.SYSTEM_UID, createMediaButtonIntent(keyEvent), sequenceId, cb);
                  } else {
                      mCb.onMediaButton(packageName, pid, uid,
                              createMediaButtonIntent(keyEvent), sequenceId, cb);
                  }
                  return true;
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
              }
              return false;
          }
  
          public boolean sendMediaButton(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, boolean asSystemService,
                  KeyEvent keyEvent) {
              try {
                  if (asSystemService) {
                      mCb.onMediaButton(mContext.getPackageName(), Process.myPid(),
                              Process.SYSTEM_UID, createMediaButtonIntent(keyEvent), 0, null);
                  } else {
                      mCb.onMediaButtonFromController(packageName, pid, uid, caller,
                              createMediaButtonIntent(keyEvent));
                  }
                  return true;
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
              }
              return false;
          }
  
          public void sendCommand(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, String command, Bundle args, ResultReceiver cb) {
              try {
                  mCb.onCommand(packageName, pid, uid, caller, command, args, cb);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in sendCommand.", e);
              }
          }
  
          public void sendCustomAction(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, String action,
                  Bundle args) {
              try {
                  mCb.onCustomAction(packageName, pid, uid, caller, action, args);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in sendCustomAction.", e);
              }
          }
  
          public void prepare(String packageName, int pid, int uid,
                  ISessionControllerCallback caller) {
              try {
                  mCb.onPrepare(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in prepare.", e);
              }
          }
  
          public void prepareFromMediaId(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, String mediaId, Bundle extras) {
              try {
                  mCb.onPrepareFromMediaId(packageName, pid, uid, caller, mediaId, extras);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in prepareFromMediaId.", e);
              }
          }
  
          public void prepareFromSearch(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, String query, Bundle extras) {
              try {
                  mCb.onPrepareFromSearch(packageName, pid, uid, caller, query, extras);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in prepareFromSearch.", e);
              }
          }
  
          public void prepareFromUri(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, Uri uri, Bundle extras) {
              try {
                  mCb.onPrepareFromUri(packageName, pid, uid, caller, uri, extras);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in prepareFromUri.", e);
              }
          }
  
          public void play(String packageName, int pid, int uid, ISessionControllerCallback caller) {
              try {
                  mCb.onPlay(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in play.", e);
              }
          }
  
          public void playFromMediaId(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, String mediaId, Bundle extras) {
              try {
                  mCb.onPlayFromMediaId(packageName, pid, uid, caller, mediaId, extras);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in playFromMediaId.", e);
              }
          }
  
          public void playFromSearch(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, String query, Bundle extras) {
              try {
                  mCb.onPlayFromSearch(packageName, pid, uid, caller, query, extras);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in playFromSearch.", e);
              }
          }
  
          public void playFromUri(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, Uri uri, Bundle extras) {
              try {
                  mCb.onPlayFromUri(packageName, pid, uid, caller, uri, extras);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in playFromUri.", e);
              }
          }
  
          public void skipToTrack(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, long id) {
              try {
                  mCb.onSkipToTrack(packageName, pid, uid, caller, id);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in skipToTrack", e);
              }
          }
  
          public void pause(String packageName, int pid, int uid, ISessionControllerCallback caller) {
              try {
                  mCb.onPause(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in pause.", e);
              }
          }
  
          public void stop(String packageName, int pid, int uid, ISessionControllerCallback caller) {
              try {
                  mCb.onStop(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in stop.", e);
              }
          }
  
          public void next(String packageName, int pid, int uid, ISessionControllerCallback caller) {
              try {
                  mCb.onNext(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in next.", e);
              }
          }
  
          public void previous(String packageName, int pid, int uid,
                  ISessionControllerCallback caller) {
              try {
                  mCb.onPrevious(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in previous.", e);
              }
          }
  
          public void fastForward(String packageName, int pid, int uid,
                  ISessionControllerCallback caller) {
              try {
                  mCb.onFastForward(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in fastForward.", e);
              }
          }
  
          public void rewind(String packageName, int pid, int uid,
                  ISessionControllerCallback caller) {
              try {
                  mCb.onRewind(packageName, pid, uid, caller);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in rewind.", e);
              }
          }
  
          public void seekTo(String packageName, int pid, int uid, ISessionControllerCallback caller,
                  long pos) {
              try {
                  mCb.onSeekTo(packageName, pid, uid, caller, pos);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in seekTo.", e);
              }
          }
  
          public void rate(String packageName, int pid, int uid, ISessionControllerCallback caller,
                  Rating rating) {
              try {
                  mCb.onRate(packageName, pid, uid, caller, rating);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in rate.", e);
              }
          }
  
          public void setPlaybackSpeed(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, float speed) {
              try {
                  mCb.onSetPlaybackSpeed(packageName, pid, uid, caller, speed);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in setPlaybackSpeed.", e);
              }
          }
  
          public void adjustVolume(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, boolean asSystemService, int direction) {
              try {
                  if (asSystemService) {
                      mCb.onAdjustVolume(mContext.getPackageName(), Process.myPid(),
                              Process.SYSTEM_UID, null, direction);
                  } else {
                      mCb.onAdjustVolume(packageName, pid, uid, caller, direction);
                  }
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in adjustVolume.", e);
              }
          }
  
          public void setVolumeTo(String packageName, int pid, int uid,
                  ISessionControllerCallback caller, int value) {
              try {
                  mCb.onSetVolumeTo(packageName, pid, uid, caller, value);
              } catch (RemoteException e) {
                  Slog.e(TAG, "Remote failure in setVolumeTo.", e);
              }
          }
  
          private Intent createMediaButtonIntent(KeyEvent keyEvent) {
              Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
              mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
              return mediaButtonIntent;
          }
      }
  
      class ControllerStub extends ISessionController.Stub {
          @Override
          public void sendCommand(String packageName, ISessionControllerCallback caller,
                  String command, Bundle args, ResultReceiver cb) {
              mSessionCb.sendCommand(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, command, args, cb);
          }
  
          @Override
          public boolean sendMediaButton(String packageName, ISessionControllerCallback cb,
                  KeyEvent keyEvent) {
              return mSessionCb.sendMediaButton(packageName, Binder.getCallingPid(),
                      Binder.getCallingUid(), cb, false, keyEvent);
          }
  
          @Override
          public void registerCallback(String packageName, ISessionControllerCallback cb) {
              synchronized (mLock) {
                  // If this session is already destroyed tell the caller and
                  // don't add them.
                  if (mDestroyed) {
                      try {
                          cb.onSessionDestroyed();
                      } catch (Exception e) {
                          // ignored
                      }
                      return;
                  }
                  if (getControllerHolderIndexForCb(cb) < 0) {
                      mControllerCallbackHolders.add(new ISessionControllerCallbackHolder(cb,
                              packageName, Binder.getCallingUid()));
                      if (DEBUG) {
                          Log.d(TAG, "registering controller callback " + cb + " from controller"
                                  + packageName);
                      }
                  }
              }
          }
  
          @Override
          public void unregisterCallback(ISessionControllerCallback cb) {
              synchronized (mLock) {
                  int index = getControllerHolderIndexForCb(cb);
                  if (index != -1) {
                      mControllerCallbackHolders.remove(index);
                  }
                  if (DEBUG) {
                      Log.d(TAG, "unregistering callback " + cb.asBinder());
                  }
              }
          }
  
          @Override
          public String getPackageName() {
              return mPackageName;
          }
  
          @Override
          public String getTag() {
              return mTag;
          }
  
          @Override
          public Bundle getSessionInfo() {
              return mSessionInfo;
          }
  
          @Override
          public PendingIntent getLaunchPendingIntent() {
              return mLaunchIntent;
          }
  
          @Override
          public long getFlags() {
              return mFlags;
          }
  
          @Override
          public PlaybackInfo getVolumeAttributes() {
              return MediaSessionRecord.this.getVolumeAttributes();
          }
  
          @Override
          public void adjustVolume(String packageName, String opPackageName,
                  ISessionControllerCallback caller, int direction, int flags) {
              int pid = Binder.getCallingPid();
              int uid = Binder.getCallingUid();
              final long token = Binder.clearCallingIdentity();
              try {
                  MediaSessionRecord.this.adjustVolume(packageName, opPackageName, pid, uid, caller,
                          false, direction, flags, false /* useSuggested */);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
          }
  
          @Override
          public void setVolumeTo(String packageName, String opPackageName,
                  ISessionControllerCallback caller, int value, int flags) {
              int pid = Binder.getCallingPid();
              int uid = Binder.getCallingUid();
              final long token = Binder.clearCallingIdentity();
              try {
                  MediaSessionRecord.this.setVolumeTo(packageName, opPackageName, pid, uid, caller,
                          value, flags);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
          }
  
          @Override
          public void prepare(String packageName, ISessionControllerCallback caller) {
              mSessionCb.prepare(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller);
          }
  
          @Override
          public void prepareFromMediaId(String packageName, ISessionControllerCallback caller,
                  String mediaId, Bundle extras) {
              mSessionCb.prepareFromMediaId(packageName, Binder.getCallingPid(),
                      Binder.getCallingUid(), caller, mediaId, extras);
          }
  
          @Override
          public void prepareFromSearch(String packageName, ISessionControllerCallback caller,
                  String query, Bundle extras) {
              mSessionCb.prepareFromSearch(packageName, Binder.getCallingPid(),
                      Binder.getCallingUid(), caller, query, extras);
          }
  
          @Override
          public void prepareFromUri(String packageName, ISessionControllerCallback caller,
                  Uri uri, Bundle extras) {
              mSessionCb.prepareFromUri(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, uri, extras);
          }
  
          @Override
          public void play(String packageName, ISessionControllerCallback caller) {
              mSessionCb.play(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller);
          }
  
          @Override
          public void playFromMediaId(String packageName, ISessionControllerCallback caller,
                  String mediaId, Bundle extras) {
              mSessionCb.playFromMediaId(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, mediaId, extras);
          }
  
          @Override
          public void playFromSearch(String packageName, ISessionControllerCallback caller,
                  String query, Bundle extras) {
              mSessionCb.playFromSearch(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, query, extras);
          }
  
          @Override
          public void playFromUri(String packageName, ISessionControllerCallback caller,
                  Uri uri, Bundle extras) {
              mSessionCb.playFromUri(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, uri, extras);
          }
  
          @Override
          public void skipToQueueItem(String packageName, ISessionControllerCallback caller,
                  long id) {
              mSessionCb.skipToTrack(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, id);
          }
  
          @Override
          public void pause(String packageName, ISessionControllerCallback caller) {
              mSessionCb.pause(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller);
          }
  
          @Override
          public void stop(String packageName, ISessionControllerCallback caller) {
              mSessionCb.stop(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller);
          }
  
          @Override
          public void next(String packageName, ISessionControllerCallback caller) {
              mSessionCb.next(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller);
          }
  
          @Override
          public void previous(String packageName, ISessionControllerCallback caller) {
              mSessionCb.previous(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller);
          }
  
          @Override
          public void fastForward(String packageName, ISessionControllerCallback caller) {
              mSessionCb.fastForward(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller);
          }
  
          @Override
          public void rewind(String packageName, ISessionControllerCallback caller) {
              mSessionCb.rewind(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller);
          }
  
          @Override
          public void seekTo(String packageName, ISessionControllerCallback caller, long pos) {
              mSessionCb.seekTo(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller,
                      pos);
          }
  
          @Override
          public void rate(String packageName, ISessionControllerCallback caller, Rating rating) {
              mSessionCb.rate(packageName, Binder.getCallingPid(), Binder.getCallingUid(), caller,
                      rating);
          }
  
          @Override
          public void setPlaybackSpeed(String packageName, ISessionControllerCallback caller,
                  float speed) {
              mSessionCb.setPlaybackSpeed(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, speed);
          }
  
          @Override
          public void sendCustomAction(String packageName, ISessionControllerCallback caller,
                  String action, Bundle args) {
              mSessionCb.sendCustomAction(packageName, Binder.getCallingPid(), Binder.getCallingUid(),
                      caller, action, args);
          }
  
          @Override
          public MediaMetadata getMetadata() {
              synchronized (mLock) {
                  return mMetadata;
              }
          }
  
          @Override
          public PlaybackState getPlaybackState() {
              return getStateWithUpdatedPosition();
          }
  
          @Override
          public ParceledListSlice getQueue() {
              synchronized (mLock) {
                  return mQueue == null ? null : new ParceledListSlice<>(mQueue);
              }
          }
  
          @Override
          public CharSequence getQueueTitle() {
              return mQueueTitle;
          }
  
          @Override
          public Bundle getExtras() {
              synchronized (mLock) {
                  return mExtras;
              }
          }
  
          @Override
          public int getRatingType() {
              return mRatingType;
          }
      }
  
      private class ISessionControllerCallbackHolder {
          private final ISessionControllerCallback mCallback;
          private final String mPackageName;
          private final int mUid;
  
          ISessionControllerCallbackHolder(ISessionControllerCallback callback, String packageName,
                  int uid) {
              mCallback = callback;
              mPackageName = packageName;
              mUid = uid;
          }
      }
  
      private class MessageHandler extends Handler {
          private static final int MSG_UPDATE_METADATA = 1;
          private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
          private static final int MSG_UPDATE_QUEUE = 3;
          private static final int MSG_UPDATE_QUEUE_TITLE = 4;
          private static final int MSG_UPDATE_EXTRAS = 5;
          private static final int MSG_SEND_EVENT = 6;
          private static final int MSG_UPDATE_SESSION_STATE = 7;
          private static final int MSG_UPDATE_VOLUME = 8;
          private static final int MSG_DESTROYED = 9;
  
          public MessageHandler(Looper looper) {
              super(looper);
          }
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case MSG_UPDATE_METADATA:
                      pushMetadataUpdate();
                      break;
                  case MSG_UPDATE_PLAYBACK_STATE:
                      pushPlaybackStateUpdate();
                      break;
                  case MSG_UPDATE_QUEUE:
                      pushQueueUpdate();
                      break;
                  case MSG_UPDATE_QUEUE_TITLE:
                      pushQueueTitleUpdate();
                      break;
                  case MSG_UPDATE_EXTRAS:
                      pushExtrasUpdate();
                      break;
                  case MSG_SEND_EVENT:
                      pushEvent((String) msg.obj, msg.getData());
                      break;
                  case MSG_UPDATE_SESSION_STATE:
                      // TODO add session state
                      break;
                  case MSG_UPDATE_VOLUME:
                      pushVolumeUpdate();
                      break;
                  case MSG_DESTROYED:
                      pushSessionDestroyed();
              }
          }
  
          public void post(int what) {
              post(what, null);
          }
  
          public void post(int what, Object obj) {
              obtainMessage(what, obj).sendToTarget();
          }
  
          public void post(int what, Object obj, Bundle data) {
              Message msg = obtainMessage(what, obj);
              msg.setData(data);
              msg.sendToTarget();
          }
      }
  
  }
  