Adds a TransportController and TransportPerformer to session

This makes transport controls a primitive interface on sessions with
a way to create the performer, register callbacks, and send commands
and updates between controllers and performers. This still needs some
cleanup but has been tested with OneMedia.

Change-Id: I373d35f7ccc383b8421bd14044457467d80425f3
diff --git a/api/current.txt b/api/current.txt
index 0a0f148..6519008 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -13815,6 +13815,7 @@
     field public static final int RATING_4_STARS = 4; // 0x4
     field public static final int RATING_5_STARS = 5; // 0x5
     field public static final int RATING_HEART = 1; // 0x1
+    field public static final int RATING_NONE = 0; // 0x0
     field public static final int RATING_PERCENTAGE = 6; // 0x6
     field public static final int RATING_THUMB_UP_DOWN = 2; // 0x2
   }
@@ -14455,34 +14456,76 @@
 package android.media.session {
 
   public final class MediaController {
-    ctor public MediaController(android.media.session.MediaSessionToken);
     method public void addCallback(android.media.session.MediaController.Callback);
     method public void addCallback(android.media.session.MediaController.Callback, android.os.Handler);
+    method public static android.media.session.MediaController fromToken(android.media.session.MediaSessionToken);
+    method public android.media.session.TransportController getTransportController();
     method public void removeCallback(android.media.session.MediaController.Callback);
-    method public void sendCommand(java.lang.String, android.os.Bundle);
+    method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
     method public void sendMediaButton(int);
   }
 
   public static abstract class MediaController.Callback {
     ctor public MediaController.Callback();
     method public void onEvent(java.lang.String, android.os.Bundle);
-    method public void onMetadataUpdate(android.os.Bundle);
-    method public void onPlaybackStateChange(int);
     method public void onRouteChanged(android.os.Bundle);
   }
 
+  public final class MediaMetadata implements android.os.Parcelable {
+    method public int describeContents();
+    method public android.graphics.Bitmap getBitmap(java.lang.String);
+    method public long getLong(java.lang.String);
+    method public android.media.Rating getRating(java.lang.String);
+    method public java.lang.String getString(java.lang.String);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+    field public static final java.lang.String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+    field public static final java.lang.String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+    field public static final java.lang.String METADATA_KEY_ART = "android.media.metadata.ART";
+    field public static final java.lang.String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+    field public static final java.lang.String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+    field public static final java.lang.String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+    field public static final java.lang.String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+    field public static final java.lang.String METADATA_KEY_DATE = "android.media.metadata.DATE";
+    field public static final java.lang.String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+    field public static final java.lang.String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+    field public static final java.lang.String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+    field public static final java.lang.String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+    field public static final java.lang.String METADATA_KEY_RATING = "android.media.metadata.RATING";
+    field public static final java.lang.String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+    field public static final java.lang.String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+    field public static final java.lang.String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+    field public static final java.lang.String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+    field public static final java.lang.String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+  }
+
+  public static final class MediaMetadata.Builder {
+    ctor public MediaMetadata.Builder();
+    ctor public MediaMetadata.Builder(android.media.session.MediaMetadata);
+    method public android.media.session.MediaMetadata build();
+    method public android.media.session.MediaMetadata.Builder putBitmap(java.lang.String, android.graphics.Bitmap);
+    method public android.media.session.MediaMetadata.Builder putLong(java.lang.String, long);
+    method public android.media.session.MediaMetadata.Builder putRating(java.lang.String, android.media.Rating);
+    method public android.media.session.MediaMetadata.Builder putString(java.lang.String, java.lang.String);
+  }
+
   public final class MediaSession {
     method public void addCallback(android.media.session.MediaSession.Callback);
     method public void addCallback(android.media.session.MediaSession.Callback, android.os.Handler);
     method public android.media.session.MediaSessionToken getSessionToken();
+    method public android.media.session.TransportPerformer getTransportPerformer();
+    method public void publish();
     method public void release();
     method public void removeCallback(android.media.session.MediaSession.Callback);
-    method public void setPlaybackState(int);
+    method public void sendEvent(java.lang.String, android.os.Bundle);
+    method public android.media.session.TransportPerformer setTransportPerformerEnabled();
   }
 
   public static abstract class MediaSession.Callback {
     ctor public MediaSession.Callback();
-    method public void onCommand(java.lang.String, android.os.Bundle);
+    method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
     method public void onMediaButton(android.content.Intent);
     method public void onRequestRouteChange(android.os.Bundle);
   }
@@ -14498,6 +14541,137 @@
     field public static final android.os.Parcelable.Creator CREATOR;
   }
 
+  public final class PlaybackState implements android.os.Parcelable {
+    ctor public PlaybackState();
+    ctor public PlaybackState(android.media.session.PlaybackState);
+    method public int describeContents();
+    method public long getActions();
+    method public long getBufferPosition();
+    method public java.lang.String getErrorMessage();
+    method public long getPosition();
+    method public float getSpeed();
+    method public int getState();
+    method public void setActions(long);
+    method public void setBufferPosition(long);
+    method public void setErrorMessage(java.lang.String);
+    method public void setPosition(long);
+    method public void setSpeed(float);
+    method public void setState(int);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final long ACTION_FASTFORWARD = 64L; // 0x40L
+    field public static final long ACTION_NEXT_ITEM = 32L; // 0x20L
+    field public static final long ACTION_PAUSE = 2L; // 0x2L
+    field public static final long ACTION_PLAY = 4L; // 0x4L
+    field public static final long ACTION_PREVIOUS_ITEM = 16L; // 0x10L
+    field public static final long ACTION_RATING = 128L; // 0x80L
+    field public static final long ACTION_REWIND = 8L; // 0x8L
+    field public static final long ACTION_SEEK_TO = 256L; // 0x100L
+    field public static final long ACTION_STOP = 1L; // 0x1L
+    field public static final android.os.Parcelable.Creator CREATOR;
+    field public static final int PLAYSTATE_BUFFERING = 6; // 0x6
+    field public static final int PLAYSTATE_ERROR = 7; // 0x7
+    field public static final int PLAYSTATE_FAST_FORWARDING = 4; // 0x4
+    field public static final int PLAYSTATE_NONE = 0; // 0x0
+    field public static final int PLAYSTATE_PAUSED = 2; // 0x2
+    field public static final int PLAYSTATE_PLAYING = 3; // 0x3
+    field public static final int PLAYSTATE_REWINDING = 5; // 0x5
+    field public static final int PLAYSTATE_STOPPED = 1; // 0x1
+  }
+
+  public final class RouteInterface {
+    method public void addListener(android.media.session.RouteInterface.EventListener);
+    method public void addListener(android.media.session.RouteInterface.EventListener, android.os.Handler);
+    method public void removeListener(android.media.session.RouteInterface.EventListener);
+    method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+  }
+
+  public static abstract class RouteInterface.EventListener {
+    ctor public RouteInterface.EventListener();
+    method public abstract void onEvent(java.lang.String, android.os.Bundle);
+  }
+
+  public static abstract class RouteInterface.Stub {
+    ctor public RouteInterface.Stub();
+    method public abstract java.lang.String getName();
+    method public abstract void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+    method public final void sendEvent(android.media.session.MediaSession, java.lang.String, android.os.Bundle);
+  }
+
+  public final class RouteTransportControls {
+    method public void addListener(android.media.session.RouteTransportControls.Listener);
+    method public void addListener(android.media.session.RouteTransportControls.Listener, android.os.Handler);
+    method public void fastForward(float);
+    method public static android.media.session.RouteTransportControls from(android.media.session.MediaController);
+    method public void getCapabilities(android.os.ResultReceiver);
+    method public void getCurrentPosition(android.os.ResultReceiver);
+    method public void pause();
+    method public void play();
+    method public void removeListener(android.media.session.RouteTransportControls.Listener);
+    field public static final java.lang.String NAME = "android.media.session.RouteTransportControls";
+  }
+
+  public static abstract class RouteTransportControls.Listener {
+    ctor public RouteTransportControls.Listener();
+    method public void onMetadataUpdate(android.os.Bundle);
+    method public void onPlaybackStateChange(int);
+  }
+
+  public static abstract class RouteTransportControls.Stub extends android.media.session.RouteInterface.Stub {
+    ctor public RouteTransportControls.Stub(android.media.session.MediaSession);
+    method public void fastForward(float);
+    method public long getCapabilities();
+    method public long getCurrentPosition();
+    method public java.lang.String getName();
+    method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+    method public final void updatePlaybackState(int);
+  }
+
+  public final class TransportController {
+    method public void addStateListener(android.media.session.TransportController.TransportStateListener);
+    method public void addStateListener(android.media.session.TransportController.TransportStateListener, android.os.Handler);
+    method public void fastForward();
+    method public android.media.session.MediaMetadata getMetadata();
+    method public android.media.session.PlaybackState getPlaybackState();
+    method public int getRatingType();
+    method public void next();
+    method public void pause();
+    method public void play();
+    method public void previous();
+    method public void rate(android.media.Rating);
+    method public void removeStateListener(android.media.session.TransportController.TransportStateListener);
+    method public void rewind();
+    method public void seekTo(long);
+    method public void stop();
+  }
+
+  public static abstract class TransportController.TransportStateListener {
+    ctor public TransportController.TransportStateListener();
+    method public void onMetadataChanged(android.media.session.MediaMetadata);
+    method public void onPlaybackStateChanged(android.media.session.PlaybackState);
+  }
+
+  public final class TransportPerformer {
+    method public void addListener(android.media.session.TransportPerformer.Listener);
+    method public void addListener(android.media.session.TransportPerformer.Listener, android.os.Handler);
+    method public void removeListener(android.media.session.TransportPerformer.Listener);
+    method public final void setMetadata(android.media.session.MediaMetadata);
+    method public final void setPlaybackState(android.media.session.PlaybackState);
+  }
+
+  public static abstract class TransportPerformer.Listener {
+    ctor public TransportPerformer.Listener();
+    method public void onFastForward();
+    method public void onNext();
+    method public void onPause();
+    method public void onPlay();
+    method public void onPrevious();
+    method public void onRate(android.media.Rating);
+    method public void onRewind();
+    method public void onRouteFocusChange(int);
+    method public void onSeekTo(long);
+    method public void onStop();
+  }
+
 }
 
 package android.mtp {
diff --git a/media/java/android/media/Rating.java b/media/java/android/media/Rating.java
index b94db18..f4fbe2c 100644
--- a/media/java/android/media/Rating.java
+++ b/media/java/android/media/Rating.java
@@ -29,8 +29,13 @@
  * through one of the factory methods.
  */
 public final class Rating implements Parcelable {
-
     private final static String TAG = "Rating";
+    /**
+     * Indicates a rating style is not supported. A Rating will never have this
+     * type, but can be used by other classes to indicate they do not support
+     * Rating.
+     */
+    public final static int RATING_NONE = 0;
 
     /**
      * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to
diff --git a/media/java/android/media/session/IMediaController.aidl b/media/java/android/media/session/IMediaController.aidl
index 8ca0e45..d34e973 100644
--- a/media/java/android/media/session/IMediaController.aidl
+++ b/media/java/android/media/session/IMediaController.aidl
@@ -16,9 +16,12 @@
 package android.media.session;
 
 import android.content.Intent;
+import android.media.Rating;
 import android.media.session.IMediaControllerCallback;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
-import android.os.IBinder;
+import android.os.ResultReceiver;
 import android.view.KeyEvent;
 
 /**
@@ -26,9 +29,23 @@
  * @hide
  */
 interface IMediaController {
-    void sendCommand(String command, in Bundle extras);
+    void sendCommand(String command, in Bundle extras, in ResultReceiver cb);
     void sendMediaButton(in KeyEvent mediaButton);
     void registerCallbackListener(in IMediaControllerCallback cb);
     void unregisterCallbackListener(in IMediaControllerCallback cb);
-    int getPlaybackState();
+    boolean isTransportControlEnabled();
+
+    // These commands are for the TransportController
+    void play();
+    void pause();
+    void stop();
+    void next();
+    void previous();
+    void fastForward();
+    void rewind();
+    void seekTo(long pos);
+    void rate(in Rating rating);
+    MediaMetadata getMetadata();
+    PlaybackState getPlaybackState();
+    int getRatingType();
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/IMediaControllerCallback.aidl b/media/java/android/media/session/IMediaControllerCallback.aidl
index 3aa0ee4..3651f1b 100644
--- a/media/java/android/media/session/IMediaControllerCallback.aidl
+++ b/media/java/android/media/session/IMediaControllerCallback.aidl
@@ -15,6 +15,8 @@
 
 package android.media.session;
 
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 
 /**
@@ -22,7 +24,9 @@
  */
 oneway interface IMediaControllerCallback {
     void onEvent(String event, in Bundle extras);
-    void onMetadataUpdate(in Bundle metadata);
-    void onPlaybackUpdate(int newState);
     void onRouteChanged(in Bundle route);
+
+    // These callbacks are for the TransportController
+    void onPlaybackStateChanged(in PlaybackState state);
+    void onMetadataChanged(in MediaMetadata metadata);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/IMediaSession.aidl b/media/java/android/media/session/IMediaSession.aidl
index 19f7092..aed7641 100644
--- a/media/java/android/media/session/IMediaSession.aidl
+++ b/media/java/android/media/session/IMediaSession.aidl
@@ -16,6 +16,8 @@
 package android.media.session;
 
 import android.media.session.IMediaController;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 
 /**
@@ -23,11 +25,17 @@
  * @hide
  */
 interface IMediaSession {
-    void sendEvent(in Bundle data);
-    IMediaController getMediaSessionToken();
-    void setPlaybackState(int state);
-    void setMetadata(in Bundle metadata);
+    void sendEvent(String event, in Bundle data);
+    IMediaController getMediaController();
+    void setTransportPerformerEnabled();
     void setRouteState(in Bundle routeState);
     void setRoute(in Bundle mediaRouteDescriptor);
+    List<String> getSupportedInterfaces();
+    void publish();
     void destroy();
+
+    // These commands are for the TransportPerformer
+    void setMetadata(in MediaMetadata metadata);
+    void setPlaybackState(in PlaybackState state);
+    void setRatingType(int type);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/IMediaSessionCallback.aidl b/media/java/android/media/session/IMediaSessionCallback.aidl
index eb5f222..7c183e0 100644
--- a/media/java/android/media/session/IMediaSessionCallback.aidl
+++ b/media/java/android/media/session/IMediaSessionCallback.aidl
@@ -15,15 +15,27 @@
 
 package android.media.session;
 
+import android.media.Rating;
 import android.content.Intent;
 import android.os.Bundle;
-import android.os.IBinder;
+import android.os.ResultReceiver;
 
 /**
  * @hide
  */
 oneway interface IMediaSessionCallback {
-    void onCommand(String command, in Bundle extras);
+    void onCommand(String command, in Bundle extras, in ResultReceiver cb);
     void onMediaButton(in Intent mediaRequestIntent);
     void onRequestRouteChange(in Bundle route);
+
+    // These callbacks are for the TransportPerformer
+    void onPlay();
+    void onPause();
+    void onStop();
+    void onNext();
+    void onPrevious();
+    void onFastForward();
+    void onRewind();
+    void onSeekTo(long pos);
+    void onRate(in Rating rating);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java
index 09de859..afd8b11 100644
--- a/media/java/android/media/session/MediaController.java
+++ b/media/java/android/media/session/MediaController.java
@@ -16,20 +16,17 @@
 
 package android.media.session;
 
-import android.content.Intent;
-import android.media.session.IMediaController;
-import android.media.session.IMediaControllerCallback;
-import android.media.MediaMetadataRetriever;
-import android.media.RemoteControlClient;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
 /**
@@ -46,58 +43,62 @@
 public final class MediaController {
     private static final String TAG = "MediaController";
 
-    private static final int MESSAGE_EVENT = 1;
+    private static final int MSG_EVENT = 1;
     private static final int MESSAGE_PLAYBACK_STATE = 2;
     private static final int MESSAGE_METADATA = 3;
-    private static final int MESSAGE_ROUTE = 4;
-
-    private static final String KEY_EVENT = "event";
-    private static final String KEY_EXTRAS = "extras";
+    private static final int MSG_ROUTE = 4;
 
     private final IMediaController mSessionBinder;
 
-    private final CallbackStub mCbStub = new CallbackStub();
-    private final ArrayList<Callback> mCbs = new ArrayList<Callback>();
+    private final CallbackStub mCbStub = new CallbackStub(this);
+    private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
     private final Object mLock = new Object();
 
     private boolean mCbRegistered = false;
 
-    /**
-     * If you have a {@link MediaSessionToken} from the owner of the session a
-     * controller can be created directly. It is up to the session creator to
-     * handle token distribution if desired.
-     *
-     * @see MediaSession#getSessionToken()
-     * @param token A token from the creator of the session
-     */
-    public MediaController(MediaSessionToken token) {
-        mSessionBinder = token.getBinder();
+    private TransportController mTransportController;
+
+    private MediaController(IMediaController sessionBinder) {
+        mSessionBinder = sessionBinder;
     }
 
     /**
      * @hide
      */
-    public MediaController(IMediaController sessionBinder) {
-        mSessionBinder = sessionBinder;
+    public static MediaController fromBinder(IMediaController sessionBinder) {
+        MediaController controller = new MediaController(sessionBinder);
+        try {
+            controller.mSessionBinder.registerCallbackListener(controller.mCbStub);
+            if (controller.mSessionBinder.isTransportControlEnabled()) {
+                controller.mTransportController = new TransportController(sessionBinder);
+            }
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "MediaController created with expired token", e);
+            controller = null;
+        }
+        return controller;
     }
 
     /**
-     * Sends a generic command to the session. It is up to the session creator
-     * to decide what commands and parameters they will support. As such,
-     * commands should only be sent to sessions that the controller owns.
+     * Get a new MediaController for a MediaSessionToken. If successful the
+     * controller returned will be connected to the session that generated the
+     * token.
      *
-     * @param command The command to send
-     * @param params Any parameters to include with the command
+     * @param token The session token to use
+     * @return A controller for the session or null
      */
-    public void sendCommand(String command, Bundle params) {
-        if (TextUtils.isEmpty(command)) {
-            throw new IllegalArgumentException("command cannot be null or empty");
-        }
-        try {
-            mSessionBinder.sendCommand(command, params);
-        } catch (RemoteException e) {
-            Log.d(TAG, "Dead object in sendCommand.", e);
-        }
+    public static MediaController fromToken(MediaSessionToken token) {
+        return fromBinder(token.getBinder());
+    }
+
+    /**
+     * Get a TransportController if the session supports it. If it is not
+     * supported null will be returned.
+     *
+     * @return A TransportController or null
+     */
+    public TransportController getTransportController() {
+        return mTransportController;
     }
 
     /**
@@ -133,10 +134,10 @@
 
     /**
      * Adds a callback to receive updates from the session. Updates will be
-     * posted on the specified handler.
+     * posted on the specified handler's thread.
      *
      * @param cb Cannot be null.
-     * @param handler The handler to post updates on, if null the callers thread
+     * @param handler The handler to post updates on. If null the callers thread
      *            will be used
      */
     public void addCallback(Callback cb, Handler handler) {
@@ -160,6 +161,26 @@
         }
     }
 
+    /**
+     * Sends a generic command to the session. It is up to the session creator
+     * to decide what commands and parameters they will support. As such,
+     * commands should only be sent to sessions that the controller owns.
+     *
+     * @param command The command to send
+     * @param params Any parameters to include with the command
+     * @param cb The callback to receive the result on
+     */
+    public void sendCommand(String command, Bundle params, ResultReceiver cb) {
+        if (TextUtils.isEmpty(command)) {
+            throw new IllegalArgumentException("command cannot be null or empty");
+        }
+        try {
+            mSessionBinder.sendCommand(command, params, cb);
+        } catch (RemoteException e) {
+            Log.d(TAG, "Dead object in sendCommand.", e);
+        }
+    }
+
     /*
      * @hide
      */
@@ -174,14 +195,13 @@
         if (handler == null) {
             throw new IllegalArgumentException("Handler cannot be null");
         }
-        if (mCbs.contains(cb)) {
+        if (getHandlerForCallbackLocked(cb) != null) {
             Log.w(TAG, "Callback is already added, ignoring");
             return;
         }
-        cb.setHandler(handler);
-        mCbs.add(cb);
+        MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
+        mCallbacks.add(holder);
 
-        // Only register one cb binder, track callbacks internally and notify
         if (!mCbRegistered) {
             try {
                 mSessionBinder.registerCallbackListener(mCbStub);
@@ -192,56 +212,58 @@
         }
     }
 
-    private void removeCallbackLocked(Callback cb) {
+    private boolean removeCallbackLocked(Callback cb) {
         if (cb == null) {
             throw new IllegalArgumentException("Callback cannot be null");
         }
-        mCbs.remove(cb);
-
-        if (mCbs.size() == 0 && mCbRegistered) {
-            try {
-                mSessionBinder.unregisterCallbackListener(mCbStub);
-            } catch (RemoteException e) {
-                Log.d(TAG, "Dead object in unregisterCallback", e);
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                mCallbacks.remove(i);
+                return true;
             }
-            mCbRegistered = false;
+        }
+        return false;
+    }
+
+    private MessageHandler getHandlerForCallbackLocked(Callback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Callback cannot be null");
+        }
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private void postEvent(String event, Bundle extras) {
+        synchronized (mLock) {
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                mCallbacks.get(i).post(MSG_EVENT, event, extras);
+            }
         }
     }
 
-    private void pushOnEventLocked(String event, Bundle extras) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postEvent(event, extras);
-        }
-    }
-
-    private void pushOnMetadataUpdateLocked(Bundle metadata) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postMetadataUpdate(metadata);
-        }
-    }
-
-    private void pushOnPlaybackUpdateLocked(int newState) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postPlaybackStateChange(newState);
-        }
-    }
-
-    private void pushOnRouteChangedLocked(Bundle routeDescriptor) {
-        for (int i = mCbs.size() - 1; i >= 0; i--) {
-            mCbs.get(i).postRouteChanged(routeDescriptor);
+    private void postRouteChanged(Bundle routeDescriptor) {
+        synchronized (mLock) {
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                mCallbacks.get(i).post(MSG_ROUTE, null, routeDescriptor);
+            }
         }
     }
 
     /**
-     * MediaSession callbacks will be posted on the thread that created the
-     * Callback object.
+     * Callback for receiving updates on from the session. A Callback can be
+     * registered using {@link #addCallback}
      */
     public static abstract class Callback {
-        private Handler mHandler;
-
         /**
-         * Override to handle custom events sent by the session owner.
-         * Controllers should only handle these for sessions they own.
+         * Override to handle custom events sent by the session owner without a
+         * specified interface. Controllers should only handle these for
+         * sessions they own.
          *
          * @param event
          */
@@ -249,119 +271,83 @@
         }
 
         /**
-         * Override to handle updates to the playback state. Valid values are in
-         * {@link RemoteControlClient}. TODO put playstate values somewhere more
-         * generic.
-         *
-         * @param state
-         */
-        public void onPlaybackStateChange(int state) {
-        }
-
-        /**
-         * Override to handle metadata changes for this session's media. The
-         * default supported fields are those in {@link MediaMetadataRetriever}.
-         *
-         * @param metadata
-         */
-        public void onMetadataUpdate(Bundle metadata) {
-        }
-
-        /**
          * Override to handle route changes for this session.
          *
          * @param route
          */
         public void onRouteChanged(Bundle route) {
         }
-
-        private void setHandler(Handler handler) {
-            mHandler = new MessageHandler(handler.getLooper(), this);
-        }
-
-        private void postEvent(String event, Bundle extras) {
-            Bundle eventBundle = new Bundle();
-            eventBundle.putString(KEY_EVENT, event);
-            eventBundle.putBundle(KEY_EXTRAS, extras);
-            Message msg = mHandler.obtainMessage(MESSAGE_EVENT, eventBundle);
-            mHandler.sendMessage(msg);
-        }
-
-        private void postPlaybackStateChange(final int state) {
-            Message msg = mHandler.obtainMessage(MESSAGE_PLAYBACK_STATE, state, 0);
-            mHandler.sendMessage(msg);
-        }
-
-        private void postMetadataUpdate(final Bundle metadata) {
-            Message msg = mHandler.obtainMessage(MESSAGE_METADATA, metadata);
-            mHandler.sendMessage(msg);
-        }
-
-        private void postRouteChanged(final Bundle descriptor) {
-            Message msg = mHandler.obtainMessage(MESSAGE_ROUTE, descriptor);
-            mHandler.sendMessage(msg);
-        }
     }
 
-    private final class CallbackStub extends IMediaControllerCallback.Stub {
+    private final static class CallbackStub extends IMediaControllerCallback.Stub {
+        private final WeakReference<MediaController> mController;
+
+        public CallbackStub(MediaController controller) {
+            mController = new WeakReference<MediaController>(controller);
+        }
 
         @Override
-        public void onEvent(String event, Bundle extras) throws RemoteException {
-            synchronized (mLock) {
-                pushOnEventLocked(event, extras);
+        public void onEvent(String event, Bundle extras) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                controller.postEvent(event, extras);
             }
         }
 
         @Override
-        public void onMetadataUpdate(Bundle metadata) throws RemoteException {
-            synchronized (mLock) {
-                pushOnMetadataUpdateLocked(metadata);
+        public void onRouteChanged(Bundle mediaRouteDescriptor) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                controller.postRouteChanged(mediaRouteDescriptor);
             }
         }
 
         @Override
-        public void onPlaybackUpdate(final int newState) throws RemoteException {
-            synchronized (mLock) {
-                pushOnPlaybackUpdateLocked(newState);
+        public void onPlaybackStateChanged(PlaybackState state) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                TransportController tc = controller.getTransportController();
+                if (tc != null) {
+                    tc.postPlaybackStateChanged(state);
+                }
             }
         }
 
         @Override
-        public void onRouteChanged(Bundle mediaRouteDescriptor) throws RemoteException {
-            synchronized (mLock) {
-                pushOnRouteChangedLocked(mediaRouteDescriptor);
+        public void onMetadataChanged(MediaMetadata metadata) {
+            MediaController controller = mController.get();
+            if (controller != null) {
+                TransportController tc = controller.getTransportController();
+                if (tc != null) {
+                    tc.postMetadataChanged(metadata);
+                }
             }
         }
 
     }
 
     private final static class MessageHandler extends Handler {
-        private final MediaController.Callback mCb;
+        private final MediaController.Callback mCallback;
 
         public MessageHandler(Looper looper, MediaController.Callback cb) {
-            super(looper);
-            mCb = cb;
+            super(looper, null, true);
+            mCallback = cb;
         }
 
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MESSAGE_EVENT:
-                    Bundle eventBundle = (Bundle) msg.obj;
-                    String event = eventBundle.getString(KEY_EVENT);
-                    Bundle extras = eventBundle.getBundle(KEY_EXTRAS);
-                    mCb.onEvent(event, extras);
+                case MSG_EVENT:
+                    mCallback.onEvent((String) msg.obj, msg.getData());
                     break;
-                case MESSAGE_PLAYBACK_STATE:
-                    mCb.onPlaybackStateChange(msg.arg1);
-                    break;
-                case MESSAGE_METADATA:
-                    mCb.onMetadataUpdate((Bundle) msg.obj);
-                    break;
-                case MESSAGE_ROUTE:
-                    mCb.onRouteChanged((Bundle) msg.obj);
+                case MSG_ROUTE:
+                    mCallback.onRouteChanged(msg.getData());
             }
         }
+
+        public void post(int what, Object obj, Bundle data) {
+            obtainMessage(what, obj).sendToTarget();
+        }
     }
 
 }
diff --git a/media/java/android/media/session/MediaMetadata.aidl b/media/java/android/media/session/MediaMetadata.aidl
new file mode 100644
index 0000000..4431d9d
--- /dev/null
+++ b/media/java/android/media/session/MediaMetadata.aidl
@@ -0,0 +1,18 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.media.session;
+
+parcelable MediaMetadata;
diff --git a/media/java/android/media/session/MediaMetadata.java b/media/java/android/media/session/MediaMetadata.java
new file mode 100644
index 0000000..e2330f7
--- /dev/null
+++ b/media/java/android/media/session/MediaMetadata.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.session;
+
+import android.graphics.Bitmap;
+import android.media.Rating;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.Log;
+
+/**
+ * Contains metadata about an item, such as the title, artist, etc.
+ */
+public final class MediaMetadata implements Parcelable {
+    private static final String TAG = "MediaMetadata";
+
+    /**
+     * The title of the media.
+     */
+    public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+
+    /**
+     * The artist of the media.
+     */
+    public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+
+    /**
+     * The duration of the media in ms. A duration of 0 is the default.
+     */
+    public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+
+    /**
+     * The album title for the media.
+     */
+    public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+
+    /**
+     * The author of the media.
+     */
+    public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+    /**
+     * The writer of the media.
+     */
+    public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+
+    /**
+     * The composer of the media.
+     */
+    public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+    /**
+     * The date the media was created or published as TODO determine format.
+     */
+    public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
+
+    /**
+     * The year the media was created or published as a numeric String.
+     */
+    public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+
+    /**
+     * The genre of the media.
+     */
+    public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+
+    /**
+     * The track number for the media.
+     */
+    public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+    /**
+     * The number of tracks in the media's original source.
+     */
+    public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+
+    /**
+     * The disc number for the media's original source.
+     */
+    public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+    /**
+     * The artist for the album of the media's original source.
+     */
+    public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+    /**
+     * The artwork for the media as a {@link Bitmap}.
+     */
+    public static final String METADATA_KEY_ART = "android.media.metadata.ART";
+
+    /**
+     * The artwork for the media as a Uri style String.
+     */
+    public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+
+    /**
+     * The artwork for the album of the media's original source as a
+     * {@link Bitmap}.
+     */
+    public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+
+    /**
+     * The artwork for the album of the media's original source as a Uri style
+     * String.
+     */
+    public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+
+    /**
+     * The user's rating for the media.
+     *
+     * @see Rating
+     */
+    public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+
+    /**
+     * The overall rating for the media.
+     *
+     * @see Rating
+     */
+    public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
+
+    private static final int METADATA_TYPE_INVALID = -1;
+    private static final int METADATA_TYPE_LONG = 0;
+    private static final int METADATA_TYPE_STRING = 1;
+    private static final int METADATA_TYPE_BITMAP = 2;
+    private static final int METADATA_TYPE_RATING = 3;
+    private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
+
+    static {
+        METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
+        METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_STRING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
+        METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
+    }
+    private final Bundle mBundle;
+
+    private MediaMetadata(Bundle bundle) {
+        mBundle = new Bundle(bundle);
+    }
+
+    private MediaMetadata(Parcel in) {
+        mBundle = in.readBundle();
+    }
+
+    /**
+     * Returns the value associated with the given key, or null if no mapping of
+     * the desired type exists for the given key or a null value is explicitly
+     * associated with the key.
+     *
+     * @param key The key the value is stored under
+     * @return a String value, or null
+     */
+    public String getString(String key) {
+        return mBundle.getString(key);
+    }
+
+    /**
+     * Returns the value associated with the given key, or 0L if no long exists
+     * for the given key.
+     *
+     * @param key The key the value is stored under
+     * @return a long value
+     */
+    public long getLong(String key) {
+        return mBundle.getLong(key);
+    }
+
+    /**
+     * Return a {@link Rating} for the given key or null if no rating exists for
+     * the given key.
+     *
+     * @param key The key the value is stored under
+     * @return A {@link Rating} or null
+     */
+    public Rating getRating(String key) {
+        Rating rating = null;
+        try {
+            rating = mBundle.getParcelable(key);
+        } catch (Exception e) {
+            // ignore, value was not a bitmap
+            Log.d(TAG, "Failed to retrieve a key as Rating.", e);
+        }
+        return rating;
+    }
+
+    /**
+     * Return a {@link Bitmap} for the given key or null if no bitmap exists for
+     * the given key.
+     *
+     * @param key The key the value is stored under
+     * @return A {@link Bitmap} or null
+     */
+    public Bitmap getBitmap(String key) {
+        Bitmap bmp = null;
+        try {
+            bmp = mBundle.getParcelable(key);
+        } catch (Exception e) {
+            // ignore, value was not a bitmap
+            Log.d(TAG, "Failed to retrieve a key as Bitmap.", e);
+        }
+        return bmp;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeBundle(mBundle);
+    }
+
+    public static final Parcelable.Creator<MediaMetadata> CREATOR
+            = new Parcelable.Creator<MediaMetadata>() {
+                @Override
+                public MediaMetadata createFromParcel(Parcel in) {
+                    return new MediaMetadata(in);
+                }
+
+                @Override
+                public MediaMetadata[] newArray(int size) {
+                    return new MediaMetadata[size];
+                }
+            };
+
+    /**
+     * Use to build MediaMetadata objects. The system defined metadata keys must
+     * use the appropriate data type.
+     */
+    public static final class Builder {
+        private final Bundle mBundle;
+
+        /**
+         * Create an empty Builder. Any field that should be included in the
+         * {@link MediaMetadata} must be added.
+         */
+        public Builder() {
+            mBundle = new Bundle();
+        }
+
+        /**
+         * Create a Builder using a {@link MediaMetadata} instance to set the
+         * initial values. All fields in the source metadata will be included in
+         * the new metadata. Fields can be overwritten by adding the same key.
+         *
+         * @param source
+         */
+        public Builder(MediaMetadata source) {
+            mBundle = new Bundle(source.mBundle);
+        }
+
+        /**
+         * Put a String value into the metadata. Custom keys may be used, but if
+         * the METADATA_KEYs defined in this class are used they may only be one
+         * of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_TITLE}</li>
+         * <li>{@link #METADATA_KEY_ARTIST}</li>
+         * <li>{@link #METADATA_KEY_ALBUM}</li>
+         * <li>{@link #METADATA_KEY_AUTHOR}</li>
+         * <li>{@link #METADATA_KEY_WRITER}</li>
+         * <li>{@link #METADATA_KEY_COMPOSER}</li>
+         * <li>{@link #METADATA_KEY_DATE}</li>
+         * <li>{@link #METADATA_KEY_YEAR}</li>
+         * <li>{@link #METADATA_KEY_GENRE}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>li>
+         * <li>{@link #METADATA_KEY_ART_URI}</li>li>
+         * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putString(String key, String value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_STRING) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a String");
+                }
+            }
+            mBundle.putString(key, value);
+            return this;
+        }
+
+        /**
+         * Put a long value into the metadata. Custom keys may be used, but if
+         * the METADATA_KEYs defined in this class are used they may only be one
+         * of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_DURATION}</li>
+         * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
+         * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
+         * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putLong(String key, long value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a long");
+                }
+            }
+            mBundle.putLong(key, value);
+            return this;
+        }
+
+        /**
+         * Put a {@link Rating} into the metadata. Custom keys may be used, but
+         * if the METADATA_KEYs defined in this class are used they may only be
+         * one of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_RATING}</li>
+         * <li>{@link #METADATA_KEY_USER_RATING}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The String value to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putRating(String key, Rating value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a Rating");
+                }
+            }
+            mBundle.putParcelable(key, value);
+            return this;
+        }
+
+        /**
+         * Put a {@link Bitmap} into the metadata. Custom keys may be used, but
+         * if the METADATA_KEYs defined in this class are used they may only be
+         * one of the following:
+         * <ul>
+         * <li>{@link #METADATA_KEY_ART}</li>
+         * <li>{@link #METADATA_KEY_ALBUM_ART}</li>
+         * </ul>
+         *
+         * @param key The key for referencing this value
+         * @param value The Bitmap to store
+         * @return The Builder to allow chaining
+         */
+        public Builder putBitmap(String key, Bitmap value) {
+            if (METADATA_KEYS_TYPE.containsKey(key)) {
+                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
+                    throw new IllegalArgumentException("The " + key
+                            + " key cannot be used to put a Bitmap");
+                }
+            }
+            mBundle.putParcelable(key, value);
+            return this;
+        }
+
+        /**
+         * Creates a {@link MediaMetadata} instance with the specified fields.
+         *
+         * @return The new MediaMetadata instance
+         */
+        public MediaMetadata build() {
+            return new MediaMetadata(mBundle);
+        }
+    }
+
+}
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 1f1533b..23c3035 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -17,17 +17,21 @@
 package android.media.session;
 
 import android.content.Intent;
+import android.media.Rating;
 import android.media.session.IMediaController;
 import android.media.session.IMediaSession;
 import android.media.session.IMediaSessionCallback;
-import android.media.RemoteControlClient;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
 /**
@@ -58,12 +62,13 @@
 public final class MediaSession {
     private static final String TAG = "MediaSession";
 
-    private static final int MESSAGE_MEDIA_BUTTON = 1;
-    private static final int MESSAGE_COMMAND = 2;
-    private static final int MESSAGE_ROUTE_CHANGE = 3;
+    private static final int MSG_MEDIA_BUTTON = 1;
+    private static final int MSG_COMMAND = 2;
+    private static final int MSG_ROUTE_CHANGE = 3;
 
     private static final String KEY_COMMAND = "command";
     private static final String KEY_EXTRAS = "extras";
+    private static final String KEY_CALLBACK = "callback";
 
     private final Object mLock = new Object();
 
@@ -71,7 +76,14 @@
     private final IMediaSession mBinder;
     private final CallbackStub mCbStub;
 
-    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+    private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
+    // TODO route interfaces
+    private final ArrayMap<String, RouteInterface.Stub> mInterfaces
+            = new ArrayMap<String, RouteInterface.Stub>();
+
+    private TransportPerformer mPerformer;
+
+    private boolean mPublished = false;;
 
     /**
      * @hide
@@ -81,7 +93,7 @@
         mCbStub = cbStub;
         IMediaController controllerBinder = null;
         try {
-            controllerBinder = mBinder.getMediaSessionToken();
+            controllerBinder = mBinder.getMediaController();
         } catch (RemoteException e) {
             throw new RuntimeException("Dead object in MediaSessionController constructor: ", e);
         }
@@ -102,34 +114,117 @@
             throw new IllegalArgumentException("Callback cannot be null");
         }
         synchronized (mLock) {
-            if (mCallbacks.contains(callback)) {
+            if (getHandlerForCallbackLocked(callback) != null) {
                 Log.w(TAG, "Callback is already added, ignoring");
+                return;
             }
             if (handler == null) {
                 handler = new Handler();
             }
             MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback);
-            callback.setHandler(msgHandler);
-            mCallbacks.add(callback);
+            mCallbacks.add(msgHandler);
         }
     }
 
     public void removeCallback(Callback callback) {
-        mCallbacks.remove(callback);
+        synchronized (mLock) {
+            removeCallbackLocked(callback);
+        }
     }
 
     /**
-     * Publish the current playback state to the system and any controllers.
-     * Valid values are defined in {@link RemoteControlClient}. TODO move play
-     * states somewhere else.
+     * Start using a TransportPerformer with this media session. This must be
+     * called before calling publish and cannot be called more than once.
+     * Calling this will allow MediaControllers to retrieve a
+     * TransportController.
      *
-     * @param state
+     * @see TransportController
+     * @return The TransportPerformer created for this session
      */
-    public void setPlaybackState(int state) {
+    public TransportPerformer setTransportPerformerEnabled() {
+        if (mPerformer != null) {
+            throw new IllegalStateException("setTransportPerformer can only be called once.");
+        }
+        if (mPublished) {
+            throw new IllegalStateException("setTransportPerformer cannot be called after publish");
+        }
+
+        mPerformer = new TransportPerformer(mBinder);
         try {
-            mBinder.setPlaybackState(state);
+            mBinder.setTransportPerformerEnabled();
         } catch (RemoteException e) {
-            Log.e(TAG, "Dead object in setPlaybackState: ", e);
+            Log.wtf(TAG, "Failure in setTransportPerformerEnabled.", e);
+        }
+        return mPerformer;
+    }
+
+    /**
+     * Retrieves the TransportPerformer used by this session. If called before
+     * {@link #setTransportPerformerEnabled} null will be returned.
+     *
+     * @return The TransportPerformer associated with this session or null
+     */
+    public TransportPerformer getTransportPerformer() {
+        return mPerformer;
+    }
+
+    /**
+     * Call after you have finished setting up the session. This will make it
+     * available to listeners and begin pushing updates to MediaControllers.
+     * This can only be called once.
+     */
+    public void publish() {
+        if (mPublished) {
+            throw new RuntimeException("publish() may only be called once.");
+        }
+        try {
+            mBinder.publish();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Failure in publish.", e);
+        }
+        mPublished = true;
+    }
+
+    /**
+     * Add an interface that can be used by MediaSessions. TODO make this a
+     * route provider api
+     *
+     * @see RouteInterface
+     * @param iface The interface to add
+     * @hide
+     */
+    public void addInterface(RouteInterface.Stub iface) {
+        if (iface == null) {
+            throw new IllegalArgumentException("Stub cannot be null");
+        }
+        String name = iface.getName();
+        if (TextUtils.isEmpty(name)) {
+            throw new IllegalArgumentException("Stub must return a valid name");
+        }
+        if (mInterfaces.containsKey(iface)) {
+            throw new IllegalArgumentException("Interface is already added");
+        }
+        synchronized (mLock) {
+            mInterfaces.put(iface.getName(), iface);
+        }
+    }
+
+    /**
+     * 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 sendEvent(String event, Bundle extras) {
+        if (TextUtils.isEmpty(event)) {
+            throw new IllegalArgumentException("event cannot be null or empty");
+        }
+        try {
+            mBinder.sendEvent(event, extras);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error sending event", e);
         }
     }
 
@@ -142,7 +237,7 @@
         try {
             mBinder.destroy();
         } catch (RemoteException e) {
-            Log.e(TAG, "Dead object in onDestroy: ", e);
+            Log.wtf(TAG, "Error releasing session: ", e);
         }
     }
 
@@ -158,15 +253,38 @@
         return mSessionToken;
     }
 
-    private void postCommand(String command, Bundle extras) {
-        Bundle commandBundle = new Bundle();
-        commandBundle.putString(KEY_COMMAND, command);
-        commandBundle.putBundle(KEY_EXTRAS, extras);
+    private MessageHandler getHandlerForCallbackLocked(Callback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Callback cannot be null");
+        }
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private boolean removeCallbackLocked(Callback cb) {
+        if (cb == null) {
+            throw new IllegalArgumentException("Callback cannot be null");
+        }
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mCallbacks.get(i);
+            if (cb == handler.mCallback) {
+                mCallbacks.remove(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void postCommand(String command, Bundle extras, ResultReceiver resultCb) {
+        Command cmd = new Command(command, extras, resultCb);
         synchronized (mLock) {
             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                Callback cb = mCallbacks.get(i);
-                Message msg = cb.mHandler.obtainMessage(MESSAGE_COMMAND, commandBundle);
-                cb.mHandler.sendMessage(msg);
+                mCallbacks.get(i).post(MSG_COMMAND, cmd);
             }
         }
     }
@@ -174,9 +292,7 @@
     private void postMediaButton(Intent mediaButtonIntent) {
         synchronized (mLock) {
             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                Callback cb = mCallbacks.get(i);
-                Message msg = cb.mHandler.obtainMessage(MESSAGE_MEDIA_BUTTON, mediaButtonIntent);
-                cb.mHandler.sendMessage(msg);
+                mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent);
             }
         }
     }
@@ -184,9 +300,7 @@
     private void postRequestRouteChange(Bundle mediaRouteDescriptor) {
         synchronized (mLock) {
             for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                Callback cb = mCallbacks.get(i);
-                Message msg = cb.mHandler.obtainMessage(MESSAGE_ROUTE_CHANGE, mediaRouteDescriptor);
-                cb.mHandler.sendMessage(msg);
+                mCallbacks.get(i).post(MSG_ROUTE_CHANGE, mediaRouteDescriptor);
             }
         }
     }
@@ -197,7 +311,6 @@
      * MediaSession (TODO).
      */
     public abstract static class Callback {
-        private MessageHandler mHandler;
 
         public Callback() {
         }
@@ -225,7 +338,7 @@
          * @param command
          * @param extras optional
          */
-        public void onCommand(String command, Bundle extras) {
+        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
         }
 
         /**
@@ -237,35 +350,140 @@
          */
         public void onRequestRouteChange(Bundle descriptor) {
         }
-
-        private void setHandler(MessageHandler handler) {
-            mHandler = handler;
-        }
     }
 
     /**
      * @hide
      */
     public static class CallbackStub extends IMediaSessionCallback.Stub {
-        private MediaSession mMediaSession;
+        private WeakReference<MediaSession> mMediaSession;
 
         public void setMediaSession(MediaSession session) {
-            mMediaSession = session;
+            mMediaSession = new WeakReference<MediaSession>(session);
         }
 
         @Override
-        public void onCommand(String command, Bundle extras) throws RemoteException {
-            mMediaSession.postCommand(command, extras);
+        public void onCommand(String command, Bundle extras, ResultReceiver cb)
+                throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                session.postCommand(command, extras, cb);
+            }
         }
 
         @Override
         public void onMediaButton(Intent mediaButtonIntent) throws RemoteException {
-            mMediaSession.postMediaButton(mediaButtonIntent);
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                session.postMediaButton(mediaButtonIntent);
+            }
         }
 
         @Override
         public void onRequestRouteChange(Bundle mediaRouteDescriptor) throws RemoteException {
-            mMediaSession.postRequestRouteChange(mediaRouteDescriptor);
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                session.postRequestRouteChange(mediaRouteDescriptor);
+            }
+        }
+
+        @Override
+        public void onPlay() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onPlay();
+                }
+            }
+        }
+
+        @Override
+        public void onPause() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onPause();
+                }
+            }
+        }
+
+        @Override
+        public void onStop() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onStop();
+                }
+            }
+        }
+
+        @Override
+        public void onNext() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onNext();
+                }
+            }
+        }
+
+        @Override
+        public void onPrevious() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onPrevious();
+                }
+            }
+        }
+
+        @Override
+        public void onFastForward() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onFastForward();
+                }
+            }
+        }
+
+        @Override
+        public void onRewind() throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onRewind();
+                }
+            }
+        }
+
+        @Override
+        public void onSeekTo(long pos) throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onSeekTo(pos);
+                }
+            }
+        }
+
+        @Override
+        public void onRate(Rating rating) throws RemoteException {
+            MediaSession session = mMediaSession.get();
+            if (session != null) {
+                TransportPerformer tp = session.getTransportPerformer();
+                if (tp != null) {
+                    tp.onRate(rating);
+                }
+            }
         }
 
     }
@@ -274,7 +492,7 @@
         private MediaSession.Callback mCallback;
 
         public MessageHandler(Looper looper, MediaSession.Callback callback) {
-            super(looper);
+            super(looper, null, true);
             mCallback = callback;
         }
 
@@ -285,21 +503,35 @@
                     return;
                 }
                 switch (msg.what) {
-                    case MESSAGE_MEDIA_BUTTON:
+                    case MSG_MEDIA_BUTTON:
                         mCallback.onMediaButton((Intent) msg.obj);
                         break;
-                    case MESSAGE_COMMAND:
-                        Bundle commandBundle = (Bundle) msg.obj;
-                        String command = commandBundle.getString(KEY_COMMAND);
-                        Bundle extras = commandBundle.getBundle(KEY_EXTRAS);
-                        mCallback.onCommand(command, extras);
+                    case MSG_COMMAND:
+                        Command cmd = (Command) msg.obj;
+                        mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
                         break;
-                    case MESSAGE_ROUTE_CHANGE:
+                    case MSG_ROUTE_CHANGE:
                         mCallback.onRequestRouteChange((Bundle) msg.obj);
                         break;
                 }
             }
             msg.recycle();
         }
+
+        public void post(int what, Object obj) {
+            obtainMessage(what, obj).sendToTarget();
+        }
+    }
+
+    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;
+        }
     }
 }
diff --git a/media/java/android/media/session/PlaybackState.aidl b/media/java/android/media/session/PlaybackState.aidl
new file mode 100644
index 0000000..0876ebd
--- /dev/null
+++ b/media/java/android/media/session/PlaybackState.aidl
@@ -0,0 +1,18 @@
+/* Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.media.session;
+
+parcelable PlaybackState;
diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java
new file mode 100644
index 0000000..b3506b3
--- /dev/null
+++ b/media/java/android/media/session/PlaybackState.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.session;
+
+import android.media.RemoteControlClient;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Playback state for a {@link MediaSession}. This includes a state like
+ * {@link PlaybackState#PLAYSTATE_PLAYING}, the current playback position,
+ * and the current control capabilities.
+ */
+public final class PlaybackState implements Parcelable {
+    /**
+     * Indicates this performer supports the stop command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_STOP = 1 << 0;
+
+    /**
+     * Indicates this performer supports the pause command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_PAUSE = 1 << 1;
+
+    /**
+     * Indicates this performer supports the play command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_PLAY = 1 << 2;
+
+    /**
+     * Indicates this performer supports the rewind command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_REWIND = 1 << 3;
+
+    /**
+     * Indicates this performer supports the previous command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_PREVIOUS_ITEM = 1 << 4;
+
+    /**
+     * Indicates this performer supports the next command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_NEXT_ITEM = 1 << 5;
+
+    /**
+     * Indicates this performer supports the fast forward command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_FASTFORWARD = 1 << 6;
+
+    /**
+     * Indicates this performer supports the set rating command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_RATING = 1 << 7;
+
+    /**
+     * Indicates this performer supports the seek to command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_SEEK_TO = 1 << 8;
+
+    /**
+     * This is the default playback state and indicates that no media has been
+     * added yet, or the performer has been reset and has no content to play.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_NONE = 0;
+
+    /**
+     * State indicating this item is currently stopped.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_STOPPED = 1;
+
+    /**
+     * State indicating this item is currently paused.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_PAUSED = 2;
+
+    /**
+     * State indicating this item is currently playing.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_PLAYING = 3;
+
+    /**
+     * State indicating this item is currently fast forwarding.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_FAST_FORWARDING = 4;
+
+    /**
+     * State indicating this item is currently rewinding.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_REWINDING = 5;
+
+    /**
+     * State indicating this item is currently buffering and will begin playing
+     * when enough data has buffered.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_BUFFERING = 6;
+
+    /**
+     * State indicating this item is currently in an error state. The error
+     * message should also be set when entering this state.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_ERROR = 7;
+
+    private int mState;
+    private long mPosition;
+    private long mBufferPosition;
+    private float mSpeed;
+    private long mCapabilities;
+    private String mErrorMessage;
+
+    /**
+     * Create an empty PlaybackState. At minimum a state and actions should be
+     * set before publishing a PlaybackState.
+     */
+    public PlaybackState() {
+    }
+
+    /**
+     * Create a new PlaybackState from an existing PlaybackState. All fields
+     * will be copied to the new state.
+     *
+     * @param from The PlaybackState to duplicate
+     */
+    public PlaybackState(PlaybackState from) {
+        this.setState(from.getState());
+        this.setPosition(from.getPosition());
+        this.setBufferPosition(from.getBufferPosition());
+        this.setSpeed(from.getSpeed());
+        this.setActions(from.getActions());
+        this.setErrorMessage(from.getErrorMessage());
+    }
+
+    private PlaybackState(Parcel in) {
+        this.setState(in.readInt());
+        this.setPosition(in.readLong());
+        this.setBufferPosition(in.readLong());
+        this.setSpeed(in.readFloat());
+        this.setActions(in.readLong());
+        this.setErrorMessage(in.readString());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(getState());
+        dest.writeLong(getPosition());
+        dest.writeLong(getBufferPosition());
+        dest.writeFloat(getSpeed());
+        dest.writeLong(getActions());
+        dest.writeString(getErrorMessage());
+    }
+
+    /**
+     * Get the current state of playback. One of the following:
+     * <ul>
+     * <li> {@link PlaybackState#PLAYSTATE_NONE}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_STOPPED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PLAYING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PAUSED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_FAST_FORWARDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_REWINDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_BUFFERING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_ERROR}</li>
+     */
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * Set the current state of playback. One of the following:
+     * <ul>
+     * <li> {@link PlaybackState#PLAYSTATE_NONE}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_STOPPED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PLAYING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_PAUSED}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_FAST_FORWARDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_REWINDING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_BUFFERING}</li>
+     * <li> {@link PlaybackState#PLAYSTATE_ERROR}</li>
+     */
+    public void setState(int mState) {
+        this.mState = mState;
+    }
+
+    /**
+     * Get the current playback position in ms.
+     */
+    public long getPosition() {
+        return mPosition;
+    }
+
+    /**
+     * Set the current playback position in ms.
+     */
+    public void setPosition(long position) {
+        mPosition = position;
+    }
+
+    /**
+     * Get the current buffer position in ms. This is the farthest playback
+     * point that can be reached from the current position using only buffered
+     * content.
+     */
+    public long getBufferPosition() {
+        return mBufferPosition;
+    }
+
+    /**
+     * Set the current buffer position in ms. This is the farthest playback
+     * point that can be reached from the current position using only buffered
+     * content.
+     */
+    public void setBufferPosition(long bufferPosition) {
+        mBufferPosition = bufferPosition;
+    }
+
+    /**
+     * Get the current playback speed as a multiple of normal playback. This
+     * should be negative when rewinding. A value of 1 means normal playback and
+     * 0 means paused.
+     */
+    public float getSpeed() {
+        return mSpeed;
+    }
+
+    /**
+     * Set the current playback speed as a multiple of normal playback. This
+     * should be negative when rewinding. A value of 1 means normal playback and
+     * 0 means paused.
+     */
+    public void setSpeed(float speed) {
+        mSpeed = speed;
+    }
+
+    /**
+     * Get the current actions available on this session. This should use a
+     * bitmask of the available actions.
+     * <ul>
+     * <li> {@link PlaybackState#ACTION_PREVIOUS_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_REWIND}</li>
+     * <li> {@link PlaybackState#ACTION_PLAY}</li>
+     * <li> {@link PlaybackState#ACTION_PAUSE}</li>
+     * <li> {@link PlaybackState#ACTION_STOP}</li>
+     * <li> {@link PlaybackState#ACTION_FASTFORWARD}</li>
+     * <li> {@link PlaybackState#ACTION_NEXT_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_SEEK_TO}</li>
+     * <li> {@link PlaybackState#ACTION_RATING}</li>
+     * </ul>
+     */
+    public long getActions() {
+        return mCapabilities;
+    }
+
+    /**
+     * Set the current capabilities available on this session. This should use a
+     * bitmask of the available capabilities.
+     * <ul>
+     * <li> {@link PlaybackState#ACTION_PREVIOUS_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_REWIND}</li>
+     * <li> {@link PlaybackState#ACTION_PLAY}</li>
+     * <li> {@link PlaybackState#ACTION_PAUSE}</li>
+     * <li> {@link PlaybackState#ACTION_STOP}</li>
+     * <li> {@link PlaybackState#ACTION_FASTFORWARD}</li>
+     * <li> {@link PlaybackState#ACTION_NEXT_ITEM}</li>
+     * <li> {@link PlaybackState#ACTION_SEEK_TO}</li>
+     * <li> {@link PlaybackState#ACTION_RATING}</li>
+     * </ul>
+     */
+    public void setActions(long capabilities) {
+        mCapabilities = capabilities;
+    }
+
+    /**
+     * Get a user readable error message. This should be set when the state is
+     * {@link PlaybackState#PLAYSTATE_ERROR}.
+     */
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    /**
+     * Set a user readable error message. This should be set when the state is
+     * {@link PlaybackState#PLAYSTATE_ERROR}.
+     */
+    public void setErrorMessage(String errorMessage) {
+        mErrorMessage = errorMessage;
+    }
+
+    public static final Parcelable.Creator<PlaybackState> CREATOR
+            = new Parcelable.Creator<PlaybackState>() {
+        @Override
+        public PlaybackState createFromParcel(Parcel in) {
+            return new PlaybackState(in);
+        }
+
+        @Override
+        public PlaybackState[] newArray(int size) {
+            return new PlaybackState[size];
+        }
+    };
+}
diff --git a/media/java/android/media/session/RouteInterface.java b/media/java/android/media/session/RouteInterface.java
new file mode 100644
index 0000000..2391f27
--- /dev/null
+++ b/media/java/android/media/session/RouteInterface.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.session;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+
+/**
+ * Routes can support multiple interfaces for MediaSessions to interact with. To
+ * add a standard interface you should implement that interface's RouteInterface
+ * Stub and register it with the session. The set of supported commands is
+ * dependent on the specific interface's implementation.
+ * <p>
+ * A MediaInterface can be registered by calling TODO. Once added an interface
+ * will be used by Sessions to decide how they communicate with a session and
+ * cannot be removed, so all interfaces that you plan to support should be added
+ * when the route is created.
+ *
+ * @see RouteTransportControls
+ */
+public final class RouteInterface {
+    private static final String TAG = "MediaInterface";
+
+    private static final String KEY_RESULT = "result";
+
+    private final MediaController mController;
+    private final String mIface;
+
+    /**
+     * @hide
+     */
+    RouteInterface(MediaController controller, String iface) {
+        mController = controller;
+        mIface = iface;
+    }
+
+    public void sendCommand(String command, Bundle params, ResultReceiver cb) {
+        // TODO
+    }
+
+    public void addListener(EventListener listener) {
+        addListener(listener, null);
+    }
+
+    public void addListener(EventListener listener, Handler handler) {
+        // TODO See MediaController for add/remove pattern
+    }
+
+    public void removeListener(EventListener listener) {
+        // TODO
+    }
+
+    // TODO decide on list of supported types
+    private static Bundle writeResultToBundle(Object v) {
+        Bundle b = new Bundle();
+        if (v == null) {
+            // Don't send anything if null
+        } else if (v instanceof String) {
+            b.putString(KEY_RESULT, (String) v);
+        } else if (v instanceof Integer) {
+            b.putInt(KEY_RESULT, (Integer) v);
+        } else if (v instanceof Bundle) {
+            // Must be before Parcelable
+            b.putBundle(KEY_RESULT, (Bundle) v);
+        } else if (v instanceof Parcelable) {
+            b.putParcelable(KEY_RESULT, (Parcelable) v);
+        } else if (v instanceof Short) {
+            b.putShort(KEY_RESULT, (Short) v);
+        } else if (v instanceof Long) {
+            b.putLong(KEY_RESULT, (Long) v);
+        } else if (v instanceof Float) {
+            b.putFloat(KEY_RESULT, (Float) v);
+        } else if (v instanceof Double) {
+            b.putDouble(KEY_RESULT, (Double) v);
+        } else if (v instanceof Boolean) {
+            b.putBoolean(KEY_RESULT, (Boolean) v);
+        } else if (v instanceof CharSequence) {
+            // Must be after String
+            b.putCharSequence(KEY_RESULT, (CharSequence) v);
+        } else if (v instanceof boolean[]) {
+            b.putBooleanArray(KEY_RESULT, (boolean[]) v);
+        } else if (v instanceof byte[]) {
+            b.putByteArray(KEY_RESULT, (byte[]) v);
+        } else if (v instanceof String[]) {
+            b.putStringArray(KEY_RESULT, (String[]) v);
+        } else if (v instanceof CharSequence[]) {
+            // Must be after String[] and before Object[]
+            b.putCharSequenceArray(KEY_RESULT, (CharSequence[]) v);
+        } else if (v instanceof IBinder) {
+            b.putBinder(KEY_RESULT, (IBinder) v);
+        } else if (v instanceof Parcelable[]) {
+            b.putParcelableArray(KEY_RESULT, (Parcelable[]) v);
+        } else if (v instanceof int[]) {
+            b.putIntArray(KEY_RESULT, (int[]) v);
+        } else if (v instanceof long[]) {
+            b.putLongArray(KEY_RESULT, (long[]) v);
+        } else if (v instanceof Byte) {
+            b.putByte(KEY_RESULT, (Byte) v);
+        }
+        return b;
+    }
+
+    public abstract static class Stub {
+
+        /**
+         * The name of an interface should be a fully qualified name to prevent
+         * namespace collisions. Example: "com.myproject.MyPlaybackInterface"
+         *
+         * @return The name of this interface
+         */
+        public abstract String getName();
+
+        /**
+         * This is called when a command is received that matches the interface
+         * you registered. Commands can come from any app with a MediaController
+         * reference to the session.
+         *
+         * @see MediaController
+         * @see MediaSession
+         * @param command The command or method to invoke.
+         * @param args Any args that were included with the command. May be
+         *            null.
+         * @param cb The callback provided to send a response on. May be null.
+         */
+        public abstract void onCommand(String command, Bundle args, ResultReceiver cb);
+
+        public final void sendEvent(MediaSession session, String event, Bundle extras) {
+            // TODO
+        }
+    }
+
+    /**
+     * An EventListener can be registered by an app with TODO to handle events
+     * sent by the session on a specific interface.
+     */
+    public static abstract class EventListener {
+        /**
+         * This is called when an event is received from the interface. Events
+         * are sent by the session owner and will be delivered to all
+         * controllers that are listening to the interface.
+         *
+         * @param event The event that occurred.
+         * @param args Any extras that were included with the event. May be
+         *            null.
+         */
+        public abstract void onEvent(String event, Bundle args);
+    }
+
+    private static final class EventHandler extends Handler {
+
+        private final RouteInterface.EventListener mListener;
+
+        public EventHandler(Looper looper, RouteInterface.EventListener cb) {
+            super(looper, null, true);
+            mListener = cb;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            mListener.onEvent((String) msg.obj, msg.getData());
+        }
+
+        public void postEvent(String event, Bundle args) {
+            Message msg = obtainMessage(0, event);
+            msg.setData(args);
+            msg.sendToTarget();
+        }
+    }
+}
diff --git a/media/java/android/media/session/RouteTransportControls.java b/media/java/android/media/session/RouteTransportControls.java
new file mode 100644
index 0000000..665fd10
--- /dev/null
+++ b/media/java/android/media/session/RouteTransportControls.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.session;
+
+import android.media.RemoteControlClient;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * A standard media control interface for Routes. Routes can support multiple
+ * interfaces for MediaSessions to interact with. TODO rewrite for routes
+ */
+public final class RouteTransportControls {
+    private static final String TAG = "RouteTransportControls";
+    public static final String NAME = "android.media.session.RouteTransportControls";
+
+    private static final String KEY_VALUE1 = "value1";
+
+    private static final String METHOD_FAST_FORWARD = "fastForward";
+    private static final String METHOD_GET_CURRENT_POSITION = "getCurrentPosition";
+    private static final String METHOD_GET_CAPABILITIES = "getCapabilities";
+
+    private static final String EVENT_PLAYSTATE_CHANGE = "playstateChange";
+    private static final String EVENT_METADATA_CHANGE = "metadataChange";
+
+    private final MediaController mController;
+    private final RouteInterface mIface;
+
+    private RouteTransportControls(RouteInterface iface, MediaController controller) {
+        mIface = iface;
+        mController = controller;
+    }
+
+    public static RouteTransportControls from(MediaController controller) {
+//        MediaInterface iface = controller.getInterface(NAME);
+//        if (iface != null) {
+//            return new RouteTransportControls(iface, controller);
+//        }
+        return null;
+    }
+
+    /**
+     * Send a play command to the route. TODO rename resume() and use messaging
+     * protocol, not KeyEvent
+     */
+    public void play() {
+        // TODO
+    }
+
+    /**
+     * Send a pause command to the session.
+     */
+    public void pause() {
+        // TODO
+    }
+
+    /**
+     * Set the rate at which to fastforward. Valid values are in the range [0,1]
+     * with actual rates depending on the implementation.
+     *
+     * @param rate
+     */
+    public void fastForward(float rate) {
+        if (rate < 0 || rate > 1) {
+            throw new IllegalArgumentException("Rate must be between 0 and 1 inclusive");
+        }
+        Bundle b = new Bundle();
+        b.putFloat(KEY_VALUE1, rate);
+        mIface.sendCommand(METHOD_FAST_FORWARD, b, null);
+    }
+
+    public void getCurrentPosition(ResultReceiver cb) {
+        mIface.sendCommand(METHOD_GET_CURRENT_POSITION, null, cb);
+    }
+
+    public void getCapabilities(ResultReceiver cb) {
+        mIface.sendCommand(METHOD_GET_CAPABILITIES, null, cb);
+    }
+
+    public void addListener(Listener listener) {
+        mIface.addListener(listener.mListener);
+    }
+
+    public void addListener(Listener listener, Handler handler) {
+        mIface.addListener(listener.mListener, handler);
+    }
+
+    public void removeListener(Listener listener) {
+        mIface.removeListener(listener.mListener);
+    }
+
+    public static abstract class Stub extends RouteInterface.Stub {
+        private final MediaSession mSession;
+
+        public Stub(MediaSession session) {
+            mSession = session;
+        }
+
+        @Override
+        public String getName() {
+            return NAME;
+        }
+
+        @Override
+        public void onCommand(String method, Bundle extras, ResultReceiver cb) {
+            if (TextUtils.isEmpty(method)) {
+                return;
+            }
+            Bundle result;
+            if (METHOD_FAST_FORWARD.equals(method)) {
+                fastForward(extras.getFloat(KEY_VALUE1, -1));
+            } else if (METHOD_GET_CURRENT_POSITION.equals(method)) {
+                if (cb != null) {
+                    result = new Bundle();
+                    result.putLong(KEY_VALUE1, getCurrentPosition());
+                    cb.send(0, result);
+                }
+            } else if (METHOD_GET_CAPABILITIES.equals(method)) {
+                if (cb != null) {
+                    result = new Bundle();
+                    result.putLong(KEY_VALUE1, getCapabilities());
+                    cb.send(0, result);
+                }
+            }
+        }
+
+        /**
+         * Override to handle fast forwarding. Valid values are [0,1] inclusive.
+         * The interpretation of the rate is up to the implementation. If no
+         * rate was included with the command a rate of -1 will be used by
+         * default.
+         *
+         * @param rate The rate at which to fast forward as a multiplier
+         */
+        public void fastForward(float rate) {
+            Log.w(TAG, "fastForward is not supported.");
+        }
+
+        /**
+         * Override to handle getting the current position of playback in
+         * millis.
+         *
+         * @return The current position in millis or -1
+         */
+        public long getCurrentPosition() {
+            Log.w(TAG, "getCurrentPosition is not supported");
+            return -1;
+        }
+
+        /**
+         * Override to handle getting the set of capabilities currently
+         * available.
+         *
+         * @return A bit mask of the supported capabilities
+         */
+        public long getCapabilities() {
+            Log.w(TAG, "getCapabilities is not supported");
+            return 0;
+        }
+
+        /**
+         * Publish the current playback state to the system and any controllers.
+         * Valid values are defined in {@link RemoteControlClient}. TODO move
+         * play states somewhere else.
+         *
+         * @param state
+         */
+        public final void updatePlaybackState(int state) {
+            Bundle extras = new Bundle();
+            extras.putInt(KEY_VALUE1, state);
+            sendEvent(mSession, EVENT_PLAYSTATE_CHANGE, extras);
+        }
+    }
+
+    /**
+     * Register this event listener using TODO to receive
+     * TransportControlInterface events from a session.
+     *
+     * @see RouteInterface.EventListener
+     */
+    public static abstract class Listener {
+
+        private RouteInterface.EventListener mListener = new RouteInterface.EventListener() {
+            @Override
+            public final void onEvent(String event, Bundle args) {
+                if (EVENT_PLAYSTATE_CHANGE.equals(event)) {
+                    onPlaybackStateChange(args.getInt(KEY_VALUE1));
+                } else if (EVENT_METADATA_CHANGE.equals(event)) {
+                    onMetadataUpdate(args);
+                }
+            }
+        };
+
+        /**
+         * Override to handle updates to the playback state. Valid values are in
+         * {@link TransportPerformer}. TODO put playstate values somewhere more
+         * generic.
+         *
+         * @param state
+         */
+        public void onPlaybackStateChange(int state) {
+        }
+
+        /**
+         * Override to handle metadata changes for this session's media. The
+         * default supported fields are those in {@link MediaMetadata}.
+         *
+         * @param metadata
+         */
+        public void onMetadataUpdate(Bundle metadata) {
+        }
+    }
+
+}
diff --git a/media/java/android/media/session/TransportController.java b/media/java/android/media/session/TransportController.java
new file mode 100644
index 0000000..15b11f3
--- /dev/null
+++ b/media/java/android/media/session/TransportController.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.session;
+
+import android.media.Rating;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Interface for controlling media playback on a session. This allows an app to
+ * request changes in playback, retrieve the current playback state and
+ * metadata, and listen for changes to the playback state and metadata.
+ */
+public final class TransportController {
+    private static final String TAG = "TransportController";
+
+    private final Object mLock = new Object();
+    private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>();
+    private final IMediaController mBinder;
+
+    /**
+     * @hide
+     */
+    public TransportController(IMediaController binder) {
+        mBinder = binder;
+    }
+
+    /**
+     * Start listening to changes in playback state.
+     */
+    public void addStateListener(TransportStateListener listener) {
+        addStateListener(listener, null);
+    }
+
+    public void addStateListener(TransportStateListener listener, Handler handler) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            if (getHandlerForListenerLocked(listener) != null) {
+                Log.w(TAG, "Listener is already added, ignoring");
+                return;
+            }
+            if (handler == null) {
+                handler = new Handler();
+            }
+
+            MessageHandler msgHandler = new MessageHandler(handler.getLooper(), listener);
+            mListeners.add(msgHandler);
+        }
+    }
+
+    /**
+     * Stop listening to changes in playback state.
+     */
+    public void removeStateListener(TransportStateListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            removeStateListenerLocked(listener);
+        }
+    }
+
+    /**
+     * Request that the player start its playback at its current position.
+     */
+    public void play() {
+        try {
+            mBinder.play();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling play.", e);
+        }
+    }
+
+    /**
+     * Request that the player pause its playback and stay at its current
+     * position.
+     */
+    public void pause() {
+        try {
+            mBinder.pause();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling pause.", e);
+        }
+    }
+
+    /**
+     * Request that the player stop its playback; it may clear its state in
+     * whatever way is appropriate.
+     */
+    public void stop() {
+        try {
+            mBinder.stop();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling stop.", e);
+        }
+    }
+
+    /**
+     * Move to a new location in the media stream.
+     *
+     * @param pos Position to move to, in milliseconds.
+     */
+    public void seekTo(long pos) {
+        try {
+            mBinder.seekTo(pos);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling seekTo.", e);
+        }
+    }
+
+    /**
+     * Start fast forwarding. If playback is already fast forwarding this may
+     * increase the rate.
+     */
+    public void fastForward() {
+        try {
+            mBinder.fastForward();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling fastForward.", e);
+        }
+    }
+
+    /**
+     * Skip to the next item.
+     */
+    public void next() {
+        try {
+            mBinder.next();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling next.", e);
+        }
+    }
+
+    /**
+     * Start rewinding. If playback is already rewinding this may increase the
+     * rate.
+     */
+    public void rewind() {
+        try {
+            mBinder.rewind();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling rewind.", e);
+        }
+    }
+
+    /**
+     * Skip to the previous item.
+     */
+    public void previous() {
+        try {
+            mBinder.previous();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling previous.", e);
+        }
+    }
+
+    /**
+     * Rate the current content. This will cause the rating to be set for the
+     * current user. The Rating type must match the type returned by
+     * {@link #getRatingType()}.
+     *
+     * @param rating The rating to set for the current content
+     */
+    public void rate(Rating rating) {
+        try {
+            mBinder.rate(rating);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling rate.", e);
+        }
+    }
+
+    /**
+     * Get the rating type supported by the session. One of:
+     * <ul>
+     * <li>{@link Rating#RATING_NONE}</li>
+     * <li>{@link Rating#RATING_HEART}</li>
+     * <li>{@link Rating#RATING_THUMB_UP_DOWN}</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_PERCENTAGE}</li>
+     * </ul>
+     *
+     * @return The supported rating type
+     */
+    public int getRatingType() {
+        try {
+            return mBinder.getRatingType();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling getRatingType.", e);
+            return Rating.RATING_NONE;
+        }
+    }
+
+    /**
+     * Get the current playback state for this session.
+     *
+     * @return The current PlaybackState or null
+     */
+    public PlaybackState getPlaybackState() {
+        try {
+            return mBinder.getPlaybackState();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling getPlaybackState.", e);
+            return null;
+        }
+    }
+
+    /**
+     * Get the current metadata for this session.
+     *
+     * @return The current MediaMetadata or null.
+     */
+    public MediaMetadata getMetadata() {
+        try {
+            return mBinder.getMetadata();
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Error calling getMetadata.", e);
+            return null;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public final void postPlaybackStateChanged(PlaybackState state) {
+        synchronized (mLock) {
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                mListeners.get(i).post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state);
+            }
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public final void postMetadataChanged(MediaMetadata metadata) {
+        synchronized (mLock) {
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                mListeners.get(i).post(MessageHandler.MSG_UPDATE_METADATA,
+                        metadata);
+            }
+        }
+    }
+
+    private MessageHandler getHandlerForListenerLocked(TransportStateListener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mListeners.get(i);
+            if (listener == handler.mListener) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private boolean removeStateListenerLocked(TransportStateListener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            if (listener == mListeners.get(i).mListener) {
+                mListeners.remove(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Register using {@link #addStateListener} to receive updates when there
+     * are playback changes on the session.
+     */
+    public static abstract class TransportStateListener {
+        private MessageHandler mHandler;
+        /**
+         * Override to handle changes in playback state.
+         *
+         * @param state The new playback state of the session
+         */
+        public void onPlaybackStateChanged(PlaybackState state) {
+        }
+
+        /**
+         * Override to handle changes to the current metadata.
+         *
+         * @see MediaMetadata
+         * @param metadata The current metadata for the session or null
+         */
+        public void onMetadataChanged(MediaMetadata metadata) {
+        }
+
+        private void setHandler(Handler handler) {
+            mHandler = new MessageHandler(handler.getLooper(), this);
+        }
+    }
+
+    private static class MessageHandler extends Handler {
+        private static final int MSG_UPDATE_PLAYBACK_STATE = 1;
+        private static final int MSG_UPDATE_METADATA = 2;
+
+        private TransportStateListener mListener;
+
+        public MessageHandler(Looper looper, TransportStateListener cb) {
+            super(looper, null, true);
+            mListener = cb;
+        }
+
+        public void post(int msg, Object obj) {
+            obtainMessage(msg, obj).sendToTarget();
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_UPDATE_PLAYBACK_STATE:
+                    mListener.onPlaybackStateChanged((PlaybackState) msg.obj);
+                    break;
+                case MSG_UPDATE_METADATA:
+                    mListener.onMetadataChanged((MediaMetadata) msg.obj);
+                    break;
+            }
+        }
+    }
+
+}
diff --git a/media/java/android/media/session/TransportPerformer.java b/media/java/android/media/session/TransportPerformer.java
new file mode 100644
index 0000000..b96db20
--- /dev/null
+++ b/media/java/android/media/session/TransportPerformer.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.session;
+
+import android.media.AudioManager;
+import android.media.Rating;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Allows broadcasting of playback changes.
+ */
+public final class TransportPerformer {
+    private static final String TAG = "TransportPerformer";
+    private final Object mLock = new Object();
+    private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>();
+
+    private IMediaSession mBinder;
+
+    /**
+     * @hide
+     */
+    public TransportPerformer(IMediaSession binder) {
+        mBinder = binder;
+    }
+
+    /**
+     * Add a listener to receive updates on.
+     *
+     * @param listener The callback object
+     */
+    public void addListener(Listener listener) {
+        addListener(listener, null);
+    }
+
+    /**
+     * Add a listener to receive updates on. The updates will be posted to the
+     * specified handler. If no handler is provided they will be posted to the
+     * caller's thread.
+     *
+     * @param listener The listener to receive updates on
+     * @param handler The handler to post the updates on
+     */
+    public void addListener(Listener listener, Handler handler) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            if (getHandlerForListenerLocked(listener) != null) {
+                Log.w(TAG, "Listener is already added, ignoring");
+            }
+            if (handler == null) {
+                handler = new Handler();
+            }
+            MessageHandler msgHandler = new MessageHandler(handler.getLooper(), listener);
+            mListeners.add(msgHandler);
+        }
+    }
+
+    /**
+     * Stop receiving updates on the specified handler. If an update has already
+     * been posted you may still receive it after this call returns.
+     *
+     * @param listener The listener to stop receiving updates on
+     */
+    public void removeListener(Listener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener cannot be null");
+        }
+        synchronized (mLock) {
+            removeListenerLocked(listener);
+        }
+    }
+
+    /**
+     * Update the current playback state.
+     *
+     * @param state The current state of playback
+     */
+    public final void setPlaybackState(PlaybackState state) {
+        try {
+            mBinder.setPlaybackState(state);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
+        }
+    }
+
+    /**
+     * Update the current metadata. New metadata can be created using
+     * {@link MediaMetadata.Builder}.
+     *
+     * @param metadata The new metadata
+     */
+    public final void setMetadata(MediaMetadata metadata) {
+        try {
+            mBinder.setMetadata(metadata);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public final void onPlay() {
+        post(MessageHandler.MESSAGE_PLAY);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onPause() {
+        post(MessageHandler.MESSAGE_PAUSE);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onStop() {
+        post(MessageHandler.MESSAGE_STOP);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onNext() {
+        post(MessageHandler.MESSAGE_NEXT);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onPrevious() {
+        post(MessageHandler.MESSAGE_PREVIOUS);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onFastForward() {
+        post(MessageHandler.MESSAGE_FAST_FORWARD);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onRewind() {
+        post(MessageHandler.MESSAGE_REWIND);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onSeekTo(long pos) {
+        post(MessageHandler.MESSAGE_SEEK_TO, pos);
+    }
+
+    /**
+     * @hide
+     */
+    public final void onRate(Rating rating) {
+        post(MessageHandler.MESSAGE_RATE, rating);
+    }
+
+    private MessageHandler getHandlerForListenerLocked(Listener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            MessageHandler handler = mListeners.get(i);
+            if (listener == handler.mListener) {
+                return handler;
+            }
+        }
+        return null;
+    }
+
+    private boolean removeListenerLocked(Listener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            if (listener == mListeners.get(i).mListener) {
+                mListeners.remove(i);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void post(int what, Object obj) {
+        synchronized (mLock) {
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                mListeners.get(i).post(what, obj);
+            }
+        }
+    }
+
+    private void post(int what) {
+        post(what, null);
+    }
+
+    /**
+     * Extend Listener to handle transport controls. Listeners can be registered
+     * using {@link #addListener}.
+     */
+    public static abstract class Listener {
+
+        /**
+         * Override to handle requests to begin playback.
+         */
+        public void onPlay() {
+        }
+
+        /**
+         * Override to handle requests to pause playback.
+         */
+        public void onPause() {
+        }
+
+        /**
+         * Override to handle requests to skip to the next media item.
+         */
+        public void onNext() {
+        }
+
+        /**
+         * Override to handle requests to skip to the previous media item.
+         */
+        public void onPrevious() {
+        }
+
+        /**
+         * Override to handle requests to fast forward.
+         */
+        public void onFastForward() {
+        }
+
+        /**
+         * Override to handle requests to rewind.
+         */
+        public void onRewind() {
+        }
+
+        /**
+         * Override to handle requests to stop playback.
+         */
+        public void 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) {
+        }
+
+        /**
+         * Override to handle the item being rated.
+         *
+         * @param rating
+         */
+        public void onRate(Rating rating) {
+        }
+
+        /**
+         * Report that audio focus has changed on the app. This only happens if
+         * you have indicated you have started playing with
+         * {@link #setPlaybackState}. TODO figure out route focus apis/handling.
+         *
+         * @param focusChange The type of focus change, TBD. The default
+         *            implementation will deliver a call to {@link #onPause}
+         *            when focus is lost.
+         */
+        public void onRouteFocusChange(int focusChange) {
+            switch (focusChange) {
+                case AudioManager.AUDIOFOCUS_LOSS:
+                    onPause();
+                    break;
+            }
+        }
+    }
+
+    private class MessageHandler extends Handler {
+        private static final int MESSAGE_PLAY = 1;
+        private static final int MESSAGE_PAUSE = 2;
+        private static final int MESSAGE_STOP = 3;
+        private static final int MESSAGE_NEXT = 4;
+        private static final int MESSAGE_PREVIOUS = 5;
+        private static final int MESSAGE_FAST_FORWARD = 6;
+        private static final int MESSAGE_REWIND = 7;
+        private static final int MESSAGE_SEEK_TO = 8;
+        private static final int MESSAGE_RATE = 9;
+
+        private Listener mListener;
+
+        public MessageHandler(Looper looper, Listener cb) {
+            super(looper);
+            mListener = cb;
+        }
+
+        public void post(int what, Object obj) {
+            obtainMessage(what, obj).sendToTarget();
+        }
+
+        public void post(int what) {
+            post(what, null);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MESSAGE_PLAY:
+                    mListener.onPlay();
+                    break;
+                case MESSAGE_PAUSE:
+                    mListener.onPause();
+                    break;
+                case MESSAGE_STOP:
+                    mListener.onStop();
+                    break;
+                case MESSAGE_NEXT:
+                    mListener.onNext();
+                    break;
+                case MESSAGE_PREVIOUS:
+                    mListener.onPrevious();
+                    break;
+                case MESSAGE_FAST_FORWARD:
+                    mListener.onFastForward();
+                    break;
+                case MESSAGE_REWIND:
+                    mListener.onRewind();
+                    break;
+                case MESSAGE_SEEK_TO:
+                    mListener.onSeekTo((Long) msg.obj);
+                    break;
+                case MESSAGE_RATE:
+                    mListener.onRate((Rating) msg.obj);
+                    break;
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 89acec9..1ff925c 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -21,14 +21,22 @@
 import android.media.session.IMediaControllerCallback;
 import android.media.session.IMediaSession;
 import android.media.session.IMediaSessionCallback;
-import android.media.RemoteControlClient;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
+import android.media.Rating;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.util.Log;
+import android.util.Slog;
 import android.view.KeyEvent;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * This is the system implementation of a Session. Apps will interact with the
@@ -37,6 +45,8 @@
 public class MediaSessionRecord implements IBinder.DeathRecipient {
     private static final String TAG = "MediaSessionImpl";
 
+    private final MessageHandler mHandler;
+
     private final int mPid;
     private final String mPackageName;
     private final String mTag;
@@ -45,13 +55,25 @@
     private final SessionCb mSessionCb;
     private final MediaSessionService mService;
 
-    private final ArrayList<IMediaControllerCallback> mSessionCallbacks =
+    private final Object mControllerLock = new Object();
+    private final ArrayList<IMediaControllerCallback> mControllerCallbacks =
             new ArrayList<IMediaControllerCallback>();
+    private final ArrayList<String> mInterfaces = new ArrayList<String>();
 
-    private int mPlaybackState = RemoteControlClient.PLAYSTATE_NONE;
+    private boolean mTransportPerformerEnabled = false;
+    private Bundle mRoute;
+
+    // TransportPerformer fields
+
+    private MediaMetadata mMetadata;
+    private PlaybackState mPlaybackState;
+    private int mRatingType;
+    // End TransportPerformer fields
+
+    private boolean mIsPublished = false;
 
     public MediaSessionRecord(int pid, String packageName, IMediaSessionCallback cb, String tag,
-            MediaSessionService service) {
+            MediaSessionService service, Handler handler) {
         mPid = pid;
         mPackageName = packageName;
         mTag = tag;
@@ -59,6 +81,7 @@
         mSession = new SessionStub();
         mSessionCb = new SessionCb(cb);
         mService = service;
+        mHandler = new MessageHandler(handler.getLooper());
     }
 
     public IMediaSession getSessionBinder() {
@@ -69,61 +92,132 @@
         return mController;
     }
 
-    public void setPlaybackStateInternal(int state) {
-        mPlaybackState = state;
-        for (int i = mSessionCallbacks.size() - 1; i >= 0; i--) {
-            IMediaControllerCallback cb = mSessionCallbacks.get(i);
-            try {
-                cb.onPlaybackUpdate(state);
-            } catch (RemoteException e) {
-                Log.d(TAG, "SessionCallback object dead in setPlaybackState.", e);
-                mSessionCallbacks.remove(i);
-            }
-        }
-    }
-
     @Override
     public void binderDied() {
         mService.sessionDied(this);
     }
 
+    public boolean isPublished() {
+        return mIsPublished;
+    }
+
     private void onDestroy() {
         mService.destroySession(this);
     }
 
+    private void pushPlaybackStateUpdate() {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onPlaybackStateChanged(mPlaybackState);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushPlaybackStateUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
+    private void pushMetadataUpdate() {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onMetadataChanged(mMetadata);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushMetadataUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
+    private void pushRouteUpdate() {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onRouteChanged(mRoute);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
+    private void pushEvent(String event, Bundle data) {
+        synchronized (mControllerLock) {
+            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
+                IMediaControllerCallback cb = mControllerCallbacks.get(i);
+                try {
+                    cb.onEvent(event, data);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
+                    mControllerCallbacks.remove(i);
+                }
+            }
+        }
+    }
+
     private final class SessionStub extends IMediaSession.Stub {
 
         @Override
-        public void setPlaybackState(int state) throws RemoteException {
-            setPlaybackStateInternal(state);
-        }
-
-        @Override
-        public void destroy() throws RemoteException {
+        public void destroy() {
             onDestroy();
         }
 
         @Override
-        public void sendEvent(Bundle data) throws RemoteException {
+        public void sendEvent(String event, Bundle data) {
+            mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
         }
 
         @Override
-        public IMediaController getMediaSessionToken() throws RemoteException {
+        public IMediaController getMediaController() {
             return mController;
         }
 
         @Override
-        public void setMetadata(Bundle metadata) throws RemoteException {
+        public void setRouteState(Bundle routeState) {
         }
 
         @Override
-        public void setRouteState(Bundle routeState) throws RemoteException {
+        public void setRoute(Bundle mediaRouteDescriptor) {
+            mRoute = mediaRouteDescriptor;
+            mHandler.post(MessageHandler.MSG_UPDATE_ROUTE);
         }
 
         @Override
-        public void setRoute(Bundle medaiRouteDescriptor) throws RemoteException {
+        public void publish() {
+            mIsPublished = true; // TODO push update to service
+        }
+        @Override
+        public void setTransportPerformerEnabled() {
+            mTransportPerformerEnabled = true;
         }
 
+        @Override
+        public List<String> getSupportedInterfaces() {
+            return mInterfaces;
+        }
+
+        @Override
+        public void setMetadata(MediaMetadata metadata) {
+            mMetadata = metadata;
+            mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
+        }
+
+        @Override
+        public void setPlaybackState(PlaybackState state) {
+            mPlaybackState = state;
+            mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
+        }
+
+        @Override
+        public void setRatingType(int type) {
+            mRatingType = type;
+        }
     }
 
     class SessionCb {
@@ -139,32 +233,96 @@
             try {
                 mCb.onMediaButton(mediaButtonIntent);
             } catch (RemoteException e) {
-                Log.d(TAG, "Controller object dead in sendMediaRequest.", e);
-                onDestroy();
+                Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
             }
         }
 
-        public void sendCommand(String command, Bundle extras) {
+        public void sendCommand(String command, Bundle extras, ResultReceiver cb) {
             try {
-                mCb.onCommand(command, extras);
+                mCb.onCommand(command, extras, cb);
             } catch (RemoteException e) {
-                Log.d(TAG, "Controller object dead in sendCommand.", e);
-                onDestroy();
+                Slog.e(TAG, "Remote failure in sendCommand.", e);
             }
         }
 
-        public void registerCallbackListener(IMediaSessionCallback cb) {
-
+        public void play() {
+            try {
+                mCb.onPlay();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in play.", e);
+            }
         }
 
+        public void pause() {
+            try {
+                mCb.onPause();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in pause.", e);
+            }
+        }
+
+        public void stop() {
+            try {
+                mCb.onStop();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in stop.", e);
+            }
+        }
+
+        public void next() {
+            try {
+                mCb.onNext();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in next.", e);
+            }
+        }
+
+        public void previous() {
+            try {
+                mCb.onPrevious();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in previous.", e);
+            }
+        }
+
+        public void fastForward() {
+            try {
+                mCb.onFastForward();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in fastForward.", e);
+            }
+        }
+
+        public void rewind() {
+            try {
+                mCb.onRewind();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in rewind.", e);
+            }
+        }
+
+        public void seekTo(long pos) {
+            try {
+                mCb.onSeekTo(pos);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in seekTo.", e);
+            }
+        }
+
+        public void rate(Rating rating) {
+            try {
+                mCb.onRate(rating);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in rate.", e);
+            }
+        }
     }
 
     class ControllerStub extends IMediaController.Stub {
-        /*
-         */
         @Override
-        public void sendCommand(String command, Bundle extras) throws RemoteException {
-            mSessionCb.sendCommand(command, extras);
+        public void sendCommand(String command, Bundle extras, ResultReceiver cb)
+                throws RemoteException {
+            mSessionCb.sendCommand(command, extras, cb);
         }
 
         @Override
@@ -172,29 +330,130 @@
             mSessionCb.sendMediaButton(mediaButtonIntent);
         }
 
-        /*
-         */
         @Override
-        public void registerCallbackListener(IMediaControllerCallback cb) throws RemoteException {
-            if (!mSessionCallbacks.contains(cb)) {
-                mSessionCallbacks.add(cb);
+        public void registerCallbackListener(IMediaControllerCallback cb) {
+            synchronized (mControllerLock) {
+                if (!mControllerCallbacks.contains(cb)) {
+                    mControllerCallbacks.add(cb);
+                }
             }
         }
 
-        /*
-         */
         @Override
         public void unregisterCallbackListener(IMediaControllerCallback cb)
                 throws RemoteException {
-            mSessionCallbacks.remove(cb);
+            synchronized (mControllerLock) {
+                mControllerCallbacks.remove(cb);
+            }
         }
 
-        /*
-         */
         @Override
-        public int getPlaybackState() throws RemoteException {
+        public void play() throws RemoteException {
+            mSessionCb.play();
+        }
+
+        @Override
+        public void pause() throws RemoteException {
+            mSessionCb.pause();
+        }
+
+        @Override
+        public void stop() throws RemoteException {
+            mSessionCb.stop();
+        }
+
+        @Override
+        public void next() throws RemoteException {
+            mSessionCb.next();
+        }
+
+        @Override
+        public void previous() throws RemoteException {
+            mSessionCb.previous();
+        }
+
+        @Override
+        public void fastForward() throws RemoteException {
+            mSessionCb.fastForward();
+        }
+
+        @Override
+        public void rewind() throws RemoteException {
+            mSessionCb.rewind();
+        }
+
+        @Override
+        public void seekTo(long pos) throws RemoteException {
+            mSessionCb.seekTo(pos);
+        }
+
+        @Override
+        public void rate(Rating rating) throws RemoteException {
+            mSessionCb.rate(rating);
+        }
+
+
+        @Override
+        public MediaMetadata getMetadata() {
+            return mMetadata;
+        }
+
+        @Override
+        public PlaybackState getPlaybackState() {
             return mPlaybackState;
         }
+
+        @Override
+        public int getRatingType() {
+            return mRatingType;
+        }
+
+        @Override
+        public boolean isTransportControlEnabled() throws RemoteException {
+            return mTransportPerformerEnabled;
+        }
+    }
+
+    private class MessageHandler extends Handler {
+        private static final int MSG_UPDATE_METADATA = 1;
+        private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
+        private static final int MSG_UPDATE_ROUTE = 3;
+        private static final int MSG_SEND_EVENT = 4;
+
+        public MessageHandler(Looper looper) {
+            super(looper);
+        }
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_UPDATE_METADATA:
+                    pushMetadataUpdate();
+                    break;
+                case MSG_UPDATE_PLAYBACK_STATE:
+                    pushPlaybackStateUpdate();
+                    break;
+                case MSG_UPDATE_ROUTE:
+                    pushRouteUpdate();
+                    break;
+                case MSG_SEND_EVENT:
+                    pushEvent((String) msg.obj, msg.getData());
+                    break;
+            }
+        }
+
+        public void post(int what) {
+            post(what, null);
+        }
+
+        public void post(int what, Object obj) {
+            obtainMessage(what, obj).sendToTarget();
+        }
+
+        public void post(int what, Object obj, Bundle data) {
+            Message msg = obtainMessage(what, obj);
+            msg.setData(data);
+            msg.sendToTarget();
+        }
     }
 
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index a7ff926..8fe6055 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -21,6 +21,7 @@
 import android.media.session.IMediaSessionCallback;
 import android.media.session.IMediaSessionManager;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Log;
@@ -41,6 +42,8 @@
     private final ArrayList<MediaSessionRecord> mSessions
             = new ArrayList<MediaSessionRecord>();
     private final Object mLock = new Object();
+    // TODO do we want a separate thread for handling mediasession messages?
+    private final Handler mHandler = new Handler();
 
     public MediaSessionService(Context context) {
         super(context);
@@ -91,7 +94,8 @@
 
     private MediaSessionRecord createSessionLocked(int pid, String packageName,
             IMediaSessionCallback cb, String tag) {
-        final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this);
+        final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this,
+                mHandler);
         try {
             cb.asBinder().linkToDeath(session, 0);
         } catch (RemoteException e) {
diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
index 7ff81e4..3114ca9 100644
--- a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
+++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
@@ -1,7 +1,24 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 
 import android.app.Activity;
+import android.media.session.MediaMetadata;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.Menu;
@@ -79,10 +96,10 @@
             switch (v.getId()) {
                 case R.id.play_button:
                     Log.d(TAG, "Play button pressed, in state " + mPlaybackState);
-                    if (mPlaybackState == Renderer.STATE_PAUSED
-                            || mPlaybackState == Renderer.STATE_ENDED) {
+                    if (mPlaybackState == PlaybackState.PLAYSTATE_PAUSED
+                            || mPlaybackState == PlaybackState.PLAYSTATE_STOPPED) {
                         mPlayer.play();
-                    } else if (mPlaybackState == Renderer.STATE_PLAYING) {
+                    } else if (mPlaybackState == PlaybackState.PLAYSTATE_PLAYING) {
                         mPlayer.pause();
                     }
                     break;
@@ -97,48 +114,55 @@
 
     private PlayerController.Listener mListener = new PlayerController.Listener() {
         @Override
-        public void onSessionStateChange(int state) {
-            mPlaybackState = state;
+        public void onPlaybackStateChange(PlaybackState state) {
+            mPlaybackState = state.getState();
             boolean enablePlay = false;
+            StringBuilder statusBuilder = new StringBuilder();
             switch (mPlaybackState) {
-                case Renderer.STATE_PLAYING:
-                    mStatusView.setText("playing");
+                case PlaybackState.PLAYSTATE_PLAYING:
+                    statusBuilder.append("playing");
                     mPlayButton.setText("Pause");
                     enablePlay = true;
                     break;
-                case Renderer.STATE_PAUSED:
-                    mStatusView.setText("paused");
+                case PlaybackState.PLAYSTATE_PAUSED:
+                    statusBuilder.append("paused");
                     mPlayButton.setText("Play");
                     enablePlay = true;
                     break;
-                case Renderer.STATE_ENDED:
-                    mStatusView.setText("ended");
+                case PlaybackState.PLAYSTATE_STOPPED:
+                    statusBuilder.append("ended");
                     mPlayButton.setText("Play");
                     enablePlay = true;
                     break;
-                case Renderer.STATE_ERROR:
-                    mStatusView.setText("error");
+                case PlaybackState.PLAYSTATE_ERROR:
+                    statusBuilder.append("error: ").append(state.getErrorMessage());
                     break;
-                case Renderer.STATE_PREPARING:
-                    mStatusView.setText("preparing");
+                case PlaybackState.PLAYSTATE_BUFFERING:
+                    statusBuilder.append("buffering");
                     break;
-                case Renderer.STATE_READY:
-                    mStatusView.setText("ready");
+                case PlaybackState.PLAYSTATE_NONE:
+                    statusBuilder.append("none");
                     break;
-                case Renderer.STATE_STOPPED:
-                    mStatusView.setText("stopped");
-                    break;
+                default:
+                    statusBuilder.append(mPlaybackState);
             }
+            statusBuilder.append(" -- At position: ").append(state.getPosition());
+            mStatusView.setText(statusBuilder.toString());
             mPlayButton.setEnabled(enablePlay);
         }
 
         @Override
-        public void onPlayerStateChange(int state) {
+        public void onConnectionStateChange(int state) {
             if (state == PlayerController.STATE_DISCONNECTED) {
                 setControlsEnabled(false);
             } else if (state == PlayerController.STATE_CONNECTED) {
                 setControlsEnabled(true);
             }
         }
+
+        @Override
+        public void onMetadataChange(MediaMetadata metadata) {
+            Log.d(TAG, "Metadata update! Title: " + metadata);
+        }
     };
 }
diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
index 01610cd..573f7ff 100644
--- a/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
+++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.content.Context;
@@ -5,9 +20,6 @@
 
 import java.util.ArrayList;
 
-/**
- * TODO: Insert description here. (generated by epastern)
- */
 public class OnePlayerService extends PlayerService {
     private static final String TAG = "OnePlayerService";
 
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerController.java b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
index 3f15db5..e831ec6 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerController.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
@@ -1,8 +1,27 @@
 
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.media.session.MediaController;
+import android.media.session.MediaMetadata;
 import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.media.session.TransportController;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -11,22 +30,23 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.util.Log;
-import android.view.KeyEvent;
 
 import com.android.onemedia.playback.RequestUtils;
 
 public class PlayerController {
-    private static final String TAG = "PlayerSession";
+    private static final String TAG = "PlayerController";
 
     public static final int STATE_DISCONNECTED = 0;
     public static final int STATE_CONNECTED = 1;
 
     protected MediaController mController;
     protected IPlayerService mBinder;
+    protected TransportController mTransportControls;
 
     private final Intent mServiceIntent;
     private Context mContext;
     private Listener mListener;
+    private TransportListener mTransportListener = new TransportListener();
     private SessionCallback mControllerCb;
     private MediaSessionManager mManager;
     private Handler mHandler = new Handler();
@@ -52,7 +72,7 @@
         Log.d(TAG, "Listener set to " + listener + " session is " + mController);
         if (mListener != null) {
             mHandler = new Handler();
-            mListener.onPlayerStateChange(
+            mListener.onConnectionStateChange(
                     mController == null ? STATE_DISCONNECTED : STATE_CONNECTED);
         }
     }
@@ -70,11 +90,15 @@
     }
 
     public void play() {
-        mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PLAY);
+        if (mTransportControls != null) {
+            mTransportControls.play();
+        }
     }
 
     public void pause() {
-        mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PAUSE);
+        if (mTransportControls != null) {
+            mTransportControls.pause();
+        }
     }
 
     public void setContent(String source) {
@@ -113,10 +137,11 @@
             }
             mBinder = null;
             mController = null;
+            mTransportControls = null;
             Log.d(TAG, "Disconnected from PlayerService");
 
             if (mListener != null) {
-                mListener.onPlayerStateChange(STATE_DISCONNECTED);
+                mListener.onConnectionStateChange(STATE_DISCONNECTED);
             }
         }
 
@@ -125,33 +150,60 @@
             mBinder = IPlayerService.Stub.asInterface(service);
             Log.d(TAG, "service is " + service + " binder is " + mBinder);
             try {
-                mController = new MediaController(mBinder.getSessionToken());
+                mController = MediaController.fromToken(mBinder.getSessionToken());
             } catch (RemoteException e) {
                 Log.e(TAG, "Error getting session", e);
                 return;
             }
             mController.addCallback(mControllerCb, mHandler);
+            mTransportControls = mController.getTransportController();
+            if (mTransportControls != null) {
+                mTransportControls.addStateListener(mTransportListener);
+            }
             Log.d(TAG, "Ready to use PlayerService");
 
             if (mListener != null) {
-                mListener.onPlayerStateChange(STATE_CONNECTED);
+                mListener.onConnectionStateChange(STATE_CONNECTED);
+                if (mTransportControls != null) {
+                    mListener.onPlaybackStateChange(mTransportControls.getPlaybackState());
+                }
             }
         }
     };
 
     private class SessionCallback extends MediaController.Callback {
         @Override
-        public void onPlaybackStateChange(int state) {
-            if (mListener != null) {
-                mListener.onSessionStateChange(state);
+        public void onRouteChanged(Bundle route) {
+            // TODO
+        }
+    }
+
+    private class TransportListener extends TransportController.TransportStateListener {
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            if (state == null) {
+                return;
             }
+            Log.d(TAG, "Received playback state change to state " + state.getState());
+            if (mListener != null) {
+                mListener.onPlaybackStateChange(state);
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            if (metadata == null) {
+                return;
+            }
+            Log.d(TAG, "Received metadata change, title is "
+                    + metadata.getString(MediaMetadata.METADATA_KEY_TITLE));
         }
     }
 
     public interface Listener {
-        public void onSessionStateChange(int state);
-
-        public void onPlayerStateChange(int state);
+        public void onPlaybackStateChange(PlaybackState state);
+        public void onMetadataChange(MediaMetadata metadata);
+        public void onConnectionStateChange(int state);
     }
 
 }
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerService.java b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
index 0b2ba8f..0ad6dd1 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerService.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
@@ -1,11 +1,28 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.app.Service;
 import android.content.Intent;
 import android.media.session.MediaSessionToken;
+import android.media.session.PlaybackState;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.Log;
 
 import com.android.onemedia.playback.IRequestCallback;
 import com.android.onemedia.playback.RequestUtils;
@@ -18,14 +35,19 @@
     private PlayerBinder mBinder;
     private PlayerSession mSession;
     private Intent mIntent;
+    private boolean mStarted = false;
 
     private ArrayList<IPlayerCallback> mCbs = new ArrayList<IPlayerCallback>();
 
     @Override
     public void onCreate() {
+        Log.d(TAG, "onCreate");
         mIntent = onCreateServiceIntent();
-        mSession = onCreatePlayerController();
-        mSession.createSession();
+        if (mSession == null) {
+            mSession = onCreatePlayerController();
+            mSession.createSession();
+            mSession.setListener(mPlayerListener);
+        }
     }
 
     @Override
@@ -38,12 +60,31 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.d(TAG, "onStartCommand");
         return START_STICKY;
     }
 
     @Override
     public void onDestroy() {
+        Log.d(TAG, "onDestroy");
         mSession.onDestroy();
+        mSession = null;
+    }
+
+    public void onPlaybackStarted() {
+        if (!mStarted) {
+            Log.d(TAG, "Starting self");
+            startService(onCreateServiceIntent());
+            mStarted = true;
+        }
+    }
+
+    public void onPlaybackEnded() {
+        if (mStarted) {
+            Log.d(TAG, "Stopping self");
+            stopSelf();
+            mStarted = false;
+        }
     }
 
     protected Intent onCreateServiceIntent() {
@@ -58,6 +99,21 @@
         return null;
     }
 
+    private final PlayerSession.Listener mPlayerListener = new PlayerSession.Listener() {
+        @Override
+        public void onPlayStateChanged(PlaybackState state) {
+            switch (state.getState()) {
+                case PlaybackState.PLAYSTATE_PLAYING:
+                    onPlaybackStarted();
+                    break;
+                case PlaybackState.PLAYSTATE_STOPPED:
+                case PlaybackState.PLAYSTATE_ERROR:
+                    onPlaybackEnded();
+                    break;
+            }
+        }
+    };
+
     public class PlayerBinder extends IPlayerService.Stub {
         @Override
         public void sendRequest(String action, Bundle params, IRequestCallback cb) {
@@ -94,7 +150,6 @@
 
         @Override
         public MediaSessionToken getSessionToken() throws RemoteException {
-            // TODO(epastern): Auto-generated method stub
             return mSession.getSessionToken();
         }
     }
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
index e5fb0d0..a2d7897 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.onemedia;
 
 import android.content.Context;
@@ -5,6 +20,8 @@
 import android.media.session.MediaSession;
 import android.media.session.MediaSessionManager;
 import android.media.session.MediaSessionToken;
+import android.media.session.PlaybackState;
+import android.media.session.TransportPerformer;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -14,14 +31,18 @@
 import com.android.onemedia.playback.RendererFactory;
 
 public class PlayerSession {
-    private static final String TAG = "PlayerController";
+    private static final String TAG = "PlayerSession";
 
     protected MediaSession mSession;
     protected Context mContext;
     protected RendererFactory mRendererFactory;
     protected LocalRenderer mRenderer;
-    protected ControllerCb mCallback;
-    protected RenderListener mRenderListener;
+    protected MediaSession.Callback mCallback;
+    protected Renderer.Listener mRenderListener;
+    protected TransportPerformer mPerformer;
+
+    protected PlaybackState mPlaybackState;
+    protected Listener mListener;
 
     public PlayerSession(Context context) {
         mContext = context;
@@ -29,6 +50,9 @@
         mRenderer = new LocalRenderer(context, null);
         mCallback = new ControllerCb();
         mRenderListener = new RenderListener();
+        mPlaybackState = new PlaybackState();
+        mPlaybackState.setActions(PlaybackState.ACTION_PAUSE
+                | PlaybackState.ACTION_PLAY);
 
         mRenderer.registerListener(mRenderListener);
     }
@@ -42,6 +66,10 @@
         Log.d(TAG, "Creating session for package " + mContext.getBasePackageName());
         mSession = man.createSession("OneMedia");
         mSession.addCallback(mCallback);
+        mPerformer = mSession.setTransportPerformerEnabled();
+        mPerformer.addListener(new TransportListener());
+        mPerformer.setPlaybackState(mPlaybackState);
+        mSession.publish();
     }
 
     public void onDestroy() {
@@ -54,6 +82,10 @@
         }
     }
 
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
     public MediaSessionToken getSessionToken() {
         return mSession.getSessionToken();
     }
@@ -66,16 +98,58 @@
         mRenderer.setNextContent(request);
     }
 
-    protected class RenderListener implements Renderer.Listener {
+    public interface Listener {
+        public void onPlayStateChanged(PlaybackState state);
+    }
+
+    private class RenderListener implements Renderer.Listener {
 
         @Override
         public void onError(int type, int extra, Bundle extras, Throwable error) {
-            mSession.setPlaybackState(Renderer.STATE_ERROR);
+            Log.d(TAG, "Sending onError with type " + type + " and extra " + extra);
+            mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+            if (error != null) {
+                mPlaybackState.setErrorMessage(error.getLocalizedMessage());
+            }
+            mPerformer.setPlaybackState(mPlaybackState);
+            if (mListener != null) {
+                mListener.onPlayStateChanged(mPlaybackState);
+            }
         }
 
         @Override
         public void onStateChanged(int newState) {
-            mSession.setPlaybackState(newState);
+            if (newState != Renderer.STATE_ERROR) {
+                mPlaybackState.setErrorMessage(null);
+            }
+            switch (newState) {
+                case Renderer.STATE_ENDED:
+                case Renderer.STATE_STOPPED:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED);
+                    break;
+                case Renderer.STATE_INIT:
+                case Renderer.STATE_PREPARING:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING);
+                    break;
+                case Renderer.STATE_ERROR:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    break;
+                case Renderer.STATE_PAUSED:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+                    break;
+                case Renderer.STATE_PLAYING:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING);
+                    break;
+                default:
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    mPlaybackState.setErrorMessage("unkown state");
+                    break;
+            }
+            mPlaybackState.setPosition(mRenderer.getSeekPosition());
+            mPerformer.setPlaybackState(mPlaybackState);
+            if (mListener != null) {
+                mListener.onPlayStateChanged(mPlaybackState);
+            }
         }
 
         @Override
@@ -84,7 +158,13 @@
 
         @Override
         public void onFocusLost() {
-            mSession.setPlaybackState(Renderer.STATE_PAUSED);
+            Log.d(TAG, "Focus lost, changing state to " + Renderer.STATE_PAUSED);
+            mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+            mPlaybackState.setPosition(mRenderer.getSeekPosition());
+            mPerformer.setPlaybackState(mPlaybackState);
+            if (mListener != null) {
+                mListener.onPlayStateChanged(mPlaybackState);
+            }
         }
 
         @Override
@@ -93,7 +173,7 @@
 
     }
 
-    protected class ControllerCb extends MediaSession.Callback {
+    private class ControllerCb extends MediaSession.Callback {
 
         @Override
         public void onMediaButton(Intent mediaRequestIntent) {
@@ -114,4 +194,16 @@
         }
     }
 
+    private class TransportListener extends TransportPerformer.Listener {
+        @Override
+        public void onPlay() {
+            mRenderer.onPlay();
+        }
+
+        @Override
+        public void onPause() {
+            mRenderer.onPause();
+        }
+    }
+
 }
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
index 7493366..7f62f66 100644
--- a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
+++ b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
@@ -499,11 +499,12 @@
     @Override
     public boolean onPause() {
         MediaPlayer player = mPlayer;
+        // If the user paused us make sure we won't start playing again until
+        // asked to
+        mPlayOnReady = false;
         if (player != null && (mState & CAN_PAUSE) != 0) {
             player.pause();
             setState(STATE_PAUSED);
-        } else if ((mState & CAN_READY_PLAY) != 0) {
-            mPlayOnReady = false;
         } else if (!isPaused()) {
             return false;
         }