Merge "Revert "Revert "Move MediaBufferXXX from foundation to libmediaextractor"""
diff --git a/packages/MediaComponents/res/layout/media_controller.xml b/packages/MediaComponents/res/layout/media_controller.xml
index 4d05546..74a8306 100644
--- a/packages/MediaComponents/res/layout/media_controller.xml
+++ b/packages/MediaComponents/res/layout/media_controller.xml
@@ -136,7 +136,13 @@
             android:layout_height="wrap_content"
             android:layout_centerVertical="true"
             android:visibility="gone"
-            android:orientation="horizontal" >
+            android:orientation="horizontal"
+            android:gravity="center">
+
+            <LinearLayout
+                android:id="@+id/custom_buttons"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
 
             <ImageButton
                 android:id="@+id/mute"
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index 43f8473..6567cd0 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -222,38 +222,38 @@
 
     @Override
     public void play_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
     }
 
     @Override
     public void pause_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
     }
 
     @Override
     public void stop_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
     }
 
     @Override
     public void skipToPrevious_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
     }
 
     @Override
     public void skipToNext_impl() {
-        sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
     }
 
-    private void sendCommand(int code) {
-        // TODO(jaewan): optimization) Cache Command objects?
-        Command command = new Command(code);
-        // TODO(jaewan): Check if the command is in the allowed group.
+    private void sendTransportControlCommand(int commandCode) {
+        sendTransportControlCommand(commandCode, 0);
+    }
 
+    private void sendTransportControlCommand(int commandCode, long arg) {
         final IMediaSession2 binder = mSessionBinder;
         if (binder != null) {
             try {
-                binder.sendCommand(mSessionCallbackStub, command.toBundle(), null);
+                binder.sendTransportControlCommand(mSessionCallbackStub, commandCode, arg);
             } catch (RemoteException e) {
                 Log.w(TAG, "Cannot connect to the service or the session is gone", e);
             }
@@ -341,27 +341,28 @@
 
     @Override
     public void prepare_impl() {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
     }
 
     @Override
     public void fastForward_impl() {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_FAST_FORWARD);
     }
 
     @Override
     public void rewind_impl() {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_REWIND);
     }
 
     @Override
     public void seekTo_impl(long pos) {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SEEK_TO, pos);
     }
 
     @Override
     public void setCurrentPlaylistItem_impl(int index) {
-        // TODO(jaewan): Implement
+        sendTransportControlCommand(
+                MediaSession2.COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM, index);
     }
 
     @Override
diff --git a/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
index bbb3411..d713f78 100644
--- a/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
@@ -22,10 +22,9 @@
 import android.media.MediaLibraryService2;
 import android.media.MediaLibraryService2.MediaLibrarySession;
 import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
 import android.media.MediaSession2;
 import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
 import android.media.MediaSessionService2;
 import android.media.VolumeProvider;
 import android.media.update.MediaLibraryService2Provider;
@@ -68,8 +67,8 @@
         private final MediaLibrarySessionCallback mCallback;
 
         public MediaLibrarySessionImpl(Context context, MediaLibrarySession instance,
-                MediaPlayerBase player, String id, VolumeProvider volumeProvider, int ratingType,
-                PendingIntent sessionActivity, Executor callbackExecutor,
+                MediaPlayerInterface player, String id, VolumeProvider volumeProvider,
+                int ratingType, PendingIntent sessionActivity, Executor callbackExecutor,
                 MediaLibrarySessionCallback callback)  {
             super(context, instance, player, id, volumeProvider, ratingType, sessionActivity,
                     callbackExecutor, callback);
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
index 22a3187..28834a3 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
@@ -24,7 +24,7 @@
 import android.media.AudioAttributes;
 import android.media.IMediaSession2Callback;
 import android.media.MediaItem2;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
 import android.media.MediaSession2;
 import android.media.MediaSession2.Builder;
 import android.media.MediaSession2.Command;
@@ -39,11 +39,11 @@
 import android.media.session.MediaSessionManager;
 import android.media.update.MediaSession2Provider;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.ResultReceiver;
+import android.support.annotation.GuardedBy;
 import android.util.Log;
+
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
@@ -53,20 +53,21 @@
     private static final String TAG = "MediaSession2";
     private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG);
 
-    private final MediaSession2 mInstance;
+    private final Object mLock = new Object();
 
+    private final MediaSession2 mInstance;
     private final Context mContext;
     private final String mId;
-    private final Handler mHandler;
     private final Executor mCallbackExecutor;
+    private final SessionCallback mCallback;
     private final MediaSession2Stub mSessionStub;
     private final SessionToken2 mSessionToken;
-
-    private MediaPlayerBase mPlayer;
-
     private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private MediaPlayerInterface mPlayer;
+    @GuardedBy("mLock")
     private MyPlaybackListener mListener;
-    private MediaSession2 instance;
 
     /**
      * Can be only called by the {@link Builder#build()}.
@@ -80,7 +81,7 @@
      * @param ratingType
      * @param sessionActivity
      */
-    public MediaSession2Impl(Context context, MediaSession2 instance, MediaPlayerBase player,
+    public MediaSession2Impl(Context context, MediaSession2 instance, MediaPlayerInterface player,
             String id, VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
             Executor callbackExecutor, SessionCallback callback) {
         mInstance = instance;
@@ -90,9 +91,9 @@
         // Initialize finals first.
         mContext = context;
         mId = id;
-        mHandler = new Handler(Looper.myLooper());
+        mCallback = callback;
         mCallbackExecutor = callbackExecutor;
-        mSessionStub = new MediaSession2Stub(this, callback);
+        mSessionStub = new MediaSession2Stub(this);
         // Ask server to create session token for following reasons.
         //   1. Make session ID unique per package.
         //      Server can only know if the package has another process and has another session
@@ -118,7 +119,8 @@
     //               setPlayer(null). Token can be available when player is null, and
     //               controller can also attach to session.
     @Override
-    public void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider) throws IllegalArgumentException {
+    public void setPlayer_impl(MediaPlayerInterface player, VolumeProvider volumeProvider)
+            throws IllegalArgumentException {
         ensureCallingThread();
         if (player == null) {
             throw new IllegalArgumentException("player shouldn't be null");
@@ -126,27 +128,25 @@
         setPlayerInternal(player);
     }
 
-    private void setPlayerInternal(MediaPlayerBase player) {
-        if (mPlayer == player) {
-            // Player didn't changed. No-op.
-            return;
+    private void setPlayerInternal(MediaPlayerInterface player) {
+        synchronized (mLock) {
+            if (mPlayer == player) {
+                // Player didn't changed. No-op.
+                return;
+            }
+            if (mPlayer != null && mListener != null) {
+                // This might not work for a poorly implemented player.
+                mPlayer.removePlaybackListener(mListener);
+            }
+            mListener = new MyPlaybackListener(this, player);
+            player.addPlaybackListener(mCallbackExecutor, mListener);
+            mPlayer = player;
         }
-        // TODO(jaewan): Find equivalent for the executor
-        //mHandler.removeCallbacksAndMessages(null);
-        if (mPlayer != null && mListener != null) {
-            // This might not work for a poorly implemented player.
-            mPlayer.removePlaybackListener(mListener);
-        }
-        mListener = new MyPlaybackListener(this, player);
-        player.addPlaybackListener(mCallbackExecutor, mListener);
-        notifyPlaybackStateChanged(player.getPlaybackState());
-        mPlayer = player;
+        notifyPlaybackStateChangedNotLocked(player.getPlaybackState());
     }
 
     @Override
     public void close_impl() {
-        // Flush any pending messages.
-        mHandler.removeCallbacksAndMessages(null);
         if (mSessionStub != null) {
             if (DEBUG) {
                 Log.d(TAG, "session is now unavailable, id=" + mId);
@@ -154,10 +154,18 @@
             // Invalidate previously published session stub.
             mSessionStub.destroyNotLocked();
         }
+        synchronized (mLock) {
+            if (mPlayer != null) {
+                // close can be called multiple times
+                mPlayer.removePlaybackListener(mListener);
+                mPlayer = null;
+                return;
+            }
+        }
     }
 
     @Override
-    public MediaPlayerBase getPlayer_impl() {
+    public MediaPlayerInterface getPlayer_impl() {
         return getPlayer();
     }
 
@@ -233,7 +241,7 @@
     // TODO(jaewan): Implement follows
     //////////////////////////////////////////////////////////////////////////////////////
     @Override
-    public void setPlayer_impl(MediaPlayerBase player) {
+    public void setPlayer_impl(MediaPlayerInterface player) {
         // TODO(jaewan): Implement
     }
 
@@ -265,27 +273,37 @@
 
     @Override
     public void prepare_impl() {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.prepare();
     }
 
     @Override
     public void fastForward_impl() {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.fastForward();
     }
 
     @Override
     public void rewind_impl() {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.rewind();
     }
 
     @Override
     public void seekTo_impl(long pos) {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.seekTo(pos);
     }
 
     @Override
     public void setCurrentPlaylistItem_impl(int index) {
-        // TODO(jaewan): Implement
+        ensureCallingThread();
+        ensurePlayer();
+        mPlayer.setCurrentPlaylistItem(index);
     }
 
     ///////////////////////////////////////////////////
@@ -321,14 +339,14 @@
         }
     }
 
-    Handler getHandler() {
-        return mHandler;
-    }
-
-    private void notifyPlaybackStateChanged(PlaybackState2 state) {
+    private void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
+        List<PlaybackListenerHolder> listeners = new ArrayList<>();
+        synchronized (mLock) {
+            listeners.addAll(mListeners);
+        }
         // Notify to listeners added directly to this session
-        for (int i = 0; i < mListeners.size(); i++) {
-            mListeners.get(i).postPlaybackChange(state);
+        for (int i = 0; i < listeners.size(); i++) {
+            listeners.get(i).postPlaybackChange(state);
         }
         // Notify to controllers as well.
         mSessionStub.notifyPlaybackStateChangedNotLocked(state);
@@ -342,15 +360,23 @@
         return mInstance;
     }
 
-    MediaPlayerBase getPlayer() {
+    MediaPlayerInterface getPlayer() {
         return mPlayer;
     }
 
-    private static class MyPlaybackListener implements MediaPlayerBase.PlaybackListener {
-        private final WeakReference<MediaSession2Impl> mSession;
-        private final MediaPlayerBase mPlayer;
+    Executor getCallbackExecutor() {
+        return mCallbackExecutor;
+    }
 
-        private MyPlaybackListener(MediaSession2Impl session, MediaPlayerBase player) {
+    SessionCallback getCallback() {
+        return mCallback;
+    }
+
+    private static class MyPlaybackListener implements MediaPlayerInterface.PlaybackListener {
+        private final WeakReference<MediaSession2Impl> mSession;
+        private final MediaPlayerInterface mPlayer;
+
+        private MyPlaybackListener(MediaSession2Impl session, MediaPlayerInterface player) {
             mSession = new WeakReference<>(session);
             mPlayer = player;
         }
@@ -363,7 +389,7 @@
                         new IllegalStateException());
                 return;
             }
-            session.notifyPlaybackStateChanged(state);
+            session.notifyPlaybackStateChangedNotLocked(state);
         }
     }
 
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index 2f75dfa..69e8498 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
@@ -51,30 +51,19 @@
     private static final boolean DEBUG = true; // TODO(jaewan): Rename.
 
     private final Object mLock = new Object();
-    private final CommandHandler mCommandHandler;
     private final WeakReference<MediaSession2Impl> mSession;
-    private final Context mContext;
-    private final SessionCallback mSessionCallback;
-    private final MediaLibrarySessionCallback mLibraryCallback;
 
     @GuardedBy("mLock")
     private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
 
-    public MediaSession2Stub(MediaSession2Impl session, SessionCallback callback) {
+    public MediaSession2Stub(MediaSession2Impl session) {
         mSession = new WeakReference<>(session);
-        mContext = session.getContext();
-        // TODO(jaewan): Should be executor from the session builder
-        mCommandHandler = new CommandHandler(session.getHandler().getLooper());
-        mSessionCallback = callback;
-        mLibraryCallback = (callback instanceof MediaLibrarySessionCallback)
-                ? (MediaLibrarySessionCallback) callback : null;
     }
 
     public void destroyNotLocked() {
         final List<ControllerInfo> list;
         synchronized (mLock) {
             mSession.clear();
-            mCommandHandler.removeCallbacksAndMessages(null);
             list = getControllers();
             mControllers.clear();
         }
@@ -99,14 +88,43 @@
     }
 
     @Override
-    public void connect(String callingPackage, IMediaSession2Callback callback) {
-        if (callback == null) {
-            // Requesting connect without callback to receive result.
-            return;
-        }
-        ControllerInfo request = new ControllerInfo(mContext,
+    public void connect(String callingPackage, IMediaSession2Callback callback)
+            throws RuntimeException {
+        final MediaSession2Impl sessionImpl = getSession();
+        final ControllerInfo request = new ControllerInfo(sessionImpl.getContext(),
                 Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, callback);
-        mCommandHandler.postConnect(request);
+        sessionImpl.getCallbackExecutor().execute(() -> {
+            final MediaSession2Impl session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            CommandGroup allowedCommands = session.getCallback().onConnect(request);
+            // Don't reject connection for the request from trusted app.
+            // Otherwise server will fail to retrieve session's information to dispatch
+            // media keys to.
+            boolean accept = allowedCommands != null || request.isTrusted();
+            ControllerInfoImpl impl = ControllerInfoImpl.from(request);
+            if (accept) {
+                synchronized (mLock) {
+                    mControllers.put(impl.getId(), request);
+                }
+                if (allowedCommands == null) {
+                    // For trusted apps, send non-null allowed commands to keep connection.
+                    allowedCommands = new CommandGroup();
+                }
+            }
+            if (DEBUG) {
+                Log.d(TAG, "onConnectResult, request=" + request
+                        + " accept=" + accept);
+            }
+            try {
+                impl.getControllerBinder().onConnectionChanged(
+                        accept ? MediaSession2Stub.this : null,
+                        allowedCommands == null ? null : allowedCommands.toBundle());
+            } catch (RemoteException e) {
+                // Controller may be died prematurely.
+            }
+        });
     }
 
     @Override
@@ -122,20 +140,79 @@
     @Override
     public void sendCommand(IMediaSession2Callback caller, Bundle command, Bundle args)
             throws RuntimeException {
-        ControllerInfo controller = getController(caller);
+        // TODO(jaewan): Generic command
+    }
+
+    @Override
+    public void sendTransportControlCommand(IMediaSession2Callback caller,
+            int commandCode, long arg) throws RuntimeException {
+        final MediaSession2Impl sessionImpl = getSession();
+        final ControllerInfo controller = getController(caller);
         if (controller == null) {
             if (DEBUG) {
                 Log.d(TAG, "Command from a controller that hasn't connected. Ignore");
             }
             return;
         }
-        mCommandHandler.postCommand(controller, Command.fromBundle(command), args);
+        sessionImpl.getCallbackExecutor().execute(() -> {
+            final MediaSession2Impl session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            // TODO(jaewan): Sanity check.
+            Command command = new Command(commandCode);
+            boolean accepted = session.getCallback().onCommandRequest(controller, command);
+            if (!accepted) {
+                // Don't run rejected command.
+                if (DEBUG) {
+                    Log.d(TAG, "Command " + commandCode + " from "
+                            + controller + " was rejected by " + session);
+                }
+                return;
+            }
+
+            switch (commandCode) {
+                case MediaSession2.COMMAND_CODE_PLAYBACK_START:
+                    session.getInstance().play();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
+                    session.getInstance().pause();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
+                    session.getInstance().stop();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
+                    session.getInstance().skipToPrevious();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
+                    session.getInstance().skipToNext();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE:
+                    session.getInstance().prepare();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_FAST_FORWARD:
+                    session.getInstance().fastForward();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_REWIND:
+                    session.getInstance().rewind();
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SEEK_TO:
+                    session.getInstance().seekTo(arg);
+                    break;
+                case MediaSession2.COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM:
+                    session.getInstance().setCurrentPlaylistItem((int) arg);
+                    break;
+                default:
+                    // TODO(jaewan): Resend unknown (new) commands through the custom command.
+            }
+        });
     }
 
     @Override
     public void getBrowserRoot(IMediaSession2Callback caller, Bundle rootHints)
             throws RuntimeException {
-        if (mLibraryCallback == null) {
+        final MediaSession2Impl sessionImpl = getSession();
+        if (!(sessionImpl.getCallback() instanceof MediaLibrarySessionCallback)) {
             if (DEBUG) {
                 Log.d(TAG, "Session cannot hand getBrowserRoot()");
             }
@@ -148,7 +225,24 @@
             }
             return;
         }
-        mCommandHandler.postOnGetRoot(controller, rootHints);
+        sessionImpl.getCallbackExecutor().execute(() -> {
+            final MediaSession2Impl session = mSession.get();
+            if (session == null) {
+                return;
+            }
+            final MediaLibrarySessionCallback libraryCallback =
+                    (MediaLibrarySessionCallback) session.getCallback();
+            final ControllerInfoImpl controllerImpl = ControllerInfoImpl.from(controller);
+            BrowserRoot root = libraryCallback.onGetRoot(controller, rootHints);
+            try {
+                controllerImpl.getControllerBinder().onGetRootResult(rootHints,
+                        root == null ? null : root.getRootId(),
+                        root == null ? null : root.getExtras());
+            } catch (RemoteException e) {
+                // Controller may be died prematurely.
+                // TODO(jaewan): Handle this.
+            }
+        });
     }
 
     @Deprecated
@@ -250,131 +344,4 @@
             // TODO(jaewan): What to do when the controller is gone?
         }
     }
-
-    // TODO(jaewan): Remove this. We should use Executor given by the session builder.
-    private class CommandHandler extends Handler {
-        public static final int MSG_CONNECT = 1000;
-        public static final int MSG_COMMAND = 1001;
-        public static final int MSG_ON_GET_ROOT = 2000;
-
-        public CommandHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            final MediaSession2Impl session = MediaSession2Stub.this.mSession.get();
-            if (session == null || session.getPlayer() == null) {
-                return;
-            }
-
-            switch (msg.what) {
-                case MSG_CONNECT: {
-                    ControllerInfo request = (ControllerInfo) msg.obj;
-                    CommandGroup allowedCommands = mSessionCallback.onConnect(request);
-                    // Don't reject connection for the request from trusted app.
-                    // Otherwise server will fail to retrieve session's information to dispatch
-                    // media keys to.
-                    boolean accept = allowedCommands != null || request.isTrusted();
-                    ControllerInfoImpl impl = ControllerInfoImpl.from(request);
-                    if (accept) {
-                        synchronized (mLock) {
-                            mControllers.put(impl.getId(), request);
-                        }
-                        if (allowedCommands == null) {
-                            // For trusted apps, send non-null allowed commands to keep connection.
-                            allowedCommands = new CommandGroup();
-                        }
-                    }
-                    if (DEBUG) {
-                        Log.d(TAG, "onConnectResult, request=" + request
-                                + " accept=" + accept);
-                    }
-                    try {
-                        impl.getControllerBinder().onConnectionChanged(
-                                accept ? MediaSession2Stub.this : null,
-                                allowedCommands == null ? null : allowedCommands.toBundle());
-                    } catch (RemoteException e) {
-                        // Controller may be died prematurely.
-                    }
-                    break;
-                }
-                case MSG_COMMAND: {
-                    CommandParam param = (CommandParam) msg.obj;
-                    Command command = param.command;
-                    boolean accepted = mSessionCallback.onCommandRequest(
-                            param.controller, command);
-                    if (!accepted) {
-                        // Don't run rejected command.
-                        if (DEBUG) {
-                            Log.d(TAG, "Command " + command + " from "
-                                    + param.controller + " was rejected by " + session);
-                        }
-                        return;
-                    }
-
-                    switch (param.command.getCommandCode()) {
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_START:
-                            session.getInstance().play();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
-                            session.getInstance().pause();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
-                            session.getInstance().stop();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
-                            session.getInstance().skipToPrevious();
-                            break;
-                        case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
-                            session.getInstance().skipToNext();
-                            break;
-                        default:
-                            // TODO(jaewan): Handle custom command.
-                    }
-                    break;
-                }
-                case MSG_ON_GET_ROOT: {
-                    final CommandParam param = (CommandParam) msg.obj;
-                    final ControllerInfoImpl controller = ControllerInfoImpl.from(param.controller);
-                    BrowserRoot root = mLibraryCallback.onGetRoot(param.controller, param.args);
-                    try {
-                        controller.getControllerBinder().onGetRootResult(param.args,
-                                root == null ? null : root.getRootId(),
-                                root == null ? null : root.getExtras());
-                    } catch (RemoteException e) {
-                        // Controller may be died prematurely.
-                        // TODO(jaewan): Handle this.
-                    }
-                    break;
-                }
-            }
-        }
-
-        public void postConnect(ControllerInfo request) {
-            obtainMessage(MSG_CONNECT, request).sendToTarget();
-        }
-
-        public void postCommand(ControllerInfo controller, Command command, Bundle args) {
-            CommandParam param = new CommandParam(controller, command, args);
-            obtainMessage(MSG_COMMAND, param).sendToTarget();
-        }
-
-        public void postOnGetRoot(ControllerInfo controller, Bundle rootHints) {
-            CommandParam param = new CommandParam(controller, null, rootHints);
-            obtainMessage(MSG_ON_GET_ROOT, param).sendToTarget();
-        }
-    }
-
-    private static class CommandParam {
-        public final ControllerInfo controller;
-        public final Command command;
-        public final Bundle args;
-
-        private CommandParam(ControllerInfo controller, Command command, Bundle args) {
-            this.controller = controller;
-            this.command = command;
-            this.args = args;
-        }
-    }
 }
diff --git a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
index 9d24082..b9db305 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
@@ -22,7 +22,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.media.MediaSession2;
 import android.media.MediaSessionService2;
 import android.media.MediaSessionService2.MediaNotification;
@@ -30,7 +30,6 @@
 import android.media.session.PlaybackState;
 import android.media.update.MediaSessionService2Provider;
 import android.os.IBinder;
-import android.os.Looper;
 import android.support.annotation.GuardedBy;
 import android.util.Log;
 
@@ -152,11 +151,6 @@
                 return;
             }
             MediaSession2Impl impl = (MediaSession2Impl) mSession.getProvider();
-            if (impl.getHandler().getLooper() != Looper.myLooper()) {
-                Log.w(TAG, "Ignoring " + state + ". Expected " + impl.getHandler().getLooper()
-                        + " but " + Looper.myLooper());
-                return;
-            }
             updateNotification(state);
         }
     }
diff --git a/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java b/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
index 7b336c4..4241f85 100644
--- a/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
+++ b/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
@@ -16,11 +16,9 @@
 
 package com.android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.media.PlaybackState2;
-import android.media.session.PlaybackState;
 import android.os.Handler;
-import android.os.Message;
 import android.support.annotation.NonNull;
 
 import java.util.List;
diff --git a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
index 8ce7bef..bb67bcd 100644
--- a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
+++ b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
@@ -27,7 +27,7 @@
 import android.media.MediaLibraryService2;
 import android.media.MediaLibraryService2.MediaLibrarySession;
 import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
 import android.media.MediaSession2;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.MediaSession2.SessionCallback;
@@ -83,7 +83,7 @@
 
     @Override
     public MediaSession2Provider createMediaSession2(Context context, MediaSession2 instance,
-            MediaPlayerBase player, String id, VolumeProvider volumeProvider,
+            MediaPlayerInterface player, String id, VolumeProvider volumeProvider,
             int ratingType, PendingIntent sessionActivity, Executor callbackExecutor,
             SessionCallback callback) {
         return new MediaSession2Impl(context, instance, player, id, volumeProvider, ratingType,
@@ -112,7 +112,7 @@
 
     @Override
     public MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
-            Context context, MediaLibrarySession instance, MediaPlayerBase player,
+            Context context, MediaLibrarySession instance, MediaPlayerInterface player,
             String id, VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
             Executor callbackExecutor, MediaLibrarySessionCallback callback) {
         return new MediaLibrarySessionImpl(context, instance, player, id, volumeProvider,
diff --git a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
index bc370d8..f96ff70 100644
--- a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
@@ -24,11 +24,11 @@
 import android.media.update.ViewProvider;
 import android.os.Bundle;
 import android.util.Log;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityManager;
+import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.MediaControlView2;
 import android.widget.ProgressBar;
@@ -40,6 +40,7 @@
 import com.android.media.update.R;
 
 import java.util.Formatter;
+import java.util.List;
 import java.util.Locale;
 
 public class MediaControlView2Impl implements MediaControlView2Provider {
@@ -97,6 +98,7 @@
     private ImageButton mOverflowButtonRight;
 
     private ViewGroup mExtraControls;
+    private ViewGroup mCustomButtons;
     private ImageButton mOverflowButtonLeft;
     private ImageButton mMuteButton;
     private ImageButton mAspectRationButton;
@@ -305,62 +307,11 @@
     }
 
     @Override
-    public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
-        return mSuperProvider.onKeyDown_impl(keyCode, event);
-    }
-
-    @Override
     public void onFinishInflate_impl() {
         mSuperProvider.onFinishInflate_impl();
     }
 
     @Override
-    public boolean dispatchKeyEvent_impl(KeyEvent event) {
-        int keyCode = event.getKeyCode();
-        final boolean uniqueDown = event.getRepeatCount() == 0
-                && event.getAction() == KeyEvent.ACTION_DOWN;
-        if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
-                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
-                || keyCode == KeyEvent.KEYCODE_SPACE) {
-            if (uniqueDown) {
-                togglePausePlayState();
-                mInstance.show(DEFAULT_TIMEOUT_MS);
-                if (mPlayPauseButton != null) {
-                    mPlayPauseButton.requestFocus();
-                }
-            }
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
-            if (uniqueDown && !isPlaying()) {
-                togglePausePlayState();
-                mInstance.show(DEFAULT_TIMEOUT_MS);
-            }
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
-                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
-            if (uniqueDown && isPlaying()) {
-                togglePausePlayState();
-                mInstance.show(DEFAULT_TIMEOUT_MS);
-            }
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
-                || keyCode == KeyEvent.KEYCODE_VOLUME_UP
-                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
-                || keyCode == KeyEvent.KEYCODE_CAMERA) {
-            // don't show the controls for volume adjustment
-            return mSuperProvider.dispatchKeyEvent_impl(event);
-        } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
-            if (uniqueDown) {
-                mInstance.hide();
-            }
-            return true;
-        }
-
-        mInstance.show(DEFAULT_TIMEOUT_MS);
-        return mSuperProvider.dispatchKeyEvent_impl(event);
-    }
-
-    @Override
     public void setEnabled_impl(boolean enabled) {
         if (mPlayPauseButton != null) {
             mPlayPauseButton.setEnabled(enabled);
@@ -501,6 +452,7 @@
 
         // TODO: should these buttons be shown as default?
         mExtraControls = v.findViewById(R.id.extra_controls);
+        mCustomButtons = v.findViewById(R.id.custom_buttons);
         mOverflowButtonLeft = v.findViewById(R.id.overflow_left);
         if (mOverflowButtonLeft != null) {
             mOverflowButtonLeft.setOnClickListener(mOverflowLeftListener);
@@ -875,6 +827,31 @@
                 }
                 mPlaybackActions = newActions;
             }
+
+            // Add buttons if custom actions are present.
+            List<PlaybackState.CustomAction> customActions = mPlaybackState.getCustomActions();
+            mCustomButtons.removeAllViews();
+            if (customActions.size() > 0) {
+                for (PlaybackState.CustomAction action : customActions) {
+                    ImageButton button = new ImageButton(mInstance.getContext(),
+                            null /* AttributeSet */, 0 /* Style */);
+                    // TODO: Apply R.style.BottomBarButton to this button using library context.
+                    // Refer Constructor with argument (int defStyleRes) of View.java
+                    button.setImageResource(action.getIcon());
+                    button.setTooltipText(action.getName());
+                    final String actionString = action.getAction().toString();
+                    button.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            // TODO: Currently, we are just sending extras that came from session.
+                            // Is it the right behavior?
+                            mControls.sendCustomAction(actionString, action.getExtras());
+                            mInstance.show(DEFAULT_TIMEOUT_MS);
+                        }
+                    });
+                    mCustomButtons.addView(button);
+                }
+            }
         }
 
         @Override
diff --git a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
index 09f3cda..8cd932d 100644
--- a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
@@ -25,11 +25,9 @@
 import android.media.AudioManager;
 import android.media.MediaMetadata;
 import android.media.MediaPlayer;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
 import android.media.Cea708CaptionRenderer;
 import android.media.ClosedCaptionRenderer;
-import android.media.MediaMetadata;
-import android.media.MediaPlayer;
 import android.media.Metadata;
 import android.media.PlaybackParams;
 import android.media.SubtitleController;
@@ -47,7 +45,6 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Gravity;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.FrameLayout.LayoutParams;
@@ -80,8 +77,8 @@
     private final AudioManager mAudioManager;
     private AudioAttributes mAudioAttributes;
     private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
-    private int mAudioSession;
 
+    private VideoView2.OnCustomActionListener mOnCustomActionListener;
     private VideoView2.OnPreparedListener mOnPreparedListener;
     private VideoView2.OnCompletionListener mOnCompletionListener;
     private VideoView2.OnErrorListener mOnErrorListener;
@@ -101,6 +98,7 @@
     private String mTitle;
 
     private PlaybackState.Builder mStateBuilder;
+    private List<PlaybackState.CustomAction> mCustomActionList;
     private int mTargetState = STATE_IDLE;
     private int mCurrentState = STATE_IDLE;
     private int mCurrentBufferPercentage;
@@ -194,16 +192,6 @@
     }
 
     @Override
-    public int getAudioSessionId_impl() {
-        if (mAudioSession == 0) {
-            MediaPlayer foo = new MediaPlayer();
-            mAudioSession = foo.getAudioSessionId();
-            foo.release();
-        }
-        return mAudioSession;
-    }
-
-    @Override
     public void showSubtitle_impl() {
         // Retrieve all tracks that belong to the current video.
         MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
@@ -234,7 +222,7 @@
     @Override
     public void setFullScreen_impl(boolean fullScreen) {
         if (mOnFullScreenChangedListener != null) {
-            mOnFullScreenChangedListener.onFullScreenChanged(fullScreen);
+            mOnFullScreenChangedListener.onFullScreenChanged(mInstance, fullScreen);
         }
     }
 
@@ -273,22 +261,22 @@
     }
 
     @Override
-    public void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player) {
+    public void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerInterface player) {
         // TODO: implement this.
     }
 
     @Override
     public void setVideoPath_impl(String path) {
-        mInstance.setVideoURI(Uri.parse(path));
+        mInstance.setVideoUri(Uri.parse(path));
     }
 
     @Override
-    public void setVideoURI_impl(Uri uri) {
-        mInstance.setVideoURI(uri, null);
+    public void setVideoUri_impl(Uri uri) {
+        mInstance.setVideoUri(uri, null);
     }
 
     @Override
-    public void setVideoURI_impl(Uri uri, Map<String, String> headers) {
+    public void setVideoUri_impl(Uri uri, Map<String, String> headers) {
         mSeekWhenPrepared = 0;
         openVideo(uri, headers);
     }
@@ -319,6 +307,17 @@
     }
 
     @Override
+    public void setCustomActions_impl(List<PlaybackState.CustomAction> actionList,
+            VideoView2.OnCustomActionListener listener) {
+        mCustomActionList = actionList;
+        mOnCustomActionListener = listener;
+
+        // Create a new playback builder in order to clear existing the custom actions.
+        mStateBuilder = null;
+        updatePlaybackState();
+    }
+
+    @Override
     public void setOnPreparedListener_impl(VideoView2.OnPreparedListener l) {
         mOnPreparedListener = l;
     }
@@ -365,6 +364,7 @@
         mSuperProvider.onDetachedFromWindow_impl();
         mMediaSession.release();
         mMediaSession = null;
+        mMediaController = null;
     }
 
     @Override
@@ -395,58 +395,11 @@
     }
 
     @Override
-    public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
-        Log.v(TAG, "onKeyDown_impl: " + keyCode);
-        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK
-                && keyCode != KeyEvent.KEYCODE_VOLUME_UP
-                && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN
-                && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE
-                && keyCode != KeyEvent.KEYCODE_MENU
-                && keyCode != KeyEvent.KEYCODE_CALL
-                && keyCode != KeyEvent.KEYCODE_ENDCALL;
-        if (isInPlaybackState() && isKeyCodeSupported && mMediaControlView != null) {
-            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
-                    || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
-                if (mMediaPlayer.isPlaying()) {
-                    mMediaController.getTransportControls().pause();
-                    mMediaControlView.show();
-                } else {
-                    mMediaController.getTransportControls().play();
-                    mMediaControlView.hide();
-                }
-                return true;
-            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
-                if (!mMediaPlayer.isPlaying()) {
-                    mMediaController.getTransportControls().play();
-                    mMediaControlView.hide();
-                }
-                return true;
-            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
-                    || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
-                if (mMediaPlayer.isPlaying()) {
-                    mMediaController.getTransportControls().pause();
-                    mMediaControlView.show();
-                }
-                return true;
-            } else {
-                toggleMediaControlViewVisibility();
-            }
-        }
-
-        return mSuperProvider.onKeyDown_impl(keyCode, event);
-    }
-
-    @Override
     public void onFinishInflate_impl() {
         mSuperProvider.onFinishInflate_impl();
     }
 
     @Override
-    public boolean dispatchKeyEvent_impl(KeyEvent event) {
-        return mSuperProvider.dispatchKeyEvent_impl(event);
-    }
-
-    @Override
     public void setEnabled_impl(boolean enabled) {
         mSuperProvider.setEnabled_impl(enabled);
     }
@@ -494,7 +447,7 @@
         }
         mCurrentView = view;
         if (mOnViewTypeChangedListener != null) {
-            mOnViewTypeChangedListener.onViewTypeChanged(view.getViewType());
+            mOnViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
         }
         if (needToStart()) {
             mMediaController.getTransportControls().play();
@@ -556,12 +509,6 @@
             controller.registerRenderer(new Cea708CaptionRenderer(context));
             controller.registerRenderer(new ClosedCaptionRenderer(context));
             mMediaPlayer.setSubtitleAnchor(controller, (SubtitleController.Anchor) mSubtitleView);
-
-            if (mAudioSession != 0) {
-                mMediaPlayer.setAudioSessionId(mAudioSession);
-            } else {
-                mAudioSession = mMediaPlayer.getAudioSessionId();
-            }
             mMediaPlayer.setOnPreparedListener(mPreparedListener);
             mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
             mMediaPlayer.setOnCompletionListener(mCompletionListener);
@@ -660,8 +607,12 @@
             }
             mStateBuilder = new PlaybackState.Builder();
             mStateBuilder.setActions(playbackActions);
-            mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_SHOW_SUBTITLE, null, -1);
-            mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_HIDE_SUBTITLE, null, -1);
+
+            if (mCustomActionList != null) {
+                for (PlaybackState.CustomAction action : mCustomActionList) {
+                    mStateBuilder.addCustomAction(action);
+                }
+            }
         }
         mStateBuilder.setState(getCorrespondingPlaybackState(),
                 mMediaPlayer.getCurrentPosition(), mSpeed);
@@ -754,7 +705,7 @@
             }
             mCurrentState = STATE_PREPARED;
             if (mOnPreparedListener != null) {
-                mOnPreparedListener.onPrepared();
+                mOnPreparedListener.onPrepared(mInstance);
             }
             if (mMediaControlView != null) {
                 mMediaControlView.setEnabled(true);
@@ -829,7 +780,7 @@
                     updatePlaybackState();
 
                     if (mOnCompletionListener != null) {
-                        mOnCompletionListener.onCompletion();
+                        mOnCompletionListener.onCompletion(mInstance);
                     }
                     if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
                         mAudioManager.abandonAudioFocus(null);
@@ -841,7 +792,7 @@
             new MediaPlayer.OnInfoListener() {
                 public boolean onInfo(MediaPlayer mp, int what, int extra) {
                     if (mOnInfoListener != null) {
-                        mOnInfoListener.onInfo(what, extra);
+                        mOnInfoListener.onInfo(mInstance, what, extra);
                     }
                     return true;
                 }
@@ -863,7 +814,7 @@
 
                     /* If an error handler has been supplied, use it and finish. */
                     if (mOnErrorListener != null) {
-                        if (mOnErrorListener.onError(frameworkErr, implErr)) {
+                        if (mOnErrorListener.onError(mInstance, frameworkErr, implErr)) {
                             return true;
                         }
                     }
@@ -894,7 +845,7 @@
                                                 * at least inform them that the video is over.
                                                 */
                                                 if (mOnCompletionListener != null) {
-                                                    mOnCompletionListener.onCompletion();
+                                                    mOnCompletionListener.onCompletion(mInstance);
                                                 }
                                             }
                                         })
@@ -931,6 +882,11 @@
         }
 
         @Override
+        public void onCustomAction(String action, Bundle extras) {
+            mOnCustomActionListener.onCustomAction(action, extras);
+        }
+
+        @Override
         public void onPlay() {
             if (isInPlaybackState() && mCurrentView.hasAvailableSurface()) {
                 applySpeed();
diff --git a/packages/MediaComponents/test/src/android/media/MediaController2Test.java b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
index ae67a95..3e39f40 100644
--- a/packages/MediaComponents/test/src/android/media/MediaController2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
@@ -16,7 +16,7 @@
 
 package android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.MediaSession2.SessionCallback;
 import android.media.TestUtils.SyncHandler;
@@ -45,6 +45,7 @@
  */
 // TODO(jaewan): Implement host-side test so controller and session can run in different processes.
 // TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 @FlakyTest
@@ -60,11 +61,10 @@
     public void setUp() throws Exception {
         super.setUp();
         // Create this test specific MediaSession2 to use our own Handler.
-        sHandler.postAndSync(()->{
-            mPlayer = new MockPlayer(1);
-            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
-        });
-
+        mPlayer = new MockPlayer(1);
+        mSession = new MediaSession2.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback())
+                .setId(TAG).build();
         mController = createController(mSession.getToken());
         TestServiceRegistry.getInstance().setHandler(sHandler);
     }
@@ -73,11 +73,9 @@
     @Override
     public void cleanUp() throws Exception {
         super.cleanUp();
-        sHandler.postAndSync(() -> {
-            if (mSession != null) {
-                mSession.close();
-            }
-        });
+        if (mSession != null) {
+            mSession.close();
+        }
         TestServiceRegistry.getInstance().cleanUp();
     }
 
@@ -103,7 +101,6 @@
         assertTrue(mPlayer.mPauseCalled);
     }
 
-
     @Test
     public void testSkipToPrevious() throws InterruptedException {
         mController.skipToPrevious();
@@ -138,6 +135,65 @@
     }
 
     @Test
+    public void testPrepare() throws InterruptedException {
+        mController.prepare();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mPrepareCalled);
+    }
+
+    @Test
+    public void testFastForward() throws InterruptedException {
+        mController.fastForward();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mFastForwardCalled);
+    }
+
+    @Test
+    public void testRewind() throws InterruptedException {
+        mController.rewind();
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mRewindCalled);
+    }
+
+    @Test
+    public void testSeekTo() throws InterruptedException {
+        final long seekPosition = 12125L;
+        mController.seekTo(seekPosition);
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSeekToCalled);
+        assertEquals(seekPosition, mPlayer.mSeekPosition);
+    }
+
+    @Test
+    public void testSetCurrentPlaylistItem() throws InterruptedException {
+        final int itemIndex = 9;
+        mController.setCurrentPlaylistItem(itemIndex);
+        try {
+            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail(e.getMessage());
+        }
+        assertTrue(mPlayer.mSetCurrentPlaylistItemCalled);
+        assertEquals(itemIndex, mPlayer.mItemIndex);
+    }
+
+    @Test
     public void testGetPackageName() {
         assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
     }
@@ -147,7 +203,7 @@
         // TODO(jaewan): add equivalent test later
         /*
         final CountDownLatch latch = new CountDownLatch(1);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
+        final MediaPlayerInterface.PlaybackListener listener = (state) -> {
             assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
             latch.countDown();
         };
@@ -165,7 +221,7 @@
     @Test
     public void testAddPlaybackListener() throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(2);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
+        final MediaPlayerInterface.PlaybackListener listener = (state) -> {
             switch ((int) latch.getCount()) {
                 case 2:
                     assertEquals(PlaybackState.STATE_PLAYING, state.getState());
@@ -188,7 +244,7 @@
     @Test
     public void testRemovePlaybackListener() throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(1);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
+        final MediaPlayerInterface.PlaybackListener listener = (state) -> {
             fail();
             latch.countDown();
         };
@@ -275,6 +331,7 @@
             final MockPlayer player = new MockPlayer(0);
             sessionHandler.postAndSync(() -> {
                 mSession = new MediaSession2.Builder(mContext, mPlayer)
+                        .setSessionCallback(sHandlerExecutor, new SessionCallback())
                         .setId("testDeadlock").build();
             });
             final MediaController2 controller = createController(mSession.getToken());
@@ -462,7 +519,9 @@
         sHandler.postAndSync(() -> {
             // Recreated session has different session stub, so previously created controller
             // shouldn't be available.
-            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
+            mSession = new MediaSession2.Builder(mContext, mPlayer)
+                    .setSessionCallback(sHandlerExecutor, new SessionCallback())
+                    .setId(id).build();
         });
         testNoInteraction();
     }
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
index 045dcd5..8329cf0 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -16,13 +16,10 @@
 
 package android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
 import android.media.MediaSession2.Builder;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.MediaSession2.SessionCallback;
-import android.media.session.PlaybackState;
 import android.os.Process;
-import android.os.Looper;
 import android.support.annotation.NonNull;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -33,10 +30,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-import static android.media.TestUtils.createPlaybackState;
 import static org.junit.Assert.*;
 
 /**
@@ -54,19 +49,16 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        sHandler.postAndSync(() -> {
-            mPlayer = new MockPlayer(0);
-            mSession = new MediaSession2.Builder(mContext, mPlayer).build();
-        });
+        mPlayer = new MockPlayer(0);
+        mSession = new MediaSession2.Builder(mContext, mPlayer)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback()).build();
     }
 
     @After
     @Override
     public void cleanUp() throws Exception {
         super.cleanUp();
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
+        mSession.close();
     }
 
     @Test
diff --git a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
index 192cbc2..6037619 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
@@ -59,9 +59,8 @@
         // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
         // per test thread differs across the {@link MediaSession2} with the same TAG.
         final MockPlayer player = new MockPlayer(1);
-        sHandler.postAndSync(() -> {
-            mSession = new MediaSession2.Builder(mContext, player).setId(TAG).build();
-        });
+        mSession = new MediaSession2.Builder(mContext, player)
+                .setSessionCallback(sHandlerExecutor, new SessionCallback()).setId(TAG).build();
         ensureChangeInSession();
     }
 
@@ -70,9 +69,7 @@
     public void cleanUp() throws Exception {
         super.cleanUp();
         sHandler.removeCallbacksAndMessages(null);
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
+        mSession.close();
     }
 
     // TODO(jaewan): Make this host-side test to see per-user behavior.
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
index b058117..5c5c7d2 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
@@ -28,6 +28,8 @@
 import android.media.session.PlaybackState;
 import android.os.Process;
 
+import java.util.concurrent.Executor;
+
 /**
  * Mock implementation of {@link android.media.MediaSessionService2} for testing.
  */
@@ -46,11 +48,12 @@
     public MediaSession2 onCreateSession(String sessionId) {
         final MockPlayer player = new MockPlayer(1);
         final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+        final Executor executor = (runnable) -> handler.post(runnable);
         try {
             handler.postAndSync(() -> {
                 mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
-                        .setId(sessionId).setSessionCallback((runnable)->handler.post(runnable),
-                                new MySessionCallback()).build();
+                        .setSessionCallback(executor, new MySessionCallback())
+                        .setId(sessionId).build();
             });
         } catch (InterruptedException e) {
             fail(e.toString());
diff --git a/packages/MediaComponents/test/src/android/media/MockPlayer.java b/packages/MediaComponents/test/src/android/media/MockPlayer.java
index ad7ba2f..d0d1178 100644
--- a/packages/MediaComponents/test/src/android/media/MockPlayer.java
+++ b/packages/MediaComponents/test/src/android/media/MockPlayer.java
@@ -16,6 +16,7 @@
 
 package android.media;
 
+import android.media.MediaPlayerInterface;
 import android.media.MediaSession2.PlaylistParams;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -26,9 +27,9 @@
 import java.util.concurrent.Executor;
 
 /**
- * A mock implementation of {@link MediaPlayerBase} for testing.
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
  */
-public class MockPlayer extends MediaPlayerBase {
+public class MockPlayer implements MediaPlayerInterface {
     public final CountDownLatch mCountDownLatch;
 
     public boolean mPlayCalled;
@@ -36,6 +37,14 @@
     public boolean mStopCalled;
     public boolean mSkipToPreviousCalled;
     public boolean mSkipToNextCalled;
+    public boolean mPrepareCalled;
+    public boolean mFastForwardCalled;
+    public boolean mRewindCalled;
+    public boolean mSeekToCalled;
+    public long mSeekPosition;
+    public boolean mSetCurrentPlaylistItemCalled;
+    public int mItemIndex;
+
     public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
     private PlaybackState2 mLastPlaybackState;
 
@@ -83,7 +92,47 @@
         }
     }
 
+    @Override
+    public void prepare() {
+        mPrepareCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
 
+    @Override
+    public void fastForward() {
+        mFastForwardCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void rewind() {
+        mRewindCalled = true;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        mSeekToCalled = true;
+        mSeekPosition = pos;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
+
+    @Override
+    public void setCurrentPlaylistItem(int index) {
+        mSetCurrentPlaylistItemCalled = true;
+        mItemIndex = index;
+        if (mCountDownLatch != null) {
+            mCountDownLatch.countDown();
+        }
+    }
 
     @Nullable
     @Override
@@ -112,23 +161,6 @@
         }
     }
 
-    // No-op. Should be added for test later.
-    @Override
-    public void prepare() {
-    }
-
-    @Override
-    public void seekTo(long pos) {
-    }
-
-    @Override
-    public void fastFoward() {
-    }
-
-    @Override
-    public void rewind() {
-    }
-
     @Override
     public AudioAttributes getAudioAttributes() {
         return null;
@@ -137,8 +169,4 @@
     @Override
     public void setPlaylist(List<MediaItem2> item, PlaylistParams param) {
     }
-
-    @Override
-    public void setCurrentPlaylistItem(int index) {
-    }
 }
diff --git a/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
index 4e19d4d..0f1644c 100644
--- a/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
+++ b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
@@ -16,10 +16,8 @@
 
 package android.media;
 
-import android.media.MediaPlayerBase.PlaybackListener;
-import android.media.session.PlaybackState;
+import android.media.MediaPlayerInterface.PlaybackListener;
 import android.os.Handler;
-import android.os.Message;
 import android.support.annotation.NonNull;
 
 import java.util.List;