Add Session API calls to RCC and AudioManager

This makes RCC and MediaButtonReceiver (via AudioManager) also use the new Session APIs in parallel to their existing code. This will allow us to bring up the Session compatibility pieces without disrupting the old behavior and then switch everything over to just using the new APIs when ready.

Change-Id: I33ce0a044dea3ec763f2302b91a5e415be27d4a4
diff --git a/api/current.txt b/api/current.txt
index 1055c24..1865a52 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -15029,6 +15029,7 @@
 package android.media.session {
 
   public final class MediaMetadata implements android.os.Parcelable {
+    method public boolean containsKey(java.lang.String);
     method public int describeContents();
     method public android.graphics.Bitmap getBitmap(java.lang.String);
     method public long getLong(java.lang.String);
@@ -15044,6 +15045,7 @@
     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_COMPILATION = "android.media.metadata.COMPILATION";
     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";
@@ -15076,25 +15078,25 @@
     method public long getBufferPosition();
     method public java.lang.String getErrorMessage();
     method public long getPosition();
-    method public float getSpeed();
+    method public float getRate();
     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 setState(int, long, float);
     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_PLAY_PAUSE = 512L; // 0x200L
     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 long PLAYBACK_POSITION_UNKNOWN = -1L; // 0xffffffffffffffffL
     field public static final int PLAYSTATE_BUFFERING = 6; // 0x6
     field public static final int PLAYSTATE_CONNECTING = 8; // 0x8
     field public static final int PLAYSTATE_ERROR = 7; // 0x7
@@ -15103,6 +15105,8 @@
     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_SKIPPING_BACKWARDS = 9; // 0x9
+    field public static final int PLAYSTATE_SKIPPING_FORWARDS = 10; // 0xa
     field public static final int PLAYSTATE_STOPPED = 1; // 0x1
   }
 
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 8ae06e0..1dcfcb8 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.media.RemoteController.OnClientUpdateListener;
+import android.media.session.MediaSessionLegacyHelper;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -48,6 +49,12 @@
  */
 public class AudioManager {
 
+    // If we should use the new sessions APIs.
+    private final static boolean USE_SESSIONS = true;
+    // If we should use the legacy APIs. If both are true information will be
+    // duplicated through both paths. Currently this flag isn't used.
+    private final static boolean USE_LEGACY = true;
+
     private final Context mContext;
     private long mVolumeKeyUpTime;
     private final boolean mUseMasterVolume;
@@ -421,6 +428,7 @@
     public static final int USE_DEFAULT_STREAM_TYPE = Integer.MIN_VALUE;
 
     private static IAudioService sService;
+    private MediaSessionLegacyHelper mSessionHelper;
 
     /**
      * @hide
@@ -431,6 +439,9 @@
                 com.android.internal.R.bool.config_useMasterVolume);
         mUseVolumeKeySounds = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_useVolumeKeySounds);
+        if (USE_SESSIONS) {
+            mSessionHelper = MediaSessionLegacyHelper.getHelper(context);
+        }
     }
 
     private static IAudioService getService()
@@ -2166,6 +2177,9 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Dead object in registerMediaButtonIntent"+e);
         }
+        if (USE_SESSIONS) {
+            mSessionHelper.addMediaButtonListener(pi, mContext);
+        }
     }
 
     /**
@@ -2239,6 +2253,9 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Dead object in unregisterMediaButtonIntent"+e);
         }
+        if (USE_SESSIONS) {
+            mSessionHelper.removeMediaButtonListener(pi);
+        }
     }
 
     /**
@@ -2263,6 +2280,9 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Dead object in registerRemoteControlClient"+e);
         }
+        if (USE_SESSIONS) {
+            rcClient.registerWithSession(mSessionHelper);
+        }
     }
 
     /**
@@ -2282,6 +2302,9 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Dead object in unregisterRemoteControlClient"+e);
         }
+        if (USE_SESSIONS) {
+            rcClient.unregisterWithSession(mSessionHelper);
+        }
     }
 
     /**
diff --git a/media/java/android/media/MediaMetadataEditor.java b/media/java/android/media/MediaMetadataEditor.java
index 3bfdb5a..1a4e8da 100644
--- a/media/java/android/media/MediaMetadataEditor.java
+++ b/media/java/android/media/MediaMetadataEditor.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import android.graphics.Bitmap;
+import android.media.session.MediaMetadata;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.util.Log;
@@ -106,6 +107,10 @@
      */
     protected Bundle mEditorMetadata;
 
+    /**
+     * @hide
+     */
+    protected MediaMetadata.Builder mMetadataBuilder;
 
     /**
      * Clears all the pending metadata changes set since the MediaMetadataEditor instance was
@@ -120,6 +125,7 @@
         }
         mEditorMetadata.clear();
         mEditorArtwork = null;
+        mMetadataBuilder = new MediaMetadata.Builder();
     }
 
     /**
diff --git a/media/java/android/media/RemoteControlClient.java b/media/java/android/media/RemoteControlClient.java
index c2c61d3..8368df94 100644
--- a/media/java/android/media/RemoteControlClient.java
+++ b/media/java/android/media/RemoteControlClient.java
@@ -24,6 +24,11 @@
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.RectF;
+import android.media.session.MediaMetadata;
+import android.media.session.MediaSessionLegacyHelper;
+import android.media.session.PlaybackState;
+import android.media.session.Session;
+import android.media.session.TransportPerformer;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -336,6 +341,8 @@
      */
     public final static int FLAG_INFORMATION_REQUEST_ALBUM_ART = 1 << 3;
 
+    private Session mSession;
+
     /**
      * Class constructor.
      * @param mediaButtonIntent The intent that will be sent for the media button events sent
@@ -385,6 +392,22 @@
     }
 
     /**
+     * @hide
+     */
+    public void registerWithSession(MediaSessionLegacyHelper helper) {
+        helper.addRccListener(mRcMediaIntent, mTransportListener);
+        mSession = helper.getSession(mRcMediaIntent);
+    }
+
+    /**
+     * @hide
+     */
+    public void unregisterWithSession(MediaSessionLegacyHelper helper) {
+        helper.removeRccListener(mRcMediaIntent);
+        mSession = null;
+    }
+
+    /**
      * Class used to modify metadata in a {@link RemoteControlClient} object.
      * Use {@link RemoteControlClient#editMetadata(boolean)} to create an instance of an editor,
      * on which you set the metadata for the RemoteControlClient instance. Once all the information
@@ -438,6 +461,15 @@
         public synchronized MetadataEditor putString(int key, String value)
                 throws IllegalArgumentException {
             super.putString(key, value);
+            if (mMetadataBuilder != null) {
+                // MediaMetadata supports all the same fields as MetadataEditor
+                String metadataKey = MediaMetadata.getKeyFromMetadataEditorKey(key);
+                // But just in case, don't add things we don't understand
+                if (metadataKey != null) {
+                    mMetadataBuilder.putString(metadataKey, value);
+                }
+            }
+
             return this;
         }
 
@@ -459,6 +491,14 @@
         public synchronized MetadataEditor putLong(int key, long value)
                 throws IllegalArgumentException {
             super.putLong(key, value);
+            if (mMetadataBuilder != null) {
+                // MediaMetadata supports all the same fields as MetadataEditor
+                String metadataKey = MediaMetadata.getKeyFromMetadataEditorKey(key);
+                // But just in case, don't add things we don't understand
+                if (metadataKey != null) {
+                    mMetadataBuilder.putLong(metadataKey, value);
+                }
+            }
             return this;
         }
 
@@ -476,6 +516,14 @@
         public synchronized MetadataEditor putBitmap(int key, Bitmap bitmap)
                 throws IllegalArgumentException {
             super.putBitmap(key, bitmap);
+            if (mMetadataBuilder != null) {
+                // MediaMetadata supports all the same fields as MetadataEditor
+                String metadataKey = MediaMetadata.getKeyFromMetadataEditorKey(key);
+                // But just in case, don't add things we don't understand
+                if (metadataKey != null) {
+                    mMetadataBuilder.putBitmap(metadataKey, bitmap);
+                }
+            }
             return this;
         }
 
@@ -501,7 +549,7 @@
                 Log.e(TAG, "Can't apply a previously applied MetadataEditor");
                 return;
             }
-            synchronized(mCacheLock) {
+            synchronized (mCacheLock) {
                 // assign the edited data
                 mMetadata = new Bundle(mEditorMetadata);
                 // add the information about editable keys
@@ -521,6 +569,11 @@
                     // send to remote control display if conditions are met
                     sendArtwork_syncCacheLock(null, 0, 0);
                 }
+
+                // USE_SESSIONS
+                if (mSession != null && mMetadataBuilder != null) {
+                    mSession.getTransportPerformer().setMetadata(mMetadataBuilder.build());
+                }
                 mApplied = true;
             }
         }
@@ -546,6 +599,12 @@
             editor.mMetadataChanged = false;
             editor.mArtworkChanged = false;
         }
+        // USE_SESSIONS
+        if (startEmpty || mMediaMetadata == null) {
+            editor.mMetadataBuilder = new MediaMetadata.Builder();
+        } else {
+            editor.mMetadataBuilder = new MediaMetadata.Builder(mMediaMetadata);
+        }
         return editor;
     }
 
@@ -624,6 +683,15 @@
 
                 // handle automatic playback position refreshes
                 initiateCheckForDrift_syncCacheLock();
+
+                // USE_SESSIONS
+                if (mSession != null) {
+                    int pbState = PlaybackState.getStateFromRccState(state);
+                    mSessionPlaybackState.setState(pbState, hasPosition ?
+                            mPlaybackPositionMs : PlaybackState.PLAYBACK_POSITION_UNKNOWN,
+                            playbackSpeed);
+                    mSession.getTransportPerformer().setPlaybackState(mSessionPlaybackState);
+                }
             }
         }
     }
@@ -704,6 +772,13 @@
 
             // send to remote control display if conditions are met
             sendTransportControlInfo_syncCacheLock(null);
+
+            // USE_SESSIONS
+            if (mSession != null) {
+                mSessionPlaybackState.setActions(PlaybackState
+                        .getActionsFromRccControlFlags(transportControlFlags));
+                mSession.getTransportPerformer().setPlaybackState(mSessionPlaybackState);
+            }
         }
     }
 
@@ -1038,6 +1113,16 @@
     private boolean mNeedsPositionSync = false;
 
     /**
+     * Cache for the current playback state using Session APIs.
+     */
+    private final PlaybackState mSessionPlaybackState = new PlaybackState();
+
+    /**
+     * Cache for metadata using Session APIs. This is re-initialized in apply().
+     */
+    private MediaMetadata mMediaMetadata;
+
+    /**
      * A class to encapsulate all the information about a remote control display.
      * A RemoteControlClient's metadata and state may be displayed on multiple IRemoteControlDisplay
      */
@@ -1219,6 +1304,26 @@
         return mRcseId;
     }
 
+    // USE_SESSIONS
+    private TransportPerformer.Listener mTransportListener = new TransportPerformer.Listener() {
+
+        @Override
+        public void onSeekTo(long pos) {
+            RemoteControlClient.this.onSeekTo(mCurrentClientGenId, pos);
+        }
+
+        @Override
+        public void onRate(Rating rating) {
+            if ((mTransportControlFlags & FLAG_KEY_MEDIA_RATING) != 0) {
+                if (mEventHandler != null) {
+                    mEventHandler.sendMessage(mEventHandler.obtainMessage(
+                            MSG_UPDATE_METADATA, mCurrentClientGenId,
+                            MetadataEditor.RATING_KEY_BY_USER, rating));
+                }
+            }
+        }
+    };
+
     private EventHandler mEventHandler;
     private final static int MSG_REQUEST_PLAYBACK_STATE = 1;
     private final static int MSG_REQUEST_METADATA = 2;
@@ -1325,7 +1430,7 @@
             // target == null implies all displays must be updated
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mEnabled) {
                     try {
                         di.mRcDisplay.setPlaybackState(mInternalClientGenId,
@@ -1353,7 +1458,7 @@
             // target == null implies all displays must be updated
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mEnabled) {
                     try {
                         di.mRcDisplay.setMetadata(mInternalClientGenId, mMetadata);
@@ -1381,7 +1486,7 @@
             // target == null implies all displays must be updated
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mEnabled) {
                     try {
                         di.mRcDisplay.setTransportControlInfo(mInternalClientGenId,
@@ -1407,7 +1512,7 @@
             // target == null implies all displays must be updated
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                if (!sendArtworkToDisplay((DisplayInfoForClient) displayIterator.next())) {
+                if (!sendArtworkToDisplay(displayIterator.next())) {
                     displayIterator.remove();
                 }
             }
@@ -1453,7 +1558,7 @@
             // target == null implies all displays must be updated
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 try {
                     if (di.mEnabled) {
                         if ((di.mArtworkExpectedWidth > 0) && (di.mArtworkExpectedHeight > 0)) {
@@ -1537,7 +1642,7 @@
             boolean displayKnown = false;
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext() && !displayKnown) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 displayKnown = di.mRcDisplay.asBinder().equals(rcd.asBinder());
                 if (displayKnown) {
                     // this display was known but the change in artwork size will cause the
@@ -1562,7 +1667,7 @@
         synchronized(mCacheLock) {
             Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
                     displayIterator.remove();
                     break;
@@ -1573,7 +1678,7 @@
             boolean newNeedsPositionSync = false;
             displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mWantsPositionSync) {
                     newNeedsPositionSync = true;
                     break;
@@ -1592,7 +1697,7 @@
         synchronized(mCacheLock) {
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mRcDisplay.asBinder().equals(rcd.asBinder()) &&
                         ((di.mArtworkExpectedWidth != w) || (di.mArtworkExpectedHeight != h))) {
                     di.mArtworkExpectedWidth = w;
@@ -1617,7 +1722,7 @@
             // go through the list of RCDs and for each entry, check both whether this is the RCD
             //  that gets upated, and whether the list has one entry that wants position sync
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mEnabled) {
                     if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
                         di.mWantsPositionSync = wantsSync;
@@ -1640,7 +1745,7 @@
         synchronized(mCacheLock) {
             final Iterator<DisplayInfoForClient> displayIterator = mRcDisplays.iterator();
             while (displayIterator.hasNext()) {
-                final DisplayInfoForClient di = (DisplayInfoForClient) displayIterator.next();
+                final DisplayInfoForClient di = displayIterator.next();
                 if (di.mRcDisplay.asBinder().equals(rcd.asBinder())) {
                     di.mEnabled = enable;
                 }
diff --git a/media/java/android/media/session/MediaMetadata.java b/media/java/android/media/session/MediaMetadata.java
index e2330f7..56bdf68 100644
--- a/media/java/android/media/session/MediaMetadata.java
+++ b/media/java/android/media/session/MediaMetadata.java
@@ -16,12 +16,15 @@
 package android.media.session;
 
 import android.graphics.Bitmap;
+import android.media.MediaMetadataEditor;
+import android.media.MediaMetadataRetriever;
 import android.media.Rating;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.SparseArray;
 
 /**
  * Contains metadata about an item, such as the title, artist, etc.
@@ -40,7 +43,8 @@
     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.
+     * The duration of the media in ms. A negative duration indicates that the
+     * duration is unknown (or infinite).
      */
     public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
 
@@ -65,12 +69,17 @@
     public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
 
     /**
+     * The compilation status of the media.
+     */
+    public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+
+    /**
      * 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.
+     * The year the media was created or published as a long.
      */
     public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
 
@@ -151,8 +160,9 @@
         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_COMPILATION, 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_YEAR, METADATA_TYPE_LONG);
         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);
@@ -165,6 +175,36 @@
         METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
         METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
     }
+
+    private static final SparseArray<String> EDITOR_KEY_MAPPING;
+
+    static {
+        EDITOR_KEY_MAPPING = new SparseArray<String>();
+        EDITOR_KEY_MAPPING.put(MediaMetadataEditor.BITMAP_KEY_ARTWORK, METADATA_KEY_ART);
+        EDITOR_KEY_MAPPING.put(MediaMetadataEditor.RATING_KEY_BY_OTHERS, METADATA_KEY_RATING);
+        EDITOR_KEY_MAPPING.put(MediaMetadataEditor.RATING_KEY_BY_USER, METADATA_KEY_USER_RATING);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ALBUM, METADATA_KEY_ALBUM);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
+                METADATA_KEY_ALBUM_ARTIST);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_ARTIST, METADATA_KEY_ARTIST);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_AUTHOR, METADATA_KEY_AUTHOR);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER,
+                METADATA_KEY_TRACK_NUMBER);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_COMPOSER, METADATA_KEY_COMPOSER);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_COMPILATION,
+                METADATA_KEY_COMPILATION);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DATE, METADATA_KEY_DATE);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER,
+                METADATA_KEY_DISC_NUMBER);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_DURATION, METADATA_KEY_DURATION);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_GENRE, METADATA_KEY_GENRE);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS,
+                METADATA_KEY_NUM_TRACKS);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_TITLE, METADATA_KEY_TITLE);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_WRITER, METADATA_KEY_WRITER);
+        EDITOR_KEY_MAPPING.put(MediaMetadataRetriever.METADATA_KEY_YEAR, METADATA_KEY_YEAR);
+    }
+
     private final Bundle mBundle;
 
     private MediaMetadata(Bundle bundle) {
@@ -176,6 +216,16 @@
     }
 
     /**
+     * Returns true if the given key is contained in the metadata
+     *
+     * @param key a String key
+     * @return true if the key exists in this metadata, false otherwise
+     */
+    public boolean containsKey(String key) {
+        return mBundle.containsKey(key);
+    }
+
+    /**
      * 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.
@@ -195,7 +245,7 @@
      * @return a long value
      */
     public long getLong(String key) {
-        return mBundle.getLong(key);
+        return mBundle.getLong(key, 0);
     }
 
     /**
@@ -244,6 +294,18 @@
         dest.writeBundle(mBundle);
     }
 
+    /**
+     * Helper for getting the String key used by {@link MediaMetadata} from the
+     * integer key that {@link MediaMetadataEditor} uses.
+     *
+     * @param editorKey The key used by the editor
+     * @return The key used by this class or null if no mapping exists
+     * @hide
+     */
+    public static String getKeyFromMetadataEditorKey(int editorKey) {
+        return EDITOR_KEY_MAPPING.get(editorKey, null);
+    }
+
     public static final Parcelable.Creator<MediaMetadata> CREATOR
             = new Parcelable.Creator<MediaMetadata>() {
                 @Override
@@ -295,10 +357,9 @@
          * <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_ARTIST}</li>
+         * <li>{@link #METADATA_KEY_ART_URI}</li>
          * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
          * </ul>
          *
@@ -326,6 +387,7 @@
          * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
          * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
          * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+         * <li>{@link #METADATA_KEY_YEAR}</li>
          * </ul>
          *
          * @param key The key for referencing this value
diff --git a/media/java/android/media/session/MediaSessionLegacyHelper.java b/media/java/android/media/session/MediaSessionLegacyHelper.java
new file mode 100644
index 0000000..4ee67d1
--- /dev/null
+++ b/media/java/android/media/session/MediaSessionLegacyHelper.java
@@ -0,0 +1,213 @@
+/*
+ * 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.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.KeyEvent;
+
+/**
+ * Helper for connecting existing APIs up to the new session APIs. This can be
+ * used by RCC, AudioFocus, etc. to create a single session that translates to
+ * all those components.
+ *
+ * @hide
+ */
+public class MediaSessionLegacyHelper {
+    private static final String TAG = "MediaSessionHelper";
+
+    private static final Object sLock = new Object();
+    private static MediaSessionLegacyHelper sInstance;
+
+    private SessionManager mSessionManager;
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+    // The legacy APIs use PendingIntents to register/unregister media button
+    // receivers and these are associated with RCC.
+    private ArrayMap<PendingIntent, SessionHolder> mSessions = new ArrayMap<PendingIntent, SessionHolder>();
+
+    private MediaSessionLegacyHelper(Context context) {
+        mSessionManager = (SessionManager) context
+                .getSystemService(Context.MEDIA_SESSION_SERVICE);
+    }
+
+    public static MediaSessionLegacyHelper getHelper(Context context) {
+        synchronized (sLock) {
+            if (sInstance == null) {
+                sInstance = new MediaSessionLegacyHelper(context);
+            }
+        }
+        return sInstance;
+    }
+
+    public Session getSession(PendingIntent pi) {
+        SessionHolder holder = mSessions.get(pi);
+        return holder == null ? null : holder.mSession;
+    }
+
+    public void addRccListener(PendingIntent pi, TransportPerformer.Listener listener) {
+
+        SessionHolder holder = getHolder(pi, true);
+        TransportPerformer performer = holder.mSession.getTransportPerformer();
+        if (holder.mRccListener != null) {
+            if (holder.mRccListener == listener) {
+                // This is already the registered listener, ignore
+                return;
+            }
+            // Otherwise it changed so we need to switch to the new one
+            performer.removeListener(holder.mRccListener);
+        }
+        performer.addListener(listener, mHandler);
+        holder.mRccListener = listener;
+        holder.update();
+    }
+
+    public void removeRccListener(PendingIntent pi) {
+        SessionHolder holder = getHolder(pi, false);
+        if (holder != null && holder.mRccListener != null) {
+            holder.mSession.getTransportPerformer().removeListener(holder.mRccListener);
+            holder.mRccListener = null;
+            holder.update();
+        }
+    }
+
+    public void addMediaButtonListener(PendingIntent pi,
+            Context context) {
+        SessionHolder holder = getHolder(pi, true);
+        if (holder.mMediaButtonListener != null) {
+            // Already have this listener registered
+            return;
+        }
+        holder.mMediaButtonListener = new MediaButtonListener(pi, context);
+        holder.mSession.getTransportPerformer().addListener(holder.mMediaButtonListener, mHandler);
+    }
+
+    public void removeMediaButtonListener(PendingIntent pi) {
+        SessionHolder holder = getHolder(pi, false);
+        if (holder != null && holder.mMediaButtonListener != null) {
+            holder.mSession.getTransportPerformer().removeListener(holder.mMediaButtonListener);
+            holder.update();
+        }
+    }
+
+    private SessionHolder getHolder(PendingIntent pi, boolean createIfMissing) {
+        SessionHolder holder = mSessions.get(pi);
+        if (holder == null && createIfMissing) {
+            Session session = mSessionManager.createSession(TAG);
+            session.setTransportPerformerEnabled();
+            session.publish();
+            holder = new SessionHolder(session, pi);
+            mSessions.put(pi, holder);
+        }
+        return holder;
+    }
+
+    public static class MediaButtonListener extends TransportPerformer.Listener {
+        private final PendingIntent mPendingIntent;
+        private final Context mContext;
+
+        public MediaButtonListener(PendingIntent pi, Context context) {
+            mPendingIntent = pi;
+            mContext = context;
+        }
+
+        @Override
+        public void onPlay() {
+            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY);
+        }
+
+        @Override
+        public void onPause() {
+            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE);
+        }
+
+        @Override
+        public void onNext() {
+            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT);
+        }
+
+        @Override
+        public void onPrevious() {
+            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
+        }
+
+        @Override
+        public void onFastForward() {
+            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
+        }
+
+        @Override
+        public void onRewind() {
+            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND);
+        }
+
+        @Override
+        public void onStop() {
+            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP);
+        }
+
+        private void sendKeyEvent(int keyCode) {
+            KeyEvent ke = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
+            Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+
+            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
+            try {
+                mPendingIntent.send(mContext, 0, intent);
+            } catch (CanceledException e) {
+                Log.e(TAG, "Error sending media key down event:", e);
+                // Don't bother sending up if down failed
+                return;
+            }
+
+            ke = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
+            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
+            try {
+                mPendingIntent.send(mContext, 0, intent);
+            } catch (CanceledException e) {
+                Log.e(TAG, "Error sending media key up event:", e);
+            }
+        }
+    }
+
+    private class SessionHolder {
+        public final Session mSession;
+        public final PendingIntent mPi;
+        public MediaButtonListener mMediaButtonListener;
+        public TransportPerformer.Listener mRccListener;
+
+        public SessionHolder(Session session, PendingIntent pi) {
+            mSession = session;
+            mPi = pi;
+        }
+
+        public void update() {
+            if (mMediaButtonListener == null && mRccListener == null) {
+                mSession.release();
+                mSessions.remove(mPi);
+            } else if (mMediaButtonListener != null && mRccListener != null) {
+                // TODO set session to active
+            } else {
+                // TODO set session to inactive
+            }
+        }
+    }
+}
diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java
index 14d9fb1..9e58ea8 100644
--- a/media/java/android/media/session/PlaybackState.java
+++ b/media/java/android/media/session/PlaybackState.java
@@ -15,8 +15,10 @@
  */
 package android.media.session;
 
+import android.media.RemoteControlClient;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.SystemClock;
 
 /**
  * Playback state for a {@link Session}. This includes a state like
@@ -88,6 +90,13 @@
     public static final long ACTION_SEEK_TO = 1 << 8;
 
     /**
+     * Indicates this performer supports the play/pause toggle command.
+     *
+     * @see #setActions
+     */
+    public static final long ACTION_PLAY_PAUSE = 1 << 9;
+
+    /**
      * 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.
      *
@@ -154,12 +163,33 @@
      */
     public final static int PLAYSTATE_CONNECTING = 8;
 
+    /**
+     * State indicating the player is currently skipping to the previous item.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_SKIPPING_BACKWARDS = 9;
+
+    /**
+     * State indicating the player is currently skipping to the next item.
+     *
+     * @see #setState
+     */
+    public final static int PLAYSTATE_SKIPPING_FORWARDS = 10;
+
+    /**
+     * Set this value on {@link #setPosition(long)} to indicate the position is
+     * not known for this item.
+     */
+    public final static long PLAYBACK_POSITION_UNKNOWN = -1;
+
     private int mState;
     private long mPosition;
     private long mBufferPosition;
-    private float mSpeed;
-    private long mCapabilities;
+    private float mRate;
+    private long mActions;
     private String mErrorMessage;
+    private long mUpdateTime;
 
     /**
      * Create an empty PlaybackState. At minimum a state and actions should be
@@ -175,21 +205,24 @@
      * @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());
+        mState = from.mState;
+        mPosition = from.mPosition;
+        mRate = from.mRate;
+        mUpdateTime = from.mUpdateTime;
+        mBufferPosition = from.mBufferPosition;
+        mActions = from.mActions;
+        mErrorMessage = from.mErrorMessage;
     }
 
     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());
+        mState = in.readInt();
+        mPosition = in.readLong();
+        mRate = in.readFloat();
+        mUpdateTime = in.readLong();
+        mBufferPosition = in.readLong();
+        mActions = in.readLong();
+        mErrorMessage = in.readString();
+
     }
 
     @Override
@@ -199,12 +232,13 @@
 
     @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());
+        dest.writeInt(mState);
+        dest.writeLong(mPosition);
+        dest.writeFloat(mRate);
+        dest.writeLong(mUpdateTime);
+        dest.writeLong(mBufferPosition);
+        dest.writeLong(mActions);
+        dest.writeString(mErrorMessage);
     }
 
     /**
@@ -224,7 +258,16 @@
     }
 
     /**
-     * Set the current state of playback. One of the following:
+     * Set the current state of playback.
+     * <p>
+     * The position must be in ms and indicates the current playback position
+     * within the track. If the position is unknown use
+     * {@link #PLAYBACK_POSITION_UNKNOWN}.
+     * <p>
+     * The rate is a multiple of normal playback and should be 0 when paused and
+     * negative when rewinding. Normal playback rate is 1.0.
+     * <p>
+     * The state must be one of the following:
      * <ul>
      * <li> {@link PlaybackState#PLAYSTATE_NONE}</li>
      * <li> {@link PlaybackState#PLAYSTATE_STOPPED}</li>
@@ -234,9 +277,18 @@
      * <li> {@link PlaybackState#PLAYSTATE_REWINDING}</li>
      * <li> {@link PlaybackState#PLAYSTATE_BUFFERING}</li>
      * <li> {@link PlaybackState#PLAYSTATE_ERROR}</li>
+     * </ul>
+     *
+     * @param state The current state of playback.
+     * @param position The position in the current track in ms.
+     * @param rate The current rate of playback as a multiple of normal
+     *            playback.
      */
-    public void setState(int mState) {
-        this.mState = mState;
+    public void setState(int state, long position, float rate) {
+        this.mState = state;
+        this.mPosition = position;
+        this.mRate = rate;
+        mUpdateTime = SystemClock.elapsedRealtime();
     }
 
     /**
@@ -247,13 +299,6 @@
     }
 
     /**
-     * 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.
@@ -272,21 +317,14 @@
     }
 
     /**
-     * Get the current playback speed as a multiple of normal playback. This
+     * Get the current playback rate as a multiple of normal playback. This
      * should be negative when rewinding. A value of 1 means normal playback and
      * 0 means paused.
+     *
+     * @return The current rate of playback.
      */
-    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;
+    public float getRate() {
+        return mRate;
     }
 
     /**
@@ -305,7 +343,7 @@
      * </ul>
      */
     public long getActions() {
-        return mCapabilities;
+        return mActions;
     }
 
     /**
@@ -324,7 +362,7 @@
      * </ul>
      */
     public void setActions(long capabilities) {
-        mCapabilities = capabilities;
+        mActions = capabilities;
     }
 
     /**
@@ -336,6 +374,17 @@
     }
 
     /**
+     * Get the elapsed real time at which position was last updated. If the
+     * position has never been set this will return 0;
+     *
+     * @return The last time the position was updated.
+     * @hide
+     */
+    public long getLastPositionUpdateTime() {
+        return mUpdateTime;
+    }
+
+    /**
      * Set a user readable error message. This should be set when the state is
      * {@link PlaybackState#PLAYSTATE_ERROR}.
      */
@@ -343,6 +392,80 @@
         mErrorMessage = errorMessage;
     }
 
+    /**
+     * Get the {@link PlaybackState} state for the given
+     * {@link RemoteControlClient} state.
+     *
+     * @param rccState The state used by {@link RemoteControlClient}.
+     * @return The equivalent state used by {@link PlaybackState}.
+     * @hide
+     */
+    public static int getStateFromRccState(int rccState) {
+        switch (rccState) {
+            case RemoteControlClient.PLAYSTATE_BUFFERING:
+                return PLAYSTATE_BUFFERING;
+            case RemoteControlClient.PLAYSTATE_ERROR:
+                return PLAYSTATE_ERROR;
+            case RemoteControlClient.PLAYSTATE_FAST_FORWARDING:
+                return PLAYSTATE_FAST_FORWARDING;
+            case RemoteControlClient.PLAYSTATE_NONE:
+                return PLAYSTATE_NONE;
+            case RemoteControlClient.PLAYSTATE_PAUSED:
+                return PLAYSTATE_PAUSED;
+            case RemoteControlClient.PLAYSTATE_PLAYING:
+                return PLAYSTATE_PLAYING;
+            case RemoteControlClient.PLAYSTATE_REWINDING:
+                return PLAYSTATE_REWINDING;
+            case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS:
+                return PLAYSTATE_SKIPPING_BACKWARDS;
+            case RemoteControlClient.PLAYSTATE_STOPPED:
+                return PLAYSTATE_STOPPED;
+            default:
+                return -1;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static long getActionsFromRccControlFlags(int rccFlags) {
+        long actions = 0;
+        long flag = 1;
+        while (flag <= rccFlags) {
+            if ((flag & rccFlags) != 0) {
+                actions |= getActionForRccFlag((int) flag);
+            }
+            flag = flag << 1;
+        }
+        return actions;
+    }
+
+    private static long getActionForRccFlag(int flag) {
+        switch (flag) {
+            case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS:
+                return ACTION_PREVIOUS_ITEM;
+            case RemoteControlClient.FLAG_KEY_MEDIA_REWIND:
+                return ACTION_REWIND;
+            case RemoteControlClient.FLAG_KEY_MEDIA_PLAY:
+                return ACTION_PLAY;
+            case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE:
+                return ACTION_PLAY_PAUSE;
+            case RemoteControlClient.FLAG_KEY_MEDIA_PAUSE:
+                return ACTION_PAUSE;
+            case RemoteControlClient.FLAG_KEY_MEDIA_STOP:
+                return ACTION_STOP;
+            case RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD:
+                return ACTION_FASTFORWARD;
+            case RemoteControlClient.FLAG_KEY_MEDIA_NEXT:
+                return ACTION_NEXT_ITEM;
+            case RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE:
+                return ACTION_SEEK_TO;
+            case RemoteControlClient.FLAG_KEY_MEDIA_RATING:
+                return ACTION_RATING;
+        }
+        return 0;
+    }
+
     public static final Parcelable.Creator<PlaybackState> CREATOR
             = new Parcelable.Creator<PlaybackState>() {
         @Override
diff --git a/media/java/android/media/session/TransportPerformer.java b/media/java/android/media/session/TransportPerformer.java
index eddffd1..187f48d 100644
--- a/media/java/android/media/session/TransportPerformer.java
+++ b/media/java/android/media/session/TransportPerformer.java
@@ -280,18 +280,11 @@
         /**
          * 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.
+         * {@link #setPlaybackState}.
          *
-         * @param focusChange The type of focus change, TBD. The default
-         *            implementation will deliver a call to {@link #onPause}
-         *            when focus is lost.
+         * @param focusChange The type of focus change, TBD.
          */
         public void onRouteFocusChange(int focusChange) {
-            switch (focusChange) {
-                case AudioManager.AUDIOFOCUS_LOSS:
-                    onPause();
-                    break;
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index ac7f4f3..15566d6 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -40,6 +40,7 @@
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -301,6 +302,34 @@
         }
     }
 
+    private PlaybackState getStateWithUpdatedPosition() {
+        PlaybackState state = mPlaybackState;
+        long duration = -1;
+        if (mMetadata != null && mMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+            duration = mMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+        }
+        PlaybackState result = null;
+        if (state != null) {
+            if (state.getState() == PlaybackState.PLAYSTATE_PLAYING
+                    || state.getState() == PlaybackState.PLAYSTATE_FAST_FORWARDING
+                    || state.getState() == PlaybackState.PLAYSTATE_REWINDING) {
+                long updateTime = state.getLastPositionUpdateTime();
+                if (updateTime > 0) {
+                    long position = (long) (state.getRate()
+                            * (SystemClock.elapsedRealtime() - updateTime)) + state.getPosition();
+                    if (duration >= 0 && position > duration) {
+                        position = duration;
+                    } else if (position < 0) {
+                        position = 0;
+                    }
+                    result = new PlaybackState(state);
+                    result.setState(state.getState(), position, state.getRate());
+                }
+            }
+        }
+        return result == null ? state : result;
+    }
+
     private final RouteConnectionRecord.Listener mConnectionListener
             = new RouteConnectionRecord.Listener() {
         @Override
@@ -603,7 +632,7 @@
 
         @Override
         public PlaybackState getPlaybackState() {
-            return mPlaybackState;
+            return getStateWithUpdatedPosition();
         }
 
         @Override
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
index 5dc3904..2e029f0 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
@@ -119,7 +119,9 @@
     }
 
     private void updateState(int newState) {
-        mPlaybackState.setState(newState);
+        float rate = newState == PlaybackState.PLAYSTATE_PLAYING ? 1 : 0;
+        long position = mRenderer == null ? -1 : mRenderer.getSeekPosition();
+        mPlaybackState.setState(newState, position, rate);
         mPerformer.setPlaybackState(mPlaybackState);
     }
 
@@ -132,7 +134,7 @@
         @Override
         public void onError(int type, int extra, Bundle extras, Throwable error) {
             Log.d(TAG, "Sending onError with type " + type + " and extra " + extra);
-            mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+            mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR, -1, 0);
             if (error != null) {
                 mPlaybackState.setErrorMessage(error.getLocalizedMessage());
             }
@@ -147,34 +149,33 @@
             if (newState != Renderer.STATE_ERROR) {
                 mPlaybackState.setErrorMessage(null);
             }
+            long position = -1;
+            if (mRenderer != null) {
+                position = mRenderer.getSeekPosition();
+            }
             switch (newState) {
                 case Renderer.STATE_ENDED:
                 case Renderer.STATE_STOPPED:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED, position, 0);
                     break;
                 case Renderer.STATE_INIT:
                 case Renderer.STATE_PREPARING:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING, position, 0);
                     break;
                 case Renderer.STATE_ERROR:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR, position, 0);
                     break;
                 case Renderer.STATE_PAUSED:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED, position, 0);
                     break;
                 case Renderer.STATE_PLAYING:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING, position, 1);
                     break;
                 default:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR, position, 0);
                     mPlaybackState.setErrorMessage("unkown state");
                     break;
             }
-            if (mRenderer != null) {
-                mPlaybackState.setPosition(mRenderer.getSeekPosition());
-            } else {
-                mPlaybackState.setPosition(-1);
-            }
             mPerformer.setPlaybackState(mPlaybackState);
             if (mListener != null) {
                 mListener.onPlayStateChanged(mPlaybackState);
@@ -188,8 +189,8 @@
         @Override
         public void onFocusLost() {
             Log.d(TAG, "Focus lost, changing state to " + Renderer.STATE_PAUSED);
-            mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
-            mPlaybackState.setPosition(mRenderer.getSeekPosition());
+            long position = mRenderer == null ? -1 : mRenderer.getSeekPosition();
+            mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED, position, 0);
             mPerformer.setPlaybackState(mPlaybackState);
             if (mListener != null) {
                 mListener.onPlayStateChanged(mPlaybackState);
diff --git a/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java b/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java
index 6edcd7d..6537d49 100644
--- a/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java
+++ b/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java
@@ -158,30 +158,33 @@
             if (newState != Renderer.STATE_ERROR) {
                 mPlaybackState.setErrorMessage(null);
             }
+            long position = -1;
+            if (mRenderer != null) {
+                position = mRenderer.getSeekPosition();
+            }
             switch (newState) {
                 case Renderer.STATE_ENDED:
                 case Renderer.STATE_STOPPED:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED, position, 0);
                     break;
                 case Renderer.STATE_INIT:
                 case Renderer.STATE_PREPARING:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING, position, 0);
                     break;
                 case Renderer.STATE_ERROR:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR, position, 0);
                     break;
                 case Renderer.STATE_PAUSED:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED, position, 0);
                     break;
                 case Renderer.STATE_PLAYING:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING, position, 1);
                     break;
                 default:
-                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+                    mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR, position, 0);
                     mPlaybackState.setErrorMessage("unkown state");
                     break;
             }
-            mPlaybackState.setPosition(mRenderer.getSeekPosition());
 
             mControls.sendPlaybackChangeEvent(mPlaybackState.getState());
         }
@@ -193,8 +196,8 @@
         @Override
         public void onFocusLost() {
             Log.d(TAG, "Focus lost, changing state to " + Renderer.STATE_PAUSED);
-            mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
-            mPlaybackState.setPosition(mRenderer.getSeekPosition());
+            mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED, mRenderer.getSeekPosition(), 0);
+            mRenderer.onPause();
         }
 
         @Override