Introduce MediaSessionEngine

MediaSession will be a wrapper of MediaSessionEngine, and
MediaSessionEngine will be moved into mainline module.

Bug: 119749862
Test: atest CtsMediaTestCases:android.media.cts.MediaSessionTest
    atest CtsMediaTestCases:android.media.cts.MediaControllerTest
Change-Id: I122b1e13c9c9d658ee03b91d0ebd8a41a954a79a
diff --git a/api/system-current.txt b/api/system-current.txt
index 2088eaf..33f7a00 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3530,6 +3530,76 @@
     method public void unregisterCallback(@NonNull android.media.session.ControllerCallbackLink);
   }
 
+  public abstract static class MediaSession.Callback {
+    method public void onSetMediaButtonEventDelegate(@NonNull android.media.session.MediaSessionEngine.MediaButtonEventDelegate);
+  }
+
+  public final class MediaSessionEngine implements java.lang.AutoCloseable {
+    ctor public MediaSessionEngine(@NonNull android.content.Context, @NonNull android.media.session.SessionLink, @NonNull android.media.session.SessionCallbackLink, @NonNull android.media.session.MediaSessionEngine.CallbackStub, int);
+    method public void close();
+    method public String getCallingPackage();
+    method @NonNull public android.media.session.MediaController getController();
+    method @NonNull public android.media.session.MediaSessionManager.RemoteUserInfo getCurrentControllerInfo();
+    method @NonNull public android.media.session.MediaSession.Token getSessionToken();
+    method public boolean isActive();
+    method public static boolean isActiveState(int);
+    method public void sendSessionEvent(@NonNull String, @Nullable android.os.Bundle);
+    method public void setActive(boolean);
+    method public void setCallback(@Nullable android.media.session.MediaSession.Callback);
+    method public void setCallback(@Nullable android.media.session.MediaSession.Callback, @NonNull android.os.Handler);
+    method public void setExtras(@Nullable android.os.Bundle);
+    method public void setFlags(int);
+    method public void setMediaButtonReceiver(@Nullable android.app.PendingIntent);
+    method public void setMetadata(@Nullable android.media.MediaMetadata);
+    method public void setPlaybackState(@Nullable android.media.session.PlaybackState);
+    method public void setPlaybackToLocal(android.media.AudioAttributes);
+    method public void setPlaybackToRemote(@NonNull android.media.VolumeProvider);
+    method public void setQueue(@Nullable java.util.List<android.media.session.MediaSession.QueueItem>);
+    method public void setQueueTitle(@Nullable CharSequence);
+    method public void setRatingType(int);
+    method public void setSessionActivity(@Nullable android.app.PendingIntent);
+  }
+
+  public static final class MediaSessionEngine.CallbackStub {
+    ctor public MediaSessionEngine.CallbackStub();
+    method public void onAdjustVolume(String, int, int, android.media.session.ControllerCallbackLink, int);
+    method public void onCommand(String, int, int, android.media.session.ControllerCallbackLink, String, android.os.Bundle, android.os.ResultReceiver);
+    method public void onCustomAction(String, int, int, android.media.session.ControllerCallbackLink, String, android.os.Bundle);
+    method public void onFastForward(String, int, int, android.media.session.ControllerCallbackLink);
+    method public void onMediaButton(String, int, int, android.content.Intent, int, android.os.ResultReceiver);
+    method public void onMediaButtonFromController(String, int, int, android.media.session.ControllerCallbackLink, android.content.Intent);
+    method public void onNext(String, int, int, android.media.session.ControllerCallbackLink);
+    method public void onPause(String, int, int, android.media.session.ControllerCallbackLink);
+    method public void onPlay(String, int, int, android.media.session.ControllerCallbackLink);
+    method public void onPlayFromMediaId(String, int, int, android.media.session.ControllerCallbackLink, String, android.os.Bundle);
+    method public void onPlayFromSearch(String, int, int, android.media.session.ControllerCallbackLink, String, android.os.Bundle);
+    method public void onPlayFromUri(String, int, int, android.media.session.ControllerCallbackLink, android.net.Uri, android.os.Bundle);
+    method public void onPrepare(String, int, int, android.media.session.ControllerCallbackLink);
+    method public void onPrepareFromMediaId(String, int, int, android.media.session.ControllerCallbackLink, String, android.os.Bundle);
+    method public void onPrepareFromSearch(String, int, int, android.media.session.ControllerCallbackLink, String, android.os.Bundle);
+    method public void onPrepareFromUri(String, int, int, android.media.session.ControllerCallbackLink, android.net.Uri, android.os.Bundle);
+    method public void onPrevious(String, int, int, android.media.session.ControllerCallbackLink);
+    method public void onRate(String, int, int, android.media.session.ControllerCallbackLink, android.media.Rating);
+    method public void onRewind(String, int, int, android.media.session.ControllerCallbackLink);
+    method public void onSeekTo(String, int, int, android.media.session.ControllerCallbackLink, long);
+    method public void onSetVolumeTo(String, int, int, android.media.session.ControllerCallbackLink, int);
+    method public void onSkipToTrack(String, int, int, android.media.session.ControllerCallbackLink, long);
+    method public void onStop(String, int, int, android.media.session.ControllerCallbackLink);
+  }
+
+  public static interface MediaSessionEngine.MediaButtonEventDelegate {
+    method public boolean onMediaButtonIntent(android.content.Intent);
+  }
+
+  public static final class MediaSessionEngine.QueueItem {
+    ctor public MediaSessionEngine.QueueItem(android.media.MediaDescription, long);
+    ctor public MediaSessionEngine.QueueItem(android.os.Parcel);
+    method public android.media.MediaDescription getDescription();
+    method public long getQueueId();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final int UNKNOWN_ID = -1; // 0xffffffff
+  }
+
   public final class MediaSessionManager {
     method @RequiresPermission(android.Manifest.permission.SET_MEDIA_KEY_LISTENER) public void setOnMediaKeyListener(android.media.session.MediaSessionManager.OnMediaKeyListener, @Nullable android.os.Handler);
     method @RequiresPermission(android.Manifest.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER) public void setOnVolumeKeyLongPressListener(android.media.session.MediaSessionManager.OnVolumeKeyLongPressListener, @Nullable android.os.Handler);
diff --git a/media/java/android/media/session/ControllerCallbackLink.java b/media/java/android/media/session/ControllerCallbackLink.java
index 95e19d2..28845e4 100644
--- a/media/java/android/media/session/ControllerCallbackLink.java
+++ b/media/java/android/media/session/ControllerCallbackLink.java
@@ -254,7 +254,7 @@
 
         @Override
         public void notifyPlaybackStateChanged(PlaybackState state) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPlaybackStateChanged(state);
@@ -265,7 +265,7 @@
 
         @Override
         public void notifyMetadataChanged(MediaMetadata metadata) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onMetadataChanged(metadata);
@@ -276,7 +276,7 @@
 
         @Override
         public void notifyQueueChanged(List<QueueItem> queue) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onQueueChanged(queue);
@@ -287,7 +287,7 @@
 
         @Override
         public void notifyQueueTitleChanged(CharSequence title) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onQueueTitleChanged(title);
@@ -303,7 +303,7 @@
 
         @Override
         public void notifyVolumeInfoChanged(PlaybackInfo info) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onVolumeInfoChanged(info);
@@ -312,7 +312,7 @@
             }
         }
 
-        private void ensureMediasControlPermission() {
+        private void ensureMediaControlPermission() {
             // Allow API calls from the System UI
             if (mContext.checkCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE)
                     == PackageManager.PERMISSION_GRANTED) {
diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java
index 9d537c8..057c9cb 100644
--- a/media/java/android/media/session/MediaController.java
+++ b/media/java/android/media/session/MediaController.java
@@ -117,7 +117,7 @@
      * @param token The token for the session.
      */
     public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) {
-        this(context, token.getBinder());
+        this(context, token.getControllerLink());
     }
 
     /**
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index f02d9ba..eda913e 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.annotation.UnsupportedAppUsage;
 import android.app.Activity;
 import android.app.PendingIntent;
@@ -33,23 +34,15 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ResultReceiver;
 import android.service.media.MediaBrowserService;
 import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-import android.view.KeyEvent;
-import android.view.ViewConfiguration;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
 import java.util.List;
-import java.util.Objects;
 
 /**
  * Allows interaction with media controllers, volume keys, media buttons, and
@@ -74,7 +67,7 @@
  * MediaSession objects are thread safe.
  */
 public final class MediaSession {
-    private static final String TAG = "MediaSession";
+    static final String TAG = "MediaSession";
 
     /**
      * Set this flag on the session to indicate that it can handle media button
@@ -121,21 +114,11 @@
             FLAG_EXCLUSIVE_GLOBAL_PRIORITY })
     public @interface SessionFlags { }
 
-    private final Object mLock = new Object();
-    private final int mMaxBitmapSize;
+    private final MediaSessionEngine mImpl;
 
-    private final MediaSession.Token mSessionToken;
-    private final MediaController mController;
-    private final SessionLink mSessionLink;
-    private final SessionCallbackLink mCbStub;
-
-    // Do not change the name of mCallback. Support lib accesses this by using reflection.
+    // Do not change the name of mCallbackWrapper. Support lib accesses this by using reflection.
     @UnsupportedAppUsage
-    private CallbackMessageHandler mCallback;
-    private VolumeProvider mVolumeProvider;
-    private PlaybackState mPlaybackState;
-
-    private boolean mActive = false;
+    private Object mCallback;
 
     /**
      * Creates a new session. The session will automatically be registered with
@@ -153,15 +136,15 @@
         if (TextUtils.isEmpty(tag)) {
             throw new IllegalArgumentException("tag cannot be null or empty");
         }
-        mMaxBitmapSize = context.getResources().getDimensionPixelSize(
-                android.R.dimen.config_mediaMetadataBitmapMaxSize);
-        mCbStub = new SessionCallbackLink(context, new CallbackStub(this));
         MediaSessionManager manager = (MediaSessionManager) context
                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
         try {
-            mSessionLink = manager.createSession(mCbStub, tag);
-            mSessionToken = new Token(mSessionLink.getController());
-            mController = new MediaController(context, mSessionToken);
+            MediaSessionEngine.CallbackStub cbStub = new MediaSessionEngine.CallbackStub();
+            SessionCallbackLink cbLink = new SessionCallbackLink(context, cbStub);
+            SessionLink sessionLink = manager.createSession(cbLink, tag);
+            mImpl = new MediaSessionEngine(context, sessionLink, cbLink, cbStub,
+                    context.getResources().getDimensionPixelSize(
+                            com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize));
         } catch (RuntimeException e) {
             throw new RuntimeException("Remote error creating session.", e);
         }
@@ -177,7 +160,7 @@
      * @param callback The callback object
      */
     public void setCallback(@Nullable Callback callback) {
-        setCallback(callback, null);
+        mImpl.setCallback(callback);
     }
 
     /**
@@ -190,24 +173,7 @@
      * @param handler The handler that events should be posted on.
      */
     public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
-        synchronized (mLock) {
-            if (mCallback != null) {
-                // We're updating the callback, clear the session from the old one.
-                mCallback.mCallback.mSession = null;
-                mCallback.removeCallbacksAndMessages(null);
-            }
-            if (callback == null) {
-                mCallback = null;
-                return;
-            }
-            if (handler == null) {
-                handler = new Handler();
-            }
-            callback.mSession = this;
-            CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
-                    callback);
-            mCallback = msgHandler;
-        }
+        mImpl.setCallback(callback, handler);
     }
 
     /**
@@ -218,11 +184,7 @@
      * @param pi The intent to launch to show UI for this Session.
      */
     public void setSessionActivity(@Nullable PendingIntent pi) {
-        try {
-            mSessionLink.setLaunchPendingIntent(pi);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Failure in setLaunchPendingIntent.", e);
-        }
+        mImpl.setSessionActivity(pi);
     }
 
     /**
@@ -234,11 +196,7 @@
      * @param mbr The {@link PendingIntent} to send the media button event to.
      */
     public void setMediaButtonReceiver(@Nullable PendingIntent mbr) {
-        try {
-            mSessionLink.setMediaButtonReceiver(mbr);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
-        }
+        mImpl.setMediaButtonReceiver(mbr);
     }
 
     /**
@@ -247,11 +205,7 @@
      * @param flags The flags to set for this session.
      */
     public void setFlags(@SessionFlags int flags) {
-        try {
-            mSessionLink.setFlags(flags);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Failure in setFlags.", e);
-        }
+        mImpl.setFlags(flags);
     }
 
     /**
@@ -266,14 +220,7 @@
      * @param attributes The {@link AudioAttributes} for this session's audio.
      */
     public void setPlaybackToLocal(AudioAttributes attributes) {
-        if (attributes == null) {
-            throw new IllegalArgumentException("Attributes cannot be null for local playback.");
-        }
-        try {
-            mSessionLink.setPlaybackToLocal(attributes);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Failure in setPlaybackToLocal.", e);
-        }
+        mImpl.setPlaybackToLocal(attributes);
     }
 
     /**
@@ -288,26 +235,7 @@
      *            not be null.
      */
     public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) {
-        if (volumeProvider == null) {
-            throw new IllegalArgumentException("volumeProvider may not be null!");
-        }
-        synchronized (mLock) {
-            mVolumeProvider = volumeProvider;
-        }
-        volumeProvider.setCallback(new VolumeProvider.Callback() {
-            @Override
-            public void onVolumeChanged(VolumeProvider volumeProvider) {
-                notifyRemoteVolumeChanged(volumeProvider);
-            }
-        });
-
-        try {
-            mSessionLink.setPlaybackToRemote(volumeProvider.getVolumeControl(),
-                    volumeProvider.getMaxVolume());
-            mSessionLink.setCurrentVolume(volumeProvider.getCurrentVolume());
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
-        }
+        mImpl.setPlaybackToRemote(volumeProvider);
     }
 
     /**
@@ -319,15 +247,7 @@
      * @param active Whether this session is active or not.
      */
     public void setActive(boolean active) {
-        if (mActive == active) {
-            return;
-        }
-        try {
-            mSessionLink.setActive(active);
-            mActive = active;
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Failure in setActive.", e);
-        }
+        mImpl.setActive(active);
     }
 
     /**
@@ -336,7 +256,7 @@
      * @return True if the session is active, false otherwise.
      */
     public boolean isActive() {
-        return mActive;
+        return mImpl.isActive();
     }
 
     /**
@@ -348,14 +268,7 @@
      * @param extras Any extras included with the event
      */
     public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
-        if (TextUtils.isEmpty(event)) {
-            throw new IllegalArgumentException("event cannot be null or empty");
-        }
-        try {
-            mSessionLink.sendEvent(event, extras);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Error sending event", e);
-        }
+        mImpl.sendSessionEvent(event, extras);
     }
 
     /**
@@ -364,11 +277,7 @@
      * but it must be released if your activity or service is being destroyed.
      */
     public void release() {
-        try {
-            mSessionLink.destroySession();
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Error releasing session: ", e);
-        }
+        mImpl.close();
     }
 
     /**
@@ -380,7 +289,7 @@
      *         session
      */
     public @NonNull Token getSessionToken() {
-        return mSessionToken;
+        return mImpl.getSessionToken();
     }
 
     /**
@@ -390,7 +299,7 @@
      * @return A controller for this session.
      */
     public @NonNull MediaController getController() {
-        return mController;
+        return mImpl.getController();
     }
 
     /**
@@ -399,12 +308,7 @@
      * @param state The current state of playback
      */
     public void setPlaybackState(@Nullable PlaybackState state) {
-        mPlaybackState = state;
-        try {
-            mSessionLink.setPlaybackState(state);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
-        }
+        mImpl.setPlaybackState(state);
     }
 
     /**
@@ -416,24 +320,7 @@
      * @see android.media.MediaMetadata.Builder#putBitmap
      */
     public void setMetadata(@Nullable MediaMetadata metadata) {
-        long duration = -1;
-        int fields = 0;
-        MediaDescription description = null;
-        if (metadata != null) {
-            metadata = (new MediaMetadata.Builder(metadata, mMaxBitmapSize)).build();
-            if (metadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
-                duration = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
-            }
-            fields = metadata.size();
-            description = metadata.getDescription();
-        }
-        String metadataDescription = "size=" + fields + ", description=" + description;
-
-        try {
-            mSessionLink.setMetadata(metadata, duration, metadataDescription);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
-        }
+        mImpl.setMetadata(metadata);
     }
 
     /**
@@ -448,11 +335,7 @@
      * @param queue A list of items in the play queue.
      */
     public void setQueue(@Nullable List<QueueItem> queue) {
-        try {
-            mSessionLink.setQueue(queue);
-        } catch (RuntimeException e) {
-            Log.wtf("Dead object in setQueue.", e);
-        }
+        mImpl.setQueue(queue);
     }
 
     /**
@@ -463,11 +346,7 @@
      * @param title The title of the play queue.
      */
     public void setQueueTitle(@Nullable CharSequence title) {
-        try {
-            mSessionLink.setQueueTitle(title);
-        } catch (RuntimeException e) {
-            Log.wtf("Dead object in setQueueTitle.", e);
-        }
+        mImpl.setQueueTitle(title);
     }
 
     /**
@@ -484,11 +363,7 @@
      * </ul>
      */
     public void setRatingType(@Rating.Style int type) {
-        try {
-            mSessionLink.setRatingType(type);
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Error in setRatingType.", e);
-        }
+        mImpl.setRatingType(type);
     }
 
     /**
@@ -499,11 +374,7 @@
      * @param extras The extras associated with the {@link MediaSession}.
      */
     public void setExtras(@Nullable Bundle extras) {
-        try {
-            mSessionLink.setExtras(extras);
-        } catch (RuntimeException e) {
-            Log.wtf("Dead object in setExtras.", e);
-        }
+        mImpl.setExtras(extras);
     }
 
     /**
@@ -515,11 +386,7 @@
      * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
      */
     public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
-        if (mCallback == null || mCallback.mCurrentControllerInfo == null) {
-            throw new IllegalStateException(
-                    "This should be called inside of MediaSession.Callback methods");
-        }
-        return mCallback.mCurrentControllerInfo;
+        return mImpl.getCurrentControllerInfo();
     }
 
     /**
@@ -529,17 +396,7 @@
      * @hide
      */
     public void notifyRemoteVolumeChanged(VolumeProvider provider) {
-        synchronized (mLock) {
-            if (provider == null || provider != mVolumeProvider) {
-                Log.w(TAG, "Received update from stale volume provider");
-                return;
-            }
-        }
-        try {
-            mSessionLink.setCurrentVolume(provider.getCurrentVolume());
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Error in notifyVolumeChanged", e);
-        }
+        mImpl.notifyRemoteVolumeChanged(provider);
     }
 
     /**
@@ -551,119 +408,7 @@
      */
     @UnsupportedAppUsage
     public String getCallingPackage() {
-        if (mCallback != null && mCallback.mCurrentControllerInfo != null) {
-            return mCallback.mCurrentControllerInfo.getPackageName();
-        }
-        return null;
-    }
-
-    private void dispatchPrepare(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE, null, null);
-    }
-
-    private void dispatchPrepareFromMediaId(RemoteUserInfo caller, String mediaId, Bundle extras) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras);
-    }
-
-    private void dispatchPrepareFromSearch(RemoteUserInfo caller, String query, Bundle extras) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_SEARCH, query, extras);
-    }
-
-    private void dispatchPrepareFromUri(RemoteUserInfo caller, Uri uri, Bundle extras) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_URI, uri, extras);
-    }
-
-    private void dispatchPlay(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PLAY, null, null);
-    }
-
-    private void dispatchPlayFromMediaId(RemoteUserInfo caller, String mediaId, Bundle extras) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras);
-    }
-
-    private void dispatchPlayFromSearch(RemoteUserInfo caller, String query, Bundle extras) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras);
-    }
-
-    private void dispatchPlayFromUri(RemoteUserInfo caller, Uri uri, Bundle extras) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PLAY_URI, uri, extras);
-    }
-
-    private void dispatchSkipToItem(RemoteUserInfo caller, long id) {
-        postToCallback(caller, CallbackMessageHandler.MSG_SKIP_TO_ITEM, id, null);
-    }
-
-    private void dispatchPause(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PAUSE, null, null);
-    }
-
-    private void dispatchStop(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_STOP, null, null);
-    }
-
-    private void dispatchNext(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_NEXT, null, null);
-    }
-
-    private void dispatchPrevious(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_PREVIOUS, null, null);
-    }
-
-    private void dispatchFastForward(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_FAST_FORWARD, null, null);
-    }
-
-    private void dispatchRewind(RemoteUserInfo caller) {
-        postToCallback(caller, CallbackMessageHandler.MSG_REWIND, null, null);
-    }
-
-    private void dispatchSeekTo(RemoteUserInfo caller, long pos) {
-        postToCallback(caller, CallbackMessageHandler.MSG_SEEK_TO, pos, null);
-    }
-
-    private void dispatchRate(RemoteUserInfo caller, Rating rating) {
-        postToCallback(caller, CallbackMessageHandler.MSG_RATE, rating, null);
-    }
-
-    private void dispatchCustomAction(RemoteUserInfo caller, String action, Bundle args) {
-        postToCallback(caller, CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
-    }
-
-    private void dispatchMediaButton(RemoteUserInfo caller, Intent mediaButtonIntent) {
-        postToCallback(caller, CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent, null);
-    }
-
-    private void dispatchMediaButtonDelayed(RemoteUserInfo info, Intent mediaButtonIntent,
-            long delay) {
-        postToCallbackDelayed(info, CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT,
-                mediaButtonIntent, null, delay);
-    }
-
-    private void dispatchAdjustVolume(RemoteUserInfo caller, int direction) {
-        postToCallback(caller, CallbackMessageHandler.MSG_ADJUST_VOLUME, direction, null);
-    }
-
-    private void dispatchSetVolumeTo(RemoteUserInfo caller, int volume) {
-        postToCallback(caller, CallbackMessageHandler.MSG_SET_VOLUME, volume, null);
-    }
-
-    private void dispatchCommand(RemoteUserInfo caller, String command, Bundle args,
-            ResultReceiver resultCb) {
-        Command cmd = new Command(command, args, resultCb);
-        postToCallback(caller, CallbackMessageHandler.MSG_COMMAND, cmd, null);
-    }
-
-    private void postToCallback(RemoteUserInfo caller, int what, Object obj, Bundle data) {
-        postToCallbackDelayed(caller, what, obj, data, 0);
-    }
-
-    private void postToCallbackDelayed(RemoteUserInfo caller, int what, Object obj, Bundle data,
-            long delay) {
-        synchronized (mLock) {
-            if (mCallback != null) {
-                mCallback.post(caller, what, obj, data, delay);
-            }
-        }
+        return mImpl.getCallingPackage();
     }
 
     /**
@@ -672,17 +417,7 @@
      * @hide
      */
     public static boolean isActiveState(int state) {
-        switch (state) {
-            case PlaybackState.STATE_FAST_FORWARDING:
-            case PlaybackState.STATE_REWINDING:
-            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
-            case PlaybackState.STATE_SKIPPING_TO_NEXT:
-            case PlaybackState.STATE_BUFFERING:
-            case PlaybackState.STATE_CONNECTING:
-            case PlaybackState.STATE_PLAYING:
-                return true;
-        }
-        return false;
+        return MediaSessionEngine.isActiveState(state);
     }
 
     /**
@@ -692,13 +427,13 @@
      */
     public static final class Token implements Parcelable {
 
-        private ControllerLink mBinder;
+        private ControllerLink mControllerLink;
 
         /**
          * @hide
          */
-        public Token(ControllerLink binder) {
-            mBinder = binder;
+        public Token(ControllerLink controllerLink) {
+            mControllerLink = controllerLink;
         }
 
         @Override
@@ -708,14 +443,15 @@
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
-            dest.writeParcelable(mBinder, flags);
+            dest.writeParcelable(mControllerLink, flags);
         }
 
         @Override
         public int hashCode() {
             final int prime = 31;
             int result = 1;
-            result = prime * result + ((mBinder == null) ? 0 : mBinder.getBinder().hashCode());
+            result = prime * result + ((mControllerLink == null)
+                    ? 0 : mControllerLink.getBinder().hashCode());
             return result;
         }
 
@@ -728,17 +464,23 @@
             if (getClass() != obj.getClass())
                 return false;
             Token other = (Token) obj;
-            if (mBinder == null) {
-                if (other.mBinder != null)
+            if (mControllerLink == null) {
+                if (other.mControllerLink != null) {
                     return false;
-            } else if (!mBinder.getBinder().equals(other.mBinder.getBinder())) {
+                }
+            } else if (!mControllerLink.getBinder().equals(other.mControllerLink.getBinder())) {
                 return false;
             }
             return true;
         }
 
-        ControllerLink getBinder() {
-            return mBinder;
+        /**
+         * Gets the controller link in this token.
+         * @hide
+         */
+        @SystemApi
+        ControllerLink getControllerLink() {
+            return mControllerLink;
         }
 
         public static final Parcelable.Creator<Token> CREATOR =
@@ -762,9 +504,7 @@
      */
     public abstract static class Callback {
 
-        private MediaSession mSession;
-        private CallbackMessageHandler mHandler;
-        private boolean mMediaPlayPauseKeyPending;
+        MediaSessionEngine.MediaButtonEventDelegate mMediaButtonEventDelegate;
 
         public Callback() {
         }
@@ -796,110 +536,12 @@
          * @return True if the event was handled, false otherwise.
          */
         public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
-            if (mSession != null && mHandler != null
-                    && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
-                KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
-                if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
-                    PlaybackState state = mSession.mPlaybackState;
-                    long validActions = state == null ? 0 : state.getActions();
-                    switch (ke.getKeyCode()) {
-                        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-                        case KeyEvent.KEYCODE_HEADSETHOOK:
-                            if (ke.getRepeatCount() > 0) {
-                                // Consider long-press as a single tap.
-                                handleMediaPlayPauseKeySingleTapIfPending();
-                            } else if (mMediaPlayPauseKeyPending) {
-                                // Consider double tap as the next.
-                                mHandler.removeMessages(CallbackMessageHandler
-                                        .MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
-                                mMediaPlayPauseKeyPending = false;
-                                if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
-                                    onSkipToNext();
-                                }
-                            } else {
-                                mMediaPlayPauseKeyPending = true;
-                                mSession.dispatchMediaButtonDelayed(
-                                        mSession.getCurrentControllerInfo(),
-                                        mediaButtonIntent, ViewConfiguration.getDoubleTapTimeout());
-                            }
-                            return true;
-                        default:
-                            // If another key is pressed within double tap timeout, consider the
-                            // pending play/pause as a single tap to handle media keys in order.
-                            handleMediaPlayPauseKeySingleTapIfPending();
-                            break;
-                    }
-
-                    switch (ke.getKeyCode()) {
-                        case KeyEvent.KEYCODE_MEDIA_PLAY:
-                            if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
-                                onPlay();
-                                return true;
-                            }
-                            break;
-                        case KeyEvent.KEYCODE_MEDIA_PAUSE:
-                            if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
-                                onPause();
-                                return true;
-                            }
-                            break;
-                        case KeyEvent.KEYCODE_MEDIA_NEXT:
-                            if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
-                                onSkipToNext();
-                                return true;
-                            }
-                            break;
-                        case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
-                            if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
-                                onSkipToPrevious();
-                                return true;
-                            }
-                            break;
-                        case KeyEvent.KEYCODE_MEDIA_STOP:
-                            if ((validActions & PlaybackState.ACTION_STOP) != 0) {
-                                onStop();
-                                return true;
-                            }
-                            break;
-                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
-                            if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
-                                onFastForward();
-                                return true;
-                            }
-                            break;
-                        case KeyEvent.KEYCODE_MEDIA_REWIND:
-                            if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
-                                onRewind();
-                                return true;
-                            }
-                            break;
-                    }
-                }
+            if (mMediaButtonEventDelegate != null) {
+                return mMediaButtonEventDelegate.onMediaButtonIntent(mediaButtonIntent);
             }
             return false;
         }
 
-        private void handleMediaPlayPauseKeySingleTapIfPending() {
-            if (!mMediaPlayPauseKeyPending) {
-                return;
-            }
-            mMediaPlayPauseKeyPending = false;
-            mHandler.removeMessages(CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
-            PlaybackState state = mSession.mPlaybackState;
-            long validActions = state == null ? 0 : state.getActions();
-            boolean isPlaying = state != null
-                    && state.getState() == PlaybackState.STATE_PLAYING;
-            boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
-                        | PlaybackState.ACTION_PLAY)) != 0;
-            boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
-                        | PlaybackState.ACTION_PAUSE)) != 0;
-            if (isPlaying && canPause) {
-                onPause();
-            } else if (!isPlaying && canPlay) {
-                onPlay();
-            }
-        }
-
         /**
          * Override to handle requests to prepare playback. During the preparation, a session should
          * not hold audio focus in order to allow other sessions play seamlessly. The state of
@@ -1042,251 +684,14 @@
          */
         public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
         }
-    }
 
-    /**
-     * @hide
-     */
-    public static final class CallbackStub extends SessionCallbackLink.CallbackStub {
-        private WeakReference<MediaSession> mMediaSession;
-
-        public CallbackStub(MediaSession session) {
-            mMediaSession = new WeakReference<>(session);
-        }
-
-        private static RemoteUserInfo createRemoteUserInfo(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            return new RemoteUserInfo(packageName, pid, uid,
-                    caller != null ? caller.getBinder() : null);
-        }
-
-        @Override
-        public void onCommand(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, String command, Bundle args, ResultReceiver cb) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchCommand(createRemoteUserInfo(packageName, pid, uid, caller),
-                        command, args, cb);
-            }
-        }
-
-        @Override
-        public void onMediaButton(String packageName, int pid, int uid, Intent mediaButtonIntent,
-                int sequenceNumber, ResultReceiver cb) {
-            MediaSession session = mMediaSession.get();
-            try {
-                if (session != null) {
-                    session.dispatchMediaButton(createRemoteUserInfo(packageName, pid, uid, null),
-                            mediaButtonIntent);
-                }
-            } finally {
-                if (cb != null) {
-                    cb.send(sequenceNumber, null);
-                }
-            }
-        }
-
-        @Override
-        public void onMediaButtonFromController(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, Intent mediaButtonIntent) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchMediaButton(createRemoteUserInfo(packageName, pid, uid, caller),
-                        mediaButtonIntent);
-            }
-        }
-
-        @Override
-        public void onPrepare(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPrepare(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onPrepareFromMediaId(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, String mediaId,
-                Bundle extras) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPrepareFromMediaId(
-                        createRemoteUserInfo(packageName, pid, uid, caller), mediaId, extras);
-            }
-        }
-
-        @Override
-        public void onPrepareFromSearch(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, String query,
-                Bundle extras) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPrepareFromSearch(
-                        createRemoteUserInfo(packageName, pid, uid, caller), query, extras);
-            }
-        }
-
-        @Override
-        public void onPrepareFromUri(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, Uri uri, Bundle extras) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPrepareFromUri(createRemoteUserInfo(packageName, pid, uid, caller),
-                        uri, extras);
-            }
-        }
-
-        @Override
-        public void onPlay(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPlay(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onPlayFromMediaId(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, String mediaId,
-                Bundle extras) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPlayFromMediaId(createRemoteUserInfo(packageName, pid, uid, caller),
-                        mediaId, extras);
-            }
-        }
-
-        @Override
-        public void onPlayFromSearch(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, String query,
-                Bundle extras) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPlayFromSearch(createRemoteUserInfo(packageName, pid, uid, caller),
-                        query, extras);
-            }
-        }
-
-        @Override
-        public void onPlayFromUri(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, Uri uri, Bundle extras) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPlayFromUri(createRemoteUserInfo(packageName, pid, uid, caller),
-                        uri, extras);
-            }
-        }
-
-        @Override
-        public void onSkipToTrack(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, long id) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchSkipToItem(createRemoteUserInfo(packageName, pid, uid, caller), id);
-            }
-        }
-
-        @Override
-        public void onPause(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPause(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onStop(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchStop(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onNext(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchNext(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onPrevious(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchPrevious(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onFastForward(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchFastForward(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onRewind(String packageName, int pid, int uid,
-                ControllerCallbackLink caller) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchRewind(createRemoteUserInfo(packageName, pid, uid, caller));
-            }
-        }
-
-        @Override
-        public void onSeekTo(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, long pos) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchSeekTo(createRemoteUserInfo(packageName, pid, uid, caller), pos);
-            }
-        }
-
-        @Override
-        public void onRate(String packageName, int pid, int uid, ControllerCallbackLink caller,
-                Rating rating) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchRate(createRemoteUserInfo(packageName, pid, uid, caller), rating);
-            }
-        }
-
-        @Override
-        public void onCustomAction(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, String action, Bundle args) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchCustomAction(createRemoteUserInfo(packageName, pid, uid, caller),
-                        action, args);
-            }
-        }
-
-        @Override
-        public void onAdjustVolume(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, int direction) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchAdjustVolume(createRemoteUserInfo(packageName, pid, uid, caller),
-                        direction);
-            }
-        }
-
-        @Override
-        public void onSetVolumeTo(String packageName, int pid, int uid,
-                ControllerCallbackLink caller, int value) {
-            MediaSession session = mMediaSession.get();
-            if (session != null) {
-                session.dispatchSetVolumeTo(createRemoteUserInfo(packageName, pid, uid, caller),
-                        value);
-            }
+        /**
+         * @hide
+         */
+        @SystemApi
+        public void onSetMediaButtonEventDelegate(
+                @NonNull MediaSessionEngine.MediaButtonEventDelegate delegate) {
+            mMediaButtonEventDelegate = delegate;
         }
     }
 
@@ -1300,7 +705,7 @@
          */
         public static final int UNKNOWN_ID = -1;
 
-        private final MediaDescription mDescription;
+        private final MediaSessionEngine.QueueItem mImpl;
         @UnsupportedAppUsage
         private final long mId;
 
@@ -1312,18 +717,12 @@
          *            play queue and cannot be {@link #UNKNOWN_ID}.
          */
         public QueueItem(MediaDescription description, long id) {
-            if (description == null) {
-                throw new IllegalArgumentException("Description cannot be null.");
-            }
-            if (id == UNKNOWN_ID) {
-                throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID");
-            }
-            mDescription = description;
+            mImpl = new MediaSessionEngine.QueueItem(description, id);
             mId = id;
         }
 
         private QueueItem(Parcel in) {
-            mDescription = MediaDescription.CREATOR.createFromParcel(in);
+            mImpl = new MediaSessionEngine.QueueItem(in);
             mId = in.readLong();
         }
 
@@ -1331,20 +730,19 @@
          * Get the description for this item.
          */
         public MediaDescription getDescription() {
-            return mDescription;
+            return mImpl.getDescription();
         }
 
         /**
          * Get the queue id for this item.
          */
         public long getQueueId() {
-            return mId;
+            return mImpl.getQueueId();
         }
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
-            mDescription.writeToParcel(dest, flags);
-            dest.writeLong(mId);
+            mImpl.writeToParcel(dest, flags);
         }
 
         @Override
@@ -1368,9 +766,7 @@
 
         @Override
         public String toString() {
-            return "MediaSession.QueueItem {" +
-                    "Description=" + mDescription +
-                    ", Id=" + mId + " }";
+            return mImpl.toString();
         }
 
         @Override
@@ -1383,167 +779,7 @@
                 return false;
             }
 
-            final QueueItem item = (QueueItem) o;
-            if (mId != item.mId) {
-                return false;
-            }
-
-            if (!Objects.equals(mDescription, item.mDescription)) {
-                return false;
-            }
-
-            return true;
-        }
-    }
-
-    private static final class Command {
-        public final String command;
-        public final Bundle extras;
-        public final ResultReceiver stub;
-
-        public Command(String command, Bundle extras, ResultReceiver stub) {
-            this.command = command;
-            this.extras = extras;
-            this.stub = stub;
-        }
-    }
-
-    private class CallbackMessageHandler extends Handler {
-        private static final int MSG_COMMAND = 1;
-        private static final int MSG_MEDIA_BUTTON = 2;
-        private static final int MSG_PREPARE = 3;
-        private static final int MSG_PREPARE_MEDIA_ID = 4;
-        private static final int MSG_PREPARE_SEARCH = 5;
-        private static final int MSG_PREPARE_URI = 6;
-        private static final int MSG_PLAY = 7;
-        private static final int MSG_PLAY_MEDIA_ID = 8;
-        private static final int MSG_PLAY_SEARCH = 9;
-        private static final int MSG_PLAY_URI = 10;
-        private static final int MSG_SKIP_TO_ITEM = 11;
-        private static final int MSG_PAUSE = 12;
-        private static final int MSG_STOP = 13;
-        private static final int MSG_NEXT = 14;
-        private static final int MSG_PREVIOUS = 15;
-        private static final int MSG_FAST_FORWARD = 16;
-        private static final int MSG_REWIND = 17;
-        private static final int MSG_SEEK_TO = 18;
-        private static final int MSG_RATE = 19;
-        private static final int MSG_CUSTOM_ACTION = 20;
-        private static final int MSG_ADJUST_VOLUME = 21;
-        private static final int MSG_SET_VOLUME = 22;
-        private static final int MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT = 23;
-
-        private MediaSession.Callback mCallback;
-        private RemoteUserInfo mCurrentControllerInfo;
-
-        public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
-            super(looper);
-            mCallback = callback;
-            mCallback.mHandler = this;
-        }
-
-        public void post(RemoteUserInfo caller, int what, Object obj, Bundle data, long delayMs) {
-            Pair<RemoteUserInfo, Object> objWithCaller = Pair.create(caller, obj);
-            Message msg = obtainMessage(what, objWithCaller);
-            msg.setAsynchronous(true);
-            msg.setData(data);
-            if (delayMs > 0) {
-                sendMessageDelayed(msg, delayMs);
-            } else {
-                sendMessage(msg);
-            }
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            mCurrentControllerInfo = ((Pair<RemoteUserInfo, Object>) msg.obj).first;
-
-            VolumeProvider vp;
-            Object obj = ((Pair<RemoteUserInfo, Object>) msg.obj).second;
-
-            switch (msg.what) {
-                case MSG_COMMAND:
-                    Command cmd = (Command) obj;
-                    mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
-                    break;
-                case MSG_MEDIA_BUTTON:
-                    mCallback.onMediaButtonEvent((Intent) obj);
-                    break;
-                case MSG_PREPARE:
-                    mCallback.onPrepare();
-                    break;
-                case MSG_PREPARE_MEDIA_ID:
-                    mCallback.onPrepareFromMediaId((String) obj, msg.getData());
-                    break;
-                case MSG_PREPARE_SEARCH:
-                    mCallback.onPrepareFromSearch((String) obj, msg.getData());
-                    break;
-                case MSG_PREPARE_URI:
-                    mCallback.onPrepareFromUri((Uri) obj, msg.getData());
-                    break;
-                case MSG_PLAY:
-                    mCallback.onPlay();
-                    break;
-                case MSG_PLAY_MEDIA_ID:
-                    mCallback.onPlayFromMediaId((String) obj, msg.getData());
-                    break;
-                case MSG_PLAY_SEARCH:
-                    mCallback.onPlayFromSearch((String) obj, msg.getData());
-                    break;
-                case MSG_PLAY_URI:
-                    mCallback.onPlayFromUri((Uri) obj, msg.getData());
-                    break;
-                case MSG_SKIP_TO_ITEM:
-                    mCallback.onSkipToQueueItem((Long) obj);
-                    break;
-                case MSG_PAUSE:
-                    mCallback.onPause();
-                    break;
-                case MSG_STOP:
-                    mCallback.onStop();
-                    break;
-                case MSG_NEXT:
-                    mCallback.onSkipToNext();
-                    break;
-                case MSG_PREVIOUS:
-                    mCallback.onSkipToPrevious();
-                    break;
-                case MSG_FAST_FORWARD:
-                    mCallback.onFastForward();
-                    break;
-                case MSG_REWIND:
-                    mCallback.onRewind();
-                    break;
-                case MSG_SEEK_TO:
-                    mCallback.onSeekTo((Long) obj);
-                    break;
-                case MSG_RATE:
-                    mCallback.onSetRating((Rating) obj);
-                    break;
-                case MSG_CUSTOM_ACTION:
-                    mCallback.onCustomAction((String) obj, msg.getData());
-                    break;
-                case MSG_ADJUST_VOLUME:
-                    synchronized (mLock) {
-                        vp = mVolumeProvider;
-                    }
-                    if (vp != null) {
-                        vp.onAdjustVolume((int) obj);
-                    }
-                    break;
-                case MSG_SET_VOLUME:
-                    synchronized (mLock) {
-                        vp = mVolumeProvider;
-                    }
-                    if (vp != null) {
-                        vp.onSetVolumeTo((int) obj);
-                    }
-                    break;
-                case MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT:
-                    mCallback.handleMediaPlayPauseKeySingleTapIfPending();
-                    break;
-            }
-            mCurrentControllerInfo = null;
+            return mImpl.equals(((QueueItem) o).mImpl);
         }
     }
 }
diff --git a/media/java/android/media/session/MediaSessionEngine.java b/media/java/android/media/session/MediaSessionEngine.java
new file mode 100644
index 0000000..c4634a9
--- /dev/null
+++ b/media/java/android/media/session/MediaSessionEngine.java
@@ -0,0 +1,1481 @@
+/*
+ * 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 android.media.session;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioAttributes;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.Rating;
+import android.media.VolumeProvider;
+import android.media.session.MediaSessionManager.RemoteUserInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.service.media.MediaBrowserService;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.ViewConfiguration;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+@SystemApi
+public final class MediaSessionEngine implements AutoCloseable {
+    private static final String TAG = MediaSession.TAG;
+
+    private final Object mLock = new Object();
+    private final int mMaxBitmapSize;
+
+    private final MediaSession.Token mSessionToken;
+    private final MediaController mController;
+    private final SessionLink mSessionLink;
+    private final SessionCallbackLink mCbLink;
+
+    // Do not change the name of mCallbackWrapper. Support lib accesses this by using reflection.
+    @UnsupportedAppUsage
+    private CallbackMessageHandler mCallbackHandler;
+    private VolumeProvider mVolumeProvider;
+    private PlaybackState mPlaybackState;
+
+    private boolean mActive = false;
+
+    /**
+     * Creates a new session. The session will automatically be registered with
+     * the system but will not be published until {@link #setActive(boolean)
+     * setActive(true)} is called. You must call {@link #close()} when
+     * finished with the session.
+     *
+     * @param context The context to use to create the session.
+     * @param sessionLink A session link for the binder of MediaSessionRecord
+     * @param cbStub A callback link that handles incoming command to {@link MediaSession.Callback}.
+     */
+    public MediaSessionEngine(@NonNull Context context, @NonNull SessionLink sessionLink,
+            @NonNull SessionCallbackLink cbLink, @NonNull CallbackStub cbStub, int maxBitmapSize) {
+        mSessionLink = sessionLink;
+        mCbLink = cbLink;
+        mMaxBitmapSize = maxBitmapSize;
+
+        cbStub.setSessionImpl(this);
+        mSessionToken = new MediaSession.Token(mSessionLink.getController());
+        mController = new MediaController(context, mSessionToken);
+    }
+
+    /**
+     * Set the callback to receive updates for the MediaSession. This includes
+     * media button events and transport controls. The caller's thread will be
+     * used to post updates.
+     * <p>
+     * Set the callback to null to stop receiving updates.
+     *
+     * @param callback The callback object
+     */
+    public void setCallback(@Nullable MediaSession.Callback callback) {
+        setCallback(callback, new Handler());
+    }
+
+    /**
+     * Set the callback to receive updates for the MediaSession. This includes
+     * media button events and transport controls.
+     * <p>
+     * Set the callback to null to stop receiving updates.
+     *
+     * @param callback The callback to receive updates on.
+     * @param handler The handler that events should be posted on.
+     */
+    public void setCallback(@Nullable MediaSession.Callback callback, @NonNull Handler handler) {
+        setCallbackInternal(callback == null ? null : new CallbackWrapper(callback), handler);
+    }
+
+    private void setCallbackInternal(CallbackWrapper callback, Handler handler) {
+        synchronized (mLock) {
+            if (mCallbackHandler != null) {
+                // We're updating the callback, clear the session from the old one.
+                mCallbackHandler.mCallbackWrapper.mSessionImpl = null;
+                mCallbackHandler.removeCallbacksAndMessages(null);
+            }
+            if (callback == null) {
+                mCallbackHandler = null;
+                return;
+            }
+            callback.mSessionImpl = this;
+            CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
+                    callback);
+            mCallbackHandler = msgHandler;
+        }
+    }
+
+    /**
+     * Set an intent for launching UI for this Session. This can be used as a
+     * quick link to an ongoing media screen. The intent should be for an
+     * activity that may be started using {@link Activity#startActivity(Intent)}.
+     *
+     * @param pi The intent to launch to show UI for this Session.
+     */
+    public void setSessionActivity(@Nullable PendingIntent pi) {
+        try {
+            mSessionLink.setLaunchPendingIntent(pi);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Failure in setLaunchPendingIntent.", e);
+        }
+    }
+
+    /**
+     * Set a pending intent for your media button receiver to allow restarting
+     * playback after the session has been stopped. If your app is started in
+     * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via
+     * the pending intent.
+     *
+     * @param mbr The {@link PendingIntent} to send the media button event to.
+     */
+    public void setMediaButtonReceiver(@Nullable PendingIntent mbr) {
+        try {
+            mSessionLink.setMediaButtonReceiver(mbr);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
+        }
+    }
+
+    /**
+     * Set any flags for the session.
+     *
+     * @param flags The flags to set for this session.
+     */
+    public void setFlags(@MediaSession.SessionFlags int flags) {
+        try {
+            mSessionLink.setFlags(flags);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Failure in setFlags.", e);
+        }
+    }
+
+    /**
+     * Set the attributes for this session's audio. This will affect the
+     * system's volume handling for this session. If
+     * {@link #setPlaybackToRemote} was previously called it will stop receiving
+     * volume commands and the system will begin sending volume changes to the
+     * appropriate stream.
+     * <p>
+     * By default sessions use attributes for media.
+     *
+     * @param attributes The {@link AudioAttributes} for this session's audio.
+     */
+    public void setPlaybackToLocal(AudioAttributes attributes) {
+        if (attributes == null) {
+            throw new IllegalArgumentException("Attributes cannot be null for local playback.");
+        }
+        try {
+            mSessionLink.setPlaybackToLocal(attributes);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Failure in setPlaybackToLocal.", e);
+        }
+    }
+
+    /**
+     * Configure this session to use remote volume handling. This must be called
+     * to receive volume button events, otherwise the system will adjust the
+     * appropriate stream volume for this session. If
+     * {@link #setPlaybackToLocal} was previously called the system will stop
+     * handling volume changes for this session and pass them to the volume
+     * provider instead.
+     *
+     * @param volumeProvider The provider that will handle volume changes. May
+     *            not be null.
+     */
+    public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) {
+        if (volumeProvider == null) {
+            throw new IllegalArgumentException("volumeProvider may not be null!");
+        }
+        synchronized (mLock) {
+            mVolumeProvider = volumeProvider;
+        }
+        volumeProvider.setCallback(new VolumeProvider.Callback() {
+            @Override
+            public void onVolumeChanged(VolumeProvider volumeProvider) {
+                notifyRemoteVolumeChanged(volumeProvider);
+            }
+        });
+
+        try {
+            mSessionLink.setPlaybackToRemote(volumeProvider.getVolumeControl(),
+                    volumeProvider.getMaxVolume());
+            mSessionLink.setCurrentVolume(volumeProvider.getCurrentVolume());
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
+        }
+    }
+
+    /**
+     * Set if this session is currently active and ready to receive commands. If
+     * set to false your session's controller may not be discoverable. You must
+     * set the session to active before it can start receiving media button
+     * events or transport commands.
+     *
+     * @param active Whether this session is active or not.
+     */
+    public void setActive(boolean active) {
+        if (mActive == active) {
+            return;
+        }
+        try {
+            mSessionLink.setActive(active);
+            mActive = active;
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Failure in setActive.", e);
+        }
+    }
+
+    /**
+     * Get the current active state of this session.
+     *
+     * @return True if the session is active, false otherwise.
+     */
+    public boolean isActive() {
+        return mActive;
+    }
+
+    /**
+     * Send a proprietary event to all MediaControllers listening to this
+     * Session. It's up to the Controller/Session owner to determine the meaning
+     * of any events.
+     *
+     * @param event The name of the event to send
+     * @param extras Any extras included with the event
+     */
+    public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
+        if (TextUtils.isEmpty(event)) {
+            throw new IllegalArgumentException("event cannot be null or empty");
+        }
+        try {
+            mSessionLink.sendEvent(event, extras);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Error sending event", e);
+        }
+    }
+
+    /**
+     * This must be called when an app has finished performing playback. If
+     * playback is expected to start again shortly the session can be left open,
+     * but it must be released if your activity or service is being destroyed.
+     */
+    public void close() {
+        try {
+            mSessionLink.destroySession();
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Error releasing session: ", e);
+        }
+    }
+
+    /**
+     * Retrieve a token object that can be used by apps to create a
+     * {@link MediaController} for interacting with this session. The owner of
+     * the session is responsible for deciding how to distribute these tokens.
+     *
+     * @return A token that can be used to create a MediaController for this
+     *         session
+     */
+    public @NonNull MediaSession.Token getSessionToken() {
+        return mSessionToken;
+    }
+
+    /**
+     * Get a controller for this session. This is a convenience method to avoid
+     * having to cache your own controller in process.
+     *
+     * @return A controller for this session.
+     */
+    public @NonNull MediaController getController() {
+        return mController;
+    }
+
+    /**
+     * Update the current playback state.
+     *
+     * @param state The current state of playback
+     */
+    public void setPlaybackState(@Nullable PlaybackState state) {
+        mPlaybackState = state;
+        try {
+            mSessionLink.setPlaybackState(state);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
+        }
+    }
+
+    /**
+     * Update the current metadata. New metadata can be created using
+     * {@link android.media.MediaMetadata.Builder}. This operation may take time proportional to
+     * the size of the bitmap to replace large bitmaps with a scaled down copy.
+     *
+     * @param metadata The new metadata
+     * @see android.media.MediaMetadata.Builder#putBitmap
+     */
+    public void setMetadata(@Nullable MediaMetadata metadata) {
+        long duration = -1;
+        int fields = 0;
+        MediaDescription description = null;
+        if (metadata != null) {
+            metadata = (new MediaMetadata.Builder(metadata, mMaxBitmapSize)).build();
+            if (metadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+                duration = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+            }
+            fields = metadata.size();
+            description = metadata.getDescription();
+        }
+        String metadataDescription = "size=" + fields + ", description=" + description;
+
+        try {
+            mSessionLink.setMetadata(metadata, duration, metadataDescription);
+        } catch (RuntimeException e) {
+            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
+        }
+    }
+
+    /**
+     * Update the list of items in the play queue. It is an ordered list and
+     * should contain the current item, and previous or upcoming items if they
+     * exist. Specify null if there is no current play queue.
+     * <p>
+     * The queue should be of reasonable size. If the play queue is unbounded
+     * within your app, it is better to send a reasonable amount in a sliding
+     * window instead.
+     *
+     * @param queue A list of items in the play queue.
+     */
+    public void setQueue(@Nullable List<MediaSession.QueueItem> queue) {
+        try {
+            mSessionLink.setQueue(queue);
+        } catch (RuntimeException e) {
+            Log.wtf("Dead object in setQueue.", e);
+        }
+    }
+
+    /**
+     * Set the title of the play queue. The UI should display this title along
+     * with the play queue itself.
+     * e.g. "Play Queue", "Now Playing", or an album name.
+     *
+     * @param title The title of the play queue.
+     */
+    public void setQueueTitle(@Nullable CharSequence title) {
+        try {
+            mSessionLink.setQueueTitle(title);
+        } catch (RuntimeException e) {
+            Log.wtf("Dead object in setQueueTitle.", e);
+        }
+    }
+
+    /**
+     * Set the style of rating used by this session. Apps trying to set the
+     * rating should use this style. Must be one of the following:
+     * <ul>
+     * <li>{@link Rating#RATING_NONE}</li>
+     * <li>{@link Rating#RATING_3_STARS}</li>
+     * <li>{@link Rating#RATING_4_STARS}</li>
+     * <li>{@link Rating#RATING_5_STARS}</li>
+     * <li>{@link Rating#RATING_HEART}</li>
+     * <li>{@link Rating#RATING_PERCENTAGE}</li>
+     * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
+     * </ul>
+     */
+    public void setRatingType(@Rating.Style int type) {
+        try {
+            mSessionLink.setRatingType(type);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Error in setRatingType.", e);
+        }
+    }
+
+    /**
+     * Set some extras that can be associated with the {@link MediaSession}. No assumptions should
+     * be made as to how a {@link MediaController} will handle these extras.
+     * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts.
+     *
+     * @param extras The extras associated with the {@link MediaSession}.
+     */
+    public void setExtras(@Nullable Bundle extras) {
+        try {
+            mSessionLink.setExtras(extras);
+        } catch (RuntimeException e) {
+            Log.wtf("Dead object in setExtras.", e);
+        }
+    }
+
+    /**
+     * Gets the controller information who sent the current request.
+     * <p>
+     * Note: This is only valid while in a request callback, such as
+     * {@link MediaSession.Callback#onPlay}.
+     *
+     * @throws IllegalStateException If this method is called outside of
+     * {@link MediaSession.Callback} methods.
+     * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+     */
+    public @NonNull RemoteUserInfo getCurrentControllerInfo() {
+        if (mCallbackHandler == null || mCallbackHandler.mCurrentControllerInfo == null) {
+            throw new IllegalStateException(
+                    "This should be called inside of MediaSession.Callback methods");
+        }
+        return mCallbackHandler.mCurrentControllerInfo;
+    }
+
+    /**
+     * Notify the system that the remote volume changed.
+     *
+     * @param provider The provider that is handling volume changes.
+     * @hide
+     */
+    public void notifyRemoteVolumeChanged(VolumeProvider provider) {
+        synchronized (mLock) {
+            if (provider == null || provider != mVolumeProvider) {
+                Log.w(TAG, "Received update from stale volume provider");
+                return;
+            }
+        }
+        try {
+            mSessionLink.setCurrentVolume(provider.getCurrentVolume());
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Error in notifyVolumeChanged", e);
+        }
+    }
+
+    /**
+     * Returns the name of the package that sent the last media button, transport control, or
+     * command from controllers and the system. This is only valid while in a request callback, such
+     * as {@link MediaSession.Callback#onPlay}.
+     */
+    public String getCallingPackage() {
+        if (mCallbackHandler != null && mCallbackHandler.mCurrentControllerInfo != null) {
+            return mCallbackHandler.mCurrentControllerInfo.getPackageName();
+        }
+        return null;
+    }
+
+    private void dispatchPrepare(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE, null, null);
+    }
+
+    private void dispatchPrepareFromMediaId(RemoteUserInfo caller, String mediaId, Bundle extras) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras);
+    }
+
+    private void dispatchPrepareFromSearch(RemoteUserInfo caller, String query, Bundle extras) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_SEARCH, query, extras);
+    }
+
+    private void dispatchPrepareFromUri(RemoteUserInfo caller, Uri uri, Bundle extras) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_URI, uri, extras);
+    }
+
+    private void dispatchPlay(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PLAY, null, null);
+    }
+
+    private void dispatchPlayFromMediaId(RemoteUserInfo caller, String mediaId, Bundle extras) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras);
+    }
+
+    private void dispatchPlayFromSearch(RemoteUserInfo caller, String query, Bundle extras) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras);
+    }
+
+    private void dispatchPlayFromUri(RemoteUserInfo caller, Uri uri, Bundle extras) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PLAY_URI, uri, extras);
+    }
+
+    private void dispatchSkipToItem(RemoteUserInfo caller, long id) {
+        postToCallback(caller, CallbackMessageHandler.MSG_SKIP_TO_ITEM, id, null);
+    }
+
+    private void dispatchPause(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PAUSE, null, null);
+    }
+
+    private void dispatchStop(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_STOP, null, null);
+    }
+
+    private void dispatchNext(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_NEXT, null, null);
+    }
+
+    private void dispatchPrevious(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_PREVIOUS, null, null);
+    }
+
+    private void dispatchFastForward(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_FAST_FORWARD, null, null);
+    }
+
+    private void dispatchRewind(RemoteUserInfo caller) {
+        postToCallback(caller, CallbackMessageHandler.MSG_REWIND, null, null);
+    }
+
+    private void dispatchSeekTo(RemoteUserInfo caller, long pos) {
+        postToCallback(caller, CallbackMessageHandler.MSG_SEEK_TO, pos, null);
+    }
+
+    private void dispatchRate(RemoteUserInfo caller, Rating rating) {
+        postToCallback(caller, CallbackMessageHandler.MSG_RATE, rating, null);
+    }
+
+    private void dispatchCustomAction(RemoteUserInfo caller, String action, Bundle args) {
+        postToCallback(caller, CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
+    }
+
+    private void dispatchMediaButton(RemoteUserInfo caller, Intent mediaButtonIntent) {
+        postToCallback(caller, CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent, null);
+    }
+
+    private void dispatchMediaButtonDelayed(RemoteUserInfo info, Intent mediaButtonIntent,
+            long delay) {
+        postToCallbackDelayed(info, CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT,
+                mediaButtonIntent, null, delay);
+    }
+
+    private void dispatchAdjustVolume(RemoteUserInfo caller, int direction) {
+        postToCallback(caller, CallbackMessageHandler.MSG_ADJUST_VOLUME, direction, null);
+    }
+
+    private void dispatchSetVolumeTo(RemoteUserInfo caller, int volume) {
+        postToCallback(caller, CallbackMessageHandler.MSG_SET_VOLUME, volume, null);
+    }
+
+    private void dispatchCommand(RemoteUserInfo caller, String command, Bundle args,
+            ResultReceiver resultCb) {
+        Command cmd = new Command(command, args, resultCb);
+        postToCallback(caller, CallbackMessageHandler.MSG_COMMAND, cmd, null);
+    }
+
+    private void postToCallback(RemoteUserInfo caller, int what, Object obj, Bundle data) {
+        postToCallbackDelayed(caller, what, obj, data, 0);
+    }
+
+    private void postToCallbackDelayed(RemoteUserInfo caller, int what, Object obj, Bundle data,
+            long delay) {
+        synchronized (mLock) {
+            if (mCallbackHandler != null) {
+                mCallbackHandler.post(caller, what, obj, data, delay);
+            }
+        }
+    }
+
+    /**
+     * Return true if this is considered an active playback state.
+     */
+    public static boolean isActiveState(int state) {
+        switch (state) {
+            case PlaybackState.STATE_FAST_FORWARDING:
+            case PlaybackState.STATE_REWINDING:
+            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+            case PlaybackState.STATE_SKIPPING_TO_NEXT:
+            case PlaybackState.STATE_BUFFERING:
+            case PlaybackState.STATE_CONNECTING:
+            case PlaybackState.STATE_PLAYING:
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Interface for handling MediaButtoneEvent
+     */
+    public interface MediaButtonEventDelegate {
+        /**
+         * Called when a media button is pressed and this session has the
+         * highest priority or a controller sends a media button event to the
+         * session.
+         *
+         * @param mediaButtonIntent an intent containing the KeyEvent as an extra
+         * @return True if the event was handled, false otherwise.
+         */
+        boolean onMediaButtonIntent(Intent mediaButtonIntent);
+    }
+
+    /**
+     * Receives media buttons, transport controls, and commands from controllers
+     * and the system. A callback may be set using {@link #setCallback}.
+     * @hide
+     */
+    public static class CallbackWrapper implements MediaButtonEventDelegate {
+
+        private final MediaSession.Callback mCallback;
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+                MediaSessionEngine mSessionImpl;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        CallbackMessageHandler mHandler;
+        private boolean mMediaPlayPauseKeyPending;
+
+        public CallbackWrapper(MediaSession.Callback callback) {
+            mCallback = callback;
+            if (mCallback != null) {
+                mCallback.onSetMediaButtonEventDelegate(this);
+            }
+        }
+
+        /**
+         * Called when a controller has sent a command to this session.
+         * The owner of the session may handle custom commands but is not
+         * required to.
+         *
+         * @param command The command name.
+         * @param args Optional parameters for the command, may be null.
+         * @param cb A result receiver to which a result may be sent by the command, may be null.
+         */
+        public void onCommand(@NonNull String command, @Nullable Bundle args,
+                @Nullable ResultReceiver cb) {
+            if (mCallback != null) {
+                mCallback.onCommand(command, args, cb);
+            }
+        }
+
+        /**
+         * Called when a media button is pressed and this session has the
+         * highest priority or a controller sends a media button event to the
+         * session. The default behavior will call the relevant method if the
+         * action for it was set.
+         * <p>
+         * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
+         * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
+         *
+         * @param mediaButtonIntent an intent containing the KeyEvent as an
+         *            extra
+         * @return True if the event was handled, false otherwise.
+         */
+        public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
+            return mCallback == null ? false : mCallback.onMediaButtonEvent(mediaButtonIntent);
+        }
+
+        private void handleMediaPlayPauseKeySingleTapIfPending() {
+            if (!mMediaPlayPauseKeyPending) {
+                return;
+            }
+            mMediaPlayPauseKeyPending = false;
+            mHandler.removeMessages(CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
+            PlaybackState state = mSessionImpl.mPlaybackState;
+            long validActions = state == null ? 0 : state.getActions();
+            boolean isPlaying = state != null
+                    && state.getState() == PlaybackState.STATE_PLAYING;
+            boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
+                    | PlaybackState.ACTION_PLAY)) != 0;
+            boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
+                    | PlaybackState.ACTION_PAUSE)) != 0;
+            if (isPlaying && canPause) {
+                onPause();
+            } else if (!isPlaying && canPlay) {
+                onPlay();
+            }
+        }
+
+        /**
+         * Override to handle requests to prepare playback. During the preparation, a session should
+         * not hold audio focus in order to allow other sessions play seamlessly. The state of
+         * playback should be updated to {@link PlaybackState#STATE_PAUSED} after the preparation is
+         * done.
+         */
+        public void onPrepare() {
+            if (mCallback != null) {
+                mCallback.onPrepare();
+            }
+        }
+
+        /**
+         * Override to handle requests to prepare for playing a specific mediaId that was provided
+         * by your app's {@link MediaBrowserService}. During the preparation, a session should not
+         * hold audio focus in order to allow other sessions play seamlessly. The state of playback
+         * should be updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+         * The playback of the prepared content should start in the implementation of
+         * {@link #onPlay}. Override {@link #onPlayFromMediaId} to handle requests for starting
+         * playback without preparation.
+         */
+        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+            if (mCallback != null) {
+                mCallback.onPrepareFromMediaId(mediaId, extras);
+            }
+        }
+
+        /**
+         * Override to handle requests to prepare playback from a search query. An empty query
+         * indicates that the app may prepare any music. The implementation should attempt to make a
+         * smart choice about what to play. During the preparation, a session should not hold audio
+         * focus in order to allow other sessions play seamlessly. The state of playback should be
+         * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done. The playback
+         * of the prepared content should start in the implementation of {@link #onPlay}. Override
+         * {@link #onPlayFromSearch} to handle requests for starting playback without preparation.
+         */
+        public void onPrepareFromSearch(String query, Bundle extras) {
+            if (mCallback != null) {
+                mCallback.onPrepareFromSearch(query, extras);
+            }
+        }
+
+        /**
+         * Override to handle requests to prepare a specific media item represented by a URI.
+         * During the preparation, a session should not hold audio focus in order to allow
+         * other sessions play seamlessly. The state of playback should be updated to
+         * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+         * The playback of the prepared content should start in the implementation of
+         * {@link #onPlay}. Override {@link #onPlayFromUri} to handle requests
+         * for starting playback without preparation.
+         */
+        public void onPrepareFromUri(Uri uri, Bundle extras) {
+            if (mCallback != null) {
+                mCallback.onPrepareFromUri(uri, extras);
+            }
+        }
+
+        /**
+         * Override to handle requests to begin playback.
+         */
+        public void onPlay() {
+            if (mCallback != null) {
+                mCallback.onPlay();
+            }
+        }
+
+        /**
+         * Override to handle requests to begin playback from a search query. An
+         * empty query indicates that the app may play any music. The
+         * implementation should attempt to make a smart choice about what to
+         * play.
+         */
+        public void onPlayFromSearch(String query, Bundle extras) {
+            if (mCallback != null) {
+                mCallback.onPlayFromSearch(query, extras);
+            }
+        }
+
+        /**
+         * Override to handle requests to play a specific mediaId that was
+         * provided by your app's {@link MediaBrowserService}.
+         */
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            if (mCallback != null) {
+                mCallback.onPlayFromMediaId(mediaId, extras);
+            }
+        }
+
+        /**
+         * Override to handle requests to play a specific media item represented by a URI.
+         */
+        public void onPlayFromUri(Uri uri, Bundle extras) {
+            if (mCallback != null) {
+                mCallback.onPlayFromUri(uri, extras);
+            }
+        }
+
+        /**
+         * Override to handle requests to play an item with a given id from the
+         * play queue.
+         */
+        public void onSkipToQueueItem(long id) {
+            if (mCallback != null) {
+                mCallback.onSkipToQueueItem(id);
+            }
+        }
+
+        /**
+         * Override to handle requests to pause playback.
+         */
+        public void onPause() {
+            if (mCallback != null) {
+                mCallback.onPause();
+            }
+        }
+
+        /**
+         * Override to handle requests to skip to the next media item.
+         */
+        public void onSkipToNext() {
+            if (mCallback != null) {
+                mCallback.onSkipToNext();
+            }
+        }
+
+        /**
+         * Override to handle requests to skip to the previous media item.
+         */
+        public void onSkipToPrevious() {
+            if (mCallback != null) {
+                mCallback.onSkipToPrevious();
+            }
+        }
+
+        /**
+         * Override to handle requests to fast forward.
+         */
+        public void onFastForward() {
+            if (mCallback != null) {
+                mCallback.onFastForward();
+            }
+        }
+
+        /**
+         * Override to handle requests to rewind.
+         */
+        public void onRewind() {
+            if (mCallback != null) {
+                mCallback.onRewind();
+            }
+        }
+
+        /**
+         * Override to handle requests to stop playback.
+         */
+        public void onStop() {
+            if (mCallback != null) {
+                mCallback.onStop();
+            }
+        }
+
+        /**
+         * Override to handle requests to seek to a specific position in ms.
+         *
+         * @param pos New position to move to, in milliseconds.
+         */
+        public void onSeekTo(long pos) {
+            if (mCallback != null) {
+                mCallback.onSeekTo(pos);
+            }
+        }
+
+        /**
+         * Override to handle the item being rated.
+         *
+         * @param rating
+         */
+        public void onSetRating(@NonNull Rating rating) {
+            if (mCallback != null) {
+                mCallback.onSetRating(rating);
+            }
+        }
+
+        /**
+         * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be
+         * performed.
+         *
+         * @param action The action that was originally sent in the
+         *               {@link PlaybackState.CustomAction}.
+         * @param extras Optional extras specified by the {@link MediaController}.
+         */
+        public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
+            if (mCallback != null) {
+                mCallback.onCustomAction(action, extras);
+            }
+        }
+
+        @Override
+        public boolean onMediaButtonIntent(Intent mediaButtonIntent) {
+            if (mSessionImpl != null && mHandler != null
+                    && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
+                KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+                if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
+                    PlaybackState state = mSessionImpl.mPlaybackState;
+                    long validActions = state == null ? 0 : state.getActions();
+                    switch (ke.getKeyCode()) {
+                        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+                        case KeyEvent.KEYCODE_HEADSETHOOK:
+                            if (ke.getRepeatCount() > 0) {
+                                // Consider long-press as a single tap.
+                                handleMediaPlayPauseKeySingleTapIfPending();
+                            } else if (mMediaPlayPauseKeyPending) {
+                                // Consider double tap as the next.
+                                mHandler.removeMessages(CallbackMessageHandler
+                                        .MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT);
+                                mMediaPlayPauseKeyPending = false;
+                                if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
+                                    onSkipToNext();
+                                }
+                            } else {
+                                mMediaPlayPauseKeyPending = true;
+                                mSessionImpl.dispatchMediaButtonDelayed(
+                                        mSessionImpl.getCurrentControllerInfo(),
+                                        mediaButtonIntent, ViewConfiguration.getDoubleTapTimeout());
+                            }
+                            return true;
+                        default:
+                            // If another key is pressed within double tap timeout, consider the
+                            // pending play/pause as a single tap to handle media keys in order.
+                            handleMediaPlayPauseKeySingleTapIfPending();
+                            break;
+                    }
+
+                    switch (ke.getKeyCode()) {
+                        case KeyEvent.KEYCODE_MEDIA_PLAY:
+                            if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
+                                onPlay();
+                                return true;
+                            }
+                            break;
+                        case KeyEvent.KEYCODE_MEDIA_PAUSE:
+                            if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
+                                onPause();
+                                return true;
+                            }
+                            break;
+                        case KeyEvent.KEYCODE_MEDIA_NEXT:
+                            if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
+                                onSkipToNext();
+                                return true;
+                            }
+                            break;
+                        case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+                            if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
+                                onSkipToPrevious();
+                                return true;
+                            }
+                            break;
+                        case KeyEvent.KEYCODE_MEDIA_STOP:
+                            if ((validActions & PlaybackState.ACTION_STOP) != 0) {
+                                onStop();
+                                return true;
+                            }
+                            break;
+                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+                            if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
+                                onFastForward();
+                                return true;
+                            }
+                            break;
+                        case KeyEvent.KEYCODE_MEDIA_REWIND:
+                            if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
+                                onRewind();
+                                return true;
+                            }
+                            break;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @SystemApi
+    public static final class CallbackStub extends SessionCallbackLink.CallbackStub {
+        private WeakReference<MediaSessionEngine> mSessionImpl;
+
+        private static RemoteUserInfo createRemoteUserInfo(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            return new RemoteUserInfo(packageName, pid, uid,
+                    caller != null ? caller.getBinder() : null);
+        }
+
+        public CallbackStub() {
+        }
+
+        @Override
+        public void onCommand(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, String command, Bundle args, ResultReceiver cb) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchCommand(createRemoteUserInfo(packageName, pid, uid, caller),
+                        command, args, cb);
+            }
+        }
+
+        @Override
+        public void onMediaButton(String packageName, int pid, int uid, Intent mediaButtonIntent,
+                int sequenceNumber, ResultReceiver cb) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            try {
+                if (sessionImpl != null) {
+                    sessionImpl.dispatchMediaButton(
+                            createRemoteUserInfo(packageName, pid, uid, null), mediaButtonIntent);
+                }
+            } finally {
+                if (cb != null) {
+                    cb.send(sequenceNumber, null);
+                }
+            }
+        }
+
+        @Override
+        public void onMediaButtonFromController(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, Intent mediaButtonIntent) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchMediaButton(createRemoteUserInfo(packageName, pid, uid, caller),
+                        mediaButtonIntent);
+            }
+        }
+
+        @Override
+        public void onPrepare(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPrepare(createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onPrepareFromMediaId(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, String mediaId,
+                Bundle extras) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPrepareFromMediaId(
+                        createRemoteUserInfo(packageName, pid, uid, caller), mediaId, extras);
+            }
+        }
+
+        @Override
+        public void onPrepareFromSearch(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, String query,
+                Bundle extras) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPrepareFromSearch(
+                        createRemoteUserInfo(packageName, pid, uid, caller), query, extras);
+            }
+        }
+
+        @Override
+        public void onPrepareFromUri(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, Uri uri, Bundle extras) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPrepareFromUri(
+                        createRemoteUserInfo(packageName, pid, uid, caller), uri, extras);
+            }
+        }
+
+        @Override
+        public void onPlay(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPlay(createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onPlayFromMediaId(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, String mediaId,
+                Bundle extras) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPlayFromMediaId(
+                        createRemoteUserInfo(packageName, pid, uid, caller), mediaId, extras);
+            }
+        }
+
+        @Override
+        public void onPlayFromSearch(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, String query,
+                Bundle extras) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPlayFromSearch(
+                        createRemoteUserInfo(packageName, pid, uid, caller), query, extras);
+            }
+        }
+
+        @Override
+        public void onPlayFromUri(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, Uri uri, Bundle extras) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPlayFromUri(
+                        createRemoteUserInfo(packageName, pid, uid, caller), uri, extras);
+            }
+        }
+
+        @Override
+        public void onSkipToTrack(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, long id) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchSkipToItem(
+                        createRemoteUserInfo(packageName, pid, uid, caller), id);
+            }
+        }
+
+        @Override
+        public void onPause(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPause(createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onStop(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchStop(createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onNext(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchNext(createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onPrevious(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchPrevious(createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onFastForward(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchFastForward(
+                        createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onRewind(String packageName, int pid, int uid,
+                ControllerCallbackLink caller) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchRewind(createRemoteUserInfo(packageName, pid, uid, caller));
+            }
+        }
+
+        @Override
+        public void onSeekTo(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, long pos) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchSeekTo(
+                        createRemoteUserInfo(packageName, pid, uid, caller), pos);
+            }
+        }
+
+        @Override
+        public void onRate(String packageName, int pid, int uid, ControllerCallbackLink caller,
+                Rating rating) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchRate(
+                        createRemoteUserInfo(packageName, pid, uid, caller), rating);
+            }
+        }
+
+        @Override
+        public void onCustomAction(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, String action, Bundle args) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchCustomAction(
+                        createRemoteUserInfo(packageName, pid, uid, caller), action, args);
+            }
+        }
+
+        @Override
+        public void onAdjustVolume(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, int direction) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchAdjustVolume(
+                        createRemoteUserInfo(packageName, pid, uid, caller), direction);
+            }
+        }
+
+        @Override
+        public void onSetVolumeTo(String packageName, int pid, int uid,
+                ControllerCallbackLink caller, int value) {
+            MediaSessionEngine sessionImpl = mSessionImpl.get();
+            if (sessionImpl != null) {
+                sessionImpl.dispatchSetVolumeTo(
+                        createRemoteUserInfo(packageName, pid, uid, caller), value);
+            }
+        }
+
+        void setSessionImpl(MediaSessionEngine sessionImpl) {
+            mSessionImpl = new WeakReference<>(sessionImpl);
+        }
+    }
+
+    /**
+     * A single item that is part of the play queue. It contains a description
+     * of the item and its id in the queue.
+     */
+    public static final class QueueItem {
+        /**
+         * This id is reserved. No items can be explicitly assigned this id.
+         */
+        public static final int UNKNOWN_ID = -1;
+
+        private final MediaDescription mDescription;
+        private final long mId;
+
+        /**
+         * Create a new {@link MediaSession.QueueItem}.
+         *
+         * @param description The {@link MediaDescription} for this item.
+         * @param id An identifier for this item. It must be unique within the
+         *            play queue and cannot be {@link #UNKNOWN_ID}.
+         */
+        public QueueItem(MediaDescription description, long id) {
+            if (description == null) {
+                throw new IllegalArgumentException("Description cannot be null.");
+            }
+            if (id == UNKNOWN_ID) {
+                throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID");
+            }
+            mDescription = description;
+            mId = id;
+        }
+
+        public QueueItem(Parcel in) {
+            mDescription = MediaDescription.CREATOR.createFromParcel(in);
+            mId = in.readLong();
+        }
+
+        /**
+         * Get the description for this item.
+         */
+        public MediaDescription getDescription() {
+            return mDescription;
+        }
+
+        /**
+         * Get the queue id for this item.
+         */
+        public long getQueueId() {
+            return mId;
+        }
+
+        /**
+         * Flatten this object in to a Parcel.
+         *
+         * @param dest The Parcel in which the object should be written.
+         * @param flags Additional flags about how the object should be written.
+         */
+        public void writeToParcel(Parcel dest, int flags) {
+            mDescription.writeToParcel(dest, flags);
+            dest.writeLong(mId);
+        }
+
+        @Override
+        public String toString() {
+            return "MediaSession.QueueItem {" + "Description=" + mDescription + ", Id=" + mId
+                    + " }";
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null) {
+                return false;
+            }
+
+            if (!(o instanceof QueueItem)) {
+                return false;
+            }
+
+            final QueueItem item = (QueueItem) o;
+            if (mId != item.mId) {
+                return false;
+            }
+
+            if (!Objects.equals(mDescription, item.mDescription)) {
+                return false;
+            }
+
+            return true;
+        }
+    }
+
+    private static final class Command {
+        public final String command;
+        public final Bundle extras;
+        public final ResultReceiver stub;
+
+        Command(String command, Bundle extras, ResultReceiver stub) {
+            this.command = command;
+            this.extras = extras;
+            this.stub = stub;
+        }
+    }
+
+    private class CallbackMessageHandler extends Handler {
+        private static final int MSG_COMMAND = 1;
+        private static final int MSG_MEDIA_BUTTON = 2;
+        private static final int MSG_PREPARE = 3;
+        private static final int MSG_PREPARE_MEDIA_ID = 4;
+        private static final int MSG_PREPARE_SEARCH = 5;
+        private static final int MSG_PREPARE_URI = 6;
+        private static final int MSG_PLAY = 7;
+        private static final int MSG_PLAY_MEDIA_ID = 8;
+        private static final int MSG_PLAY_SEARCH = 9;
+        private static final int MSG_PLAY_URI = 10;
+        private static final int MSG_SKIP_TO_ITEM = 11;
+        private static final int MSG_PAUSE = 12;
+        private static final int MSG_STOP = 13;
+        private static final int MSG_NEXT = 14;
+        private static final int MSG_PREVIOUS = 15;
+        private static final int MSG_FAST_FORWARD = 16;
+        private static final int MSG_REWIND = 17;
+        private static final int MSG_SEEK_TO = 18;
+        private static final int MSG_RATE = 19;
+        private static final int MSG_CUSTOM_ACTION = 20;
+        private static final int MSG_ADJUST_VOLUME = 21;
+        private static final int MSG_SET_VOLUME = 22;
+        private static final int MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT = 23;
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        CallbackWrapper mCallbackWrapper;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        RemoteUserInfo mCurrentControllerInfo;
+
+        CallbackMessageHandler(Looper looper, CallbackWrapper callbackWrapper) {
+            super(looper);
+            mCallbackWrapper = callbackWrapper;
+            mCallbackWrapper.mHandler = this;
+        }
+
+        void post(RemoteUserInfo caller, int what, Object obj, Bundle data, long delayMs) {
+            Pair<RemoteUserInfo, Object> objWithCaller = Pair.create(caller, obj);
+            Message msg = obtainMessage(what, objWithCaller);
+            msg.setAsynchronous(true);
+            msg.setData(data);
+            if (delayMs > 0) {
+                sendMessageDelayed(msg, delayMs);
+            } else {
+                sendMessage(msg);
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            mCurrentControllerInfo = ((Pair<RemoteUserInfo, Object>) msg.obj).first;
+
+            VolumeProvider vp;
+            Object obj = ((Pair<RemoteUserInfo, Object>) msg.obj).second;
+
+            switch (msg.what) {
+                case MSG_COMMAND:
+                    Command cmd = (Command) obj;
+                    mCallbackWrapper.onCommand(cmd.command, cmd.extras, cmd.stub);
+                    break;
+                case MSG_MEDIA_BUTTON:
+                    mCallbackWrapper.onMediaButtonEvent((Intent) obj);
+                    break;
+                case MSG_PREPARE:
+                    mCallbackWrapper.onPrepare();
+                    break;
+                case MSG_PREPARE_MEDIA_ID:
+                    mCallbackWrapper.onPrepareFromMediaId((String) obj, msg.getData());
+                    break;
+                case MSG_PREPARE_SEARCH:
+                    mCallbackWrapper.onPrepareFromSearch((String) obj, msg.getData());
+                    break;
+                case MSG_PREPARE_URI:
+                    mCallbackWrapper.onPrepareFromUri((Uri) obj, msg.getData());
+                    break;
+                case MSG_PLAY:
+                    mCallbackWrapper.onPlay();
+                    break;
+                case MSG_PLAY_MEDIA_ID:
+                    mCallbackWrapper.onPlayFromMediaId((String) obj, msg.getData());
+                    break;
+                case MSG_PLAY_SEARCH:
+                    mCallbackWrapper.onPlayFromSearch((String) obj, msg.getData());
+                    break;
+                case MSG_PLAY_URI:
+                    mCallbackWrapper.onPlayFromUri((Uri) obj, msg.getData());
+                    break;
+                case MSG_SKIP_TO_ITEM:
+                    mCallbackWrapper.onSkipToQueueItem((Long) obj);
+                    break;
+                case MSG_PAUSE:
+                    mCallbackWrapper.onPause();
+                    break;
+                case MSG_STOP:
+                    mCallbackWrapper.onStop();
+                    break;
+                case MSG_NEXT:
+                    mCallbackWrapper.onSkipToNext();
+                    break;
+                case MSG_PREVIOUS:
+                    mCallbackWrapper.onSkipToPrevious();
+                    break;
+                case MSG_FAST_FORWARD:
+                    mCallbackWrapper.onFastForward();
+                    break;
+                case MSG_REWIND:
+                    mCallbackWrapper.onRewind();
+                    break;
+                case MSG_SEEK_TO:
+                    mCallbackWrapper.onSeekTo((Long) obj);
+                    break;
+                case MSG_RATE:
+                    mCallbackWrapper.onSetRating((Rating) obj);
+                    break;
+                case MSG_CUSTOM_ACTION:
+                    mCallbackWrapper.onCustomAction((String) obj, msg.getData());
+                    break;
+                case MSG_ADJUST_VOLUME:
+                    synchronized (mLock) {
+                        vp = mVolumeProvider;
+                    }
+                    if (vp != null) {
+                        vp.onAdjustVolume((int) obj);
+                    }
+                    break;
+                case MSG_SET_VOLUME:
+                    synchronized (mLock) {
+                        vp = mVolumeProvider;
+                    }
+                    if (vp != null) {
+                        vp.onSetVolumeTo((int) obj);
+                    }
+                    break;
+                case MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT:
+                    mCallbackWrapper.handleMediaPlayPauseKeySingleTapIfPending();
+                    break;
+            }
+            mCurrentControllerInfo = null;
+        }
+    }
+}
diff --git a/media/java/android/media/session/SessionCallbackLink.java b/media/java/android/media/session/SessionCallbackLink.java
index 0265687b..0dbf427 100644
--- a/media/java/android/media/session/SessionCallbackLink.java
+++ b/media/java/android/media/session/SessionCallbackLink.java
@@ -669,7 +669,7 @@
         @Override
         public void notifyCommand(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, String command, Bundle args, ResultReceiver cb) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onCommand(packageName, pid, uid, caller, command, args, cb);
@@ -681,7 +681,7 @@
         @Override
         public void notifyMediaButton(String packageName, int pid, int uid,
                 Intent mediaButtonIntent, int sequenceNumber, ResultReceiver cb) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onMediaButton(packageName, pid, uid, mediaButtonIntent,
@@ -694,7 +694,7 @@
         @Override
         public void notifyMediaButtonFromController(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, Intent mediaButtonIntent) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onMediaButtonFromController(packageName, pid, uid, caller,
@@ -707,7 +707,7 @@
         @Override
         public void notifyPrepare(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPrepare(packageName, pid, uid, caller);
@@ -719,7 +719,7 @@
         @Override
         public void notifyPrepareFromMediaId(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, String mediaId, Bundle extras) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPrepareFromMediaId(packageName, pid, uid, caller, mediaId, extras);
@@ -731,7 +731,7 @@
         @Override
         public void notifyPrepareFromSearch(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, String query, Bundle extras) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPrepareFromSearch(packageName, pid, uid, caller, query, extras);
@@ -743,7 +743,7 @@
         @Override
         public void notifyPrepareFromUri(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, Uri uri, Bundle extras) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPrepareFromUri(packageName, pid, uid, caller, uri, extras);
@@ -755,7 +755,7 @@
         @Override
         public void notifyPlay(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPlay(packageName, pid, uid, caller);
@@ -767,7 +767,7 @@
         @Override
         public void notifyPlayFromMediaId(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, String mediaId, Bundle extras) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPlayFromMediaId(packageName, pid, uid, caller, mediaId, extras);
@@ -779,7 +779,7 @@
         @Override
         public void notifyPlayFromSearch(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, String query, Bundle extras) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPlayFromSearch(packageName, pid, uid, caller, query, extras);
@@ -791,7 +791,7 @@
         @Override
         public void notifyPlayFromUri(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, Uri uri, Bundle extras) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPlayFromUri(packageName, pid, uid, caller, uri, extras);
@@ -803,7 +803,7 @@
         @Override
         public void notifySkipToTrack(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, long id) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onSkipToTrack(packageName, pid, uid, caller, id);
@@ -815,7 +815,7 @@
         @Override
         public void notifyPause(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPause(packageName, pid, uid, caller);
@@ -827,7 +827,7 @@
         @Override
         public void notifyStop(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onStop(packageName, pid, uid, caller);
@@ -839,7 +839,7 @@
         @Override
         public void notifyNext(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onNext(packageName, pid, uid, caller);
@@ -851,7 +851,7 @@
         @Override
         public void notifyPrevious(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onPrevious(packageName, pid, uid, caller);
@@ -863,7 +863,7 @@
         @Override
         public void notifyFastForward(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onFastForward(packageName, pid, uid, caller);
@@ -875,7 +875,7 @@
         @Override
         public void notifyRewind(String packageName, int pid, int uid,
                 ControllerCallbackLink caller) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onRewind(packageName, pid, uid, caller);
@@ -887,7 +887,7 @@
         @Override
         public void notifySeekTo(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, long pos) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onSeekTo(packageName, pid, uid, caller, pos);
@@ -899,7 +899,7 @@
         @Override
         public void notifyRate(String packageName, int pid, int uid, ControllerCallbackLink caller,
                 Rating rating) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onRate(packageName, pid, uid, caller, rating);
@@ -910,7 +910,7 @@
 
         public void notifyCustomAction(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, String action, Bundle args) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onCustomAction(packageName, pid, uid, caller, action, args);
@@ -922,7 +922,7 @@
         @Override
         public void notifyAdjustVolume(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, int direction) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onAdjustVolume(packageName, pid, uid, caller, direction);
@@ -934,7 +934,7 @@
         @Override
         public void notifySetVolumeTo(String packageName, int pid, int uid,
                 ControllerCallbackLink caller, int value) {
-            ensureMediasControlPermission();
+            ensureMediaControlPermission();
             final long token = Binder.clearCallingIdentity();
             try {
                 mCallbackStub.onSetVolumeTo(packageName, pid, uid, caller, value);
@@ -943,7 +943,7 @@
             }
         }
 
-        private void ensureMediasControlPermission() {
+        private void ensureMediaControlPermission() {
             // Allow API calls from the System UI
             if (mContext.checkCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE)
                     == PackageManager.PERMISSION_GRANTED) {