Support7Demos: media router sample

Change-Id: Ie2a6a98cd256caae84f76dd398f9113f07cd4dd3
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index 1edf3e6..273af1e 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -21,6 +21,12 @@
      to come from a domain that you own or have control over. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.example.android.supportv7">
+    <!-- Permission for INTERNET is required for streaming video content
+         from the web, it's not required otherwise. -->
+    <uses-permission android:name="android.permission.INTERNET" />
+    <!-- Permission for SYSTEM_ALERT_WINDOW is only required for emulating
+         remote display using system alert window. -->
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
 
     <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="17" />
 
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_media_pause.png b/samples/Support7Demos/res/drawable-hdpi/ic_media_pause.png
new file mode 100644
index 0000000..1d465a4
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_media_pause.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_media_play.png b/samples/Support7Demos/res/drawable-hdpi/ic_media_play.png
new file mode 100644
index 0000000..2746d17
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_media_play.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_media_stop.png b/samples/Support7Demos/res/drawable-hdpi/ic_media_stop.png
new file mode 100644
index 0000000..a0ff136
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_media_stop.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_menu_add.png b/samples/Support7Demos/res/drawable-hdpi/ic_menu_add.png
new file mode 100644
index 0000000..444e8a5
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_menu_add.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-hdpi/ic_menu_delete.png b/samples/Support7Demos/res/drawable-hdpi/ic_menu_delete.png
new file mode 100644
index 0000000..24d8f6a
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-hdpi/ic_menu_delete.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_media_pause.png b/samples/Support7Demos/res/drawable-mdpi/ic_media_pause.png
new file mode 100644
index 0000000..3e6b2a1
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_media_pause.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_media_play.png b/samples/Support7Demos/res/drawable-mdpi/ic_media_play.png
new file mode 100644
index 0000000..7966bbc
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_media_play.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_media_stop.png b/samples/Support7Demos/res/drawable-mdpi/ic_media_stop.png
new file mode 100644
index 0000000..8ea7efe
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_media_stop.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_menu_add.png b/samples/Support7Demos/res/drawable-mdpi/ic_menu_add.png
new file mode 100644
index 0000000..361c7c4
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_menu_add.png
Binary files differ
diff --git a/samples/Support7Demos/res/drawable-mdpi/ic_menu_delete.png b/samples/Support7Demos/res/drawable-mdpi/ic_menu_delete.png
new file mode 100644
index 0000000..e2c8700
--- /dev/null
+++ b/samples/Support7Demos/res/drawable-mdpi/ic_menu_delete.png
Binary files differ
diff --git a/samples/Support7Demos/res/layout/media_item.xml b/samples/Support7Demos/res/layout/media_item.xml
new file mode 100644
index 0000000..e5d6d02
--- /dev/null
+++ b/samples/Support7Demos/res/layout/media_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<!-- Layout for list item in Library or Playlist view. Displays ImageButton
+     instead of radio button to the right of the item. -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        android:gravity="center_vertical">
+
+    <ImageButton android:id="@+id/item_action"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:minWidth="48dp"
+        android:minHeight="48dp"
+        android:layout_alignParentRight="true"
+        android:layout_centerVertical="true"
+        android:background="@null"/>
+
+    <TextView android:id="@+id/item_text"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_centerVertical="true"
+        android:layout_toLeftOf="@id/item_action"
+        android:layout_gravity="left"
+        android:gravity="left"/>
+</RelativeLayout>
diff --git a/samples/Support7Demos/res/layout/overlay_display_window.xml b/samples/Support7Demos/res/layout/overlay_display_window.xml
new file mode 100644
index 0000000..36b4a0d
--- /dev/null
+++ b/samples/Support7Demos/res/layout/overlay_display_window.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:background="#000000">
+    <TextureView android:id="@+id/overlay_display_window_texture"
+               android:layout_width="0px"
+               android:layout_height="0px" />
+    <TextView android:id="@+id/overlay_display_window_title"
+               android:layout_width="wrap_content"
+               android:layout_height="wrap_content"
+               android:layout_gravity="top|center_horizontal" />
+</FrameLayout>
diff --git a/samples/Support7Demos/res/layout/sample_media_router.xml b/samples/Support7Demos/res/layout/sample_media_router.xml
index a8b08b1..e2f7008 100644
--- a/samples/Support7Demos/res/layout/sample_media_router.xml
+++ b/samples/Support7Demos/res/layout/sample_media_router.xml
@@ -20,45 +20,118 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical">
-
-    <!-- Message to show to use. -->
-    <TextView android:id="@+id/text"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="0"
-        android:gravity="center_vertical|center_horizontal"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:text="@string/sample_media_router_text"/>
-
-    <!-- Some information about what's going on. -->
-    <TextView android:id="@+id/info"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="0"
-        android:gravity="center_vertical|center_horizontal"
-        android:textAppearance="?android:attr/textAppearanceMedium"/>
-
-    <!-- Some media to play. -->
-    <ListView android:id="@+id/media"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"/>
-
-    <!-- Control buttons for the currently selected route. -->
     <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="0">
-        <Button android:id="@+id/play_button"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="0"
-            android:text="@string/play_button_text" />
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:orientation="vertical">
+        <!-- Tabs for media library, playlist and statistics -->
+        <TabHost android:id="@+id/tabHost"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:layout_weight="1">
+            <LinearLayout
+                android:orientation="vertical"
+                android:layout_width="fill_parent"
+                android:layout_height="fill_parent">
+                <TabWidget android:id="@android:id/tabs"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content" />
 
-        <Button android:id="@+id/statistics_button"
+                <FrameLayout android:id="@android:id/tabcontent"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content">
+                    <LinearLayout android:id="@+id/tab1"
+                        android:layout_width="fill_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="vertical">
+                        <ListView android:id="@+id/media"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1" />
+                    </LinearLayout>
+
+                    <LinearLayout android:id="@+id/tab2"
+                        android:layout_width="fill_parent"
+                        android:layout_height="fill_parent"
+                        android:orientation="vertical">
+                        <ListView android:id="@+id/playlist"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1"/>
+                    </LinearLayout>
+
+                    <LinearLayout android:id="@+id/tab3"
+                        android:layout_width="fill_parent"
+                        android:layout_height="fill_parent"
+                        android:orientation="vertical">
+                        <TextView android:id="@+id/info"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_weight="1"
+                            android:textAppearance="?android:attr/textAppearanceMedium"/>
+                    </LinearLayout>
+                </FrameLayout>
+            </LinearLayout>
+        </TabHost>
+
+        <!-- Control buttons for the currently selected route. -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="0">
+
+            <SeekBar android:id="@+id/seekbar"
+                 android:layout_width="fill_parent"
+                 android:layout_height="wrap_content"
+                 style="?android:attr/progressBarStyleHorizontal"
+                 android:max="100"
+                 android:progress="0"
+                 android:layout_gravity="center"
+                 android:layout_weight="1"/>
+
+            <ImageButton android:id="@+id/pause_resume_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="0"
+                android:layout_gravity="right"
+                android:minWidth="48dp"
+                android:minHeight="48dp"
+                android:background="@null"
+                android:src="@drawable/ic_media_pause" />
+
+            <ImageButton android:id="@+id/stop_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="0"
+                android:layout_gravity="right"
+                android:minWidth="48dp"
+                android:minHeight="48dp"
+                android:background="@null"
+                android:src="@drawable/ic_media_stop" />
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <!-- Some content for visual interest in the case where no presentation is showing. -->
+    <FrameLayout android:id="@+id/player"
+        android:background="#ff000000"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center">
+            <SurfaceView android:id="@+id/surface_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"/>
+        </LinearLayout>
+        <TextView
+            android:textColor="#ffaaaaaa"
+            android:text="@string/sample_media_route_activity_local"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_weight="0"
-            android:text="@string/statistics_button_text" />
-    </LinearLayout>
+            android:layout_gravity="top|center_horizontal" />
+    </FrameLayout>
 </LinearLayout>
diff --git a/samples/Support7Demos/res/layout/sample_media_router_presentation.xml b/samples/Support7Demos/res/layout/sample_media_router_presentation.xml
new file mode 100644
index 0000000..f029627
--- /dev/null
+++ b/samples/Support7Demos/res/layout/sample_media_router_presentation.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- The content that we show on secondary displays.
+     See corresponding Java code PresentationWithMediaRouterActivity.java. -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#ff000000">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center">
+        <SurfaceView android:id="@+id/surface_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+    </LinearLayout>
+    <TextView
+        android:textColor="#ffaaaaaa"
+        android:text="@string/sample_media_route_activity_presentation"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top|center_horizontal" />
+</FrameLayout>
diff --git a/samples/Support7Demos/res/values/arrays.xml b/samples/Support7Demos/res/values/arrays.xml
index f5616a1..8d658eb 100644
--- a/samples/Support7Demos/res/values/arrays.xml
+++ b/samples/Support7Demos/res/values/arrays.xml
@@ -16,18 +16,16 @@
 
 <resources>
     <string-array name="media_names">
-        <item>My favorite video of cats</item>
-        <item>Cats on parade</item>
-        <item>Cats with hats</item>
-        <item>Hats on cats</item>
-        <item>Cats on mats</item>
+        <item>Big Buck Bunny</item>
+        <item>Elephants Dream</item>
+        <item>Sintel</item>
+        <item>Tears of Steel</item>
     </string-array>
 
     <string-array name="media_uris">
-        <item>http://cats.example.com/favorite-cats.mp4</item>
-        <item>http://cats.example.com/cats-on-parade.mp4</item>
-        <item>http://cats.example.com/cats-with-hats.mp4</item>
-        <item>http://cats.example.com/hats-on-cats.mp4</item>
-        <item>http://cats.example.com/cats-on-mats.mp4</item>
+        <item>http://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4</item>
+        <item>http://archive.org/download/ElephantsDream_277/elephant_dreams_640_512kb.mp4</item>
+        <item>http://archive.org/download/Sintel/sintel-2048-stereo_512kb.mp4</item>
+        <item>http://archive.org/download/Tears-of-Steel/tears_of_steel_720p.mp4</item>
     </string-array>
 </resources>
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index 6a8ac77..cbbc640 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -26,8 +26,9 @@
             use MediaRouter from the support library.  Select a route from the action bar.</string>
     <string name="media_route_menu_title">Play on...</string>
 
-    <string name="play_button_text">Play</string>
-    <string name="statistics_button_text">Show Statistics</string>
+    <string name="library_tab_text">Library</string>
+    <string name="playlist_tab_text">Playlist</string>
+    <string name="statistics_tab_text">Statistics</string>
 
     <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>
@@ -88,4 +89,8 @@
     <string name="action_bar_fragment_has_options_menu">Set has options menu to true</string>
     <string name="action_bar_fragment_menu_visibility">Set menu visibility to true</string>
 
+    <string name="sample_media_route_provider_remote">Remote Playback (Simulated)</string>
+    <string name="sample_media_route_activity_local">Local Playback</string>
+    <string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
+
 </resources>
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaPlayerWrapper.java b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaPlayerWrapper.java
new file mode 100644
index 0000000..9aa78d8
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaPlayerWrapper.java
@@ -0,0 +1,274 @@
+/*
+ * 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 com.example.android.supportv7.R;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.Log;
+import android.media.MediaPlayer;
+import android.view.Surface;
+import android.view.Gravity;
+import android.graphics.SurfaceTexture;
+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,
+        OverlayDisplayWindow.OverlayWindowListener,
+        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 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());
+        }
+    }
+
+    //OverlayDisplayWindow listeners
+    @Override
+    public void onWindowCreated(Surface surface) {
+        if (DEBUG) {
+            Log.d(TAG, "onWindowCreated");
+        }
+        mSurface = surface;
+        mMediaPlayer.setSurface(surface);
+    }
+
+    @Override
+    public void onWindowDestroyed() {
+        if (DEBUG) {
+            Log.d(TAG, "onWindowDestroyed");
+        }
+    }
+
+    //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();
+                }
+            }
+        });
+    }
+
+    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);
+        if (mSurface != null) {
+            mMediaPlayer.setSurface(mSurface);
+        }
+        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!");
+            }
+        }
+    }
+
+    public static abstract class Callback {
+        public void onError() {}
+        public void onCompletion() {}
+        public void onStatusChanged() {}
+        public void onSizeChanged(int width, int height) {}
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaQueueItem.java b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaQueueItem.java
new file mode 100644
index 0000000..5440d86
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaQueueItem.java
@@ -0,0 +1,106 @@
+/*
+ * 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.support.v7.media.MediaItemStatus;
+import android.net.Uri;
+import android.app.PendingIntent;
+
+/**
+ * MediaQueueItem helps keep track of the current status of an media item.
+ */
+final class MediaQueueItem {
+    // immutables
+    private final String mSessionId;
+    private final String mItemId;
+    private final Uri mUri;
+    private final PendingIntent mUpdateReceiver;
+    // changeable states
+    private int mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING;
+    private long mContentPosition;
+    private long mContentDuration;
+
+    public MediaQueueItem(String qid, String iid, Uri uri, PendingIntent pi) {
+        mSessionId = qid;
+        mItemId = iid;
+        mUri = uri;
+        mUpdateReceiver = pi;
+    }
+
+    public void setState(int state) {
+        mPlaybackState = state;
+    }
+
+    public void setContentPosition(long pos) {
+        mContentPosition = pos;
+    }
+
+    public void setContentDuration(long duration) {
+        mContentDuration = duration;
+    }
+
+    public String getSessionId() {
+        return mSessionId;
+    }
+
+    public String getItemId() {
+        return mItemId;
+    }
+
+    public Uri getUri() {
+        return mUri;
+    }
+
+    public PendingIntent getUpdateReceiver() {
+        return mUpdateReceiver;
+    }
+
+    public int getState() {
+        return mPlaybackState;
+    }
+
+    public long getContentPosition() {
+        return mContentPosition;
+    }
+
+    public long getContentDuration() {
+        return mContentDuration;
+    }
+
+    public MediaItemStatus getStatus() {
+        return new MediaItemStatus.Builder(mPlaybackState)
+            .setContentPosition(mContentPosition)
+            .setContentDuration(mContentDuration)
+            .build();
+    }
+
+    @Override
+    public String toString() {
+        String state[] = {
+            "PENDING",
+            "PLAYING",
+            "PAUSED",
+            "BUFFERING",
+            "FINISHED",
+            "CANCELED",
+            "INVALIDATED",
+            "ERROR"
+        };
+        return "[" + mSessionId + "|" + mItemId + "|"
+            + state[mPlaybackState] + "] " + mUri.toString();
+    }
+}
\ No newline at end of file
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/media/MediaSessionManager.java b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaSessionManager.java
new file mode 100644
index 0000000..9258998
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/MediaSessionManager.java
@@ -0,0 +1,289 @@
+/*
+ * 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/OverlayDisplayWindow.java b/samples/Support7Demos/src/com/example/android/supportv7/media/OverlayDisplayWindow.java
new file mode 100644
index 0000000..11a2889
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/OverlayDisplayWindow.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2012 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 com.example.android.supportv7.R;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.display.DisplayManager;
+import android.util.Log;
+import android.view.Display;
+import android.util.DisplayMetrics;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.TextureView;
+import android.view.View;
+import android.view.Surface;
+import android.view.WindowManager;
+import android.view.TextureView.SurfaceTextureListener;
+import android.widget.TextView;
+
+/**
+ * Manages an overlay display window, used for simulating remote playback.
+ */
+public class OverlayDisplayWindow {
+    private static final String TAG = "OverlayDisplayWindow";
+    private static final boolean DEBUG = false;
+
+    private final float INITIAL_SCALE = 0.5f;
+    private final float MIN_SCALE = 0.3f;
+    private final float MAX_SCALE = 1.0f;
+    private final float WINDOW_ALPHA = 0.8f;
+
+    // When true, disables support for moving and resizing the overlay.
+    // The window is made non-touchable, which makes it possible to
+    // directly interact with the content underneath.
+    private final boolean DISABLE_MOVE_AND_RESIZE = false;
+
+    private final Context mContext;
+    private final int mWidth;
+    private final int mHeight;
+    private final int mGravity;
+    private OverlayWindowListener mListener;
+    private final String mTitle;
+
+    private final DisplayManager mDisplayManager;
+    private final WindowManager mWindowManager;
+
+
+    private final Display mDefaultDisplay;
+    private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics();
+
+    private View mWindowContent;
+    private WindowManager.LayoutParams mWindowParams;
+    private TextureView mTextureView;
+    private TextView mTitleTextView;
+
+    private GestureDetector mGestureDetector;
+    private ScaleGestureDetector mScaleGestureDetector;
+
+    private boolean mWindowVisible;
+    private int mWindowX;
+    private int mWindowY;
+    private float mWindowScale;
+
+    private float mLiveTranslationX;
+    private float mLiveTranslationY;
+    private float mLiveScale = 1.0f;
+
+    public OverlayDisplayWindow(Context context, String name,
+            int width, int height, int gravity) {
+        mContext = context;
+        mWidth = width;
+        mHeight = height;
+        mGravity = gravity;
+        mTitle = name;
+
+        mDisplayManager = (DisplayManager)context.getSystemService(
+                Context.DISPLAY_SERVICE);
+        mWindowManager = (WindowManager)context.getSystemService(
+                Context.WINDOW_SERVICE);
+
+        mDefaultDisplay = mWindowManager.getDefaultDisplay();
+        updateDefaultDisplayInfo();
+
+        createWindow();
+    }
+
+    public void setOverlayWindowListener(OverlayWindowListener listener) {
+        mListener = listener;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public void show() {
+        if (!mWindowVisible) {
+            mDisplayManager.registerDisplayListener(mDisplayListener, null);
+            if (!updateDefaultDisplayInfo()) {
+                mDisplayManager.unregisterDisplayListener(mDisplayListener);
+                return;
+            }
+
+            clearLiveState();
+            updateWindowParams();
+            mWindowManager.addView(mWindowContent, mWindowParams);
+            mWindowVisible = true;
+        }
+    }
+
+    public void dismiss() {
+        if (mWindowVisible) {
+            mDisplayManager.unregisterDisplayListener(mDisplayListener);
+            mWindowManager.removeView(mWindowContent);
+            mWindowVisible = false;
+        }
+    }
+
+    public void relayout() {
+        if (mWindowVisible) {
+            updateWindowParams();
+            mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
+        }
+    }
+
+    public void updateAspectRatio(int width, int height) {
+        if (mWidth * height < mHeight * width) {
+            mTextureView.getLayoutParams().width = mWidth;
+            mTextureView.getLayoutParams().height = mWidth * height / width;
+        } else {
+            mTextureView.getLayoutParams().width = mHeight * width / height;
+            mTextureView.getLayoutParams().height = mHeight;
+        }
+        relayout();
+    }
+
+    private boolean updateDefaultDisplayInfo() {
+        mDefaultDisplay.getMetrics(mDefaultDisplayMetrics);
+        return true;
+    }
+
+    private void createWindow() {
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+
+        mWindowContent = inflater.inflate(
+                R.layout.overlay_display_window, null);
+        mWindowContent.setOnTouchListener(mOnTouchListener);
+
+        mTextureView = (TextureView)mWindowContent.findViewById(
+                R.id.overlay_display_window_texture);
+        mTextureView.setPivotX(0);
+        mTextureView.setPivotY(0);
+        mTextureView.getLayoutParams().width = mWidth;
+        mTextureView.getLayoutParams().height = mHeight;
+        mTextureView.setOpaque(false);
+        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
+
+        mTitleTextView = (TextView)mWindowContent.findViewById(
+                R.id.overlay_display_window_title);
+        mTitleTextView.setText(mTitle);
+
+        mWindowParams = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+        mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+        if (DISABLE_MOVE_AND_RESIZE) {
+            mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+        }
+        mWindowParams.alpha = WINDOW_ALPHA;
+        mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
+        mWindowParams.setTitle(mTitle);
+
+        mGestureDetector = new GestureDetector(mContext, mOnGestureListener);
+        mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);
+
+        // Set the initial position and scale.
+        // The position and scale will be clamped when the display is first shown.
+        mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ?
+                0 : mDefaultDisplayMetrics.widthPixels;
+        mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ?
+                0 : mDefaultDisplayMetrics.heightPixels;
+        Log.d(TAG, mDefaultDisplayMetrics.toString());
+        mWindowScale = INITIAL_SCALE;
+
+        // calculate and save initial settings
+        updateWindowParams();
+        saveWindowParams();
+    }
+
+    private void updateWindowParams() {
+        float scale = mWindowScale * mLiveScale;
+        scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth);
+        scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight);
+        scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
+
+        float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f;
+        int width = (int)(mWidth * scale);
+        int height = (int)(mHeight * scale);
+        int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale);
+        int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale);
+        x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width));
+        y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height));
+
+        if (DEBUG) {
+            Log.d(TAG, "updateWindowParams: scale=" + scale
+                    + ", offsetScale=" + offsetScale
+                    + ", x=" + x + ", y=" + y
+                    + ", width=" + width + ", height=" + height);
+        }
+
+        mTextureView.setScaleX(scale);
+        mTextureView.setScaleY(scale);
+
+        mTextureView.setTranslationX(
+                (mWidth - mTextureView.getLayoutParams().width) * scale / 2);
+        mTextureView.setTranslationY(
+                (mHeight - mTextureView.getLayoutParams().height) * scale / 2);
+
+        mWindowParams.x = x;
+        mWindowParams.y = y;
+        mWindowParams.width = width;
+        mWindowParams.height = height;
+    }
+
+    private void saveWindowParams() {
+        mWindowX = mWindowParams.x;
+        mWindowY = mWindowParams.y;
+        mWindowScale = mTextureView.getScaleX();
+        clearLiveState();
+    }
+
+    private void clearLiveState() {
+        mLiveTranslationX = 0f;
+        mLiveTranslationY = 0f;
+        mLiveScale = 1.0f;
+    }
+
+    private final DisplayManager.DisplayListener mDisplayListener =
+            new DisplayManager.DisplayListener() {
+        @Override
+        public void onDisplayAdded(int displayId) {
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+            if (displayId == mDefaultDisplay.getDisplayId()) {
+                if (updateDefaultDisplayInfo()) {
+                    relayout();
+                } else {
+                    dismiss();
+                }
+            }
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+            if (displayId == mDefaultDisplay.getDisplayId()) {
+                dismiss();
+            }
+        }
+    };
+
+    private final SurfaceTextureListener mSurfaceTextureListener =
+            new SurfaceTextureListener() {
+        @Override
+        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
+                int width, int height) {
+            if (mListener != null) {
+                mListener.onWindowCreated(new Surface(surfaceTexture));
+            }
+        }
+
+        @Override
+        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+            if (mListener != null) {
+                mListener.onWindowDestroyed();
+            }
+            return true;
+        }
+
+        @Override
+        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
+                int width, int height) {
+        }
+
+        @Override
+        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+        }
+    };
+
+    private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
+        @Override
+        public boolean onTouch(View view, MotionEvent event) {
+            // Work in screen coordinates.
+            final float oldX = event.getX();
+            final float oldY = event.getY();
+            event.setLocation(event.getRawX(), event.getRawY());
+
+            mGestureDetector.onTouchEvent(event);
+            mScaleGestureDetector.onTouchEvent(event);
+
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    saveWindowParams();
+                    break;
+            }
+
+            // Revert to window coordinates.
+            event.setLocation(oldX, oldY);
+            return true;
+        }
+    };
+
+    private final GestureDetector.OnGestureListener mOnGestureListener =
+            new GestureDetector.SimpleOnGestureListener() {
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2,
+                float distanceX, float distanceY) {
+            mLiveTranslationX -= distanceX;
+            mLiveTranslationY -= distanceY;
+            relayout();
+            return true;
+        }
+    };
+
+    private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
+            new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            mLiveScale *= detector.getScaleFactor();
+            relayout();
+            return true;
+        }
+    };
+
+    // Watches for significant changes in the overlay display window lifecycle.
+    public interface OverlayWindowListener {
+        public void onWindowCreated(Surface surface);
+        public void onWindowDestroyed();
+    }
+}
\ No newline at end of file
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 0d5c21d..2ab61a5 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouteProvider.java
@@ -26,6 +26,7 @@
 import android.media.MediaRouter;
 import android.net.Uri;
 import android.os.Bundle;
+import android.app.PendingIntent;
 import android.support.v7.media.MediaControlIntent;
 import android.support.v7.media.MediaItemStatus;
 import android.support.v7.media.MediaRouteProvider;
@@ -34,9 +35,8 @@
 import android.support.v7.media.MediaRouteDescriptor;
 import android.util.Log;
 import android.widget.Toast;
-
+import android.view.Gravity;
 import java.util.ArrayList;
-import java.util.UUID;
 
 /**
  * Demonstrates how to create a custom media route provider.
@@ -77,6 +77,14 @@
     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;
     static {
         IntentFilter f1 = new IntentFilter();
@@ -88,11 +96,39 @@
         f2.addAction(MediaControlIntent.ACTION_PLAY);
         f2.addDataScheme("http");
         f2.addDataScheme("https");
+        f2.addDataScheme("rtsp");
+        f2.addDataScheme("file");
         addDataTypeUnchecked(f2, "video/*");
 
+        IntentFilter f3 = new IntentFilter();
+        f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        f3.addAction(MediaControlIntent.ACTION_SEEK);
+        f3.addAction(MediaControlIntent.ACTION_GET_STATUS);
+        f3.addAction(MediaControlIntent.ACTION_PAUSE);
+        f3.addAction(MediaControlIntent.ACTION_RESUME);
+        f3.addAction(MediaControlIntent.ACTION_STOP);
+
+        IntentFilter f4 = new IntentFilter();
+        f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+        f4.addAction(MediaControlIntent.ACTION_ENQUEUE);
+        f4.addDataScheme("http");
+        f4.addDataScheme("https");
+        f4.addDataScheme("rtsp");
+        f4.addDataScheme("file");
+        addDataTypeUnchecked(f4, "video/*");
+
+        IntentFilter f5 = new IntentFilter();
+        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);
+        }
     }
 
     private static void addDataTypeUnchecked(IntentFilter filter, String type) {
@@ -104,7 +140,7 @@
     }
 
     private int mVolume = 5;
-    private int mPlaybackCount;
+    private int mEnqueueCount;
 
     public SampleMediaRouteProvider(Context context) {
         super(context);
@@ -149,31 +185,47 @@
         setDescriptor(providerDescriptor);
     }
 
-    private String generateStreamId() {
-        return UUID.randomUUID().toString();
+    private void showToast(String msg) {
+        Toast toast = Toast.makeText(getContext(),
+                "[provider] " + msg, Toast.LENGTH_LONG);
+        toast.setGravity(Gravity.TOP, 0, 100);
+        toast.show();
     }
 
     private final class SampleRouteController extends MediaRouteProvider.RouteController {
         private final String mRouteId;
+        // Create an overlay display window (used for simulating the remote playback only)
+        private final OverlayDisplayWindow mOverlay = new OverlayDisplayWindow(getContext(),
+                getContext().getResources().getString(R.string.sample_media_route_provider_remote),
+                1024, 768, Gravity.CENTER);
+        private final MediaPlayerWrapper mMediaPlayer = new MediaPlayerWrapper(getContext());
+        private final MediaSessionManager mSessionManager = new MediaSessionManager();
 
         public SampleRouteController(String routeId) {
             mRouteId = routeId;
+            mSessionManager.setCallback(mMediaPlayer);
+            mOverlay.setOverlayWindowListener(mMediaPlayer);
+            mMediaPlayer.setCallback(new MediaPlayerCallback());
             Log.d(TAG, mRouteId + ": Controller created");
         }
 
         @Override
         public void onRelease() {
             Log.d(TAG, mRouteId + ": Controller released");
+            mMediaPlayer.release();
         }
 
         @Override
         public void onSelect() {
             Log.d(TAG, mRouteId + ": Selected");
+            mOverlay.show();
         }
 
         @Override
         public void onUnselect() {
             Log.d(TAG, mRouteId + ": Unselected");
+            mMediaPlayer.onStop();
+            mOverlay.dismiss();
         }
 
         @Override
@@ -192,6 +244,45 @@
             }
         }
 
+        @Override
+        public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
+            Log.d(TAG, mRouteId + ": Received control request " + intent);
+            String action = intent.getAction();
+            if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+                boolean success = false;
+                if (action.equals(MediaControlIntent.ACTION_PLAY)) {
+                    success = handlePlay(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
+                    success = handleEnqueue(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_REMOVE)) {
+                    success = handleRemove(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_SEEK)) {
+                    success = handleSeek(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) {
+                    success = handleGetStatus(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) {
+                    success = handlePause(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_RESUME)) {
+                    success = handleResume(intent, callback);
+                } else if (action.equals(MediaControlIntent.ACTION_STOP)) {
+                    success = handleStop(intent, callback);
+                }
+                Log.d(TAG, mSessionManager.toString());
+                return success;
+            }
+
+            if (action.equals(ACTION_GET_STATISTICS)
+                    && intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
+                Bundle data = new Bundle();
+                data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount);
+                if (callback != null) {
+                    callback.onResult(data);
+                }
+                return true;
+            }
+            return false;
+        }
+
         private void setVolumeInternal(int volume) {
             if (volume >= 0 && volume <= VOLUME_MAX) {
                 mVolume = volume;
@@ -200,63 +291,198 @@
             }
         }
 
-        @Override
-        public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
-            Log.d(TAG, mRouteId + ": Received control request " + intent);
-            if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)
-                    && intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
-                    && intent.getData() != null) {
-                mPlaybackCount +=1;
-
-                // TODO: Handle queue ids.
-                Uri uri = intent.getData();
-                long contentPositionMillis = 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);
-
-                Log.d(TAG, mRouteId + ": Received play request, uri=" + uri
-                        + ", contentPositionMillis=" + contentPositionMillis
-                        + ", metadata=" + metadata
-                        + ", headers=" + headers);
-
-                if (uri.toString().contains("hats")) {
-                    // Simulate generating an error whenever the uri contains the word 'hats'.
-                    Toast.makeText(getContext(), "Route rejected play request: uri=" + uri
-                            + ", no hats allowed!", Toast.LENGTH_LONG).show();
-                    if (callback != null) {
-                        callback.onError("Simulated error.  No hats allowed!", null);
-                    }
-                } else {
-                    Toast.makeText(getContext(), "Route received play request: uri=" + uri,
-                            Toast.LENGTH_LONG).show();
-                    String streamId = generateStreamId();
-                    if (callback != null) {
-                        MediaItemStatus status = new MediaItemStatus.Builder(
-                                MediaItemStatus.PLAYBACK_STATE_PLAYING)
-                                .setContentPosition(contentPositionMillis)
-                                .build();
-
-                        Bundle result = new Bundle();
-                        result.putString(MediaControlIntent.EXTRA_ITEM_ID, streamId);
-                        result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, status.asBundle());
-                        callback.onResult(result);
-                    }
-                }
-                return true;
-            }
-
-            if (intent.getAction().equals(ACTION_GET_STATISTICS)
-                    && intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
-                Bundle data = new Bundle();
-                data.putInt(DATA_PLAYBACK_COUNT, mPlaybackCount);
-                if (callback != null) {
-                    callback.onResult(data);
-                }
-                return true;
+        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);
             }
             return false;
         }
+
+        private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) {
+            if (intent.getData() == null) {
+                return false;
+            }
+
+            mEnqueueCount +=1;
+
+            boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE);
+            Uri uri = intent.getData();
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            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);
+            PendingIntent receiver = (PendingIntent)intent.getParcelableExtra(
+                    MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER);
+
+            Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request"
+                    + ", uri=" + uri
+                    + ", sid=" + sid
+                    + ", pos=" + pos
+                    + ", metadata=" + metadata
+                    + ", headers=" + headers
+                    + ", receiver=" + receiver);
+            MediaQueueItem item = mSessionManager.enqueue(sid, uri, receiver);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
+                    result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to open " + uri.toString(), null);
+                }
+            }
+            return true;
+        }
+
+        private boolean handleRemove(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
+            MediaQueueItem item = mSessionManager.remove(sid, iid);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to remove" +
+                            ", sid=" + sid + ", iid=" + iid, null);
+                }
+            }
+            return (item != null);
+        }
+
+        private boolean handleSeek(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            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);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to seek" +
+                            ", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null);
+                }
+            }
+            return (item != null);
+        }
+
+        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);
+            if (callback != null) {
+                if (item != null) {
+                    Bundle result = new Bundle();
+                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    callback.onResult(result);
+                } else {
+                    callback.onError("Failed to get status" +
+                            ", sid=" + sid + ", iid=" + iid, null);
+                }
+            }
+            return (item != null);
+        }
+
+        private boolean handlePause(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = mSessionManager.pause(sid);
+            if (callback != null) {
+                if (success) {
+                    callback.onResult(null);
+                } else {
+                    callback.onError("Failed to pause, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private boolean handleResume(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = mSessionManager.resume(sid);
+            if (callback != null) {
+                if (success) {
+                    callback.onResult(null);
+                } else {
+                    callback.onError("Failed to resume, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private boolean handleStop(Intent intent, ControlRequestCallback callback) {
+            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+            boolean success = mSessionManager.stop(sid);
+            if (callback != null) {
+                if (success) {
+                    callback.onResult(null);
+                } else {
+                    callback.onError("Failed to stop, sid=" + sid, null);
+                }
+            }
+            return success;
+        }
+
+        private void handleFinish(boolean error) {
+            MediaQueueItem item = mSessionManager.finish(error);
+            if (item != null) {
+                handleStatusChange(item);
+            }
+        }
+
+        private void handleStatusChange(MediaQueueItem item) {
+            if (item == null) {
+                item = mSessionManager.getCurrentItem();
+            }
+            if (item != null) {
+                PendingIntent receiver = item.getUpdateReceiver();
+                if (receiver != null) {
+                    Intent intent = new Intent();
+                    intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
+                    intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
+                    intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS,
+                            item.getStatus().asBundle());
+                    try {
+                        receiver.send(getContext(), 0, intent);
+                        Log.d(TAG, mRouteId + ": Sending status update from provider");
+                    } catch (PendingIntent.CanceledException e) {
+                        Log.d(TAG, mRouteId + ": Failed to send status update!");
+                    }
+                }
+            }
+        }
+
+        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);
+            }
+        }
     }
 }
\ No newline at end of file
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 456bd15..cca7e10 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/media/SampleMediaRouterActivity.java
@@ -18,9 +18,18 @@
 
 import com.example.android.supportv7.R;
 
+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.net.Uri;
+import android.os.Handler;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.view.MenuItemCompat;
 import android.support.v7.app.ActionBarActivity;
@@ -32,18 +41,38 @@
 import android.support.v7.media.MediaRouter.RouteInfo;
 import android.support.v7.media.MediaRouter.ProviderInfo;
 import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaItemStatus;
 import android.util.Log;
+import android.view.Gravity;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.widget.AdapterView.OnItemClickListener;
+import android.view.ViewGroup;
+import android.view.Display;
+import android.view.Surface;
+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.Button;
+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.util.ArrayList;
+import java.util.List;
+import java.io.File;
 
 /**
  * <h3>Media Router Support Activity</h3>
@@ -57,14 +86,220 @@
 public class SampleMediaRouterActivity extends ActionBarActivity {
     private static final String TAG = "MediaRouterSupport";
     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;
-    private ArrayAdapter<MediaItem> mMediaItems;
+    private LibraryAdapter mLibraryItems;
+    private PlaylistAdapter mPlayListItems;
     private TextView mInfoTextView;
-    private ListView mMediaListView;
-    private Button mPlayButton;
-    private Button mStatisticsButton;
+    private ListView mLibraryView;
+    private ListView mPlayListView;
+    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());
+            // 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) {
+            mLocalPlayer.updateSize(width, height);
+        }
+
+        @Override
+        public void onStatusChanged() {
+            if (!mSeeking) {
+                updateUi();
+            }
+        }
+    };
+
+    private final RemotePlayer mRemotePlayer = new RemotePlayer();
+    private final LocalPlayer mLocalPlayer = new LocalPlayer();
+    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
+        // for demonstration purposes.
+        @Override
+        public void onRouteAdded(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteAdded: route=" + route);
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteChanged: route=" + route);
+            mPlayer.showStatistics();
+        }
+
+        @Override
+        public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteRemoved: route=" + route);
+        }
+
+        @Override
+        public void onRouteSelected(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteSelected: route=" + route);
+
+            Player player = mPlayer;
+            MediaSessionManager.Callback playerCB = mPlayerCB;
+
+            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());
+                }
+            }
+            updateUi();
+        }
+
+        @Override
+        public void onRouteUnselected(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteUnselected: route=" + route);
+            mPlayer.showStatistics();
+        }
+
+        @Override
+        public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRouteVolumeChanged: route=" + route);
+        }
+
+        @Override
+        public void onRoutePresentationDisplayChanged(
+                MediaRouter router, RouteInfo route) {
+            Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route);
+        }
+
+        @Override
+        public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
+            Log.d(TAG, "onRouteProviderAdded: provider=" + provider);
+        }
+
+        @Override
+        public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
+            Log.d(TAG, "onRouteProviderRemoved: provider=" + provider);
+        }
+
+        @Override
+        public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
+            Log.d(TAG, "onRouteProviderChanged: provider=" + provider);
+        }
+    };
+
+    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();
+                    }
+                }
+            }
+        }
+    };
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -86,61 +321,192 @@
         // This fragment automatically adds or removes a callback whenever the activity
         // is started or stopped.
         FragmentManager fm = getSupportFragmentManager();
-        if (fm.findFragmentByTag(DISCOVERY_FRAGMENT_TAG) == null) {
-            DiscoveryFragment fragment = new DiscoveryFragment();
+        DiscoveryFragment fragment = (DiscoveryFragment)fm.findFragmentByTag(
+                DISCOVERY_FRAGMENT_TAG);
+        if (fragment == null) {
+            fragment = new DiscoveryFragment(mMediaRouterCB);
             fragment.setRouteSelector(mSelector);
             fm.beginTransaction()
                     .add(fragment, DISCOVERY_FRAGMENT_TAG)
                     .commit();
+        } else {
+            fragment.setCallback(mMediaRouterCB);
+            fragment.setRouteSelector(mSelector);
         }
 
-        // Populate an array adapter with fake media items.
+        // Populate an array adapter with streaming media items.
         String[] mediaNames = getResources().getStringArray(R.array.media_names);
         String[] mediaUris = getResources().getStringArray(R.array.media_uris);
-        mMediaItems = new ArrayAdapter<MediaItem>(this,
-                android.R.layout.simple_list_item_single_choice, android.R.id.text1);
+        mLibraryItems = new LibraryAdapter();
         for (int i = 0; i < mediaNames.length; i++) {
-            mMediaItems.add(new MediaItem(mediaNames[i], Uri.parse(mediaUris[i])));
+            mLibraryItems.add(new MediaItem(
+                    "[streaming] "+mediaNames[i], Uri.parse(mediaUris[i])));
         }
 
+        // Scan local /sdcard/ directory for media files.
+        String sdcard = "/sdcard/";
+        File file = new File(sdcard);
+        File list[] = file.listFiles();
+        for (int i = 0; i < list.length; i++) {
+            String filename = list[i].getName();
+            if (filename.matches(".*\\.(m4v|mp4)")) {
+                mLibraryItems.add(new MediaItem(
+                        "[local] "+filename, Uri.parse("file:///sdcard/" + filename)));
+            }
+        }
+
+        mPlayListItems = new PlaylistAdapter();
+
         // Initialize the layout.
         setContentView(R.layout.sample_media_router);
 
-        mInfoTextView = (TextView)findViewById(R.id.info);
+        TabHost tabHost=(TabHost)findViewById(R.id.tabHost);
+        tabHost.setup();
+        String tabName = getResources().getString(R.string.library_tab_text);
+        TabSpec spec1=tabHost.newTabSpec(tabName);
+        spec1.setContent(R.id.tab1);
+        spec1.setIndicator(tabName);
 
-        mMediaListView = (ListView)findViewById(R.id.media);
-        mMediaListView.setAdapter(mMediaItems);
-        mMediaListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
-        mMediaListView.setOnItemClickListener(new OnItemClickListener() {
+        tabName = getResources().getString(R.string.playlist_tab_text);
+        TabSpec spec2=tabHost.newTabSpec(tabName);
+        spec2.setIndicator(tabName);
+        spec2.setContent(R.id.tab2);
+
+        tabName = getResources().getString(R.string.statistics_tab_text);
+        TabSpec spec3=tabHost.newTabSpec(tabName);
+        spec3.setIndicator(tabName);
+        spec3.setContent(R.id.tab3);
+
+        tabHost.addTab(spec1);
+        tabHost.addTab(spec2);
+        tabHost.addTab(spec3);
+        tabHost.setOnTabChangedListener(new OnTabChangeListener() {
+            @Override
+            public void onTabChanged(String arg0) {
+                if (arg0.equals(getResources().getString(
+                        R.string.statistics_tab_text))) {
+                    mPlayer.showStatistics();
+                }
+                updateUi();
+            }
+        });
+
+        mLibraryView = (ListView) findViewById(R.id.media);
+        mLibraryView.setAdapter(mLibraryItems);
+        mLibraryView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mLibraryView.setOnItemClickListener(new OnItemClickListener() {
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 updateButtons();
             }
         });
 
-        mPlayButton = (Button)findViewById(R.id.play_button);
-        mPlayButton.setOnClickListener(new OnClickListener() {
+        mPlayListView = (ListView) findViewById(R.id.playlist);
+        mPlayListView.setAdapter(mPlayListItems);
+        mPlayListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mPlayListView.setOnItemClickListener(new OnItemClickListener() {
             @Override
-            public void onClick(View v) {
-                play();
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                updateButtons();
             }
         });
 
-        mStatisticsButton = (Button)findViewById(R.id.statistics_button);
-        mStatisticsButton.setOnClickListener(new OnClickListener() {
+        mInfoTextView = (TextView) findViewById(R.id.info);
+
+        mPauseResumeButton = (ImageButton)findViewById(R.id.pause_resume_button);
+        mPauseResumeButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
-                showStatistics();
+                if (!mPaused) {
+                    mPlayer.pause();
+                } else {
+                    mPlayer.resume();
+                }
             }
         });
+
+        mStopButton = (ImageButton)findViewById(R.id.stop_button);
+        mStopButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mPlayer.stop();
+                clearContent();
+            }
+        });
+
+        mSeekBar = (SeekBar) findViewById(R.id.seekbar);
+        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();
+                }
+            }
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+                mSeeking = true;
+            }
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                mSeeking = false;
+                updateUi();
+            }
+        });
+
+        // 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);
     }
 
     @Override
     public void onStart() {
         // Be sure to call the super class.
         super.onStart();
+        mPlayer.showStatistics();
+    }
 
-        updateRouteDescription();
+    @Override
+    public void onPause() {
+        // pause media player for local playback case only
+        if (!isRemotePlayback() && !mPaused) {
+            mNeedResume = true;
+            mPlayer.pause();
+        }
+        super.onPause();
+    }
+
+    @Override
+    public void onResume() {
+        // resume media player for local playback case only
+        if (!isRemotePlayback() && mNeedResume) {
+            mPlayer.resume();
+            mNeedResume = false;
+        }
+        super.onResume();
+    }
+
+    @Override
+    public void onDestroy() {
+        // Unregister broadcast receiver
+        unregisterReceiver(mReceiver);
+        mPlayer.stop();
+        mMediaPlayer.release();
+        super.onDestroy();
     }
 
     @Override
@@ -162,185 +528,864 @@
 
     private void updateRouteDescription() {
         RouteInfo route = mMediaRouter.getSelectedRoute();
-        mInfoTextView.setText("Currently selected route: " + route.getName()
-                + " from provider " + route.getProvider().getPackageName()
-                + ", description: " + route.getDescription());
+        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();
-
-        MediaItem item = getCheckedMediaItem();
-        if (item != null) {
-            mPlayButton.setEnabled(route.supportsControlRequest(makePlayIntent(item)));
-        } else {
-            mPlayButton.setEnabled(false);
-        }
-
-        mStatisticsButton.setEnabled(route.supportsControlRequest(makeStatisticsIntent()));
+        // 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);
     }
 
-    private void play() {
-        final MediaItem item = getCheckedMediaItem();
-        if (item == null) {
-            return;
-        }
-
-        MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute();
-        Intent intent = makePlayIntent(item);
-        if (route.supportsControlRequest(intent)) {
-            MediaRouter.ControlRequestCallback callback =
-                    new MediaRouter.ControlRequestCallback() {
-                @Override
-                public void onResult(Bundle data) {
-                    String streamId = data != null ? data.getString(
-                            MediaControlIntent.EXTRA_ITEM_ID) : null;
-
-                    Log.d(TAG, "Play request succeeded: data=" + data + " , streamId=" + streamId);
-                    Toast.makeText(SampleMediaRouterActivity.this,
-                            "Now playing " + item.mName,
-                            Toast.LENGTH_LONG).show();
+    private void updateProgress(MediaQueueItem queueItem) {
+        // 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();
+            if (duration <= 0) {
+                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
+                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
+                    updateUi();
                 }
-
-                @Override
-                public void onError(String error, Bundle data) {
-                    Log.d(TAG, "Play request failed: error=" + error + ", data=" + data);
-                    Toast.makeText(SampleMediaRouterActivity.this,
-                            "Unable to play " + item.mName + ", error: " + error,
-                            Toast.LENGTH_LONG).show();
-                }
-            };
-
-            Log.d(TAG, "Sending play request: intent=" + intent);
-            route.sendControlRequest(intent, callback);
-        } else {
-            Log.d(TAG, "Play request not supported: intent=" + intent);
-            Toast.makeText(SampleMediaRouterActivity.this,
-                    "Play not supported for " + item.mName, Toast.LENGTH_LONG).show();
+            } else {
+                long position = queueItem.getContentPosition();
+                long timeDelta = mPaused ? 0 :
+                        (SystemClock.elapsedRealtime() - mLastStatusTime);
+                progress = (int)(100.0 * (position + timeDelta) / duration);
+            }
         }
+        mSeekBar.setProgress(progress);
     }
 
-    private 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);
-                        Toast.makeText(SampleMediaRouterActivity.this,
-                                "Total playback count: " + playbackCount,
-                                Toast.LENGTH_LONG).show();
-                    } else {
-                        Toast.makeText(SampleMediaRouterActivity.this,
-                                "Statistics query did not return any data",
-                                Toast.LENGTH_LONG).show();
-                    }
-                }
+    private void updateUi() {
+        updatePlaylist();
+        updateButtons();
+    }
 
-                @Override
-                public void onError(String error, Bundle data) {
-                    Log.d(TAG, "Statistics request failed: error=" + error + ", data=" + data);
-                    Toast.makeText(SampleMediaRouterActivity.this,
-                            "Unable to query statistics, error: " + error,
-                            Toast.LENGTH_LONG).show();
-                }
-            };
-
-            Log.d(TAG, "Sent statistics request: intent=" + intent);
-            route.sendControlRequest(intent, callback);
-        } else {
-            Log.d(TAG, "Statistics request not supported: intent=" + intent);
-            Toast.makeText(SampleMediaRouterActivity.this,
-                    "Statistics not supported.", Toast.LENGTH_LONG).show();
+    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();
+        mPlayListView.invalidate();
 
-    private Intent makePlayIntent(MediaItem item) {
-        Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
-        intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-        intent.setDataAndType(item.mUri, "video/mp4");
-        return intent;
-    }
-
-    private Intent makeStatisticsIntent() {
-        Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS);
-        intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
-        return intent;
+        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 MediaItem getCheckedMediaItem() {
-        int index = mMediaListView.getCheckedItemPosition();
-        if (index >= 0 && index < mMediaItems.getCount()) {
-            return mMediaItems.getItem(index);
+        int index = mLibraryView.getCheckedItemPosition();
+        if (index >= 0 && index < mLibraryItems.getCount()) {
+            return mLibraryItems.getItem(index);
         }
         return null;
     }
 
-    private final class DiscoveryFragment extends MediaRouteDiscoveryFragment {
+    private MediaQueueItem getCheckedMediaQueueItem() {
+        int count = mPlayListView.getCount();
+        int index = mPlayListView.getCheckedItemPosition();
+        if (count > 0) {
+            if (index < 0 || index >= count) {
+                index = 0;
+                mPlayListView.setItemChecked(0, true);
+            }
+            return mPlayListItems.getItem(index);
+        }
+        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 abstract class Player {
+        abstract void enqueue(final Uri uri, long pos);
+        abstract void remove(final MediaQueueItem item);
+        abstract void seek(String sid, String iid, long pos);
+        abstract void getStatus(final MediaQueueItem item, final boolean update);
+        abstract void pause();
+        abstract void resume();
+        abstract void stop();
+        abstract void showStatistics();
+        abstract void onFinish(boolean error);
+    }
+
+    private class LocalPlayer extends Player implements 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.addCallback(mLocalPlayer);
+        }
+
+        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();
+            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) {
+                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.onWindowCreated(holder.getSurface());
+        }
+
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            Log.d(TAG, "surfaceCreated");
+            mMediaPlayer.onWindowCreated(holder.getSurface());
+            mLocalPlayer.updateSize(mVideoWidth, mVideoHeight);
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            Log.d(TAG, "surfaceDestroyed");
+        }
+
+        private 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)) {
+                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 mSurfaceView;
+
+            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 for visual interest.
+                mSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
+                SurfaceHolder holder = mSurfaceView.getHolder();
+                holder.addCallback(mLocalPlayer);
+            }
+
+            public void updateSize(int width, int height) {
+                int surfaceHeight=getWindow().getDecorView().getHeight();
+                int surfaceWidth=getWindow().getDecorView().getWidth();
+                ViewGroup.LayoutParams lp = mSurfaceView.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);
+                mSurfaceView.setLayoutParams(lp);
+            }
+
+            public void clearContent() {
+                //TO-DO: clear surface view
+            }
+        }
+    }
+
+    private class RemotePlayer extends Player implements 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();
+        }
+
+        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;
+
+        public DiscoveryFragment() {
+            mCallback = null;
+        }
+
+        public DiscoveryFragment(Callback cb) {
+            mCallback = cb;
+        }
+
+        public void setCallback(Callback cb) {
+            mCallback = cb;
+        }
+
         @Override
         public Callback onCreateCallback() {
-            // Return a custom callback that will simply log all of the route events
-            // for demonstration purposes.
-            return new MediaRouter.Callback() {
-                @Override
-                public void onRouteAdded(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteAdded: route=" + route);
-                }
-
-                @Override
-                public void onRouteChanged(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteChanged: route=" + route);
-                    updateRouteDescription();
-                }
-
-                @Override
-                public void onRouteRemoved(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteRemoved: route=" + route);
-                }
-
-                @Override
-                public void onRouteSelected(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteSelected: route=" + route);
-                    updateRouteDescription();
-                }
-
-                @Override
-                public void onRouteUnselected(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteUnselected: route=" + route);
-                    updateRouteDescription();
-                }
-
-                @Override
-                public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRouteVolumeChanged: route=" + route);
-                }
-
-                @Override
-                public void onRoutePresentationDisplayChanged(
-                        MediaRouter router, RouteInfo route) {
-                    Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route);
-                }
-
-                @Override
-                public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
-                    Log.d(TAG, "onRouteProviderAdded: provider=" + provider);
-                }
-
-                @Override
-                public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
-                    Log.d(TAG, "onRouteProviderRemoved: provider=" + provider);
-                }
-
-                @Override
-                public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
-                    Log.d(TAG, "onRouteProviderChanged: provider=" + provider);
-                }
-            };
+            return mCallback;
         }
 
         @Override
@@ -369,6 +1414,76 @@
         }
     }
 
+    private final class LibraryAdapter extends ArrayAdapter<MediaItem> {
+        public LibraryAdapter() {
+            super(SampleMediaRouterActivity.this, R.layout.media_item);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final View v;
+            if (convertView == null) {
+                v = getLayoutInflater().inflate(R.layout.media_item, null);
+            } else {
+                v = convertView;
+            }
+
+            final MediaItem item = getItem(position);
+
+            TextView tv = (TextView)v.findViewById(R.id.item_text);
+            tv.setText(item.mName);
+
+            ImageButton b = (ImageButton)v.findViewById(R.id.item_action);
+            b.setImageResource(R.drawable.ic_menu_add);
+            b.setTag(item);
+            b.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (item != null) {
+                        mPlayer.enqueue(item.mUri, 0);
+                    }
+                }
+            });
+
+            return v;
+        }
+    }
+
+    private final class PlaylistAdapter extends ArrayAdapter<MediaQueueItem> {
+        public PlaylistAdapter() {
+            super(SampleMediaRouterActivity.this, R.layout.media_item);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final View v;
+            if (convertView == null) {
+                v = getLayoutInflater().inflate(R.layout.media_item, null);
+            } else {
+                v = convertView;
+            }
+
+            final MediaQueueItem item = getItem(position);
+
+            TextView tv = (TextView)v.findViewById(R.id.item_text);
+            tv.setText(item.toString());
+
+            ImageButton b = (ImageButton)v.findViewById(R.id.item_action);
+            b.setImageResource(R.drawable.ic_menu_delete);
+            b.setTag(item);
+            b.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (item != null) {
+                        mPlayer.remove(item);
+                    }
+                }
+            });
+
+            return v;
+        }
+    }
+
     /**
      * Trivial subclass of this activity used to provide another copy of the
      * same activity using a light theme instead of the dark theme.