mrp sample: refactor and use helper class

Bug: 10955351

Change-Id: Ib607d27bc93a35460c6acd295d29afad3f255e25
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index cbbc640..1c6efc9 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -32,7 +32,9 @@
 
     <string name="sample_media_route_provider_service">Media Route Provider Service Support Library Sample</string>
     <string name="fixed_volume_route_name">Fixed Volume Remote Playback Route</string>
-    <string name="variable_volume_route_name">Variable Volume Remote Playback Route</string>
+    <string name="variable_volume_basic_route_name">Variable Volume (Basic) Remote Playback Route</string>
+    <string name="variable_volume_queuing_route_name">Variable Volume (Queuing) Remote Playback Route</string>
+    <string name="variable_volume_session_route_name">Variable Volume (Session) Remote Playback Route</string>
     <string name="sample_route_description">Sample route from Support7Demos</string>
 
     <!-- GridLayout -->
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/LocalPlayer.java b/samples/Support7Demos/src/com/example/android/supportv7/media/LocalPlayer.java
new file mode 100644
index 0000000..57cdd88
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/LocalPlayer.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2013 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.example.android.supportv7.media;
+
+import android.app.Activity;
+import android.app.Presentation;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.support.v7.media.MediaItemStatus;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import com.example.android.supportv7.R;
+
+import java.io.IOException;
+
+/**
+ * Handles playback of a single media item using MediaPlayer.
+ */
+public abstract class LocalPlayer extends Player implements
+        MediaPlayer.OnPreparedListener,
+        MediaPlayer.OnCompletionListener,
+        MediaPlayer.OnErrorListener,
+        MediaPlayer.OnSeekCompleteListener {
+    private static final String TAG = "LocalPlayer";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PLAY_PENDING = 1;
+    private static final int STATE_READY = 2;
+    private static final int STATE_PLAYING = 3;
+    private static final int STATE_PAUSED = 4;
+
+    private final Context mContext;
+    private final Handler mHandler = new Handler();
+    private MediaPlayer mMediaPlayer;
+    private int mState = STATE_IDLE;
+    private int mSeekToPos;
+    private int mVideoWidth;
+    private int mVideoHeight;
+    private Surface mSurface;
+    private SurfaceHolder mSurfaceHolder;
+
+    public LocalPlayer(Context context) {
+        mContext = context;
+
+        // reset media player
+        reset();
+    }
+
+    @Override
+    public boolean isRemotePlayback() {
+        return false;
+    }
+
+    @Override
+    public boolean isQueuingSupported() {
+        return false;
+    }
+
+    @Override
+    public void connect(RouteInfo route) {
+        if (DEBUG) {
+            Log.d(TAG, "connecting to: " + route);
+        }
+    }
+
+    @Override
+    public void release() {
+        if (DEBUG) {
+            Log.d(TAG, "releasing");
+        }
+        // release media player
+        if (mMediaPlayer != null) {
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+        }
+    }
+
+    // Player
+    @Override
+    public void play(final PlaylistItem item) {
+        if (DEBUG) {
+            Log.d(TAG, "play: item=" + item);
+        }
+        reset();
+        mSeekToPos = (int)item.getPosition();
+        try {
+            mMediaPlayer.setDataSource(mContext, item.getUri());
+            mMediaPlayer.prepareAsync();
+        } catch (IllegalStateException e) {
+            Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri());
+        } catch (IOException e) {
+            Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri());
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri());
+        } catch (SecurityException e) {
+            Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri());
+        }
+        if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
+            resume();
+        } else {
+            pause();
+        }
+    }
+
+    @Override
+    public void seek(final PlaylistItem item) {
+        if (DEBUG) {
+            Log.d(TAG, "seek: item=" + item);
+        }
+        int pos = (int)item.getPosition();
+        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
+            mMediaPlayer.seekTo(pos);
+            mSeekToPos = pos;
+        } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) {
+            // Seek before onPrepared() arrives,
+            // need to performed delayed seek in onPrepared()
+            mSeekToPos = pos;
+        }
+    }
+
+    @Override
+    public void getStatus(final PlaylistItem item, final boolean update) {
+        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
+            // use mSeekToPos if we're currently seeking (mSeekToPos is reset
+            // when seeking is completed)
+            item.setDuration(mMediaPlayer.getDuration());
+            item.setPosition(mSeekToPos > 0 ?
+                    mSeekToPos : mMediaPlayer.getCurrentPosition());
+            item.setTimestamp(SystemClock.elapsedRealtime());
+        }
+        if (update && mCallback != null) {
+            mCallback.onPlaylistReady();
+        }
+    }
+
+    @Override
+    public void pause() {
+        if (DEBUG) {
+            Log.d(TAG, "pause");
+        }
+        if (mState == STATE_PLAYING) {
+            mMediaPlayer.pause();
+            mState = STATE_PAUSED;
+        }
+    }
+
+    @Override
+    public void resume() {
+        if (DEBUG) {
+            Log.d(TAG, "resume");
+        }
+        if (mState == STATE_READY || mState == STATE_PAUSED) {
+            mMediaPlayer.start();
+            mState = STATE_PLAYING;
+        } else if (mState == STATE_IDLE){
+            mState = STATE_PLAY_PENDING;
+        }
+    }
+
+    @Override
+    public void stop() {
+        if (DEBUG) {
+            Log.d(TAG, "stop");
+        }
+        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
+            mMediaPlayer.stop();
+            mState = STATE_IDLE;
+        }
+    }
+
+    @Override
+    public void enqueue(final PlaylistItem item) {
+        throw new UnsupportedOperationException("LocalPlayer doesn't support enqueue!");
+    }
+
+    @Override
+    public PlaylistItem remove(String iid) {
+        throw new UnsupportedOperationException("LocalPlayer doesn't support remove!");
+    }
+
+    //MediaPlayer Listeners
+    @Override
+    public void onPrepared(MediaPlayer mp) {
+        if (DEBUG) {
+            Log.d(TAG, "onPrepared");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mState == STATE_IDLE) {
+                    mState = STATE_READY;
+                    updateVideoRect();
+                } else if (mState == STATE_PLAY_PENDING) {
+                    mState = STATE_PLAYING;
+                    updateVideoRect();
+                    if (mSeekToPos > 0) {
+                        if (DEBUG) {
+                            Log.d(TAG, "seek to initial pos: " + mSeekToPos);
+                        }
+                        mMediaPlayer.seekTo(mSeekToPos);
+                    }
+                    mMediaPlayer.start();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onCompletion(MediaPlayer mp) {
+        if (DEBUG) {
+            Log.d(TAG, "onCompletion");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mCallback != null) {
+                    mCallback.onCompletion();
+                }
+            }
+        });
+    }
+
+    @Override
+    public boolean onError(MediaPlayer mp, int what, int extra) {
+        if (DEBUG) {
+            Log.d(TAG, "onError");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mCallback != null) {
+                    mCallback.onError();
+                }
+            }
+        });
+        // return true so that onCompletion is not called
+        return true;
+    }
+
+    @Override
+    public void onSeekComplete(MediaPlayer mp) {
+        if (DEBUG) {
+            Log.d(TAG, "onSeekComplete");
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mSeekToPos = 0;
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+        });
+    }
+
+    protected Context getContext() { return mContext; }
+    protected MediaPlayer getMediaPlayer() { return mMediaPlayer; }
+    protected int getVideoWidth() { return mVideoWidth; }
+    protected int getVideoHeight() { return mVideoHeight; }
+    protected void setSurface(Surface surface) {
+        mSurface = surface;
+        mSurfaceHolder = null;
+        updateSurface();
+    }
+
+    protected void setSurface(SurfaceHolder surfaceHolder) {
+        mSurface = null;
+        mSurfaceHolder = surfaceHolder;
+        updateSurface();
+    }
+
+    protected void removeSurface(SurfaceHolder surfaceHolder) {
+        if (surfaceHolder == mSurfaceHolder) {
+            setSurface((SurfaceHolder)null);
+        }
+    }
+
+    protected void updateSurface() {
+        if (mMediaPlayer == null) {
+            // just return if media player is already gone
+            return;
+        }
+        if (mSurface != null) {
+            // The setSurface API does not exist until V14+.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+                ICSMediaPlayer.setSurface(mMediaPlayer, mSurface);
+            } else {
+                throw new UnsupportedOperationException("MediaPlayer does not support "
+                        + "setSurface() on this version of the platform.");
+            }
+        } else if (mSurfaceHolder != null) {
+            mMediaPlayer.setDisplay(mSurfaceHolder);
+        } else {
+            mMediaPlayer.setDisplay(null);
+        }
+    }
+
+    protected abstract void updateSize();
+
+    private void reset() {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+        }
+        mMediaPlayer = new MediaPlayer();
+        mMediaPlayer.setOnPreparedListener(this);
+        mMediaPlayer.setOnCompletionListener(this);
+        mMediaPlayer.setOnErrorListener(this);
+        mMediaPlayer.setOnSeekCompleteListener(this);
+        updateSurface();
+        mState = STATE_IDLE;
+        mSeekToPos = 0;
+    }
+
+    private void updateVideoRect() {
+        if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) {
+            int width = mMediaPlayer.getVideoWidth();
+            int height = mMediaPlayer.getVideoHeight();
+            if (width > 0 && height > 0) {
+                mVideoWidth = width;
+                mVideoHeight = height;
+                updateSize();
+            } else {
+                Log.e(TAG, "video rect is 0x0!");
+                mVideoWidth = mVideoHeight = 0;
+            }
+        }
+    }
+
+    private static final class ICSMediaPlayer {
+        public static final void setSurface(MediaPlayer player, Surface surface) {
+            player.setSurface(surface);
+        }
+    }
+
+    /**
+     * Handles playback of a single media item using MediaPlayer in SurfaceView
+     */
+    public static class SurfaceViewPlayer extends LocalPlayer implements
+            SurfaceHolder.Callback {
+        private static final String TAG = "SurfaceViewPlayer";
+        private RouteInfo mRoute;
+        private final SurfaceView mSurfaceView;
+        private final FrameLayout mLayout;
+        private DemoPresentation mPresentation;
+
+        public SurfaceViewPlayer(Context context) {
+            super(context);
+
+            mLayout = (FrameLayout)((Activity)context).findViewById(R.id.player);
+            mSurfaceView = (SurfaceView)((Activity)context).findViewById(R.id.surface_view);
+
+            // add surface holder callback
+            SurfaceHolder holder = mSurfaceView.getHolder();
+            holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+            holder.addCallback(this);
+        }
+
+        @Override
+        public void connect(RouteInfo route) {
+            super.connect(route);
+            mRoute = route;
+        }
+
+        @Override
+        public void release() {
+            super.release();
+
+            // dismiss presentation display
+            if (mPresentation != null) {
+                Log.i(TAG, "Dismissing presentation because the activity is no longer visible.");
+                mPresentation.dismiss();
+                mPresentation = null;
+            }
+
+            // remove surface holder callback
+            SurfaceHolder holder = mSurfaceView.getHolder();
+            holder.removeCallback(this);
+
+            // hide the surface view when SurfaceViewPlayer is destroyed
+            mSurfaceView.setVisibility(View.GONE);
+            mLayout.setVisibility(View.GONE);
+        }
+
+        @Override
+        public void updatePresentation() {
+            // Get the current route and its presentation display.
+            Display presentationDisplay = mRoute != null ? mRoute.getPresentationDisplay() : null;
+
+            // Dismiss the current presentation if the display has changed.
+            if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) {
+                Log.i(TAG, "Dismissing presentation because the current route no longer "
+                        + "has a presentation display.");
+                mPresentation.dismiss();
+                mPresentation = null;
+            }
+
+            // Show a new presentation if needed.
+            if (mPresentation == null && presentationDisplay != null) {
+                Log.i(TAG, "Showing presentation on display: " + presentationDisplay);
+                mPresentation = new DemoPresentation(getContext(), presentationDisplay);
+                mPresentation.setOnDismissListener(mOnDismissListener);
+                try {
+                    mPresentation.show();
+                } catch (WindowManager.InvalidDisplayException ex) {
+                    Log.w(TAG, "Couldn't show presentation!  Display was removed in "
+                              + "the meantime.", ex);
+                    mPresentation = null;
+                }
+            }
+
+            updateContents();
+        }
+
+        // SurfaceHolder.Callback
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format,
+                int width, int height) {
+            if (DEBUG) {
+                Log.d(TAG, "surfaceChanged: " + width + "x" + height);
+            }
+            setSurface(holder);
+        }
+
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            if (DEBUG) {
+                Log.d(TAG, "surfaceCreated");
+            }
+            setSurface(holder);
+            updateSize();
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            if (DEBUG) {
+                Log.d(TAG, "surfaceDestroyed");
+            }
+            removeSurface(holder);
+        }
+
+        @Override
+        protected void updateSize() {
+            int width = getVideoWidth();
+            int height = getVideoHeight();
+            if (width > 0 && height > 0) {
+                if (mPresentation == null) {
+                    int surfaceWidth = mLayout.getWidth();
+                    int surfaceHeight = mLayout.getHeight();
+
+                    // Calculate the new size of mSurfaceView, so that video is centered
+                    // inside the framelayout with proper letterboxing/pillarboxing
+                    ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
+                    if (surfaceWidth * height < surfaceHeight * width) {
+                        // Black bars on top&bottom, mSurfaceView has full layout width,
+                        // while height is derived from video's aspect ratio
+                        lp.width = surfaceWidth;
+                        lp.height = surfaceWidth * height / width;
+                    } else {
+                        // Black bars on left&right, mSurfaceView has full layout height,
+                        // while width is derived from video's aspect ratio
+                        lp.width = surfaceHeight * width / height;
+                        lp.height = surfaceHeight;
+                    }
+                    Log.i(TAG, "video rect is " + lp.width + "x" + lp.height);
+                    mSurfaceView.setLayoutParams(lp);
+                } else {
+                    mPresentation.updateSize(width, height);
+                }
+            }
+        }
+
+        private void updateContents() {
+            // Show either the content in the main activity or the content in the presentation
+            if (mPresentation != null) {
+                mLayout.setVisibility(View.GONE);
+                mSurfaceView.setVisibility(View.GONE);
+            } else {
+                mLayout.setVisibility(View.VISIBLE);
+                mSurfaceView.setVisibility(View.VISIBLE);
+            }
+        }
+
+        // Listens for when presentations are dismissed.
+        private final DialogInterface.OnDismissListener mOnDismissListener =
+                new DialogInterface.OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                if (dialog == mPresentation) {
+                    Log.i(TAG, "Presentation dismissed.");
+                    mPresentation = null;
+                    updateContents();
+                }
+            }
+        };
+
+        // Presentation
+        private final class DemoPresentation extends Presentation {
+            private SurfaceView mPresentationSurfaceView;
+
+            public DemoPresentation(Context context, Display display) {
+                super(context, display);
+            }
+
+            @Override
+            protected void onCreate(Bundle savedInstanceState) {
+                // Be sure to call the super class.
+                super.onCreate(savedInstanceState);
+
+                // Inflate the layout.
+                setContentView(R.layout.sample_media_router_presentation);
+
+                // Set up the surface view.
+                mPresentationSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
+                SurfaceHolder holder = mPresentationSurfaceView.getHolder();
+                holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+                holder.addCallback(SurfaceViewPlayer.this);
+                Log.i(TAG, "Presentation created");
+            }
+
+            public void updateSize(int width, int height) {
+                int surfaceHeight = getWindow().getDecorView().getHeight();
+                int surfaceWidth = getWindow().getDecorView().getWidth();
+                ViewGroup.LayoutParams lp = mPresentationSurfaceView.getLayoutParams();
+                if (surfaceWidth * height < surfaceHeight * width) {
+                    lp.width = surfaceWidth;
+                    lp.height = surfaceWidth * height / width;
+                } else {
+                    lp.width = surfaceHeight * width / height;
+                    lp.height = surfaceHeight;
+                }
+                Log.i(TAG, "Presentation video rect is " + lp.width + "x" + lp.height);
+                mPresentationSurfaceView.setLayoutParams(lp);
+            }
+        }
+    }
+
+    /**
+     * Handles playback of a single media item using MediaPlayer in
+     * OverlayDisplayWindow.
+     */
+    public static class OverlayPlayer extends LocalPlayer implements
+            OverlayDisplayWindow.OverlayWindowListener {
+        private static final String TAG = "OverlayPlayer";
+        private final OverlayDisplayWindow mOverlay;
+
+        public OverlayPlayer(Context context) {
+            super(context);
+
+            mOverlay = OverlayDisplayWindow.create(getContext(),
+                    getContext().getResources().getString(
+                            R.string.sample_media_route_provider_remote),
+                    1024, 768, Gravity.CENTER);
+
+            mOverlay.setOverlayWindowListener(this);
+        }
+
+        @Override
+        public void connect(RouteInfo route) {
+            super.connect(route);
+            mOverlay.show();
+        }
+
+        @Override
+        public void release() {
+            super.release();
+            mOverlay.dismiss();
+        }
+
+        @Override
+        protected void updateSize() {
+            int width = getVideoWidth();
+            int height = getVideoHeight();
+            if (width > 0 && height > 0) {
+                mOverlay.updateAspectRatio(width, height);
+            }
+        }
+
+        // OverlayDisplayWindow.OverlayWindowListener
+        @Override
+        public void onWindowCreated(Surface surface) {
+            setSurface(surface);
+        }
+
+        @Override
+        public void onWindowCreated(SurfaceHolder surfaceHolder) {
+            setSurface(surfaceHolder);
+        }
+
+        @Override
+        public void onWindowDestroyed() {
+            setSurface((SurfaceHolder)null);
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaPlayerWrapper.java b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaPlayerWrapper.java
deleted file mode 100644
index 8b82631..0000000
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaPlayerWrapper.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2013 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.example.android.supportv7.media;
-
-import android.content.Context;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.util.Log;
-import android.media.MediaPlayer;
-import android.view.Surface;
-import android.view.SurfaceHolder;
-import java.io.IOException;
-
-/**
- * MediaPlayerWrapper handles playback of a single media item, and is used for
- * both local and remote playback.
- */
-public class MediaPlayerWrapper implements
-        MediaPlayer.OnPreparedListener,
-        MediaPlayer.OnCompletionListener,
-        MediaPlayer.OnErrorListener,
-        MediaPlayer.OnSeekCompleteListener,
-        MediaSessionManager.Callback {
-    private static final String TAG = "MediaPlayerWrapper";
-    private static final boolean DEBUG = false;
-
-    private static final int STATE_IDLE = 0;
-    private static final int STATE_PLAY_PENDING = 1;
-    private static final int STATE_READY = 2;
-    private static final int STATE_PLAYING = 3;
-    private static final int STATE_PAUSED = 4;
-
-    private final Context mContext;
-    private final Handler mHandler = new Handler();
-    private MediaPlayer mMediaPlayer;
-    private int mState = STATE_IDLE;
-    private Callback mCallback;
-    private Surface mSurface;
-    private SurfaceHolder mSurfaceHolder;
-    private int mSeekToPos;
-
-    public MediaPlayerWrapper(Context context) {
-        mContext = context;
-        reset();
-    }
-
-    public void release() {
-        onStop();
-        mMediaPlayer.release();
-    }
-
-    public void setCallback(Callback cb) {
-        mCallback = cb;
-    }
-
-    // MediaSessionManager.Callback
-    @Override
-    public void onNewItem(Uri uri) {
-        reset();
-        try {
-            mMediaPlayer.setDataSource(mContext, uri);
-            mMediaPlayer.prepareAsync();
-        } catch (IllegalStateException e) {
-            Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + uri);
-        } catch (IOException e) {
-            Log.e(TAG, "MediaPlayer throws IOException, uri=" + uri);
-        } catch (IllegalArgumentException e) {
-            Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + uri);
-        } catch (SecurityException e) {
-            Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + uri);
-        }
-    }
-
-    @Override
-    public void onStart() {
-        if (mState == STATE_READY || mState == STATE_PAUSED) {
-            mMediaPlayer.start();
-            mState = STATE_PLAYING;
-        } else if (mState == STATE_IDLE){
-            mState = STATE_PLAY_PENDING;
-        }
-    }
-
-    @Override
-    public void onStop() {
-        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
-            mMediaPlayer.stop();
-            mState = STATE_IDLE;
-        }
-    }
-
-    @Override
-    public void onPause() {
-        if (mState == STATE_PLAYING) {
-            mMediaPlayer.pause();
-            mState = STATE_PAUSED;
-        }
-    }
-
-    @Override
-    public void onSeek(long pos) {
-        if (DEBUG) {
-            Log.d(TAG, "onSeek: pos=" + pos);
-        }
-        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
-            mMediaPlayer.seekTo((int)pos);
-            mSeekToPos = (int)pos;
-        } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) {
-            // Seek before onPrepared() arrives,
-            // need to performed delayed seek in onPrepared()
-            mSeekToPos = (int)pos;
-        }
-    }
-
-    @Override
-    public void onGetStatus(MediaQueueItem item) {
-        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
-            // use mSeekToPos if we're currently seeking (mSeekToPos is reset
-            // when seeking is completed)
-            item.setContentDuration(mMediaPlayer.getDuration());
-            item.setContentPosition(mSeekToPos > 0 ?
-                    mSeekToPos : mMediaPlayer.getCurrentPosition());
-        }
-    }
-
-    public void setSurface(Surface surface) {
-        mSurface = surface;
-        mSurfaceHolder = null;
-        updateSurface();
-    }
-
-    public void setSurface(SurfaceHolder surfaceHolder) {
-        mSurface = null;
-        mSurfaceHolder = surfaceHolder;
-        updateSurface();
-    }
-
-    //MediaPlayer Listeners
-    @Override
-    public void onPrepared(MediaPlayer mp) {
-        if (DEBUG) {
-            Log.d(TAG,"onPrepared");
-        }
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (mState == STATE_IDLE) {
-                    mState = STATE_READY;
-                    updateVideoRect();
-                } else if (mState == STATE_PLAY_PENDING) {
-                    mState = STATE_PLAYING;
-                    updateVideoRect();
-                    if (mSeekToPos > 0) {
-                        Log.d(TAG, "Seeking to initial pos " + mSeekToPos);
-                        mMediaPlayer.seekTo((int)mSeekToPos);
-                    }
-                    mMediaPlayer.start();
-                }
-                if (mCallback != null) {
-                    mCallback.onStatusChanged();
-                }
-            }
-        });
-    }
-
-    @Override
-    public void onCompletion(MediaPlayer mp) {
-        if (DEBUG) {
-            Log.d(TAG,"onCompletion");
-        }
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (mCallback != null) {
-                    mCallback.onCompletion();
-                }
-            }
-        });
-    }
-
-    @Override
-    public boolean onError(MediaPlayer mp, int what, int extra) {
-        if (DEBUG) {
-            Log.d(TAG,"onError");
-        }
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (mCallback != null) {
-                    mCallback.onError();
-                }
-            }
-        });
-        // return true so that onCompletion is not called
-        return true;
-    }
-
-    @Override
-    public void onSeekComplete(MediaPlayer mp) {
-        if (DEBUG) {
-            Log.d(TAG, "onSeekComplete");
-        }
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mSeekToPos = 0;
-                if (mCallback != null) {
-                    mCallback.onStatusChanged();
-                }
-            }
-        });
-    }
-
-    public void reset() {
-        if (mMediaPlayer != null) {
-            mMediaPlayer.stop();
-            mMediaPlayer.release();
-            mMediaPlayer = null;
-        }
-        mMediaPlayer = new MediaPlayer();
-        mMediaPlayer.setOnPreparedListener(this);
-        mMediaPlayer.setOnCompletionListener(this);
-        mMediaPlayer.setOnErrorListener(this);
-        mMediaPlayer.setOnSeekCompleteListener(this);
-        updateSurface();
-        mState = STATE_IDLE;
-        mSeekToPos = 0;
-    }
-
-    private void updateVideoRect() {
-        if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) {
-            int videoWidth = mMediaPlayer.getVideoWidth();
-            int videoHeight = mMediaPlayer.getVideoHeight();
-            if (videoWidth > 0 && videoHeight > 0) {
-                if (mCallback != null) {
-                    mCallback.onSizeChanged(videoWidth, videoHeight);
-                }
-            } else {
-                Log.e(TAG, "video rect is 0x0!");
-            }
-        }
-    }
-
-    private void updateSurface() {
-        if (mSurface != null) {
-            // The setSurface API does not exist until V14+.
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
-                ICSMediaPlayer.setSurface(mMediaPlayer, mSurface);
-            } else {
-                throw new UnsupportedOperationException("MediaPlayer does not support "
-                        + "setSurface() on this version of the platform.");
-            }
-        } else if (mSurfaceHolder != null) {
-            mMediaPlayer.setDisplay(mSurfaceHolder);
-        } else {
-            mMediaPlayer.setDisplay(null);
-        }
-    }
-
-    public static abstract class Callback {
-        public void onError() {}
-        public void onCompletion() {}
-        public void onStatusChanged() {}
-        public void onSizeChanged(int width, int height) {}
-    }
-
-    private static final class ICSMediaPlayer {
-        public static final void setSurface(MediaPlayer player, Surface surface) {
-            player.setSurface(surface);
-        }
-    }
-}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaSessionManager.java b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaSessionManager.java
deleted file mode 100644
index 9258998..0000000
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaSessionManager.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright (C) 2013 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.example.android.supportv7.media;
-
-import java.util.List;
-import java.util.ArrayList;
-import android.util.Log;
-import android.net.Uri;
-import android.app.PendingIntent;
-import android.support.v7.media.MediaItemStatus;
-
-/**
- * MediaSessionManager manages a media session as a queue. It supports common
- * queuing behaviors such as enqueue/remove of media items, pause/resume/stop,
- * etc.
- *
- * Actual playback of a single media item is abstracted into a set of
- * callbacks MediaSessionManager.Callback, and is handled outside this class.
- */
-public class MediaSessionManager {
-    private static final String TAG = "MediaSessionManager";
-
-    private String mSessionId;
-    private String mItemId;
-    private String mCurItemId;
-    private boolean mIsPlaying = true;
-    private Callback mCallback;
-    private List<MediaQueueItem> mQueue = new ArrayList<MediaQueueItem>();
-
-    public MediaSessionManager() {
-    }
-
-    // Queue item (this maps to the ENQUEUE in the API which queues the item)
-    public MediaQueueItem enqueue(String sid, Uri uri, PendingIntent receiver) {
-        // fail if queue id is invalid
-        if (sid != null && !sid.equals(mSessionId)) {
-            Log.d(TAG, "invalid session id, mSessionId="+mSessionId+", sid="+sid);
-            return null;
-        }
-
-        // if queue id is unspecified, invalidate current queue
-        if (sid == null) {
-            invalidate();
-        }
-
-        mQueue.add(new MediaQueueItem(mSessionId, mItemId, uri, receiver));
-
-        if (updatePlaybackState()) {
-            MediaQueueItem item = findItem(mItemId);
-            mItemId = inc(mItemId);
-            if (item == null) {
-                Log.d(TAG, "item not found after it's added");
-            }
-            return item;
-        }
-
-        removeItem(mItemId, MediaItemStatus.PLAYBACK_STATE_ERROR);
-        return null;
-    }
-
-    public MediaQueueItem remove(String sid, String iid) {
-        if (sid == null || !sid.equals(mSessionId)) {
-            return null;
-        }
-        return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED);
-    }
-
-    // handles ERROR / COMPLETION
-    public MediaQueueItem finish(boolean error) {
-        return removeItem(mCurItemId, error ? MediaItemStatus.PLAYBACK_STATE_ERROR :
-                MediaItemStatus.PLAYBACK_STATE_FINISHED);
-    }
-
-    public MediaQueueItem seek(String sid, String iid, long pos) {
-        if (sid == null || !sid.equals(mSessionId)) {
-            return null;
-        }
-        for (int i = 0; i < mQueue.size(); i++) {
-            MediaQueueItem item = mQueue.get(i);
-            if (iid.equals(item.getItemId())) {
-                if (pos != item.getContentPosition()) {
-                    item.setContentPosition(pos);
-                    if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
-                            || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
-                        if (mCallback != null) {
-                            mCallback.onSeek(pos);
-                        }
-                    }
-                }
-                return item;
-            }
-        }
-        return null;
-    }
-
-    public MediaQueueItem getCurrentItem() {
-        return getStatus(mSessionId, mCurItemId);
-    }
-
-    public MediaQueueItem getStatus(String sid, String iid) {
-        if (sid == null || !sid.equals(mSessionId)) {
-            return null;
-        }
-        for (int i = 0; i < mQueue.size(); i++) {
-            MediaQueueItem item = mQueue.get(i);
-            if (iid.equals(item.getItemId())) {
-                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
-                        || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
-                    if (mCallback != null) {
-                        mCallback.onGetStatus(item);
-                    }
-                }
-                return item;
-            }
-        }
-        return null;
-    }
-
-    public boolean pause(String sid) {
-        if (sid == null || !sid.equals(mSessionId)) {
-            return false;
-        }
-        mIsPlaying = false;
-        return updatePlaybackState();
-    }
-
-    public boolean resume(String sid) {
-        if (sid == null || !sid.equals(mSessionId)) {
-            return false;
-        }
-        mIsPlaying = true;
-        return updatePlaybackState();
-    }
-
-    public boolean stop(String sid) {
-        if (sid == null || !sid.equals(mSessionId)) {
-            return false;
-        }
-        clear();
-        return true;
-    }
-
-    public void setCallback(Callback cb) {
-        mCallback = cb;
-    }
-
-    @Override
-    public String toString() {
-        String result = "Media Queue: ";
-        if (!mQueue.isEmpty()) {
-            for (MediaQueueItem item : mQueue) {
-                result += "\n" + item.toString();
-            }
-        } else {
-            result += "<empty>";
-        }
-        return result;
-    }
-
-    private String inc(String id) {
-        return (id == null) ? "0" : Integer.toString(Integer.parseInt(id)+1);
-    }
-
-    // play the item at queue head
-    private void play() {
-        MediaQueueItem item = mQueue.get(0);
-        if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING
-                || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
-            mCurItemId = item.getItemId();
-            if (mCallback != null) {
-                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
-                    mCallback.onNewItem(item.getUri());
-                }
-                mCallback.onStart();
-            }
-            item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING);
-        }
-    }
-
-    // stop the currently playing item
-    private void stop() {
-        MediaQueueItem item = mQueue.get(0);
-        if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
-                || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
-            if (mCallback != null) {
-                mCallback.onStop();
-            }
-            item.setState(MediaItemStatus.PLAYBACK_STATE_FINISHED);
-        }
-    }
-
-    // pause the currently playing item
-    private void pause() {
-        MediaQueueItem item = mQueue.get(0);
-        if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING
-                || item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
-            if (mCallback != null) {
-                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
-                    mCallback.onNewItem(item.getUri());
-                } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
-                    mCallback.onPause();
-                }
-            }
-            item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED);
-        }
-    }
-
-    private void clear() {
-        if (mQueue.size() > 0) {
-            stop();
-            mQueue.clear();
-        }
-    }
-
-    private void invalidate() {
-        clear();
-        mSessionId = inc(mSessionId);
-        mItemId = "0";
-        mIsPlaying = true;
-    }
-
-    private boolean updatePlaybackState() {
-        if (mQueue.isEmpty()) {
-            return true;
-        }
-
-        if (mIsPlaying) {
-            play();
-        } else {
-            pause();
-        }
-        return true;
-    }
-
-    private MediaQueueItem findItem(String iid) {
-        for (MediaQueueItem item : mQueue) {
-            if (iid.equals(item.getItemId())) {
-                return item;
-            }
-        }
-        return null;
-    }
-
-    private MediaQueueItem removeItem(String iid, int state) {
-        List<MediaQueueItem> queue =
-                new ArrayList<MediaQueueItem>(mQueue.size());
-        MediaQueueItem found = null;
-        for (MediaQueueItem item : mQueue) {
-            if (iid.equals(item.getItemId())) {
-                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
-                        || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
-                    stop();
-                }
-                item.setState(state);
-                found = item;
-            } else {
-                queue.add(item);
-            }
-        }
-        if (found != null) {
-            mQueue = queue;
-            updatePlaybackState();
-        }
-        return found;
-    }
-
-    public interface Callback {
-        public void onStart();
-        public void onPause();
-        public void onStop();
-        public void onSeek(long pos);
-        public void onGetStatus(MediaQueueItem item);
-        public void onNewItem(Uri uri);
-    }
-}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/Player.java b/samples/Support7Demos/src/com/example/android/supportv7/media/Player.java
new file mode 100644
index 0000000..b230c1d
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/Player.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2013 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.example.android.supportv7.media;
+
+import android.net.Uri;
+import android.content.Context;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouter.RouteInfo;
+
+/**
+ * Abstraction of common playback operations of media items, such as play,
+ * seek, etc. Used by PlaybackManager as a backend to handle actual playback
+ * of media items.
+ */
+public abstract class Player {
+    protected Callback mCallback;
+
+    public abstract boolean isRemotePlayback();
+    public abstract boolean isQueuingSupported();
+
+    public abstract void connect(RouteInfo route);
+    public abstract void release();
+
+    // basic operations that are always supported
+    public abstract void play(final PlaylistItem item);
+    public abstract void seek(final PlaylistItem item);
+    public abstract void getStatus(final PlaylistItem item, final boolean update);
+    public abstract void pause();
+    public abstract void resume();
+    public abstract void stop();
+
+    // advanced queuing (enqueue & remove) are only supported
+    // if isQueuingSupported() returns true
+    public abstract void enqueue(final PlaylistItem item);
+    public abstract PlaylistItem remove(String iid);
+
+    // route statistics
+    public void updateStatistics() {}
+    public String getStatistics() { return ""; }
+
+    // presentation display
+    public void updatePresentation() {}
+
+    public void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    public static Player create(Context context, RouteInfo route) {
+        Player player;
+        if (route != null && route.supportsControlCategory(
+                MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+            player = new RemotePlayer(context);
+        } else if (route != null) {
+            player = new LocalPlayer.SurfaceViewPlayer(context);
+        } else {
+            player = new LocalPlayer.OverlayPlayer(context);
+        }
+        player.connect(route);
+        return player;
+    }
+
+    public interface Callback {
+        void onError();
+        void onCompletion();
+        void onPlaylistChanged();
+        void onPlaylistReady();
+    }
+}
\ No newline at end of file
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaQueueItem.java b/samples/Support7Demos/src/com/example/android/supportv7/media/PlaylistItem.java
similarity index 72%
rename from samples/Support7Demos/src/com/example/android/supportv7/media/MediaQueueItem.java
rename to samples/Support7Demos/src/com/example/android/supportv7/media/PlaylistItem.java
index 5440d86..9a12d59 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaQueueItem.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/PlaylistItem.java
@@ -16,40 +16,54 @@
 
 package com.example.android.supportv7.media;
 
-import android.support.v7.media.MediaItemStatus;
-import android.net.Uri;
 import android.app.PendingIntent;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.support.v7.media.MediaItemStatus;
 
 /**
- * MediaQueueItem helps keep track of the current status of an media item.
+ * PlaylistItem helps keep track of the current status of an media item.
  */
-final class MediaQueueItem {
+final class PlaylistItem {
     // immutables
     private final String mSessionId;
     private final String mItemId;
     private final Uri mUri;
+    private final String mMime;
     private final PendingIntent mUpdateReceiver;
     // changeable states
     private int mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING;
     private long mContentPosition;
     private long mContentDuration;
+    private long mTimestamp;
+    private String mRemoteItemId;
 
-    public MediaQueueItem(String qid, String iid, Uri uri, PendingIntent pi) {
+    public PlaylistItem(String qid, String iid, Uri uri, String mime, PendingIntent pi) {
         mSessionId = qid;
         mItemId = iid;
         mUri = uri;
+        mMime = mime;
         mUpdateReceiver = pi;
+        setTimestamp(SystemClock.elapsedRealtime());
+    }
+
+    public void setRemoteItemId(String riid) {
+        mRemoteItemId = riid;
     }
 
     public void setState(int state) {
         mPlaybackState = state;
     }
 
-    public void setContentPosition(long pos) {
+    public void setPosition(long pos) {
         mContentPosition = pos;
     }
 
-    public void setContentDuration(long duration) {
+    public void setTimestamp(long ts) {
+        mTimestamp = ts;
+    }
+
+    public void setDuration(long duration) {
         mContentDuration = duration;
     }
 
@@ -61,6 +75,10 @@
         return mItemId;
     }
 
+    public String getRemoteItemId() {
+        return mRemoteItemId;
+    }
+
     public Uri getUri() {
         return mUri;
     }
@@ -73,18 +91,23 @@
         return mPlaybackState;
     }
 
-    public long getContentPosition() {
+    public long getPosition() {
         return mContentPosition;
     }
 
-    public long getContentDuration() {
+    public long getDuration() {
         return mContentDuration;
     }
 
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
     public MediaItemStatus getStatus() {
         return new MediaItemStatus.Builder(mPlaybackState)
             .setContentPosition(mContentPosition)
             .setContentDuration(mContentDuration)
+            .setTimestamp(mTimestamp)
             .build();
     }
 
@@ -101,6 +124,7 @@
             "ERROR"
         };
         return "[" + mSessionId + "|" + mItemId + "|"
+            + (mRemoteItemId != null ? mRemoteItemId : "-") + "|"
             + state[mPlaybackState] + "] " + mUri.toString();
     }
 }
\ No newline at end of file
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/RemotePlayer.java b/samples/Support7Demos/src/com/example/android/supportv7/media/RemotePlayer.java
new file mode 100644
index 0000000..d25f6b3
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/RemotePlayer.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2013 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.example.android.supportv7.media;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouter.ControlRequestCallback;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.support.v7.media.MediaSessionStatus;
+import android.support.v7.media.RemotePlaybackClient;
+import android.support.v7.media.RemotePlaybackClient.ItemActionCallback;
+import android.support.v7.media.RemotePlaybackClient.SessionActionCallback;
+import android.support.v7.media.RemotePlaybackClient.StatusCallback;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles playback of media items using a remote route.
+ *
+ * This class is used as a backend by PlaybackManager to feed media items to
+ * the remote route. When the remote route doesn't support queuing, media items
+ * are fed one-at-a-time; otherwise media items are enqueued to the remote side.
+ */
+public class RemotePlayer extends Player {
+    private static final String TAG = "RemotePlayer";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private Context mContext;
+    private RouteInfo mRoute;
+    private boolean mEnqueuePending;
+    private String mStatsInfo = "";
+    private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>();
+
+    private RemotePlaybackClient mClient;
+    private StatusCallback mStatusCallback = new StatusCallback() {
+        @Override
+        public void onItemStatusChanged(Bundle data,
+                String sessionId, MediaSessionStatus sessionStatus,
+                String itemId, MediaItemStatus itemStatus) {
+            logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus);
+            if (mCallback != null) {
+                if (itemStatus.getPlaybackState() ==
+                        MediaItemStatus.PLAYBACK_STATE_FINISHED) {
+                    mCallback.onCompletion();
+                } else if (itemStatus.getPlaybackState() ==
+                        MediaItemStatus.PLAYBACK_STATE_ERROR) {
+                    mCallback.onError();
+                }
+            }
+        }
+
+        @Override
+        public void onSessionStatusChanged(Bundle data,
+                String sessionId, MediaSessionStatus sessionStatus) {
+            logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null);
+            if (mCallback != null) {
+                mCallback.onPlaylistChanged();
+            }
+        }
+
+        @Override
+        public void onSessionChanged(String sessionId) {
+            if (DEBUG) {
+                Log.d(TAG, "onSessionChanged: sessionId=" + sessionId);
+            }
+        }
+    };
+
+    public RemotePlayer(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public boolean isRemotePlayback() {
+        return true;
+    }
+
+    @Override
+    public boolean isQueuingSupported() {
+        return mClient.isQueuingSupported();
+    }
+
+    @Override
+    public void connect(RouteInfo route) {
+        mRoute = route;
+        mClient = new RemotePlaybackClient(mContext, route);
+        mClient.setStatusCallback(mStatusCallback);
+
+        if (DEBUG) {
+            Log.d(TAG, "connected to: " + route
+                    + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported()
+                    + ", isQueuingSupported: "+ mClient.isQueuingSupported());
+        }
+    }
+
+    @Override
+    public void release() {
+        mClient.release();
+
+        if (DEBUG) {
+            Log.d(TAG, "released.");
+        }
+    }
+
+    // basic playback operations that are always supported
+    @Override
+    public void play(final PlaylistItem item) {
+        if (DEBUG) {
+            Log.d(TAG, "play: item=" + item);
+        }
+        mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+                item.setRemoteItemId(itemId);
+                if (item.getPosition() > 0) {
+                    seekInternal(item);
+                }
+                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                    pause();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("play: failed", error, code);
+            }
+        });
+    }
+
+    @Override
+    public void seek(final PlaylistItem item) {
+        seekInternal(item);
+    }
+
+    @Override
+    public void getStatus(final PlaylistItem item, final boolean update) {
+        if (!mClient.hasSession() || item.getRemoteItemId() == null) {
+            // if session is not valid or item id not assigend yet.
+            // just return, it's not fatal
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "getStatus: item=" + item + ", update=" + update);
+        }
+        mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+                int state = itemStatus.getPlaybackState();
+                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED
+                        || state == MediaItemStatus.PLAYBACK_STATE_PENDING) {
+                    item.setState(state);
+                    item.setPosition(itemStatus.getContentPosition());
+                    item.setDuration(itemStatus.getContentDuration());
+                    item.setTimestamp(itemStatus.getTimestamp());
+                }
+                if (update && mCallback != null) {
+                    mCallback.onPlaylistReady();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("getStatus: failed", error, code);
+                if (update && mCallback != null) {
+                    mCallback.onPlaylistReady();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void pause() {
+        if (!mClient.hasSession()) {
+            // ignore if no session
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "pause");
+        }
+        mClient.pause(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("pause: succeeded", sessionId, sessionStatus, null, null);
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("pause: failed", error, code);
+            }
+        });
+    }
+
+    @Override
+    public void resume() {
+        if (!mClient.hasSession()) {
+            // ignore if no session
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "resume");
+        }
+        mClient.resume(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("resume: succeeded", sessionId, sessionStatus, null, null);
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("resume: failed", error, code);
+            }
+        });
+    }
+
+    @Override
+    public void stop() {
+        if (!mClient.hasSession()) {
+            // ignore if no session
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "stop");
+        }
+        mClient.stop(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("stop: succeeded", sessionId, sessionStatus, null, null);
+                if (mClient.isSessionManagementSupported()) {
+                    endSession();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("stop: failed", error, code);
+            }
+        });
+    }
+
+    // enqueue & remove are only supported if isQueuingSupported() returns true
+    @Override
+    public void enqueue(final PlaylistItem item) {
+        throwIfQueuingUnsupported();
+
+        if (!mClient.hasSession() && !mEnqueuePending) {
+            mEnqueuePending = true;
+            if (mClient.isSessionManagementSupported()) {
+                startSession(item);
+            } else {
+                enqueueInternal(item);
+            }
+        } else if (mEnqueuePending){
+            mTempQueue.add(item);
+        } else {
+            enqueueInternal(item);
+        }
+    }
+
+    @Override
+    public PlaylistItem remove(String itemId) {
+        throwIfNoSession();
+        throwIfQueuingUnsupported();
+
+        if (DEBUG) {
+            Log.d(TAG, "remove: itemId=" + itemId);
+        }
+        mClient.remove(itemId, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("remove: failed", error, code);
+            }
+        });
+
+        return null;
+    }
+
+    @Override
+    public void updateStatistics() {
+        // clear stats info first
+        mStatsInfo = "";
+
+        Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS);
+        intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
+
+        if (mRoute != null && mRoute.supportsControlRequest(intent)) {
+            ControlRequestCallback callback = new ControlRequestCallback() {
+                @Override
+                public void onResult(Bundle data) {
+                    if (DEBUG) {
+                        Log.d(TAG, "getStatistics: succeeded: data=" + data);
+                    }
+                    if (data != null) {
+                        int playbackCount = data.getInt(
+                                SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1);
+                        mStatsInfo = "Total playback count: " + playbackCount;
+                    }
+                }
+
+                @Override
+                public void onError(String error, Bundle data) {
+                    Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data);
+                }
+            };
+
+            mRoute.sendControlRequest(intent, callback);
+        }
+    }
+
+    @Override
+    public String getStatistics() {
+        return mStatsInfo;
+    }
+
+    private void enqueueInternal(final PlaylistItem item) {
+        throwIfQueuingUnsupported();
+
+        if (DEBUG) {
+            Log.d(TAG, "enqueue: item=" + item);
+        }
+        mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                    String itemId, MediaItemStatus itemStatus) {
+                logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+                item.setRemoteItemId(itemId);
+                if (item.getPosition() > 0) {
+                    seekInternal(item);
+                }
+                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                    pause();
+                }
+                if (mEnqueuePending) {
+                    mEnqueuePending = false;
+                    for (PlaylistItem item : mTempQueue) {
+                        enqueueInternal(item);
+                    }
+                    mTempQueue.clear();
+                }
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("enqueue: failed", error, code);
+                if (mCallback != null) {
+                    mCallback.onPlaylistChanged();
+                }
+            }
+        });
+    }
+
+    private void seekInternal(final PlaylistItem item) {
+        throwIfNoSession();
+
+        if (DEBUG) {
+            Log.d(TAG, "seek: item=" + item);
+        }
+        mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() {
+           @Override
+           public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+                   String itemId, MediaItemStatus itemStatus) {
+               logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus);
+               if (mCallback != null) {
+                   mCallback.onPlaylistChanged();
+               }
+           }
+
+           @Override
+           public void onError(String error, int code, Bundle data) {
+               logError("seek: failed", error, code);
+           }
+        });
+    }
+
+    private void startSession(final PlaylistItem item) {
+        mClient.startSession(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("startSession: succeeded", sessionId, sessionStatus, null, null);
+                enqueueInternal(item);
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("startSession: failed", error, code);
+            }
+        });
+    }
+
+    private void endSession() {
+        mClient.endSession(null, new SessionActionCallback() {
+            @Override
+            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+                logStatus("endSession: succeeded", sessionId, sessionStatus, null, null);
+            }
+
+            @Override
+            public void onError(String error, int code, Bundle data) {
+                logError("endSession: failed", error, code);
+            }
+        });
+    }
+
+    private void logStatus(String message,
+            String sessionId, MediaSessionStatus sessionStatus,
+            String itemId, MediaItemStatus itemStatus) {
+        if (DEBUG) {
+            String result = "";
+            if (sessionId != null && sessionStatus != null) {
+                result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus;
+            }
+            if (itemId != null & itemStatus != null) {
+                result += (result.isEmpty() ? "" : ", ")
+                        + "itemId=" + itemId + ", itemStatus=" + itemStatus;
+            }
+            Log.d(TAG, message + ": " + result);
+        }
+    }
+
+    private void logError(String message, String error, int code) {
+        Log.d(TAG, message + ": error=" + error + ", code=" + code);
+    }
+
+    private void throwIfNoSession() {
+        if (!mClient.hasSession()) {
+            throw new IllegalStateException("Session is invalid");
+        }
+    }
+
+    private void throwIfQueuingUnsupported() {
+        if (!isQueuingSupported()) {
+            throw new UnsupportedOperationException("Queuing is unsupported");
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
index 04416c4..c1cc3c0 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
@@ -34,6 +34,7 @@
 import android.support.v7.media.MediaRouter.ControlRequestCallback;
 import android.support.v7.media.MediaRouteProviderDescriptor;
 import android.support.v7.media.MediaRouteDescriptor;
+import android.support.v7.media.MediaSessionStatus;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.Surface;
@@ -50,7 +51,9 @@
     private static final String TAG = "SampleMediaRouteProvider";
 
     private static final String FIXED_VOLUME_ROUTE_ID = "fixed";
-    private static final String VARIABLE_VOLUME_ROUTE_ID = "variable";
+    private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic";
+    private static final String VARIABLE_VOLUME_QUEUING_ROUTE_ID = "variable_queuing";
+    private static final String VARIABLE_VOLUME_SESSION_ROUTE_ID = "variable_session";
     private static final int VOLUME_MAX = 10;
 
     /**
@@ -80,15 +83,10 @@
     public static final String DATA_PLAYBACK_COUNT =
             "com.example.android.supportv7.media.EXTRA_PLAYBACK_COUNT";
 
-    /*
-     * Set ENABLE_QUEUEING to true to test queuing on MRP. This will make
-     * MRP expose the following two experimental hidden APIs:
-     *     ACTION_ENQUEUE
-     *     ACTION_REMOVE
-     */
-    public static final boolean ENABLE_QUEUEING = false;
+    private static final ArrayList<IntentFilter> CONTROL_FILTERS_BASIC;
+    private static final ArrayList<IntentFilter> CONTROL_FILTERS_QUEUING;
+    private static final ArrayList<IntentFilter> CONTROL_FILTERS_SESSION;
 
-    private static final ArrayList<IntentFilter> CONTROL_FILTERS;
     static {
         IntentFilter f1 = new IntentFilter();
         f1.addCategory(CATEGORY_SAMPLE_ROUTE);
@@ -124,14 +122,25 @@
         f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
         f5.addAction(MediaControlIntent.ACTION_REMOVE);
 
-        CONTROL_FILTERS = new ArrayList<IntentFilter>();
-        CONTROL_FILTERS.add(f1);
-        CONTROL_FILTERS.add(f2);
-        CONTROL_FILTERS.add(f3);
-        if (ENABLE_QUEUEING) {
-            CONTROL_FILTERS.add(f4);
-            CONTROL_FILTERS.add(f5);
-        }
+        IntentFilter f6 = new IntentFilter();
+        f6.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        f6.addAction(MediaControlIntent.ACTION_START_SESSION);
+        f6.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+        f6.addAction(MediaControlIntent.ACTION_END_SESSION);
+
+        CONTROL_FILTERS_BASIC = new ArrayList<IntentFilter>();
+        CONTROL_FILTERS_BASIC.add(f1);
+        CONTROL_FILTERS_BASIC.add(f2);
+        CONTROL_FILTERS_BASIC.add(f3);
+
+        CONTROL_FILTERS_QUEUING =
+                new ArrayList<IntentFilter>(CONTROL_FILTERS_BASIC);
+        CONTROL_FILTERS_QUEUING.add(f4);
+        CONTROL_FILTERS_QUEUING.add(f5);
+
+        CONTROL_FILTERS_SESSION =
+                new ArrayList<IntentFilter>(CONTROL_FILTERS_QUEUING);
+        CONTROL_FILTERS_SESSION.add(f6);
     }
 
     private static void addDataTypeUnchecked(IntentFilter filter, String type) {
@@ -163,7 +172,7 @@
                 FIXED_VOLUME_ROUTE_ID,
                 r.getString(R.string.fixed_volume_route_name))
                 .setDescription(r.getString(R.string.sample_route_description))
-                .addControlFilters(CONTROL_FILTERS)
+                .addControlFilters(CONTROL_FILTERS_BASIC)
                 .setPlaybackStream(AudioManager.STREAM_MUSIC)
                 .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
                 .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED)
@@ -171,10 +180,34 @@
                 .build();
 
         MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder(
-                VARIABLE_VOLUME_ROUTE_ID,
-                r.getString(R.string.variable_volume_route_name))
+                VARIABLE_VOLUME_BASIC_ROUTE_ID,
+                r.getString(R.string.variable_volume_basic_route_name))
                 .setDescription(r.getString(R.string.sample_route_description))
-                .addControlFilters(CONTROL_FILTERS)
+                .addControlFilters(CONTROL_FILTERS_BASIC)
+                .setPlaybackStream(AudioManager.STREAM_MUSIC)
+                .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+                .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+                .setVolumeMax(VOLUME_MAX)
+                .setVolume(mVolume)
+                .build();
+
+        MediaRouteDescriptor routeDescriptor3 = new MediaRouteDescriptor.Builder(
+                VARIABLE_VOLUME_QUEUING_ROUTE_ID,
+                r.getString(R.string.variable_volume_queuing_route_name))
+                .setDescription(r.getString(R.string.sample_route_description))
+                .addControlFilters(CONTROL_FILTERS_QUEUING)
+                .setPlaybackStream(AudioManager.STREAM_MUSIC)
+                .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
+                .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+                .setVolumeMax(VOLUME_MAX)
+                .setVolume(mVolume)
+                .build();
+
+        MediaRouteDescriptor routeDescriptor4 = new MediaRouteDescriptor.Builder(
+                VARIABLE_VOLUME_SESSION_ROUTE_ID,
+                r.getString(R.string.variable_volume_session_route_name))
+                .setDescription(r.getString(R.string.sample_route_description))
+                .addControlFilters(CONTROL_FILTERS_SESSION)
                 .setPlaybackStream(AudioManager.STREAM_MUSIC)
                 .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
                 .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
@@ -186,70 +219,57 @@
                 new MediaRouteProviderDescriptor.Builder()
                 .addRoute(routeDescriptor1)
                 .addRoute(routeDescriptor2)
+                .addRoute(routeDescriptor3)
+                .addRoute(routeDescriptor4)
                 .build();
         setDescriptor(providerDescriptor);
     }
 
     private final class SampleRouteController extends MediaRouteProvider.RouteController {
         private final String mRouteId;
-        private final OverlayDisplayWindow mOverlay;
-        private final MediaPlayerWrapper mMediaPlayer;
-        private final MediaSessionManager mSessionManager;
+        private final SessionManager mSessionManager = new SessionManager("mrp");
+        private final Player mPlayer;
+        private PendingIntent mSessionReceiver;
 
         public SampleRouteController(String routeId) {
             mRouteId = routeId;
-            mMediaPlayer = new MediaPlayerWrapper(getContext());
-            mSessionManager = new MediaSessionManager();
-            mSessionManager.setCallback(mMediaPlayer);
-
-            // Create an overlay display window (used for simulating the remote playback only)
-            mOverlay = OverlayDisplayWindow.create(getContext(),
-                    getContext().getResources().getString(
-                            R.string.sample_media_route_provider_remote),
-                    1024, 768, Gravity.CENTER);
-            mOverlay.setOverlayWindowListener(new OverlayDisplayWindow.OverlayWindowListener() {
+            mPlayer = Player.create(getContext(), null);
+            mSessionManager.setPlayer(mPlayer);
+            mSessionManager.setCallback(new SessionManager.Callback() {
                 @Override
-                public void onWindowCreated(Surface surface) {
-                    mMediaPlayer.setSurface(surface);
+                public void onStatusChanged() {
                 }
 
                 @Override
-                public void onWindowCreated(SurfaceHolder surfaceHolder) {
-                    mMediaPlayer.setSurface(surfaceHolder);
-                }
-
-                @Override
-                public void onWindowDestroyed() {
+                public void onItemChanged(PlaylistItem item) {
+                    handleStatusChange(item);
                 }
             });
-
-            mMediaPlayer.setCallback(new MediaPlayerCallback());
             Log.d(TAG, mRouteId + ": Controller created");
         }
 
         @Override
         public void onRelease() {
             Log.d(TAG, mRouteId + ": Controller released");
-            mMediaPlayer.release();
+            mPlayer.release();
         }
 
         @Override
         public void onSelect() {
             Log.d(TAG, mRouteId + ": Selected");
-            mOverlay.show();
+            mPlayer.connect(null);
         }
 
         @Override
         public void onUnselect() {
             Log.d(TAG, mRouteId + ": Unselected");
-            mMediaPlayer.onStop();
-            mOverlay.dismiss();
+            mPlayer.release();
         }
 
         @Override
         public void onSetVolume(int volume) {
             Log.d(TAG, mRouteId + ": Set volume to " + volume);
-            if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
+            if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
                 setVolumeInternal(volume);
             }
         }
@@ -257,7 +277,7 @@
         @Override
         public void onUpdateVolume(int delta) {
             Log.d(TAG, mRouteId + ": Update volume by " + delta);
-            if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
+            if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
                 setVolumeInternal(mVolume + delta);
             }
         }
@@ -284,6 +304,12 @@
                     success = handleResume(intent, callback);
                 } else if (action.equals(MediaControlIntent.ACTION_STOP)) {
                     success = handleStop(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_START_SESSION)) {
+                    success = handleStartSession(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_GET_SESSION_STATUS)) {
+                    success = handleGetSessionStatus(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_END_SESSION)) {
+                    success = handleEndSession(intent, callback);
                 }
                 Log.d(TAG, mSessionManager.toString());
                 return success;
@@ -314,23 +340,31 @@
 
         private boolean handlePlay(Intent intent, ControlRequestCallback callback) {
             String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
-            if (sid == null || mSessionManager.stop(sid)) {
-                Log.d(TAG, "handleEnqueue");
-                return handleEnqueue(intent, callback);
+            if (sid != null && !sid.equals(mSessionManager.getSessionId())) {
+                Log.d(TAG, "handlePlay fails because of bad sid="+sid);
+                return false;
             }
-            return false;
+            if (mSessionManager.hasSession()) {
+                mSessionManager.stop();
+            }
+            return handleEnqueue(intent, callback);
         }
 
         private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) {
-            if (intent.getData() == null) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            if (sid != null && !sid.equals(mSessionManager.getSessionId())) {
+                Log.d(TAG, "handleEnqueue fails because of bad sid="+sid);
                 return false;
             }
 
-            mEnqueueCount +=1;
+            Uri uri = intent.getData();
+            if (uri == null) {
+                Log.d(TAG, "handleEnqueue fails because of bad uri="+uri);
+                return false;
+            }
 
             boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE);
-            Uri uri = intent.getData();
-            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            String mime = intent.getType();
             long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
             Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
             Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
@@ -339,12 +373,13 @@
 
             Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request"
                     + ", uri=" + uri
+                    + ", mime=" + mime
                     + ", sid=" + sid
                     + ", pos=" + pos
                     + ", metadata=" + metadata
                     + ", headers=" + headers
                     + ", receiver=" + receiver);
-            MediaQueueItem item = mSessionManager.enqueue(sid, uri, receiver);
+            PlaylistItem item = mSessionManager.add(uri, mime, receiver);
             if (callback != null) {
                 if (item != null) {
                     Bundle result = new Bundle();
@@ -357,13 +392,18 @@
                     callback.onError("Failed to open " + uri.toString(), null);
                 }
             }
+            mEnqueueCount +=1;
             return true;
         }
 
         private boolean handleRemove(Intent intent, ControlRequestCallback callback) {
             String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            if (sid == null || !sid.equals(mSessionManager.getSessionId())) {
+                return false;
+            }
+
             String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
-            MediaQueueItem item = mSessionManager.remove(sid, iid);
+            PlaylistItem item = mSessionManager.remove(iid);
             if (callback != null) {
                 if (item != null) {
                     Bundle result = new Bundle();
@@ -380,10 +420,14 @@
 
         private boolean handleSeek(Intent intent, ControlRequestCallback callback) {
             String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            if (sid == null || !sid.equals(mSessionManager.getSessionId())) {
+                return false;
+            }
+
             String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
             long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
             Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos);
-            MediaQueueItem item = mSessionManager.seek(sid, iid, pos);
+            PlaylistItem item = mSessionManager.seek(iid, pos);
             if (callback != null) {
                 if (item != null) {
                     Bundle result = new Bundle();
@@ -401,7 +445,8 @@
         private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) {
             String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
             String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
-            MediaQueueItem item = mSessionManager.getStatus(sid, iid);
+            Log.d(TAG, mRouteId + ": Received getStatus request, sid=" + sid + ", iid=" + iid);
+            PlaylistItem item = mSessionManager.getStatus(iid);
             if (callback != null) {
                 if (item != null) {
                     Bundle result = new Bundle();
@@ -418,10 +463,12 @@
 
         private boolean handlePause(Intent intent, ControlRequestCallback callback) {
             String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
-            boolean success = mSessionManager.pause(sid);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
+            mSessionManager.pause();
             if (callback != null) {
                 if (success) {
-                    callback.onResult(null);
+                    callback.onResult(new Bundle());
+                    handleSessionStatusChange(sid);
                 } else {
                     callback.onError("Failed to pause, sid=" + sid, null);
                 }
@@ -431,10 +478,12 @@
 
         private boolean handleResume(Intent intent, ControlRequestCallback callback) {
             String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
-            boolean success = mSessionManager.resume(sid);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
+            mSessionManager.resume();
             if (callback != null) {
                 if (success) {
-                    callback.onResult(null);
+                    callback.onResult(new Bundle());
+                    handleSessionStatusChange(sid);
                 } else {
                     callback.onError("Failed to resume, sid=" + sid, null);
                 }
@@ -444,10 +493,12 @@
 
         private boolean handleStop(Intent intent, ControlRequestCallback callback) {
             String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
-            boolean success = mSessionManager.stop(sid);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
+            mSessionManager.stop();
             if (callback != null) {
                 if (success) {
-                    callback.onResult(null);
+                    callback.onResult(new Bundle());
+                    handleSessionStatusChange(sid);
                 } else {
                     callback.onError("Failed to stop, sid=" + sid, null);
                 }
@@ -455,14 +506,64 @@
             return success;
         }
 
-        private void handleFinish(boolean error) {
-            MediaQueueItem item = mSessionManager.finish(error);
-            if (item != null) {
-                handleStatusChange(item);
+        private boolean handleStartSession(Intent intent, ControlRequestCallback callback) {
+            String sid = mSessionManager.startSession();
+            Log.d(TAG, "StartSession returns sessionId "+sid);
+            if (callback != null) {
+                if (sid != null) {
+                    Bundle result = new Bundle();
+                    result.putString(MediaControlIntent.EXTRA_SESSION_ID, sid);
+                    result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS,
+                            mSessionManager.getSessionStatus(sid).asBundle());
+                    callback.onResult(result);
+                    mSessionReceiver = (PendingIntent)intent.getParcelableExtra(
+                            MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER);
+                    handleSessionStatusChange(sid);
+                } else {
+                    callback.onError("Failed to start session.", null);
+                }
             }
+            return (sid != null);
         }
 
-        private void handleStatusChange(MediaQueueItem item) {
+        private boolean handleGetSessionStatus(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+
+            MediaSessionStatus sessionStatus = mSessionManager.getSessionStatus(sid);
+            if (callback != null) {
+                if (sessionStatus != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS,
+                            mSessionManager.getSessionStatus(sid).asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to get session status, sid=" + sid, null);
+                }
+            }
+            return (sessionStatus != null);
+        }
+
+        private boolean handleEndSession(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId())
+                    && mSessionManager.endSession();
+            if (callback != null) {
+                if (success) {
+                    Bundle result = new Bundle();
+                    MediaSessionStatus sessionStatus = new MediaSessionStatus.Builder(
+                            MediaSessionStatus.SESSION_STATE_ENDED).build();
+                    result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, sessionStatus.asBundle());
+                    callback.onResult(result);
+                    handleSessionStatusChange(sid);
+                    mSessionReceiver = null;
+                } else {
+                    callback.onError("Failed to end session, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private void handleStatusChange(PlaylistItem item) {
             if (item == null) {
                 item = mSessionManager.getCurrentItem();
             }
@@ -484,25 +585,18 @@
             }
         }
 
-        private final class MediaPlayerCallback extends MediaPlayerWrapper.Callback {
-            @Override
-            public void onError() {
-                handleFinish(true);
-            }
-
-            @Override
-            public void onCompletion() {
-                handleFinish(false);
-            }
-
-            @Override
-            public void onStatusChanged() {
-                handleStatusChange(null);
-            }
-
-            @Override
-            public void onSizeChanged(int width, int height) {
-                mOverlay.updateAspectRatio(width, height);
+        private void handleSessionStatusChange(String sid) {
+            if (mSessionReceiver != null) {
+                Intent intent = new Intent();
+                intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sid);
+                intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS,
+                        mSessionManager.getSessionStatus(sid).asBundle());
+                try {
+                    mSessionReceiver.send(getContext(), 0, intent);
+                    Log.d(TAG, mRouteId + ": Sending session status update from provider");
+                } catch (PendingIntent.CanceledException e) {
+                    Log.d(TAG, mRouteId + ": Failed to send session status update!");
+                }
             }
         }
     }
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
index cad1584..b1c8c9c 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
@@ -21,12 +21,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.BroadcastReceiver;
 import android.content.res.Resources;
-import android.content.DialogInterface;
 import android.app.PendingIntent;
-import android.app.Presentation;
 import android.media.AudioManager;
 import android.media.AudioManager.OnAudioFocusChangeListener;
 import android.media.MediaMetadataRetriever;
@@ -50,32 +46,23 @@
 import android.support.v7.media.MediaRouteSelector;
 import android.support.v7.media.MediaItemStatus;
 import android.util.Log;
-import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
-import android.view.Display;
-import android.view.SurfaceView;
-import android.view.SurfaceHolder;
-import android.view.WindowManager;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
 import android.widget.ImageButton;
 import android.widget.ListView;
 import android.widget.TextView;
-import android.widget.Toast;
-import android.widget.FrameLayout;
 import android.widget.TabHost;
 import android.widget.TabHost.TabSpec;
 import android.widget.TabHost.OnTabChangeListener;
 import android.widget.SeekBar;
 import android.widget.SeekBar.OnSeekBarChangeListener;
-
-
 import java.io.File;
 
 /**
@@ -88,10 +75,8 @@
  * </p>
  */
 public class SampleMediaRouterActivity extends ActionBarActivity {
-    private static final String TAG = "MediaRouterSupport";
+    private static final String TAG = "SampleMediaRouterActivity";
     private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment";
-    private static final String ACTION_STATUS_CHANGE =
-            "com.example.android.supportv7.media.ACTION_STATUS_CHANGE";
 
     private MediaRouter mMediaRouter;
     private MediaRouteSelector mSelector;
@@ -103,53 +88,22 @@
     private ImageButton mPauseResumeButton;
     private ImageButton mStopButton;
     private SeekBar mSeekBar;
-    private String mStatsInfo;
     private boolean mPaused;
     private boolean mNeedResume;
     private boolean mSeeking;
-    private long mLastStatusTime;
-    private PlaylistAdapter mSavedPlaylist;
 
     private final Handler mHandler = new Handler();
     private final Runnable mUpdateSeekRunnable = new Runnable() {
         @Override
         public void run() {
-            updateProgress(getCheckedMediaQueueItem());
+            updateProgress();
             // update Ui every 1 second
             mHandler.postDelayed(this, 1000);
         }
     };
 
-    private final MediaPlayerWrapper mMediaPlayer = new MediaPlayerWrapper(this);
-    private final MediaPlayerWrapper.Callback mMediaPlayerCB =
-            new MediaPlayerWrapper.Callback()  {
-        @Override
-        public void onError() {
-            mPlayer.onFinish(true);
-        }
-
-        @Override
-        public void onCompletion() {
-            mPlayer.onFinish(false);
-        }
-
-        @Override
-        public void onSizeChanged(int width, int height) {
-            mPlayer.updateSize(width, height);
-        }
-
-        @Override
-        public void onStatusChanged() {
-            if (!mSeeking) {
-                updateUi();
-            }
-        }
-    };
-
-    private final RemotePlayer mRemotePlayer = new RemotePlayer();
-    private final LocalPlayer mLocalPlayer = new LocalPlayer();
+    private final SessionManager mSessionManager = new SessionManager("app");
     private Player mPlayer;
-    private MediaSessionManager.Callback mPlayerCB;
 
     private final MediaRouter.Callback mMediaRouterCB = new MediaRouter.Callback() {
         // Return a custom callback that will simply log all of the route events
@@ -162,7 +116,6 @@
         @Override
         public void onRouteChanged(MediaRouter router, RouteInfo route) {
             Log.d(TAG, "onRouteChanged: route=" + route);
-            mPlayer.showStatistics();
         }
 
         @Override
@@ -174,82 +127,28 @@
         public void onRouteSelected(MediaRouter router, RouteInfo route) {
             Log.d(TAG, "onRouteSelected: route=" + route);
 
-            Player player = mPlayer;
-            MediaSessionManager.Callback playerCB = mPlayerCB;
+            mPlayer = Player.create(SampleMediaRouterActivity.this, route);
+            mPlayer.updatePresentation();
+            mSessionManager.setPlayer(mPlayer);
+            mSessionManager.unsuspend();
 
-            if (route.supportsControlCategory(
-                    MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
-                Intent enqueueIntent = new Intent(MediaControlIntent.ACTION_ENQUEUE);
-                enqueueIntent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-                enqueueIntent.setDataAndType(Uri.parse("http://"), "video/mp4");
-
-                Intent removeIntent = new Intent(MediaControlIntent.ACTION_REMOVE);
-                removeIntent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-
-                // Remote Playback:
-                //   If route supports remote queuing, let it manage the queue;
-                //   otherwise, manage the queue locally and feed it one item at a time
-                if (route.supportsControlRequest(enqueueIntent)
-                 && route.supportsControlRequest(removeIntent)) {
-                    player = mRemotePlayer;
-                } else {
-                    player = mLocalPlayer;
-                }
-                playerCB = mRemotePlayer;
-                mRemotePlayer.reset();
-
-            } else {
-                // Local Playback:
-                //   Use local player and feed media player one item at a time
-                player = mLocalPlayer;
-                playerCB = mMediaPlayer;
-            }
-
-            if (player != mPlayer || playerCB != mPlayerCB) {
-                // save current playlist
-                PlaylistAdapter playlist = new PlaylistAdapter();
-                for (int i = 0; i < mPlayListItems.getCount(); i++) {
-                    MediaQueueItem item = mPlayListItems.getItem(i);
-                    if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
-                            || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
-                        long position = item.getContentPosition();
-                        long timeDelta = mPaused ? 0 :
-                                (SystemClock.elapsedRealtime() - mLastStatusTime);
-                        item.setContentPosition(position + timeDelta);
-                    }
-                    playlist.add(item);
-                }
-
-                // switch players
-                mPlayer.stop();
-                mPaused = false;
-                mLocalPlayer.setCallback(playerCB);
-                mPlayerCB = playerCB;
-                mPlayer = player;
-                mPlayer.showStatistics();
-                mLocalPlayer.updatePresentation();
-
-                // migrate playlist to new route
-                int count = playlist.getCount();
-                if (isRemoteQueue()) {
-                    // if queuing is managed remotely, only enqueue the first
-                    // item, as we need to have the returned session id to
-                    // enqueue the rest of the playlist items
-                    mSavedPlaylist = playlist;
-                    count = 1;
-                }
-                for (int i = 0; i < count; i++) {
-                    final MediaQueueItem item = playlist.getItem(i);
-                    mPlayer.enqueue(item.getUri(), item.getContentPosition());
-                }
-            }
+            registerRCC();
             updateUi();
         }
 
         @Override
         public void onRouteUnselected(MediaRouter router, RouteInfo route) {
             Log.d(TAG, "onRouteUnselected: route=" + route);
-            mPlayer.showStatistics();
+            unregisterRCC();
+
+            PlaylistItem item = getCheckedPlaylistItem();
+            if (item != null) {
+                long pos = item.getPosition() + (mPaused ?
+                        0 : (SystemClock.elapsedRealtime() - item.getTimestamp()));
+                mSessionManager.suspend(pos);
+            }
+            mPlayer.updatePresentation();
+            mPlayer.release();
         }
 
         @Override
@@ -261,6 +160,7 @@
         public void onRoutePresentationDisplayChanged(
                 MediaRouter router, RouteInfo route) {
             Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route);
+            mPlayer.updatePresentation();
         }
 
         @Override
@@ -279,33 +179,6 @@
         }
     };
 
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            Log.d(TAG, "Received status update: " + intent);
-            if (intent.getAction().equals(ACTION_STATUS_CHANGE)) {
-                String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
-                String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
-                MediaItemStatus status = MediaItemStatus.fromBundle(
-                    intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_STATUS));
-
-                if (status.getPlaybackState() ==
-                        MediaItemStatus.PLAYBACK_STATE_FINISHED) {
-                    mPlayer.onFinish(false);
-                } else if (status.getPlaybackState() ==
-                        MediaItemStatus.PLAYBACK_STATE_ERROR) {
-                    mPlayer.onFinish(true);
-                    showToast("Error while playing item" +
-                            ", sid " + sid + ", iid " + iid);
-                } else {
-                    if (!mSeeking) {
-                        updateUi();
-                    }
-                }
-            }
-        }
-    };
-
     private RemoteControlClient mRemoteControlClient;
     private ComponentName mEventReceiver;
     private AudioManager mAudioManager;
@@ -363,7 +236,7 @@
         mLibraryItems = new LibraryAdapter();
         for (int i = 0; i < mediaNames.length; i++) {
             mLibraryItems.add(new MediaItem(
-                    "[streaming] "+mediaNames[i], Uri.parse(mediaUris[i])));
+                    "[streaming] "+mediaNames[i], Uri.parse(mediaUris[i]), "video/mp4"));
         }
 
         // Scan local external storage directory for media files.
@@ -375,7 +248,7 @@
                     String filename = list[i].getName();
                     if (filename.matches(".*\\.(m4v|mp4)")) {
                         mLibraryItems.add(new MediaItem("[local] " + filename,
-                                Uri.fromFile(list[i])));
+                                Uri.fromFile(list[i]), "video/mp4"));
                     }
                 }
             }
@@ -409,10 +282,6 @@
         tabHost.setOnTabChangedListener(new OnTabChangeListener() {
             @Override
             public void onTabChanged(String arg0) {
-                if (arg0.equals(getResources().getString(
-                        R.string.statistics_tab_text))) {
-                    mPlayer.showStatistics();
-                }
                 updateUi();
             }
         });
@@ -443,10 +312,11 @@
         mPauseResumeButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
-                if (!mPaused) {
-                    mPlayer.pause();
+                mPaused = !mPaused;
+                if (mPaused) {
+                    mSessionManager.pause();
                 } else {
-                    mPlayer.resume();
+                    mSessionManager.resume();
                 }
             }
         });
@@ -455,8 +325,8 @@
         mStopButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
-                mPlayer.stop();
-                clearContent();
+                mPaused = false;
+                mSessionManager.stop();
             }
         });
 
@@ -464,12 +334,12 @@
         mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                MediaQueueItem item = getCheckedMediaQueueItem();
-                if (fromUser && item != null && item.getContentDuration() > 0) {
-                    long pos = progress * item.getContentDuration() / 100;
-                    mPlayer.seek(item.getSessionId(), item.getItemId(), pos);
-                    item.setContentPosition(pos);
-                    mLastStatusTime = SystemClock.elapsedRealtime();
+                PlaylistItem item = getCheckedPlaylistItem();
+                if (fromUser && item != null && item.getDuration() > 0) {
+                    long pos = progress * item.getDuration() / 100;
+                    mSessionManager.seek(item.getItemId(), pos);
+                    item.setPosition(pos);
+                    item.setTimestamp(SystemClock.elapsedRealtime());
                 }
             }
             @Override
@@ -486,18 +356,6 @@
         // Schedule Ui update
         mHandler.postDelayed(mUpdateSeekRunnable, 1000);
 
-        // Use local playback with media player by default
-        mLocalPlayer.onCreate();
-        mMediaPlayer.setCallback(mMediaPlayerCB);
-        mLocalPlayer.setCallback(mMediaPlayer);
-        mPlayerCB = mMediaPlayer;
-        mPlayer = mLocalPlayer;
-
-        // Register broadcast receiver to receive status update from MRP
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(SampleMediaRouterActivity.ACTION_STATUS_CHANGE);
-        registerReceiver(mReceiver, filter);
-
         // Build the PendingIntent for the remote control client
         mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
         mEventReceiver = new ComponentName(getPackageName(),
@@ -508,6 +366,23 @@
 
         // Create and register the remote control client
         registerRCC();
+
+        // Set up playback manager and player
+        mPlayer = Player.create(SampleMediaRouterActivity.this,
+                mMediaRouter.getSelectedRoute());
+        mSessionManager.setPlayer(mPlayer);
+        mSessionManager.setCallback(new SessionManager.Callback() {
+            @Override
+            public void onStatusChanged() {
+                updateUi();
+            }
+
+            @Override
+            public void onItemChanged(PlaylistItem item) {
+            }
+        });
+
+        updateUi();
     }
 
     private void registerRCC() {
@@ -546,10 +421,11 @@
                 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
                 {
                     Log.d(TAG, "Received Play/Pause event from RemoteControlClient");
-                    if (!mPaused) {
-                        mPlayer.pause();
+                    mPaused = !mPaused;
+                    if (mPaused) {
+                        mSessionManager.pause();
                     } else {
-                        mPlayer.resume();
+                        mSessionManager.resume();
                     }
                     return true;
                 }
@@ -557,7 +433,8 @@
                 {
                     Log.d(TAG, "Received Play event from RemoteControlClient");
                     if (mPaused) {
-                        mPlayer.resume();
+                        mPaused = false;
+                        mSessionManager.resume();
                     }
                     return true;
                 }
@@ -565,15 +442,16 @@
                 {
                     Log.d(TAG, "Received Pause event from RemoteControlClient");
                     if (!mPaused) {
-                        mPlayer.pause();
+                        mPaused = true;
+                        mSessionManager.pause();
                     }
                     return true;
                 }
                 case KeyEvent.KEYCODE_MEDIA_STOP:
                 {
                     Log.d(TAG, "Received Stop event from RemoteControlClient");
-                    mPlayer.stop();
-                    clearContent();
+                    mPaused = false;
+                    mSessionManager.stop();
                     return true;
                 }
                 default:
@@ -597,15 +475,14 @@
     public void onStart() {
         // Be sure to call the super class.
         super.onStart();
-        mPlayer.showStatistics();
     }
 
     @Override
     public void onPause() {
         // pause media player for local playback case only
-        if (!isRemotePlayback() && !mPaused) {
+        if (!mPlayer.isRemotePlayback() && !mPaused) {
             mNeedResume = true;
-            mPlayer.pause();
+            mSessionManager.pause();
         }
         super.onPause();
     }
@@ -613,8 +490,8 @@
     @Override
     public void onResume() {
         // resume media player for local playback case only
-        if (!isRemotePlayback() && mNeedResume) {
-            mPlayer.resume();
+        if (!mPlayer.isRemotePlayback() && mNeedResume) {
+            mSessionManager.resume();
             mNeedResume = false;
         }
         super.onResume();
@@ -625,11 +502,9 @@
         // Unregister the remote control client
         unregisterRCC();
 
-        // Unregister broadcast receiver
-        unregisterReceiver(mReceiver);
-        mPlayer.stop();
-        mMediaPlayer.release();
-
+        mPaused = false;
+        mSessionManager.stop();
+        mPlayer.release();
         super.onDestroy();
     }
 
@@ -650,52 +525,24 @@
         return true;
     }
 
-    private void updateRouteDescription() {
-        RouteInfo route = mMediaRouter.getSelectedRoute();
-        mInfoTextView.setText("Currently selected route:"
-                + "\nName: " + route.getName()
-                + "\nProvider: " + route.getProvider().getPackageName()
-                + "\nDescription: " + route.getDescription()
-                + "\nStatistics: " + mStatsInfo);
-        updateButtons();
-        mLocalPlayer.updatePresentation();
-    }
-
-    private void clearContent() {
-        //TO-DO: clear surface view
-    }
-
-    private void updateButtons() {
-        MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-        // show pause or resume icon depending on current state
-        mPauseResumeButton.setImageResource(mPaused ?
-                R.drawable.ic_media_play : R.drawable.ic_media_pause);
-        // only enable seek bar when duration is known
-        MediaQueueItem item = getCheckedMediaQueueItem();
-        mSeekBar.setEnabled(item != null && item.getContentDuration() > 0);
-        if (mRemoteControlClient != null) {
-            mRemoteControlClient.setPlaybackState(mPaused ?
-                    RemoteControlClient.PLAYSTATE_PAUSED : RemoteControlClient.PLAYSTATE_PLAYING);
-        }
-    }
-
-    private void updateProgress(MediaQueueItem queueItem) {
+    private void updateProgress() {
         // Estimate content position from last status time and elapsed time.
         // (Note this might be slightly out of sync with remote side, however
         // it avoids frequent polling the MRP.)
         int progress = 0;
-        if (queueItem != null) {
-            int state = queueItem.getState();
-            long duration = queueItem.getContentDuration();
+        PlaylistItem item = getCheckedPlaylistItem();
+        if (item != null) {
+            int state = item.getState();
+            long duration = item.getDuration();
             if (duration <= 0) {
                 if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
                         || state == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
-                    updateUi();
+                    mSessionManager.updateStatus();
                 }
             } else {
-                long position = queueItem.getContentPosition();
+                long position = item.getPosition();
                 long timeDelta = mPaused ? 0 :
-                        (SystemClock.elapsedRealtime() - mLastStatusTime);
+                        (SystemClock.elapsedRealtime() - item.getTimestamp());
                 progress = (int)(100.0 * (position + timeDelta) / duration);
             }
         }
@@ -704,37 +551,47 @@
 
     private void updateUi() {
         updatePlaylist();
+        updateRouteDescription();
         updateButtons();
     }
 
     private void updatePlaylist() {
-        Log.d(TAG, "updatePlaylist");
-        final PlaylistAdapter playlist = new PlaylistAdapter();
-        // make a copy of current playlist
-        for (int i = 0; i < mPlayListItems.getCount(); i++) {
-            playlist.add(mPlayListItems.getItem(i));
-        }
-        // clear mPlayListItems first, items will be added back when we get
-        // status back from provider.
         mPlayListItems.clear();
+        for (PlaylistItem item : mSessionManager.getPlaylist()) {
+            mPlayListItems.add(item);
+        }
         mPlayListView.invalidate();
+    }
 
-        for (int i = 0; i < playlist.getCount(); i++) {
-            final MediaQueueItem item = playlist.getItem(i);
-            final boolean update = (i == playlist.getCount() - 1);
-            mPlayer.getStatus(item, update);
+
+    private void updateRouteDescription() {
+        RouteInfo route = mMediaRouter.getSelectedRoute();
+        mInfoTextView.setText("Currently selected route:"
+                + "\nName: " + route.getName()
+                + "\nProvider: " + route.getProvider().getPackageName()
+                + "\nDescription: " + route.getDescription()
+                + "\nStatistics: " + mSessionManager.getStatistics());
+    }
+
+    private void updateButtons() {
+        MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
+        // show pause or resume icon depending on current state
+        mPauseResumeButton.setImageResource(mPaused ?
+                R.drawable.ic_media_play : R.drawable.ic_media_pause);
+        // disable pause/resume/stop if no session
+        mPauseResumeButton.setEnabled(mSessionManager.hasSession());
+        mStopButton.setEnabled(mSessionManager.hasSession());
+        // only enable seek bar when duration is known
+        PlaylistItem item = getCheckedPlaylistItem();
+        mSeekBar.setEnabled(item != null && item.getDuration() > 0);
+        if (mRemoteControlClient != null) {
+            mRemoteControlClient.setPlaybackState(mPaused ?
+                    RemoteControlClient.PLAYSTATE_PAUSED :
+                        RemoteControlClient.PLAYSTATE_PLAYING);
         }
     }
 
-    private MediaItem getCheckedMediaItem() {
-        int index = mLibraryView.getCheckedItemPosition();
-        if (index >= 0 && index < mLibraryItems.getCount()) {
-            return mLibraryItems.getItem(index);
-        }
-        return null;
-    }
-
-    private MediaQueueItem getCheckedMediaQueueItem() {
+    private PlaylistItem getCheckedPlaylistItem() {
         int count = mPlayListView.getCount();
         int index = mPlayListView.getCheckedItemPosition();
         if (count > 0) {
@@ -747,768 +604,6 @@
         return null;
     }
 
-    private void enqueuePlaylist() {
-        if (mSavedPlaylist != null) {
-            final PlaylistAdapter playlist = mSavedPlaylist;
-            mSavedPlaylist = null;
-            // migrate playlist (except for the 1st item) to new route
-            for (int i = 1; i < playlist.getCount(); i++) {
-                final MediaQueueItem item = playlist.getItem(i);
-                mPlayer.enqueue(item.getUri(), item.getContentPosition());
-            }
-        }
-    }
-
-    private boolean isRemoteQueue() {
-        return mPlayer == mRemotePlayer;
-    }
-
-    private boolean isRemotePlayback() {
-        return mPlayerCB == mRemotePlayer;
-    }
-
-    private void showToast(String msg) {
-        Toast toast = Toast.makeText(SampleMediaRouterActivity.this,
-                "[app] " + msg, Toast.LENGTH_LONG);
-        toast.setGravity(Gravity.TOP, 0, 100);
-        toast.show();
-    }
-
-    private interface Player {
-        void enqueue(final Uri uri, long pos);
-        void remove(final MediaQueueItem item);
-        void seek(String sid, String iid, long pos);
-        void getStatus(final MediaQueueItem item, final boolean update);
-        void pause();
-        void resume();
-        void stop();
-        void showStatistics();
-        void onFinish(boolean error);
-        void updateSize(int width, int height);
-    }
-
-    private class LocalPlayer implements Player, SurfaceHolder.Callback {
-        private final MediaSessionManager mSessionManager = new MediaSessionManager();
-        private String mSessionId;
-        // The presentation to show on the secondary display.
-        private DemoPresentation mPresentation;
-        private SurfaceView mSurfaceView;
-        private FrameLayout mLayout;
-        private int mVideoWidth;
-        private int mVideoHeight;
-
-        public void onCreate() {
-            mLayout = (FrameLayout)findViewById(R.id.player);
-            mSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
-            SurfaceHolder holder = mSurfaceView.getHolder();
-            holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
-            holder.addCallback(this);
-        }
-
-        public void setCallback(MediaSessionManager.Callback cb) {
-            mSessionManager.setCallback(cb);
-        }
-
-        @Override
-        public void enqueue(final Uri uri, long pos) {
-            Log.d(TAG, "LocalPlayer: enqueue, uri=" + uri + ", pos=" + pos);
-            MediaQueueItem playlistItem = mSessionManager.enqueue(mSessionId, uri, null);
-            mSessionId = playlistItem.getSessionId();
-            // Set remote control client title
-            if (mPlayListItems.getCount() == 0 && mRemoteControlClient != null) {
-                RemoteControlClient.MetadataEditor ed = mRemoteControlClient.editMetadata(true);
-                ed.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,
-                        playlistItem.toString());
-                ed.apply();
-            }
-            mPlayListItems.add(playlistItem);
-            if (pos > 0) {
-                // Seek to initial position if needed
-                mPlayer.seek(mSessionId, playlistItem.getItemId(), pos);
-            }
-            updateUi();
-        }
-
-        @Override
-        public void remove(final MediaQueueItem item) {
-            Log.d(TAG, "LocalPlayer: remove, item=" + item);
-            mSessionManager.remove(item.getSessionId(), item.getItemId());
-            updateUi();
-        }
-
-        @Override
-        public void seek(String sid, String iid, long pos) {
-            Log.d(TAG, "LocalPlayer: seek, sid=" + sid + ", iid=" + iid);
-            mSessionManager.seek(sid, iid, pos);
-        }
-
-        @Override
-        public void getStatus(final MediaQueueItem item, final boolean update) {
-            Log.d(TAG, "LocalPlayer: getStatus, item=" + item + ", update=" + update);
-            MediaQueueItem playlistItem =
-                    mSessionManager.getStatus(item.getSessionId(), item.getItemId());
-            if (playlistItem != null) {
-                mLastStatusTime = playlistItem.getStatus().getTimestamp();
-                mPlayListItems.add(item);
-                mPlayListView.invalidate();
-            }
-            if (update) {
-                clearContent();
-                updateButtons();
-            }
-        }
-
-        @Override
-        public void pause() {
-            Log.d(TAG, "LocalPlayer: pause");
-            mSessionManager.pause(mSessionId);
-            mPaused = true;
-            updateUi();
-        }
-
-        @Override
-        public void resume() {
-            Log.d(TAG, "LocalPlayer: resume");
-            mSessionManager.resume(mSessionId);
-            mPaused = false;
-            updateUi();
-        }
-
-        @Override
-        public void stop() {
-            Log.d(TAG, "LocalPlayer: stop");
-            mSessionManager.stop(mSessionId);
-            mSessionId = null;
-            mPaused = false;
-            // For demo purpose, invalidate remote session when local session
-            // is stopped (note this is not necessary, remote player could reuse
-            // the same session)
-            mRemotePlayer.reset();
-            updateUi();
-        }
-
-        @Override
-        public void showStatistics() {
-            Log.d(TAG, "LocalPlayer: showStatistics");
-            mStatsInfo = null;
-            if (isRemotePlayback()) {
-                mRemotePlayer.showStatistics();
-            }
-            updateRouteDescription();
-        }
-
-        @Override
-        public void onFinish(boolean error) {
-            MediaQueueItem item = mSessionManager.finish(error);
-            updateUi();
-            if (error && item != null) {
-                showToast("Failed to play item " + item.getUri());
-            }
-        }
-
-        // SurfaceHolder.Callback
-        @Override
-        public void surfaceChanged(SurfaceHolder holder, int format,
-                int width, int height) {
-            Log.d(TAG, "surfaceChanged "+width+"x"+height);
-            mMediaPlayer.setSurface(holder);
-        }
-
-        @Override
-        public void surfaceCreated(SurfaceHolder holder) {
-            Log.d(TAG, "surfaceCreated");
-            mMediaPlayer.setSurface(holder);
-            updateSize(mVideoWidth, mVideoHeight);
-        }
-
-        @Override
-        public void surfaceDestroyed(SurfaceHolder holder) {
-            Log.d(TAG, "surfaceDestroyed");
-        }
-
-        @Override
-        public void updateSize(int width, int height) {
-            if (width > 0 && height > 0) {
-                if (mPresentation == null) {
-                    int surfaceWidth = mLayout.getWidth();
-                    int surfaceHeight = mLayout.getHeight();
-
-                    // Calculate the new size of mSurfaceView, so that video is centered
-                    // inside the framelayout with proper letterboxing/pillarboxing
-                    ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
-                    if (surfaceWidth * height < surfaceHeight * width) {
-                        // Black bars on top&bottom, mSurfaceView has full layout width,
-                        // while height is derived from video's aspect ratio
-                        lp.width = surfaceWidth;
-                        lp.height = surfaceWidth * height / width;
-                    } else {
-                        // Black bars on left&right, mSurfaceView has full layout height,
-                        // while width is derived from video's aspect ratio
-                        lp.width = surfaceHeight * width / height;
-                        lp.height = surfaceHeight;
-                    }
-                    Log.d(TAG, "video rect is "+lp.width+"x"+lp.height);
-                    mSurfaceView.setLayoutParams(lp);
-                } else {
-                    mPresentation.updateSize(width, height);
-                }
-                mVideoWidth = width;
-                mVideoHeight = height;
-            } else {
-                mVideoWidth = mVideoHeight = 0;
-            }
-        }
-
-        private void updatePresentation() {
-            // Get the current route and its presentation display.
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            Display presentationDisplay = route != null ? route.getPresentationDisplay() : null;
-
-            // Dismiss the current presentation if the display has changed.
-            if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) {
-                Log.i(TAG, "Dismissing presentation because the current route no longer "
-                        + "has a presentation display.");
-                mPresentation.dismiss();
-                mPresentation = null;
-            }
-
-            // Show a new presentation if needed.
-            if (mPresentation == null && presentationDisplay != null) {
-                Log.i(TAG, "Showing presentation on display: " + presentationDisplay);
-                mPresentation = new DemoPresentation(
-                        SampleMediaRouterActivity.this, presentationDisplay);
-                mPresentation.setOnDismissListener(mOnDismissListener);
-                try {
-                    mPresentation.show();
-                } catch (WindowManager.InvalidDisplayException ex) {
-                    Log.w(TAG, "Couldn't show presentation!  Display was removed in "
-                            + "the meantime.", ex);
-                    mPresentation = null;
-                }
-            }
-
-            if (mPresentation != null || route.supportsControlCategory(
-                    MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
-                mMediaPlayer.setSurface((SurfaceHolder)null);
-                mMediaPlayer.reset();
-                mSurfaceView.setVisibility(View.GONE);
-                mLayout.setVisibility(View.GONE);
-            } else {
-                mLayout.setVisibility(View.VISIBLE);
-                mSurfaceView.setVisibility(View.VISIBLE);
-            }
-        }
-
-        // Listens for when presentations are dismissed.
-        private final DialogInterface.OnDismissListener mOnDismissListener =
-                new DialogInterface.OnDismissListener() {
-            @Override
-            public void onDismiss(DialogInterface dialog) {
-                if (dialog == mPresentation) {
-                    Log.i(TAG, "Presentation was dismissed.");
-                    mPresentation = null;
-                    updatePresentation();
-                }
-            }
-        };
-
-        private final class DemoPresentation extends Presentation {
-            private SurfaceView mPresentationSurfaceView;
-
-            public DemoPresentation(Context context, Display display) {
-                super(context, display);
-            }
-
-            @Override
-            protected void onCreate(Bundle savedInstanceState) {
-                // Be sure to call the super class.
-                super.onCreate(savedInstanceState);
-
-                // Get the resources for the context of the presentation.
-                // Notice that we are getting the resources from the context
-                // of the presentation.
-                Resources r = getContext().getResources();
-
-                // Inflate the layout.
-                setContentView(R.layout.sample_media_router_presentation);
-
-                // Set up the surface view.
-                mPresentationSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
-                SurfaceHolder holder = mPresentationSurfaceView.getHolder();
-                holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
-                holder.addCallback(LocalPlayer.this);
-            }
-
-            public void updateSize(int width, int height) {
-                int surfaceHeight = getWindow().getDecorView().getHeight();
-                int surfaceWidth = getWindow().getDecorView().getWidth();
-                ViewGroup.LayoutParams lp = mPresentationSurfaceView.getLayoutParams();
-                if (surfaceWidth * height < surfaceHeight * width) {
-                    lp.width = surfaceWidth;
-                    lp.height = surfaceWidth * height / width;
-                } else {
-                    lp.width = surfaceHeight * width / height;
-                    lp.height = surfaceHeight;
-                }
-                Log.d(TAG, "video rect is " + lp.width + "x" + lp.height);
-                mPresentationSurfaceView.setLayoutParams(lp);
-            }
-        }
-    }
-
-    private class RemotePlayer implements Player, MediaSessionManager.Callback {
-        private MediaQueueItem mQueueItem;
-        private MediaQueueItem mPlaylistItem;
-        private String mSessionId;
-        private String mItemId;
-        private long mPosition;
-
-        public void reset() {
-            mQueueItem = null;
-            mPlaylistItem = null;
-            mSessionId = null;
-            mItemId = null;
-            mPosition = 0;
-        }
-
-        // MediaSessionManager.Callback
-        @Override
-        public void onStart() {
-            resume();
-        }
-
-        @Override
-        public void onPause() {
-            pause();
-        }
-
-        @Override
-        public void onStop() {
-            stop();
-        }
-
-        @Override
-        public void onSeek(long pos) {
-            // If we're currently performing a Play/Enqueue, do not seek
-            // until we get the result back (or we may not have valid session
-            // and item ids); otherwise do the seek now
-            if (mSessionId != null) {
-                seek(mSessionId, mItemId, pos);
-            }
-            // Set current position to seek-to position, actual position will
-            // be updated when next getStatus is completed.
-            mPosition = pos;
-        }
-
-        @Override
-        public void onGetStatus(MediaQueueItem item) {
-            if (mQueueItem != null) {
-                mPlaylistItem = item;
-                getStatus(mQueueItem, false);
-            }
-        }
-
-        @Override
-        public void onNewItem(Uri uri) {
-            mPosition = 0;
-            play(uri, false, 0);
-        }
-
-        // Player API
-        @Override
-        public void enqueue(final Uri uri, long pos) {
-            play(uri, true, pos);
-        }
-
-        @Override
-        public void remove(final MediaQueueItem item) {
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            Intent intent = makeRemoveIntent(item);
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        MediaItemStatus status = MediaItemStatus.fromBundle(
-                                data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
-                        Log.d(TAG, "Remove request succeeded: status=" + status.toString());
-                        updateUi();
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, "Remove request failed: error=" + error + ", data=" + data);
-                    }
-                };
-
-                Log.d(TAG, "Sending remove request: intent=" + intent);
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, "Remove request not supported!");
-            }
-        }
-
-        @Override
-        public void seek(String sid, String iid, long pos) {
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            Intent intent = makeSeekIntent(sid, iid, pos);
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        MediaItemStatus status = MediaItemStatus.fromBundle(
-                                data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
-                        Log.d(TAG, "Seek request succeeded: status=" + status.toString());
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, "Seek request failed: error=" + error + ", data=" + data);
-                    }
-                };
-
-                Log.d(TAG, "Sending seek request: intent=" + intent);
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, "Seek request not supported!");
-            }
-        }
-
-        @Override
-        public void getStatus(final MediaQueueItem item, final boolean update) {
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            Intent intent = makeGetStatusIntent(item);
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        if (data != null) {
-                            String sid = data.getString(MediaControlIntent.EXTRA_SESSION_ID);
-                            String iid = data.getString(MediaControlIntent.EXTRA_ITEM_ID);
-                            MediaItemStatus status = MediaItemStatus.fromBundle(
-                                    data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
-                            Log.d(TAG, "GetStatus request succeeded: status=" + status.toString());
-                            //showToast("GetStatus request succeeded " + item.mName);
-                            if (isRemoteQueue()) {
-                                int state = status.getPlaybackState();
-                                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
-                                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED
-                                        || state == MediaItemStatus.PLAYBACK_STATE_PENDING) {
-                                    item.setState(state);
-                                    item.setContentPosition(status.getContentPosition());
-                                    item.setContentDuration(status.getContentDuration());
-                                    mLastStatusTime = status.getTimestamp();
-                                    mPlayListItems.add(item);
-                                    mPlayListView.invalidate();
-                                    // update buttons as the queue count might have changed
-                                    if (update) {
-                                        clearContent();
-                                        updateButtons();
-                                    }
-                                }
-                            } else {
-                                if (mPlaylistItem != null) {
-                                    mPlaylistItem.setContentPosition(status.getContentPosition());
-                                    mPlaylistItem.setContentDuration(status.getContentDuration());
-                                    mPlaylistItem = null;
-                                    updateButtons();
-                                }
-                            }
-                        }
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, "GetStatus request failed: error=" + error + ", data=" + data);
-                        //showToast("Unable to get status ");
-                        if (isRemoteQueue()) {
-                            if (update) {
-                                clearContent();
-                                updateButtons();
-                            }
-                        }
-                    }
-                };
-
-                Log.d(TAG, "Sending GetStatus request: intent=" + intent);
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, "GetStatus request not supported!");
-            }
-        }
-
-        @Override
-        public void pause() {
-            Intent intent = makePauseIntent();
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        Log.d(TAG, "Pause request succeeded");
-                        if (isRemoteQueue()) {
-                            mPaused = true;
-                            updateUi();
-                        }
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, "Pause request failed: error=" + error);
-                    }
-                };
-
-                Log.d(TAG, "Sending pause request");
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, "Pause request not supported!");
-            }
-        }
-
-        @Override
-        public void resume() {
-            Intent intent = makeResumeIntent();
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        Log.d(TAG, "Resume request succeeded");
-                        if (isRemoteQueue()) {
-                            mPaused = false;
-                            updateUi();
-                        }
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, "Resume request failed: error=" + error);
-                    }
-                };
-
-                Log.d(TAG, "Sending resume request");
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, "Resume request not supported!");
-            }
-        }
-
-        @Override
-        public void stop() {
-            Intent intent = makeStopIntent();
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        Log.d(TAG, "Stop request succeeded");
-                        if (isRemoteQueue()) {
-                            // Reset mSessionId, so that next Play/Enqueue
-                            // starts a new session
-                            mQueueItem = null;
-                            mSessionId = null;
-                            mPaused = false;
-                            updateUi();
-                        }
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, "Stop request failed: error=" + error);
-                    }
-                };
-
-                Log.d(TAG, "Sending stop request");
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, "Stop request not supported!");
-            }
-        }
-
-        @Override
-        public void showStatistics() {
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            Intent intent = makeStatisticsIntent();
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        Log.d(TAG, "Statistics request succeeded: data=" + data);
-                        if (data != null) {
-                            int playbackCount = data.getInt(
-                                    SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1);
-                            mStatsInfo = "Total playback count: " + playbackCount;
-                        } else {
-                            showToast("Statistics query did not return any data");
-                        }
-                        updateRouteDescription();
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, "Statistics request failed: error=" + error + ", data=" + data);
-                        showToast("Unable to query statistics, error: " + error);
-                        updateRouteDescription();
-                    }
-                };
-
-                Log.d(TAG, "Sent statistics request: intent=" + intent);
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, "Statistics request not supported!");
-            }
-
-        }
-
-        @Override
-        public void onFinish(boolean error) {
-            updateUi();
-        }
-
-        @Override
-        public void updateSize(int width, int height) {
-            // nothing to do
-        }
-
-        private void play(final Uri uri, boolean enqueue, final long pos) {
-            // save the initial seek position
-            mPosition = pos;
-            MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-            Intent intent = makePlayIntent(uri, enqueue);
-            final String request = enqueue ? "Enqueue" : "Play";
-            if (route.supportsControlRequest(intent)) {
-                MediaRouter.ControlRequestCallback callback =
-                        new MediaRouter.ControlRequestCallback() {
-                    @Override
-                    public void onResult(Bundle data) {
-                        if (data != null) {
-                            String sid = data.getString(MediaControlIntent.EXTRA_SESSION_ID);
-                            String iid = data.getString(MediaControlIntent.EXTRA_ITEM_ID);
-                            MediaItemStatus status = MediaItemStatus.fromBundle(
-                                    data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
-                            Log.d(TAG, request + " request succeeded: data=" + data +
-                                    ", sid=" + sid + ", iid=" + iid);
-
-                            // perform delayed initial seek
-                            if (mSessionId == null && mPosition > 0) {
-                                seek(sid, iid, mPosition);
-                            }
-
-                            mSessionId = sid;
-                            mItemId = iid;
-                            mQueueItem = new MediaQueueItem(sid, iid, null, null);
-
-                            if (isRemoteQueue()) {
-                                MediaQueueItem playlistItem =
-                                        new MediaQueueItem(sid, iid, uri, null);
-                                playlistItem.setState(status.getPlaybackState());
-                                mPlayListItems.add(playlistItem);
-                                updateUi();
-                                enqueuePlaylist();
-                            }
-                        }
-                    }
-
-                    @Override
-                    public void onError(String error, Bundle data) {
-                        Log.d(TAG, request + " request failed: error=" + error + ", data=" + data);
-                        showToast("Unable to " + request + uri + ", error: " + error);
-                    }
-                };
-
-                Log.d(TAG, "Sending " + request + " request: intent=" + intent);
-                route.sendControlRequest(intent, callback);
-            } else {
-                Log.d(TAG, request + " request not supported!");
-            }
-        }
-
-        private Intent makePlayIntent(Uri uri, boolean enqueue) {
-            Intent intent = new Intent(
-                    enqueue ? MediaControlIntent.ACTION_ENQUEUE
-                            : MediaControlIntent.ACTION_PLAY);
-            intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-            intent.setDataAndType(uri, "video/mp4");
-
-            // Provide a valid session id, or none (which starts a new session)
-            if (mSessionId != null) {
-                intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
-            }
-
-            // PendingIntent for receiving status update from MRP
-            Intent statusIntent = new Intent(SampleMediaRouterActivity.ACTION_STATUS_CHANGE);
-            intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
-                    PendingIntent.getBroadcast(SampleMediaRouterActivity.this,
-                            0, statusIntent, 0));
-
-            return intent;
-        }
-
-        private Intent makeRemoveIntent(MediaQueueItem item) {
-            Intent intent = new Intent(MediaControlIntent.ACTION_REMOVE);
-            intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-            intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
-            intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
-            return intent;
-        }
-
-        private Intent makeSeekIntent(String sid, String iid, long pos) {
-            Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
-            intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-            intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sid);
-            intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, iid);
-            intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, pos);
-            return intent;
-        }
-
-        private Intent makePauseIntent() {
-            Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
-            intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-            if (mSessionId != null) {
-                intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
-            }
-            return intent;
-        }
-
-        private Intent makeResumeIntent() {
-            Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
-            intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-            if (mSessionId != null) {
-                intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
-            }
-            return intent;
-        }
-
-        private Intent makeStopIntent() {
-            Intent intent = new Intent(MediaControlIntent.ACTION_STOP);
-            intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-            if (mSessionId != null) {
-                intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mSessionId);
-            }
-            return intent;
-        }
-
-        private Intent makeGetStatusIntent(MediaQueueItem item) {
-            Intent intent = new Intent(MediaControlIntent.ACTION_GET_STATUS);
-            intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-            intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
-            intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
-            return intent;
-         }
-
-        private Intent makeStatisticsIntent() {
-            Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS);
-            intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
-            return intent;
-        }
-    }
-
     public static final class DiscoveryFragment extends MediaRouteDiscoveryFragment {
         private static final String TAG = "DiscoveryFragment";
         private Callback mCallback;
@@ -1544,10 +639,12 @@
     private static final class MediaItem {
         public final String mName;
         public final Uri mUri;
+        public final String mMime;
 
-        public MediaItem(String name, Uri uri) {
+        public MediaItem(String name, Uri uri, String mime) {
             mName = name;
             mUri = uri;
+            mMime = mime;
         }
 
         @Override
@@ -1582,7 +679,7 @@
                 @Override
                 public void onClick(View v) {
                     if (item != null) {
-                        mPlayer.enqueue(item.mUri, 0);
+                        mSessionManager.add(item.mUri, item.mMime);
                     }
                 }
             });
@@ -1591,7 +688,7 @@
         }
     }
 
-    private final class PlaylistAdapter extends ArrayAdapter<MediaQueueItem> {
+    private final class PlaylistAdapter extends ArrayAdapter<PlaylistItem> {
         public PlaylistAdapter() {
             super(SampleMediaRouterActivity.this, R.layout.media_item);
         }
@@ -1605,7 +702,7 @@
                 v = convertView;
             }
 
-            final MediaQueueItem item = getItem(position);
+            final PlaylistItem item = getItem(position);
 
             TextView tv = (TextView)v.findViewById(R.id.item_text);
             tv.setText(item.toString());
@@ -1617,7 +714,7 @@
                 @Override
                 public void onClick(View v) {
                     if (item != null) {
-                        mPlayer.remove(item);
+                        mSessionManager.remove(item.getItemId());
                     }
                 }
             });
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/SessionManager.java b/samples/Support7Demos/src/com/example/android/supportv7/media/SessionManager.java
new file mode 100644
index 0000000..23f2a89
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SessionManager.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2013 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.example.android.supportv7.media;
+
+import android.app.PendingIntent;
+import android.net.Uri;
+import android.support.v7.media.MediaItemStatus;
+import android.support.v7.media.MediaSessionStatus;
+import android.util.Log;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * SessionManager manages a media session as a queue. It supports common
+ * queuing behaviors such as enqueue/remove of media items, pause/resume/stop,
+ * etc.
+ *
+ * Actual playback of a single media item is abstracted into a Player interface,
+ * and is handled outside this class.
+ */
+public class SessionManager implements Player.Callback {
+    private static final String TAG = "SessionManager";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private String mName;
+    private int mSessionId;
+    private int mItemId;
+    private boolean mPaused;
+    private boolean mSessionValid;
+    private Player mPlayer;
+    private Callback mCallback;
+    private List<PlaylistItem> mPlaylist = new ArrayList<PlaylistItem>();
+
+    public SessionManager(String name) {
+        mName = name;
+    }
+
+    public boolean hasSession() {
+        return mSessionValid;
+    }
+
+    public String getSessionId() {
+        return mSessionValid ? Integer.toString(mSessionId) : null;
+    }
+
+    public PlaylistItem getCurrentItem() {
+        return mPlaylist.isEmpty() ? null : mPlaylist.get(0);
+    }
+
+    // Get the cached statistic info from the player (will not update it)
+    public String getStatistics() {
+        checkPlayer();
+        return mPlayer.getStatistics();
+    }
+
+    // Returns the cached playlist (note this is not responsible for updating it)
+    public List<PlaylistItem> getPlaylist() {
+        return mPlaylist;
+    }
+
+    // Updates the playlist asynchronously, calls onPlaylistReady() when finished.
+    public void updateStatus() {
+        if (DEBUG) {
+            log("updateStatus");
+        }
+        checkPlayer();
+        // update the statistics first, so that the stats string is valid when
+        // onPlaylistReady() gets called in the end
+        mPlayer.updateStatistics();
+
+        if (mPlaylist.isEmpty()) {
+            // If queue is empty, don't forget to call onPlaylistReady()!
+            onPlaylistReady();
+        } else if (mPlayer.isQueuingSupported()) {
+            // If player supports queuing, get status of each item. Player is
+            // responsible to call onPlaylistReady() after last getStatus().
+            // (update=1 requires player to callback onPlaylistReady())
+            for (int i = 0; i < mPlaylist.size(); i++) {
+                PlaylistItem item = mPlaylist.get(i);
+                mPlayer.getStatus(item, (i == mPlaylist.size() - 1) /* update */);
+            }
+        } else {
+            // Otherwise, only need to get status for current item. Player is
+            // responsible to call onPlaylistReady() when finished.
+            mPlayer.getStatus(getCurrentItem(), true /* update */);
+        }
+    }
+
+    public PlaylistItem add(Uri uri, String mime) {
+        return add(uri, mime, null);
+    }
+
+    public PlaylistItem add(Uri uri, String mime, PendingIntent receiver) {
+        if (DEBUG) {
+            log("add: uri=" + uri + ", receiver=" + receiver);
+        }
+        // create new session if needed
+        startSession();
+        checkPlayerAndSession();
+
+        // append new item with initial status PLAYBACK_STATE_PENDING
+        PlaylistItem item = new PlaylistItem(
+                Integer.toString(mSessionId), Integer.toString(mItemId), uri, mime, receiver);
+        mPlaylist.add(item);
+        mItemId++;
+
+        // if player supports queuing, enqueue the item now
+        if (mPlayer.isQueuingSupported()) {
+            mPlayer.enqueue(item);
+        }
+        updatePlaybackState();
+        return item;
+    }
+
+    public PlaylistItem remove(String iid) {
+        if (DEBUG) {
+            log("remove: iid=" + iid);
+        }
+        checkPlayerAndSession();
+        return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED);
+    }
+
+    public PlaylistItem seek(String iid, long pos) {
+        if (DEBUG) {
+            log("seek: iid=" + iid +", pos=" + pos);
+        }
+        checkPlayerAndSession();
+        // seeking on pending items are not yet supported
+        checkItemCurrent(iid);
+
+        PlaylistItem item = getCurrentItem();
+        if (pos != item.getPosition()) {
+            item.setPosition(pos);
+            if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                    || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                mPlayer.seek(item);
+            }
+        }
+        return item;
+    }
+
+    public PlaylistItem getStatus(String iid) {
+        checkPlayerAndSession();
+
+        // This should only be called for local player. Remote player is
+        // asynchronous, need to use updateStatus() instead.
+        if (mPlayer.isRemotePlayback()) {
+            throw new IllegalStateException(
+                    "getStatus should not be called on remote player!");
+        }
+
+        for (PlaylistItem item : mPlaylist) {
+            if (item.getItemId().equals(iid)) {
+                if (item == getCurrentItem()) {
+                    mPlayer.getStatus(item, false);
+                }
+                return item;
+            }
+        }
+        return null;
+    }
+
+    public void pause() {
+        if (DEBUG) {
+            log("pause");
+        }
+        checkPlayerAndSession();
+        mPaused = true;
+        updatePlaybackState();
+    }
+
+    public void resume() {
+        if (DEBUG) {
+            log("resume");
+        }
+        checkPlayerAndSession();
+        mPaused = false;
+        updatePlaybackState();
+    }
+
+    public void stop() {
+        if (DEBUG) {
+            log("stop");
+        }
+        checkPlayerAndSession();
+        mPlayer.stop();
+        mPlaylist.clear();
+        mPaused = false;
+        updateStatus();
+    }
+
+    public String startSession() {
+        if (!mSessionValid) {
+            mSessionId++;
+            mItemId = 0;
+            mPaused = false;
+            mSessionValid = true;
+            return Integer.toString(mSessionId);
+        }
+        return null;
+    }
+
+    public boolean endSession() {
+        if (mSessionValid) {
+            mSessionValid = false;
+            return true;
+        }
+        return false;
+    }
+
+    MediaSessionStatus getSessionStatus(String sid) {
+        int sessionState = (sid != null && sid.equals(mSessionId)) ?
+                MediaSessionStatus.SESSION_STATE_ACTIVE :
+                    MediaSessionStatus.SESSION_STATE_INVALIDATED;
+
+        return new MediaSessionStatus.Builder(sessionState)
+                .setQueuePaused(mPaused)
+                .build();
+    }
+
+    // Suspend the playback manager. Put the current item back into PENDING
+    // state, and remember the current playback position. Called when switching
+    // to a different player (route).
+    public void suspend(long pos) {
+        for (PlaylistItem item : mPlaylist) {
+            item.setRemoteItemId(null);
+            item.setDuration(0);
+        }
+        PlaylistItem item = getCurrentItem();
+        if (DEBUG) {
+            log("suspend: item=" + item + ", pos=" + pos);
+        }
+        if (item != null) {
+            if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                    || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                item.setState(MediaItemStatus.PLAYBACK_STATE_PENDING);
+                item.setPosition(pos);
+            }
+        }
+    }
+
+    // Unsuspend the playback manager. Restart playback on new player (route).
+    // This will resume playback of current item. Furthermore, if the new player
+    // supports queuing, playlist will be re-established on the remote player.
+    public void unsuspend() {
+        if (DEBUG) {
+            log("unsuspend");
+        }
+        if (mPlayer.isQueuingSupported()) {
+            for (PlaylistItem item : mPlaylist) {
+                mPlayer.enqueue(item);
+            }
+        }
+        updatePlaybackState();
+    }
+
+    // Player.Callback
+    @Override
+    public void onError() {
+        finishItem(true);
+    }
+
+    @Override
+    public void onCompletion() {
+        finishItem(false);
+    }
+
+    @Override
+    public void onPlaylistChanged() {
+        // Playlist has changed, update the cached playlist
+        updateStatus();
+    }
+
+    @Override
+    public void onPlaylistReady() {
+        // Notify activity to update Ui
+        if (mCallback != null) {
+            mCallback.onStatusChanged();
+        }
+    }
+
+    private void log(String message) {
+        Log.d(TAG, mName + ": " + message);
+    }
+
+    private void checkPlayer() {
+        if (mPlayer == null) {
+            throw new IllegalStateException("Player not set!");
+        }
+    }
+
+    private void checkSession() {
+        if (!mSessionValid) {
+            throw new IllegalStateException("Session not set!");
+        }
+    }
+
+    private void checkPlayerAndSession() {
+        checkPlayer();
+        checkSession();
+    }
+
+    private void checkItemCurrent(String iid) {
+        PlaylistItem item = getCurrentItem();
+        if (item == null || !item.getItemId().equals(iid)) {
+            throw new IllegalArgumentException("Item is not current!");
+        }
+    }
+
+    private void updatePlaybackState() {
+        PlaylistItem item = getCurrentItem();
+        if (item != null) {
+            if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
+                item.setState(mPaused ? MediaItemStatus.PLAYBACK_STATE_PAUSED
+                        : MediaItemStatus.PLAYBACK_STATE_PLAYING);
+                if (!mPlayer.isQueuingSupported()) {
+                    mPlayer.play(item);
+                }
+            } else if (mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
+                mPlayer.pause();
+                item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED);
+            } else if (!mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                mPlayer.resume();
+                item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING);
+            }
+            // notify client that item playback status has changed
+            if (mCallback != null) {
+                mCallback.onItemChanged(item);
+            }
+        }
+        updateStatus();
+    }
+
+    private PlaylistItem removeItem(String iid, int state) {
+        checkPlayerAndSession();
+        List<PlaylistItem> queue =
+                new ArrayList<PlaylistItem>(mPlaylist.size());
+        PlaylistItem found = null;
+        for (PlaylistItem item : mPlaylist) {
+            if (iid.equals(item.getItemId())) {
+                if (mPlayer.isQueuingSupported()) {
+                    mPlayer.remove(item.getRemoteItemId());
+                } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                        || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED){
+                    mPlayer.stop();
+                }
+                item.setState(state);
+                found = item;
+                // notify client that item is now removed
+                if (mCallback != null) {
+                    mCallback.onItemChanged(found);
+                }
+            } else {
+                queue.add(item);
+            }
+        }
+        if (found != null) {
+            mPlaylist = queue;
+            updatePlaybackState();
+        } else {
+            log("item not found");
+        }
+        return found;
+    }
+
+    private void finishItem(boolean error) {
+        PlaylistItem item = getCurrentItem();
+        if (item != null) {
+            removeItem(item.getItemId(), error ?
+                    MediaItemStatus.PLAYBACK_STATE_ERROR :
+                        MediaItemStatus.PLAYBACK_STATE_FINISHED);
+            updateStatus();
+        }
+    }
+
+    // set the Player that this playback manager will interact with
+    public void setPlayer(Player player) {
+        mPlayer = player;
+        checkPlayer();
+        mPlayer.setCallback(this);
+    }
+
+    // provide a callback interface to tell the UI when significant state changes occur
+    public void setCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    @Override
+    public String toString() {
+        String result = "Media Queue: ";
+        if (!mPlaylist.isEmpty()) {
+            for (PlaylistItem item : mPlaylist) {
+                result += "\n" + item.toString();
+            }
+        } else {
+            result += "<empty>";
+        }
+        return result;
+    }
+
+    public interface Callback {
+        void onStatusChanged();
+        void onItemChanged(PlaylistItem item);
+    }
+}