Add media router picker UI.

Introduced the concept of a MediaRouteSelector which is the means
by which an application states the route capabilities of routes
that it would like to discover.

Added selectors to the addCallback method along with several
other methods to assist with discovery.  Callbacks can specify
flags to perform active scans of routes or to disable filtering
of route events.

Added a workaround to scan for wifi displays on JB MR1.

Refactored the route descriptor objects to use the builder pattern
instead of simply documenting that they should be immutable
since several developers have already tripped over this.

The UI is feature complete but not final.

Bug: 8175766
Change-Id: I54ebb7488222746b0c07292e65b9ded1b9d720fa
diff --git a/CleanSpec.mk b/CleanSpec.mk
index 0805120..ea4358f 100644
--- a/CleanSpec.mk
+++ b/CleanSpec.mk
@@ -46,6 +46,8 @@
 
 $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
 $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
 
 # ************************************************
 # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
diff --git a/v4/honeycomb/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java b/v4/honeycomb/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java
new file mode 100644
index 0000000..4c5d48b
--- /dev/null
+++ b/v4/honeycomb/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java
@@ -0,0 +1,28 @@
+/*
+ * 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 android.support.v4.graphics.drawable;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Implementation of drawable compatibility that can call Honeycomb APIs.
+ */
+class DrawableCompatHoneycomb {
+    public static void jumpToCurrentState(Drawable drawable) {
+        drawable.jumpToCurrentState();
+    }
+}
diff --git a/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java b/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
new file mode 100644
index 0000000..f9c1342
--- /dev/null
+++ b/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
@@ -0,0 +1,73 @@
+/*
+ * 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 android.support.v4.graphics.drawable;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Helper for accessing features in {@link android.graphics.drawable.Drawable}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class DrawableCompat {
+    /**
+     * Interface for the full API.
+     */
+    interface DrawableImpl {
+        void jumpToCurrentState(Drawable drawable);
+    }
+
+    /**
+     * Interface implementation that doesn't use anything about v4 APIs.
+     */
+    static class BaseDrawableImpl implements DrawableImpl {
+        @Override
+        public void jumpToCurrentState(Drawable drawable) {
+        }
+    }
+
+    /**
+     * Interface implementation for devices with at least v11 APIs.
+     */
+    static class HoneycombDrawableImpl implements DrawableImpl {
+        @Override
+        public void jumpToCurrentState(Drawable drawable) {
+            DrawableCompatHoneycomb.jumpToCurrentState(drawable);
+        }
+    }
+
+    /**
+     * Select the correct implementation to use for the current platform.
+     */
+    static final DrawableImpl IMPL;
+    static {
+        final int version = android.os.Build.VERSION.SDK_INT;
+        if (version >= 11) {
+            IMPL = new HoneycombDrawableImpl();
+        } else {
+            IMPL = new BaseDrawableImpl();
+        }
+    }
+
+    /**
+     * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}.
+     * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device
+     * this method does nothing.
+     */
+    public static void jumpToCurrentState(Drawable drawable) {
+        IMPL.jumpToCurrentState(drawable);
+    }
+}
diff --git a/v7/mediarouter/Android.mk b/v7/mediarouter/Android.mk
index b3153a2..995e2bb 100644
--- a/v7/mediarouter/Android.mk
+++ b/v7/mediarouter/Android.mk
@@ -14,12 +14,27 @@
 
 LOCAL_PATH := $(call my-dir)
 
+# Build the resources using the current SDK version.
+# We do this here because the final static library must be compiled with an older
+# SDK version than the resources.  The resources library and the R class that it
+# contains will not be linked into the final static library.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-mediarouter-res
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, dummy)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \
+	frameworks/support/v7/appcompat/res
+LOCAL_AAPT_FLAGS := \
+	--auto-add-overlay \
+	--extra-packages android.support.v7.appcompat
+LOCAL_JAR_EXCLUDE_FILES := none
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
 # A helper sub-library that makes direct use of JellyBean APIs.
 include $(CLEAR_VARS)
 LOCAL_MODULE := android-support-v7-mediarouter-jellybean
 LOCAL_SDK_VERSION := 16
 LOCAL_SRC_FILES := $(call all-java-files-under, jellybean)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 # A helper sub-library that makes direct use of JellyBean MR1 APIs.
@@ -27,7 +42,6 @@
 LOCAL_MODULE := android-support-v7-mediarouter-jellybean-mr1
 LOCAL_SDK_VERSION := 17
 LOCAL_SRC_FILES := $(call all-java-files-under, jellybean-mr1)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-mediarouter-jellybean
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
@@ -36,7 +50,6 @@
 LOCAL_MODULE := android-support-v7-mediarouter-jellybean-mr2
 LOCAL_SDK_VERSION := current
 LOCAL_SRC_FILES := $(call all-java-files-under, jellybean-mr2)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-mediarouter-jellybean-mr1
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
@@ -46,11 +59,10 @@
 # in their makefiles to include the resources in their package.
 include $(CLEAR_VARS)
 LOCAL_MODULE := android-support-v7-mediarouter
-LOCAL_SDK_VERSION := 4
+LOCAL_SDK_VERSION := 7
 LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-mediarouter-jellybean-mr2
-LOCAL_JAVA_LIBRARIES := android-support-v4
+LOCAL_JAVA_LIBRARIES := android-support-v4 android-support-v7-mediarouter-res
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 # Include this library in the build server's output directory
diff --git a/v7/mediarouter/dummy/Dummy.java b/v7/mediarouter/dummy/Dummy.java
new file mode 100644
index 0000000..be16dc7
--- /dev/null
+++ b/v7/mediarouter/dummy/Dummy.java
@@ -0,0 +1 @@
+// Dummy java file used to build the resource library.
diff --git a/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java b/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java
index a6b2afd..9837fb2 100644
--- a/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java
+++ b/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java
@@ -16,9 +16,20 @@
 
 package android.support.v7.media;
 
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Handler;
+import android.util.Log;
 import android.view.Display;
 
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
 final class MediaRouterJellybeanMr1 {
+    private static final String TAG = "MediaRouterJellybeanMr1";
+
     public static Object createCallback(Callback callback) {
         return new CallbackProxy<Callback>(callback);
     }
@@ -37,6 +48,119 @@
         public void onRoutePresentationDisplayChanged(Object routeObj);
     }
 
+    /**
+     * Workaround the fact that the version of MediaRouter.addCallback() that accepts a
+     * flag to perform an active scan does not exist in JB MR1 so we need to force
+     * wifi display scans directly through the DisplayManager.
+     * Do not use on JB MR2 and above.
+     */
+    public static final class ActiveScanWorkaround implements Runnable {
+        // Time between wifi display scans when actively scanning in milliseconds.
+        private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000;
+
+        private final DisplayManager mDisplayManager;
+        private final Handler mHandler;
+        private Method mScanWifiDisplaysMethod;
+
+        private boolean mActivelyScanningWifiDisplays;
+
+        public ActiveScanWorkaround(Context context, Handler handler) {
+            if (Build.VERSION.SDK_INT != 17) {
+                throw new UnsupportedOperationException();
+            }
+
+            mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+            mHandler = handler;
+            try {
+                mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays");
+            } catch (NoSuchMethodException ex) {
+            }
+        }
+
+        public void setActiveScanRouteTypes(int routeTypes) {
+            // On JB MR1, there is no API to scan wifi display routes.
+            // Instead we must make a direct call into the DisplayManager to scan
+            // wifi displays on this version but only when live video routes are requested.
+            // See also the JellybeanMr2Impl implementation of this method.
+            // This was fixed in JB MR2 by adding a new overload of addCallback() to
+            // enable active scanning on request.
+            if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+                if (!mActivelyScanningWifiDisplays) {
+                    if (mScanWifiDisplaysMethod != null) {
+                        mActivelyScanningWifiDisplays = true;
+                        mHandler.post(this);
+                    } else {
+                        Log.w(TAG, "Cannot scan for wifi displays because the "
+                                + "DisplayManager.scanWifiDisplays() method is "
+                                + "not available on this device.");
+                    }
+                }
+            } else {
+                if (mActivelyScanningWifiDisplays) {
+                    mActivelyScanningWifiDisplays = false;
+                    mHandler.removeCallbacks(this);
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            if (mActivelyScanningWifiDisplays) {
+                try {
+                    mScanWifiDisplaysMethod.invoke(mDisplayManager);
+                } catch (IllegalAccessException ex) {
+                    Log.w(TAG, "Cannot scan for wifi displays.", ex);
+                } catch (InvocationTargetException ex) {
+                    Log.w(TAG, "Cannot scan for wifi displays.", ex);
+                }
+                mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL);
+            }
+        }
+    }
+
+    /**
+     * Workaround the fact that the isConnecting() method does not exist in JB MR1.
+     * Do not use on JB MR2 and above.
+     */
+    public static final class IsConnectingWorkaround {
+        private Method mGetStatusCodeMethod;
+        private int mStatusConnecting;
+
+        public IsConnectingWorkaround() {
+            if (Build.VERSION.SDK_INT != 17) {
+                throw new UnsupportedOperationException();
+            }
+
+            try {
+                Field statusConnectingField =
+                        android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING");
+                mStatusConnecting = statusConnectingField.getInt(null);
+                mGetStatusCodeMethod =
+                        android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode");
+            } catch (NoSuchFieldException ex) {
+            } catch (NoSuchMethodException ex) {
+            } catch (IllegalAccessException ex) {
+            }
+        }
+
+        public boolean isConnecting(Object routeObj) {
+            android.media.MediaRouter.RouteInfo route =
+                    (android.media.MediaRouter.RouteInfo)routeObj;
+
+            if (mGetStatusCodeMethod != null) {
+                try {
+                    int statusCode = (Integer)mGetStatusCodeMethod.invoke(route);
+                    return statusCode == mStatusConnecting;
+                } catch (IllegalAccessException ex) {
+                } catch (InvocationTargetException ex) {
+                }
+            }
+
+            // Assume not connecting.
+            return false;
+        }
+    }
+
     static class CallbackProxy<T extends Callback>
             extends MediaRouterJellybean.CallbackProxy<T> {
         public CallbackProxy(T callback) {
diff --git a/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java b/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java
index a78b9eb..695c436 100644
--- a/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java
+++ b/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java
@@ -20,4 +20,15 @@
     public static Object getDefaultRoute(Object routerObj) {
         return ((android.media.MediaRouter)routerObj).getDefaultRoute();
     }
+
+    public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) {
+        ((android.media.MediaRouter)routerObj).addCallback(types,
+                (android.media.MediaRouter.Callback)callbackObj, flags);
+    }
+
+    public static final class RouteInfo {
+        public static boolean isConnecting(Object routeObj) {
+            return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting();
+        }
+    }
 }
diff --git a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
index 9b8cc72..275ffa5 100644
--- a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
+++ b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
@@ -18,15 +18,26 @@
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.Log;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
 final class MediaRouterJellybean {
+    private static final String TAG = "MediaRouterJellybean";
+
     public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
     public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
     public static final int ROUTE_TYPE_USER = 0x00800000;
 
+    public static final int ALL_ROUTE_TYPES =
+            MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
+            | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
+            | MediaRouterJellybean.ROUTE_TYPE_USER;
+
     public static Object getMediaRouter(Context context) {
         return context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
     }
@@ -101,8 +112,6 @@
     }
 
     public static final class RouteInfo {
-        public static final Class<?> clazz = android.media.MediaRouter.RouteInfo.class;
-
         public static CharSequence getName(Object routeObj, Context context) {
             return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context);
         }
@@ -258,6 +267,94 @@
         public void onVolumeUpdateRequest(Object routeObj, int direction);
     }
 
+    /**
+     * Workaround for limitations of selectRoute() on JB and JB MR1.
+     * Do not use on JB MR2 and above.
+     */
+    public static final class SelectRouteWorkaround {
+        private Method mSelectRouteIntMethod;
+
+        public SelectRouteWorkaround() {
+            if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+                throw new UnsupportedOperationException();
+            }
+            try {
+                mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod(
+                        "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class);
+            } catch (NoSuchMethodException ex) {
+            }
+        }
+
+        public void selectRoute(Object routerObj, int types, Object routeObj) {
+            android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+            android.media.MediaRouter.RouteInfo route =
+                    (android.media.MediaRouter.RouteInfo)routeObj;
+
+            int routeTypes = route.getSupportedTypes();
+            if ((routeTypes & ROUTE_TYPE_USER) == 0) {
+                // Handle non-user routes.
+                // On JB and JB MR1, the selectRoute() API only supports programmatically
+                // selecting user routes.  So instead we rely on the hidden selectRouteInt()
+                // method on these versions of the platform.
+                // This limitation was removed in JB MR2.
+                if (mSelectRouteIntMethod != null) {
+                    try {
+                        mSelectRouteIntMethod.invoke(router, types, route);
+                        return; // success!
+                    } catch (IllegalAccessException ex) {
+                        Log.w(TAG, "Cannot programmatically select non-user route.  "
+                                + "Media routing may not work.", ex);
+                    } catch (InvocationTargetException ex) {
+                        Log.w(TAG, "Cannot programmatically select non-user route.  "
+                                + "Media routing may not work.", ex);
+                    }
+                } else {
+                    Log.w(TAG, "Cannot programmatically select non-user route "
+                            + "because the platform is missing the selectRouteInt() "
+                            + "method.  Media routing may not work.");
+                }
+            }
+
+            // Default handling.
+            router.selectRoute(types, route);
+        }
+    }
+
+    /**
+     * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1.
+     * Do not use on JB MR2 and above.
+     */
+    public static final class GetDefaultRouteWorkaround {
+        private Method mGetSystemAudioRouteMethod;
+
+        public GetDefaultRouteWorkaround() {
+            if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+                throw new UnsupportedOperationException();
+            }
+            try {
+                mGetSystemAudioRouteMethod =
+                        android.media.MediaRouter.class.getMethod("getSystemAudioRoute");
+            } catch (NoSuchMethodException ex) {
+            }
+        }
+
+        public Object getDefaultRoute(Object routerObj) {
+            android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+
+            if (mGetSystemAudioRouteMethod != null) {
+                try {
+                    return mGetSystemAudioRouteMethod.invoke(router);
+                } catch (IllegalAccessException ex) {
+                } catch (InvocationTargetException ex) {
+                }
+            }
+
+            // Could not find the method or it does not work.
+            // Return the first route and hope for the best.
+            return router.getRouteAt(0);
+        }
+    }
+
     static class CallbackProxy<T extends Callback>
             extends android.media.MediaRouter.Callback {
         protected final T mCallback;
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_audio_vol.png b/v7/mediarouter/res/drawable-hdpi/ic_audio_vol.png
new file mode 100644
index 0000000..6ea2693
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..b47d666
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..03b0d2a
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..13d803c
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..3ae436b
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..24824fc
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..af3819b
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..83dc251
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..8d9d592
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..1310ec9
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..1705074
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..7027b88
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..7027b88
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_audio_vol.png b/v7/mediarouter/res/drawable-mdpi/ic_audio_vol.png
new file mode 100644
index 0000000..c32fdbc
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..fa22d82
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..a686cd1
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..6764598
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..94e0bb6
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..5ce2f20
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..5105e90
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..68c06ed
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..6e9b144
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..45dc56f
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..46e743a
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..e384691
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..e384691
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_audio_vol.png b/v7/mediarouter/res/drawable-xhdpi/ic_audio_vol.png
new file mode 100644
index 0000000..4e2e20e
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..1d48e12
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..2c8d1ec
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..00b2043
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..ce1d939
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..3064b46
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..4316686
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..25c4e31
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..8e32bd2
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..aeaa78f
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..85277fa
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..b01dbe8
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..c19a2ad
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable/ic_media_route_connecting_holo_dark.xml b/v7/mediarouter/res/drawable/ic_media_route_connecting_holo_dark.xml
new file mode 100644
index 0000000..5be8b60
--- /dev/null
+++ b/v7/mediarouter/res/drawable/ic_media_route_connecting_holo_dark.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<animation-list
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:oneshot="false">
+    <item android:drawable="@drawable/ic_media_route_on_0_holo_dark" android:duration="500" />
+    <item android:drawable="@drawable/ic_media_route_on_1_holo_dark" android:duration="500" />
+    <item android:drawable="@drawable/ic_media_route_on_2_holo_dark" android:duration="500" />
+    <item android:drawable="@drawable/ic_media_route_on_1_holo_dark" android:duration="500" />
+</animation-list>
diff --git a/v7/mediarouter/res/drawable/ic_media_route_connecting_holo_light.xml b/v7/mediarouter/res/drawable/ic_media_route_connecting_holo_light.xml
new file mode 100644
index 0000000..055a27e
--- /dev/null
+++ b/v7/mediarouter/res/drawable/ic_media_route_connecting_holo_light.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<animation-list
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:oneshot="false">
+    <item android:drawable="@drawable/ic_media_route_on_0_holo_light" android:duration="500" />
+    <item android:drawable="@drawable/ic_media_route_on_1_holo_light" android:duration="500" />
+    <item android:drawable="@drawable/ic_media_route_on_2_holo_light" android:duration="500" />
+    <item android:drawable="@drawable/ic_media_route_on_1_holo_light" android:duration="500" />
+</animation-list>
diff --git a/v7/mediarouter/res/drawable/ic_media_route_holo_dark.xml b/v7/mediarouter/res/drawable/ic_media_route_holo_dark.xml
new file mode 100644
index 0000000..bba9755
--- /dev/null
+++ b/v7/mediarouter/res/drawable/ic_media_route_holo_dark.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="true" android:state_enabled="true"
+            android:drawable="@drawable/ic_media_route_on_holo_dark" />
+    <item android:state_checkable="true" android:state_enabled="true"
+            android:drawable="@drawable/ic_media_route_connecting_holo_dark" />
+    <item android:state_enabled="true"
+            android:drawable="@drawable/ic_media_route_off_holo_dark" />
+    <item android:drawable="@drawable/ic_media_route_disabled_holo_dark" />
+</selector>
diff --git a/v7/mediarouter/res/drawable/ic_media_route_holo_light.xml b/v7/mediarouter/res/drawable/ic_media_route_holo_light.xml
new file mode 100644
index 0000000..c956523
--- /dev/null
+++ b/v7/mediarouter/res/drawable/ic_media_route_holo_light.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="true" android:state_enabled="true"
+            android:drawable="@drawable/ic_media_route_on_holo_light" />
+    <item android:state_checkable="true" android:state_enabled="true"
+            android:drawable="@drawable/ic_media_route_connecting_holo_light" />
+    <item android:state_enabled="true"
+            android:drawable="@drawable/ic_media_route_off_holo_light" />
+    <item android:drawable="@drawable/ic_media_route_disabled_holo_light" />
+</selector>
diff --git a/v7/mediarouter/res/layout-v17/media_route_list_item.xml b/v7/mediarouter/res/layout-v17/media_route_list_item.xml
new file mode 100644
index 0000000..1b7ddf0
--- /dev/null
+++ b/v7/mediarouter/res/layout-v17/media_route_list_item.xml
@@ -0,0 +1,54 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="?android:attr/listPreferredItemHeight"
+              android:gravity="center_vertical">
+
+    <ImageView android:id="@android:id/icon"
+               android:layout_width="56dp"
+               android:layout_height="56dp"
+               android:scaleType="center"
+               android:duplicateParentState="true" />
+
+    <LinearLayout android:layout_width="0dp"
+                  android:layout_height="match_parent"
+                  android:layout_weight="1"
+                  android:orientation="vertical"
+                  android:gravity="start|center_vertical"
+                  android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+                  android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+                  android:duplicateParentState="true">
+
+        <TextView android:id="@android:id/text1"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
+                  android:textAppearance="?android:attr/textAppearanceMedium"
+                  android:duplicateParentState="true" />
+
+        <TextView android:id="@android:id/text2"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
+                  android:textAppearance="?android:attr/textAppearanceSmall"
+                  android:duplicateParentState="true" />
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/v7/mediarouter/res/layout/media_route_chooser_dialog.xml b/v7/mediarouter/res/layout/media_route_chooser_dialog.xml
new file mode 100644
index 0000000..d853b37
--- /dev/null
+++ b/v7/mediarouter/res/layout/media_route_chooser_dialog.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="wrap_content"
+              android:orientation="vertical">
+    <ListView android:id="@+id/media_route_list"
+              android:layout_width="fill_parent"
+              android:layout_height="wrap_content" />
+</LinearLayout>
diff --git a/v7/mediarouter/res/layout/media_route_controller_dialog.xml b/v7/mediarouter/res/layout/media_route_controller_dialog.xml
new file mode 100644
index 0000000..76b2c68
--- /dev/null
+++ b/v7/mediarouter/res/layout/media_route_controller_dialog.xml
@@ -0,0 +1,65 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="wrap_content"
+              android:orientation="vertical">
+    <!-- Optional volume slider section. -->
+    <LinearLayout android:id="@+id/media_route_volume_layout"
+                  android:layout_width="fill_parent"
+                  android:layout_height="64dp"
+                  android:gravity="center_vertical"
+                  android:padding="8dp"
+                  android:visibility="gone">
+        <ImageView android:layout_width="48dp"
+                   android:layout_height="48dp"
+                   android:src="@drawable/ic_audio_vol"
+                   android:gravity="center"
+                   android:scaleType="center" />
+        <SeekBar android:id="@+id/media_route_volume_slider"
+                 android:layout_width="0dp"
+                 android:layout_height="wrap_content"
+                 android:layout_weight="1"
+                 android:layout_marginLeft="8dp"
+                 android:layout_marginRight="8dp" />
+    </LinearLayout>
+    <ImageView android:id="@+id/media_route_volume_divider"
+               android:layout_width="fill_parent"
+               android:layout_height="wrap_content"
+               android:src="?android:attr/listDivider" />
+               android:scaleType="fitXY"
+               android:visibility="gone" />
+
+    <!-- Optional content view section. -->
+    <FrameLayout android:id="@+id/media_route_control_frame"
+                 android:layout_width="fill_parent"
+                 android:layout_height="wrap_content"
+                 android:visibility="gone" />
+    <ImageView android:id="@+id/media_route_control_divider"
+               android:layout_width="fill_parent"
+               android:layout_height="wrap_content"
+               android:src="?android:attr/listDivider" />
+               android:scaleType="fitXY"
+               android:visibility="gone" />
+
+    <!-- Disconnect button. -->
+    <Button android:id="@+id/media_route_disconnect_button"
+            android:layout_width="fill_parent"
+            android:layout_height="56dp"
+            android:gravity="center"
+            android:text="@string/media_route_controller_disconnect" />
+</LinearLayout>
diff --git a/v7/mediarouter/res/layout/media_route_list_item.xml b/v7/mediarouter/res/layout/media_route_list_item.xml
new file mode 100644
index 0000000..5d8942d
--- /dev/null
+++ b/v7/mediarouter/res/layout/media_route_list_item.xml
@@ -0,0 +1,55 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="64dp"
+              android:gravity="center_vertical">
+
+    <ImageView android:id="@android:id/icon"
+               android:layout_width="56dp"
+               android:layout_height="56dp"
+               android:scaleType="center"
+               android:visibility="gone"
+               android:duplicateParentState="true" />
+
+    <LinearLayout android:layout_width="0dp"
+                  android:layout_height="fill_parent"
+                  android:layout_weight="1"
+                  android:orientation="vertical"
+                  android:gravity="left|center_vertical"
+                  android:paddingLeft="16dp"
+                  android:paddingRight="16dp"
+                  android:duplicateParentState="true">
+
+        <TextView android:id="@android:id/text1"
+                  android:layout_width="fill_parent"
+                  android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
+                  android:textAppearance="?android:attr/textAppearanceMedium"
+                  android:duplicateParentState="true" />
+
+        <TextView android:id="@android:id/text2"
+                  android:layout_width="fill_parent"
+                  android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
+                  android:textAppearance="?android:attr/textAppearanceSmall"
+                  android:duplicateParentState="true" />
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/v7/mediarouter/res/values/attrs.xml b/v7/mediarouter/res/values/attrs.xml
new file mode 100644
index 0000000..2272e7a
--- /dev/null
+++ b/v7/mediarouter/res/values/attrs.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<resources>
+    <declare-styleable name="MediaRouteButton">
+        <!-- This drawable is a state list where the "checked" state
+             indicates active media routing.  Checkable indicates connecting
+             and non-checked / non-checkable indicates
+             that media is playing to the local device only. -->
+        <attr name="externalRouteEnabledDrawable" format="reference" />
+
+        <attr name="android:minWidth" />
+        <attr name="android:minHeight" />
+    </declare-styleable>
+
+    <attr name="mediaRouteButtonStyle" format="reference" />
+    <attr name="mediaRouteOffDrawable" format="reference" />
+    <attr name="mediaRouteConnectingDrawable" format="reference" />
+    <attr name="mediaRouteOnDrawable" format="reference" />
+</resources>
\ No newline at end of file
diff --git a/v7/mediarouter/res/values/strings.xml b/v7/mediarouter/res/values/strings.xml
index a2caff3..7aec783 100644
--- a/v7/mediarouter/res/values/strings.xml
+++ b/v7/mediarouter/res/values/strings.xml
@@ -20,4 +20,14 @@
 
     <!-- Name for the user route category created when publishing routes to the system in Jellybean and above. [CHAR LIMIT=30] -->
     <string name="user_route_category_name">Devices</string>
+
+    <!-- Content description of a MediaRouteButton for accessibility support. [CHAR LIMIT=50] -->
+    <string name="media_route_button_content_description">Media output</string>
+
+    <!-- Title of the media route chooser dialog. [CHAR LIMIT=30] -->
+    <string name="media_route_chooser_title">Connect to device</string>
+
+    <!-- Button to disconnect from a media route.  [CHAR LIMIT=30] -->
+    <string name="media_route_controller_disconnect">Disconnect</string>
+
 </resources>
diff --git a/v7/mediarouter/res/values/styles.xml b/v7/mediarouter/res/values/styles.xml
new file mode 100644
index 0000000..1c6788e
--- /dev/null
+++ b/v7/mediarouter/res/values/styles.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<resources>
+    <style name="Widget.MediaRouter.MediaRouteButton"
+            parent="Widget.AppCompat.ActionButton">
+        <item name="android:minWidth">56dp</item>
+        <item name="android:minHeight">48dp</item>
+        <item name="android:focusable">true</item>
+        <item name="android:contentDescription">@string/media_route_button_content_description</item>
+        <item name="externalRouteEnabledDrawable">@drawable/ic_media_route_holo_dark</item>
+    </style>
+
+    <style name="Widget.MediaRouter.Light.MediaRouteButton"
+            parent="Widget.AppCompat.Light.ActionButton">
+        <item name="android:minWidth">56dp</item>
+        <item name="android:minHeight">48dp</item>
+        <item name="android:focusable">true</item>
+        <item name="android:contentDescription">@string/media_route_button_content_description</item>
+        <item name="externalRouteEnabledDrawable">@drawable/ic_media_route_holo_light</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/v7/mediarouter/res/values/themes.xml b/v7/mediarouter/res/values/themes.xml
new file mode 100644
index 0000000..7cf4958
--- /dev/null
+++ b/v7/mediarouter/res/values/themes.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<resources>
+
+    <style name="Theme.MediaRouter" parent="">
+        <item name="mediaRouteButtonStyle">@style/Widget.MediaRouter.MediaRouteButton</item>
+
+        <item name="mediaRouteOffDrawable">@drawable/ic_media_route_off_holo_dark</item>
+        <item name="mediaRouteConnectingDrawable">@drawable/ic_media_route_connecting_holo_dark</item>
+        <item name="mediaRouteOnDrawable">@drawable/ic_media_route_on_holo_dark</item>
+    </style>
+
+    <style name="Theme.MediaRouter.Light" parent="">
+        <item name="mediaRouteButtonStyle">@style/Widget.MediaRouter.Light.MediaRouteButton</item>
+
+        <item name="mediaRouteOffDrawable">@drawable/ic_media_route_off_holo_light</item>
+        <item name="mediaRouteConnectingDrawable">@drawable/ic_media_route_connecting_holo_light</item>
+        <item name="mediaRouteOnDrawable">@drawable/ic_media_route_on_holo_light</item>
+    </style>
+
+</resources>
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java
new file mode 100644
index 0000000..c1775b8
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java
@@ -0,0 +1,288 @@
+/*
+ * 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 android.support.v7.app;
+
+import android.content.Context;
+import android.support.v4.view.ActionProvider;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouteSelector;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * The media route action provider displays a {@link MediaRouteButton media route button}
+ * in the application's {@link ActionBar} to allow the user to select routes and
+ * to control the currently selected route.
+ * <p>
+ * The application must specify the kinds of routes that the user should be allowed
+ * to select by specifying a {@link MediaRouteSelector selector} with the
+ * {@link #setRouteSelector} method.
+ * </p>
+ *
+ * <h3>Prerequisites</h3>
+ * <p>
+ * To use the media route action provider, the activity must be a subclass of
+ * {@link ActionBarActivity} from the <code>android.support.v7.appcompat</code>
+ * support library.  Refer to support library documentation for details.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <p>
+ * </p><p>
+ * The application should define a menu resource to include the provider in the
+ * action bar options menu.  Note that the support library action bar uses attributes
+ * that are defined in the application's resource namespace rather than the framework's
+ * resource namespace to configure each item.
+ * </p><pre>
+ * &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"
+ *         xmlns:app="http://schemas.android.com/apk/res-auto">
+ *     &lt;item android:id="@+id/media_route_menu_item"
+ *         android:title="@string/media_route_menu_title"
+ *         app:showAsAction="always"
+ *         app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"/>
+ * &lt;/menu>
+ * </pre><p>
+ * Then configure the menu and set the route selector for the chooser.
+ * </p><pre>
+ * public class MyActivity extends ActionBarActivity {
+ *     private MediaRouter mRouter;
+ *     private MediaRouter.Callback mCallback;
+ *     private MediaRouteSelector mSelector;
+ *
+ *     protected void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *
+ *         mRouter = Mediarouter.getInstance(this);
+ *         mSelector = new MediaRouteSelector.Builder()
+ *                 .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ *                 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ *                 .build();
+ *         mCallback = new MyCallback();
+ *     }
+ *
+ *     // Add the callback on resume to tell the media router what kinds of routes
+ *     // the application is interested in so that it can try to discover suitable ones.
+ *     public void onResume() {
+ *         super.onResume();
+ *
+ *         mediaRouter.addCallback(mSelector, mCallback);
+ *
+ *         MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
+ *         // do something with the route...
+ *     }
+ *
+ *     // Remove the selector on pause to tell the media router that it no longer
+ *     // needs to invest effort trying to discover routes of these kinds for now.
+ *     public void onPause() {
+ *         super.onPause();
+ *
+ *         mediaRouter.removeCallback(mCallback);
+ *     }
+ *
+ *     public boolean onCreateOptionsMenu(Menu menu) {
+ *         super.onCreateOptionsMenu(menu);
+ *
+ *         getMenuInflater().inflate(R.menu.sample_media_router_menu, menu);
+ *
+ *         MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
+ *         MediaRouteActionProvider mediaRouteActionProvider =
+ *                 (MediaRouteActionProvider)MenuItemCompat.getActionProvider(mediaRouteMenuItem);
+ *         mediaRouteActionProvider.setRouteSelector(mSelector);
+ *         return true;
+ *     }
+ *
+ *     private final class MyCallback extends MediaRouter.Callback {
+ *         // Implement callback methods as needed.
+ *     }
+ * }
+ * </pre>
+ *
+ * @see #setRouteSelector
+ */
+public class MediaRouteActionProvider extends ActionProvider {
+    private static final String TAG = "MediaRouteActionProvider";
+
+    private final MediaRouter mRouter;
+    private final MediaRouterCallback mCallback;
+
+    private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+    private boolean mAttachedToWindow;
+    private MediaRouteButton mButton;
+
+    /**
+     * Creates the action provider.
+     *
+     * @param context The context.
+     */
+    public MediaRouteActionProvider(Context context) {
+        super(context);
+
+        mRouter = MediaRouter.getInstance(context);
+        mCallback = new MediaRouterCallback();
+    }
+
+    /**
+     * Gets the media route selector for filtering the routes that the user can
+     * select using the media route chooser dialog.
+     *
+     * @return The selector, never null.
+     */
+    public MediaRouteSelector getRouteSelector() {
+        return mSelector;
+    }
+
+    /**
+     * Sets the media route selector for filtering the routes that the user can
+     * select using the media route chooser dialog.
+     *
+     * @param selector The selector, must not be null.
+     */
+    public void setRouteSelector(MediaRouteSelector selector) {
+        if (selector == null) {
+            throw new IllegalArgumentException("selector must not be null");
+        }
+
+        if (!mSelector.equals(selector)) {
+            mSelector = selector;
+
+            if (mAttachedToWindow) {
+                mRouter.removeCallback(mCallback);
+                mRouter.addCallback(selector, mCallback);
+            }
+
+            refreshRoute();
+
+            if (mButton != null) {
+                mButton.setRouteSelector(selector);
+            }
+        }
+    }
+
+    /**
+     * Gets the associated media route button, or null if it has not yet been created.
+     */
+    public MediaRouteButton getMediaRouteButton() {
+        return mButton;
+    }
+
+    /**
+     * Called when the media route button is being created.
+     */
+    @SuppressWarnings("deprecation")
+    public MediaRouteButton onCreateMediaRouteButton() {
+        if (mButton != null) {
+            Log.e(TAG, "onCreateMediaRouteButton: This ActionProvider is already associated "
+                    + "with a menu item. Don't reuse MediaRouteActionProvider instances!  "
+                    + "Abandoning the old button...");
+        }
+
+        mButton = new MediaRouteButton(getContext());
+        mButton.setCheatSheetEnabled(true);
+        mButton.setAttachCallback(new AttachCallback());
+        mButton.setRouteSelector(mSelector);
+        mButton.setLayoutParams(new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.FILL_PARENT));
+        return mButton;
+    }
+
+    @Override
+    public View onCreateActionView() {
+        if (mButton != null) {
+            Log.e(TAG, "onCreateActionView: this ActionProvider is already associated " +
+                    "with a menu item. Don't reuse MediaRouteActionProvider instances! " +
+                    "Abandoning the old menu item...");
+        }
+
+        mButton = onCreateMediaRouteButton();
+        return mButton;
+    }
+
+    @Override
+    public boolean onPerformDefaultAction() {
+        if (mButton != null) {
+            return mButton.showDialog();
+        }
+        return false;
+    }
+
+    @Override
+    public boolean overridesItemVisibility() {
+        return true;
+    }
+
+    @Override
+    public boolean isVisible() {
+        return mRouter.isRouteAvailable(mSelector,
+                MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE
+                | MediaRouter.AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN);
+    }
+
+    private void refreshRoute() {
+        if (mAttachedToWindow) {
+            refreshVisibility();
+        }
+    }
+
+    private final class MediaRouterCallback extends MediaRouter.Callback {
+        @Override
+        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
+            refreshRoute();
+        }
+    }
+
+    private final class AttachCallback implements MediaRouteButton.AttachCallback {
+        @Override
+        public void onAttachedToWindow() {
+            mAttachedToWindow = true;
+            mRouter.addCallback(mSelector, mCallback);
+            refreshRoute();
+        }
+
+        @Override
+        public void onDetachedFromWindow() {
+            mAttachedToWindow = false;
+            mRouter.removeCallback(mCallback);
+        }
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
new file mode 100644
index 0000000..b549f54
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
@@ -0,0 +1,538 @@
+/*
+ * 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 android.support.v7.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.view.GravityCompat;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.mediarouter.R;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.widget.Toast;
+
+/**
+ * The media route button allows the user to select routes and to control the
+ * currently selected route.
+ *
+ * <h3>Prerequisites</h3>
+ * <p>
+ * To use the media route button, the activity must be a subclass of
+ * {@link FragmentActivity} from the <code>android.support.v4</code>
+ * support library.  Refer to support library documentation for details.
+ * </p>
+ *
+ * @see MediaRouteActionProvider
+ * @see #setRouteSelector
+ */
+public class MediaRouteButton extends View {
+    private static final String TAG = "MediaRouteButton";
+
+    private static final String CHOOSER_FRAGMENT_TAG =
+            "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
+    private static final String CONTROLLER_FRAGMENT_TAG =
+            "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
+
+    private final MediaRouter mRouter;
+    private final MediaRouterCallback mCallback;
+
+    private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+
+    private AttachCallback mAttachCallback;
+    private boolean mAttachedToWindow;
+
+    private Drawable mRemoteIndicator;
+    private boolean mRemoteActive;
+    private boolean mCheatSheetEnabled;
+    private boolean mIsConnecting;
+
+    private int mMinWidth;
+    private int mMinHeight;
+
+    // The checked state is used when connected to a remote route.
+    private static final int[] CHECKED_STATE_SET = {
+        android.R.attr.state_checked
+    };
+
+    // The checkable state is used while connecting to a remote route.
+    private static final int[] CHECKABLE_STATE_SET = {
+        android.R.attr.state_checkable
+    };
+
+    public MediaRouteButton(Context context) {
+        this(context, null);
+    }
+
+    public MediaRouteButton(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.mediaRouteButtonStyle);
+    }
+
+    public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(MediaRouterThemeHelper.createThemedContext(context), attrs, defStyleAttr);
+        context = getContext();
+
+        mRouter = MediaRouter.getInstance(context);
+        mCallback = new MediaRouterCallback();
+
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.MediaRouteButton, defStyleAttr, 0);
+        setRemoteIndicatorDrawable(a.getDrawable(
+                R.styleable.MediaRouteButton_externalRouteEnabledDrawable));
+        mMinWidth = a.getDimensionPixelSize(
+                R.styleable.MediaRouteButton_android_minWidth, 0);
+        mMinHeight = a.getDimensionPixelSize(
+                R.styleable.MediaRouteButton_android_minHeight, 0);
+        a.recycle();
+
+        setClickable(true);
+        setLongClickable(true);
+    }
+
+    /**
+     * Gets the media route selector for filtering the routes that the user can
+     * select using the media route chooser dialog.
+     *
+     * @return The selector, never null.
+     */
+    public MediaRouteSelector getRouteSelector() {
+        return mSelector;
+    }
+
+    /**
+     * Sets the media route selector for filtering the routes that the user can
+     * select using the media route chooser dialog.
+     *
+     * @param selector The selector, must not be null.
+     */
+    public void setRouteSelector(MediaRouteSelector selector) {
+        if (selector == null) {
+            throw new IllegalArgumentException("selector must not be null");
+        }
+
+        if (!mSelector.equals(selector)) {
+            mSelector = selector;
+
+            if (mAttachedToWindow) {
+                mRouter.removeCallback(mCallback);
+                mRouter.addCallback(selector, mCallback);
+            }
+
+            refreshRoute();
+        }
+    }
+
+    /**
+     * Show the route chooser or controller dialog.
+     * <p>
+     * If the default route is selected, then shows the route chooser dialog.
+     * Otherwise, shows the route controller dialog which will offer the user
+     * a choice to disconnect from the route or perform other control actions
+     * such as setting the route's volume.
+     * </p>
+     *
+     * @return True if the dialog was actually shown.
+     *
+     * @throws IllegalStateException if the activity is not a subclass of
+     * {@link FragmentActivity}.
+     */
+    public boolean showDialog() {
+        if (!mAttachedToWindow) {
+            return false;
+        }
+
+        final FragmentManager fm = getFragmentManager();
+        if (fm == null) {
+            throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
+        }
+
+        MediaRouter.RouteInfo route = mRouter.updateSelectedRoute(mSelector);
+        if (route.isDefault()) {
+            if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
+                Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
+                return false;
+            }
+            MediaRouteChooserDialogFragment f = onCreateChooserDialogFragment();
+            f.setRouteSelector(mSelector);
+            f.show(fm, CHOOSER_FRAGMENT_TAG);
+        } else {
+            if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
+                Log.w(TAG, "showDialog(): Route controller dialog already showing!");
+                return false;
+            }
+            MediaRouteControllerDialogFragment f = onCreateControllerDialogFragment();
+            f.show(fm, CONTROLLER_FRAGMENT_TAG);
+        }
+        return true;
+    }
+
+    /**
+     * Called when the chooser dialog is being opened and it is time to create the fragment.
+     * <p>
+     * Subclasses may override this method to create a customized fragment.
+     * </p>
+     *
+     * @return The media route chooser dialog fragment, must not be null.
+     */
+    public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
+        return new MediaRouteChooserDialogFragment();
+    }
+
+    /**
+     * Called when the controller dialog is being opened and it is time to create the fragment.
+     * <p>
+     * Subclasses may override this method to create a customized fragment.
+     * </p>
+     *
+     * @return The media route controller dialog fragment, must not be null.
+     */
+    public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
+        return new MediaRouteControllerDialogFragment();
+    }
+
+    private FragmentManager getFragmentManager() {
+        Activity activity = getActivity();
+        if (activity instanceof FragmentActivity) {
+            return ((FragmentActivity)activity).getSupportFragmentManager();
+        }
+        return null;
+    }
+
+    private Activity getActivity() {
+        // Gross way of unwrapping the Activity so we can get the FragmentManager
+        Context context = getContext();
+        while (context instanceof ContextWrapper) {
+            if (context instanceof Activity) {
+                return (Activity)context;
+            }
+            context = ((ContextWrapper)context).getBaseContext();
+        }
+        return null;
+    }
+
+    /**
+     * Sets whether to enable showing a toast with the content descriptor of the
+     * button when the button is long pressed.
+     */
+    void setCheatSheetEnabled(boolean enable) {
+        mCheatSheetEnabled = enable;
+    }
+
+    /**
+     * Sets a callback to be notified when the button is attached or detached
+     * from the window.
+     */
+    void setAttachCallback(AttachCallback callback) {
+        mAttachCallback = callback;
+    }
+
+    @Override
+    public boolean performClick() {
+        // Send the appropriate accessibility events and call listeners
+        boolean handled = super.performClick();
+        if (!handled) {
+            playSoundEffect(SoundEffectConstants.CLICK);
+        }
+        return showDialog() || handled;
+    }
+
+    @Override
+    public boolean performLongClick() {
+        if (super.performLongClick()) {
+            return true;
+        }
+
+        if (!mCheatSheetEnabled) {
+            return false;
+        }
+
+        final CharSequence contentDesc = getContentDescription();
+        if (TextUtils.isEmpty(contentDesc)) {
+            // Don't show the cheat sheet if we have no description
+            return false;
+        }
+
+        final int[] screenPos = new int[2];
+        final Rect displayFrame = new Rect();
+        getLocationOnScreen(screenPos);
+        getWindowVisibleDisplayFrame(displayFrame);
+
+        final Context context = getContext();
+        final int width = getWidth();
+        final int height = getHeight();
+        final int midy = screenPos[1] + height / 2;
+        final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+
+        Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT);
+        if (midy < displayFrame.height()) {
+            // Show along the top; follow action buttons
+            cheatSheet.setGravity(Gravity.TOP | GravityCompat.END,
+                    screenWidth - screenPos[0] - width / 2, height);
+        } else {
+            // Show along the bottom center
+            cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
+        }
+        cheatSheet.show();
+        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        return true;
+    }
+
+    @Override
+    protected int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+        // Technically we should be handling this more completely, but these
+        // are implementation details here. Checkable is used to express the connecting
+        // drawable state and it's mutually exclusive with check for the purposes
+        // of state selection here.
+        if (mIsConnecting) {
+            mergeDrawableStates(drawableState, CHECKABLE_STATE_SET);
+        } else if (mRemoteActive) {
+            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+        }
+        return drawableState;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        if (mRemoteIndicator != null) {
+            int[] myDrawableState = getDrawableState();
+            mRemoteIndicator.setState(myDrawableState);
+            invalidate();
+        }
+    }
+
+    private void setRemoteIndicatorDrawable(Drawable d) {
+        if (mRemoteIndicator != null) {
+            mRemoteIndicator.setCallback(null);
+            unscheduleDrawable(mRemoteIndicator);
+        }
+        mRemoteIndicator = d;
+        if (d != null) {
+            d.setCallback(this);
+            d.setState(getDrawableState());
+            d.setVisible(getVisibility() == VISIBLE, false);
+        }
+
+        refreshDrawableState();
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return super.verifyDrawable(who) || who == mRemoteIndicator;
+    }
+
+    //@Override defined in v11
+    public void jumpDrawablesToCurrentState() {
+        // We can't call super to handle the background so we do it ourselves.
+        //super.jumpDrawablesToCurrentState();
+        if (getBackground() != null) {
+            DrawableCompat.jumpToCurrentState(getBackground());
+        }
+
+        // Handle our own remote indicator.
+        if (mRemoteIndicator != null) {
+            DrawableCompat.jumpToCurrentState(mRemoteIndicator);
+        }
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+
+        if (mRemoteIndicator != null) {
+            mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
+        }
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mAttachedToWindow = true;
+        mRouter.addCallback(mSelector, mCallback);
+        refreshRoute();
+
+        if (mAttachCallback != null) {
+            mAttachCallback.onAttachedToWindow();
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        if (mAttachCallback != null) {
+            mAttachCallback.onDetachedFromWindow();
+        }
+
+        mAttachedToWindow = false;
+        mRouter.removeCallback(mCallback);
+
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        final int minWidth = Math.max(mMinWidth,
+                mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0);
+        final int minHeight = Math.max(mMinHeight,
+                mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0);
+
+        int width;
+        switch (widthMode) {
+            case MeasureSpec.EXACTLY:
+                width = widthSize;
+                break;
+            case MeasureSpec.AT_MOST:
+                width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight());
+                break;
+            default:
+            case MeasureSpec.UNSPECIFIED:
+                width = minWidth + getPaddingLeft() + getPaddingRight();
+                break;
+        }
+
+        int height;
+        switch (heightMode) {
+            case MeasureSpec.EXACTLY:
+                height = heightSize;
+                break;
+            case MeasureSpec.AT_MOST:
+                height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom());
+                break;
+            default:
+            case MeasureSpec.UNSPECIFIED:
+                height = minHeight + getPaddingTop() + getPaddingBottom();
+                break;
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mRemoteIndicator != null) {
+            final int left = getPaddingLeft();
+            final int right = getWidth() - getPaddingRight();
+            final int top = getPaddingTop();
+            final int bottom = getHeight() - getPaddingBottom();
+
+            final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
+            final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
+            final int drawLeft = left + (right - left - drawWidth) / 2;
+            final int drawTop = top + (bottom - top - drawHeight) / 2;
+
+            mRemoteIndicator.setBounds(drawLeft, drawTop,
+                    drawLeft + drawWidth, drawTop + drawHeight);
+            mRemoteIndicator.draw(canvas);
+        }
+    }
+
+    private void refreshRoute() {
+        if (mAttachedToWindow) {
+            final MediaRouter.RouteInfo route = mRouter.updateSelectedRoute(mSelector);
+            final boolean isRemote = !route.isDefault();
+            final boolean isConnecting = route.isConnecting();
+
+            boolean needsRefresh = false;
+            if (mRemoteActive != isRemote) {
+                mRemoteActive = isRemote;
+                needsRefresh = true;
+            }
+            if (mIsConnecting != isConnecting) {
+                mIsConnecting = isConnecting;
+                needsRefresh = true;
+            }
+
+            if (needsRefresh) {
+                refreshDrawableState();
+            }
+
+            setEnabled(mRouter.isRouteAvailable(mSelector,
+                    MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE
+                    | MediaRouter.AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN));
+        }
+    }
+
+    static interface AttachCallback {
+        void onAttachedToWindow();
+        void onDetachedFromWindow();
+    }
+
+    private final class MediaRouterCallback extends MediaRouter.Callback {
+        @Override
+        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
+            refreshRoute();
+        }
+
+        @Override
+        public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
+            refreshRoute();
+        }
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
new file mode 100644
index 0000000..ee92b41
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
@@ -0,0 +1,235 @@
+/*
+ * 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 android.support.v7.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.mediarouter.R;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * This class implements the route chooser dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to choose a route that matches a given selector.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteChooserDialog extends Dialog {
+    private final MediaRouter mRouter;
+    private final MediaRouterCallback mCallback;
+
+    private final int mMediaRouteOffDrawableRes;
+
+    private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+    private RouteAdapter mAdapter;
+    private ListView mListView;
+    private boolean mAttachedToWindow;
+
+    public MediaRouteChooserDialog(Context context) {
+        this(context, 0);
+    }
+
+    public MediaRouteChooserDialog(Context context, int theme) {
+        super(MediaRouterThemeHelper.createThemedContext(context), theme);
+        context = getContext();
+
+        mRouter = MediaRouter.getInstance(context);
+        mCallback = new MediaRouterCallback();
+
+        mMediaRouteOffDrawableRes = MediaRouterThemeHelper.getThemeResource(
+                context, R.attr.mediaRouteOffDrawable);
+    }
+
+    /**
+     * Gets the media route selector for filtering the routes that the user can select.
+     *
+     * @return The selector, never null.
+     */
+    public MediaRouteSelector getRouteSelector() {
+        return mSelector;
+    }
+
+    /**
+     * Sets the media route selector for filtering the routes that the user can select.
+     *
+     * @param selector The selector, must not be null.
+     */
+    public void setRouteSelector(MediaRouteSelector selector) {
+        if (selector == null) {
+            throw new IllegalArgumentException("selector must not be null");
+        }
+
+        if (!mSelector.equals(selector)) {
+            mSelector = selector;
+
+            if (mAttachedToWindow) {
+                mRouter.removeCallback(mCallback);
+                mRouter.addCallback(selector, mCallback);
+            }
+
+            refreshRoutes();
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+
+        setContentView(R.layout.media_route_chooser_dialog);
+        setTitle(R.string.media_route_chooser_title);
+
+        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
+                mMediaRouteOffDrawableRes); // must happen after setContentView
+
+        mAdapter = new RouteAdapter(getContext());
+        mListView = (ListView)findViewById(R.id.media_route_list);
+        mListView.setAdapter(mAdapter);
+        mListView.setOnItemClickListener(mAdapter);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mAttachedToWindow = true;
+        mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_ACTIVE_SCAN);
+        refreshRoutes();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        mAttachedToWindow = false;
+        mRouter.removeCallback(mCallback);
+
+        super.onDetachedFromWindow();
+    }
+
+    private void refreshRoutes() {
+        if (mAttachedToWindow) {
+            mAdapter.update();
+        }
+    }
+
+    private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo>
+            implements ListView.OnItemClickListener {
+        private final LayoutInflater mInflater;
+
+        public RouteAdapter(Context context) {
+            super(context, 0);
+            mInflater = LayoutInflater.from(context);
+        }
+
+        public void update() {
+            clear();
+            final List<MediaRouter.RouteInfo> routes = mRouter.getRoutes();
+            final int count = routes.size();
+            for (int i = 0; i < count; i++) {
+                MediaRouter.RouteInfo route = routes.get(i);
+                if (!route.isDefault() && route.matchesSelector(mSelector)) {
+                    add(route);
+                }
+            }
+            sort(RouteComparator.sInstance);
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return getItem(position).isEnabled();
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View view = convertView;
+            if (view == null) {
+                view = mInflater.inflate(R.layout.media_route_list_item, parent, false);
+            }
+            TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+            TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+            ImageView icon = (ImageView)view.findViewById(android.R.id.icon);
+            MediaRouter.RouteInfo route = getItem(position);
+            text1.setText(route.getName());
+            text2.setText(route.getStatus());
+            icon.setImageDrawable(route.getIconDrawable());
+            view.setEnabled(route.isEnabled());
+            return view;
+        }
+
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            MediaRouter.RouteInfo route = getItem(position);
+            if (route.isEnabled()) {
+                route.select();
+                dismiss();
+            }
+        }
+    }
+
+    private final class MediaRouterCallback extends MediaRouter.Callback {
+        @Override
+        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoutes();
+        }
+
+        @Override
+        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoutes();
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+            refreshRoutes();
+        }
+
+        @Override
+        public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+            dismiss();
+        }
+    }
+
+    private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
+        public static final RouteComparator sInstance = new RouteComparator();
+
+        @Override
+        public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
+            return lhs.getName().compareTo(rhs.getName());
+        }
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialogFragment.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialogFragment.java
new file mode 100644
index 0000000..efb7b3e
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialogFragment.java
@@ -0,0 +1,97 @@
+/*
+ * 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 android.support.v7.app;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.media.MediaRouteSelector;
+
+/**
+ * Media route chooser dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteChooserDialog}.  The application may subclass this
+ * dialog fragment to customize the dialog.
+ * </p>
+ */
+public class MediaRouteChooserDialogFragment extends DialogFragment {
+    private final String ARGUMENT_SELECTOR = "selector";
+
+    private MediaRouteSelector mSelector;
+
+    public MediaRouteChooserDialogFragment() {
+        setCancelable(true);
+    }
+
+    /**
+     * Gets the media route selector for filtering the routes that the user can select.
+     *
+     * @return The selector, never null.
+     */
+    public MediaRouteSelector getRouteSelector() {
+        ensureRouteSelector();
+        return mSelector;
+    }
+
+    private void ensureRouteSelector() {
+        if (mSelector == null) {
+            Bundle args = getArguments();
+            if (args != null) {
+                mSelector = MediaRouteSelector.fromBundle(args.getBundle(ARGUMENT_SELECTOR));
+            }
+            if (mSelector == null) {
+                mSelector = MediaRouteSelector.EMPTY;
+            }
+        }
+    }
+
+    /**
+     * Sets the media route selector for filtering the routes that the user can select.
+     * This method must be called before the fragment is added.
+     *
+     * @param selector The selector to set.
+     */
+    public void setRouteSelector(MediaRouteSelector selector) {
+        if (selector == null) {
+            throw new IllegalArgumentException("selector must not be null");
+        }
+
+        ensureRouteSelector();
+        if (!mSelector.equals(selector)) {
+            mSelector = selector;
+
+            Bundle args = getArguments();
+            if (args == null) {
+                args = new Bundle();
+            }
+            args.putBundle(ARGUMENT_SELECTOR, selector.asBundle());
+            setArguments(args);
+
+            MediaRouteChooserDialog dialog = (MediaRouteChooserDialog)getDialog();
+            if (dialog != null) {
+                dialog.setRouteSelector(selector);
+            }
+        }
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        MediaRouteChooserDialog dialog = new MediaRouteChooserDialog(getActivity());
+        dialog.setRouteSelector(getRouteSelector());
+        return dialog;
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
new file mode 100644
index 0000000..8432ae0
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -0,0 +1,252 @@
+/*
+ * 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 android.support.v7.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.mediarouter.R;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+/**
+ * This class implements the route controller dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to control or disconnect from the currently selected route.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteControllerDialog extends Dialog {
+    private static final String TAG = "MediaRouteControllerDialog";
+
+    private final MediaRouter mRouter;
+    private final MediaRouterCallback mCallback;
+    private final MediaRouter.RouteInfo mRoute;
+
+    private final int mMediaRouteConnectingDrawableRes;
+    private final int mMediaRouteOnDrawableRes;
+
+    private LinearLayout mVolumeLayout;
+    private ImageView mVolumeDivider;
+    private SeekBar mVolumeSlider;
+    private boolean mVolumeSliderTouched;
+
+    private View mControlView;
+
+    private Button mDisconnectButton;
+
+    public MediaRouteControllerDialog(Context context) {
+        this(context, 0);
+    }
+
+    public MediaRouteControllerDialog(Context context, int theme) {
+        super(MediaRouterThemeHelper.createThemedContext(context), theme);
+        context = getContext();
+
+        mRouter = MediaRouter.getInstance(context);
+        mCallback = new MediaRouterCallback();
+        mRoute = mRouter.getSelectedRoute();
+
+        mMediaRouteConnectingDrawableRes = MediaRouterThemeHelper.getThemeResource(
+                context, R.attr.mediaRouteConnectingDrawable);
+        mMediaRouteOnDrawableRes = MediaRouterThemeHelper.getThemeResource(
+                context, R.attr.mediaRouteOnDrawable);
+    }
+
+    /**
+     * Gets the route that this dialog is controlling.
+     */
+    public MediaRouter.RouteInfo getRoute() {
+        return mRoute;
+    }
+
+    /**
+     * Provides the subclass an opportunity to create a view that will
+     * be included within the body of the dialog to offer additional media controls
+     * for the currently playing content.
+     *
+     * @param savedInstanceState The dialog's saved instance state.
+     * @return The media control view, or null if none.
+     */
+    public View onCreateMediaControlView(Bundle savedInstanceState) {
+        return null;
+    }
+
+    /**
+     * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
+     *
+     * @return The media control view, or null if none.
+     */
+    public View getMediaControlView() {
+        return mControlView;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+
+        setContentView(R.layout.media_route_controller_dialog);
+
+        mVolumeLayout = (LinearLayout)findViewById(R.id.media_route_volume_layout);
+        mVolumeSlider = (SeekBar)findViewById(R.id.media_route_volume_slider);
+        mVolumeDivider = (ImageView)findViewById(R.id.media_route_volume_divider);
+        mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+                mVolumeSliderTouched = true;
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                mVolumeSliderTouched = false;
+                updateVolume();
+            }
+
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (fromUser) {
+                    mRoute.requestSetVolume(progress);
+                }
+            }
+        });
+
+        mDisconnectButton = (Button)findViewById(R.id.media_route_disconnect_button);
+        mDisconnectButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mRoute.isSelected()) {
+                    mRouter.getDefaultRoute().select();
+                }
+                dismiss();
+            }
+        });
+
+        if (update()) {
+            mControlView = onCreateMediaControlView(savedInstanceState);
+            if (mControlView != null) {
+                FrameLayout controlFrame =
+                        (FrameLayout)findViewById(R.id.media_route_control_frame);
+                ImageView controlDivider =
+                        (ImageView)findViewById(R.id.media_route_control_divider);
+                controlFrame.addView(controlFrame);
+                controlFrame.setVisibility(View.VISIBLE);
+                controlDivider.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback,
+                MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
+        update();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        mRouter.removeCallback(mCallback);
+
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+                mRoute.requestUpdateVolume(-1);
+                return true;
+            } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+                mRoute.requestUpdateVolume(1);
+                return true;
+            }
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+                    || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+                return true;
+            }
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    private boolean update() {
+        if (!mRoute.isSelected() || mRoute.isDefault()) {
+            dismiss();
+            return false;
+        }
+
+        setTitle(mRoute.getName());
+        updateVolume();
+
+        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
+                mRoute.isConnecting() ? mMediaRouteConnectingDrawableRes :
+                        mMediaRouteOnDrawableRes);
+        return true;
+    }
+
+    private void updateVolume() {
+        if (!mVolumeSliderTouched) {
+            if (mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+                mVolumeLayout.setVisibility(View.VISIBLE);
+                mVolumeDivider.setVisibility(View.VISIBLE);
+                mVolumeSlider.setMax(mRoute.getVolumeMax());
+                mVolumeSlider.setProgress(mRoute.getVolume());
+            } else {
+                mVolumeLayout.setVisibility(View.GONE);
+                mVolumeDivider.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    private final class MediaRouterCallback extends MediaRouter.Callback {
+        @Override
+        public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
+            update();
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+            update();
+        }
+
+        @Override
+        public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+            if (route == mRoute) {
+                updateVolume();
+            }
+        }
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java
new file mode 100644
index 0000000..a6dc205
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java
@@ -0,0 +1,39 @@
+/*
+ * 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 android.support.v7.app;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+/**
+ * Media route controller dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteControllerDialog}.  The application may subclass this
+ * dialog fragment to customize the dialog.
+ * </p>
+ */
+public class MediaRouteControllerDialogFragment extends DialogFragment {
+    public MediaRouteControllerDialogFragment() {
+        setCancelable(true);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return new MediaRouteControllerDialog(getActivity());
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java b/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
new file mode 100644
index 0000000..24fbe34
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
@@ -0,0 +1,41 @@
+/*
+ * 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 android.support.v7.app;
+
+import android.content.Context;
+import android.support.v7.mediarouter.R;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+
+final class MediaRouterThemeHelper {
+    private MediaRouterThemeHelper() {
+    }
+
+    public static Context createThemedContext(Context context) {
+        TypedValue value = new TypedValue();
+        boolean isLightTheme =
+                context.getTheme().resolveAttribute(R.attr.isLightTheme, value, true)
+                && value.data != 0;
+        return new ContextThemeWrapper(context,
+                isLightTheme ? R.style.Theme_MediaRouter_Light : R.style.Theme_MediaRouter);
+    }
+
+    public static int getThemeResource(Context context, int attr) {
+        TypedValue value = new TypedValue();
+        return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteDescriptor.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteDescriptor.java
new file mode 100644
index 0000000..8799afc
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteDescriptor.java
@@ -0,0 +1,455 @@
+/*
+ * 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 android.support.v7.media;
+
+import android.content.IntentFilter;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the properties of a route.
+ * <p>
+ * Each route is uniquely identified by an opaque id string.  This token
+ * may take any form as long as it is unique within the media route provider.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteDescriptor {
+    private static final String KEY_ID = "id";
+    private static final String KEY_NAME = "name";
+    private static final String KEY_STATUS = "status";
+    private static final String KEY_ICON_RESOURCE = "iconResource";
+    private static final String KEY_ENABLED = "enabled";
+    private static final String KEY_CONNECTING = "connecting";
+    private static final String KEY_CONTROL_FILTERS = "controlFilters";
+    private static final String KEY_PLAYBACK_TYPE = "playbackType";
+    private static final String KEY_PLAYBACK_STREAM = "playbackStream";
+    private static final String KEY_VOLUME = "volume";
+    private static final String KEY_VOLUME_MAX = "volumeMax";
+    private static final String KEY_VOLUME_HANDLING = "volumeHandling";
+    private static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
+    private static final String KEY_EXTRAS = "extras";
+
+    private final Bundle mBundle;
+    private final Drawable mIconDrawable;
+    private List<IntentFilter> mControlFilters;
+
+    private MediaRouteDescriptor(Bundle bundle, Drawable iconDrawable,
+            List<IntentFilter> controlFilters) {
+        mBundle = bundle;
+        mIconDrawable = iconDrawable;
+        mControlFilters = controlFilters;
+    }
+
+    /**
+     * Gets the unique id of the route.
+     */
+    public String getId() {
+        return mBundle.getString(KEY_ID);
+    }
+
+    /**
+     * Gets the user-friendly name of the route.
+     */
+    public String getName() {
+        return mBundle.getString(KEY_NAME);
+    }
+
+    /**
+     * Gets the user-friendly status of the route.
+     */
+    public String getStatus() {
+        return mBundle.getString(KEY_STATUS);
+    }
+
+    /**
+     * Gets a drawable to display as the route's icon.
+     * <p>
+     * Because drawables cannot be transferred to other processes, the icon resource
+     * is usually passed in {@link #getIconResource} instead.
+     * </p>
+     */
+    public Drawable getIconDrawable() {
+        return mIconDrawable;
+    }
+
+    /**
+     * Gets the id of a drawable resource to display as the route's icon.
+     * <p>
+     * The specified drawable resource id will be loaded from the media route
+     * provider's package.
+     * </p>
+     */
+    public int getIconResource() {
+        return mBundle.getInt(KEY_ICON_RESOURCE);
+    }
+
+    /**
+     * Gets whether the route is enabled.
+     */
+    public boolean isEnabled() {
+        return mBundle.getBoolean(KEY_ENABLED, true);
+    }
+
+    /**
+     * Gets whether the route is connecting.
+     */
+    public boolean isConnecting() {
+        return mBundle.getBoolean(KEY_CONNECTING, false);
+    }
+
+    /**
+     * Gets the route's {@link MediaControlIntent media control intent} filters.
+     */
+    public List<IntentFilter> getControlFilters() {
+        ensureControlFilters();
+        return mControlFilters;
+    }
+
+    private void ensureControlFilters() {
+        if (mControlFilters == null) {
+            mControlFilters = mBundle.<IntentFilter>getParcelableArrayList(KEY_CONTROL_FILTERS);
+            if (mControlFilters == null) {
+                mControlFilters = Collections.<IntentFilter>emptyList();
+            }
+        }
+    }
+
+    /**
+     * Gets the route's playback type.
+     */
+    public int getPlaybackType() {
+        return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
+    }
+
+    /**
+     * Gets the route's playback stream.
+     */
+    public int getPlaybackStream() {
+        return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
+    }
+
+    /**
+     * Gets the route's current volume, or 0 if unknown.
+     */
+    public int getVolume() {
+        return mBundle.getInt(KEY_VOLUME);
+    }
+
+    /**
+     * Gets the route's maximum volume, or 0 if unknown.
+     */
+    public int getVolumeMax() {
+        return mBundle.getInt(KEY_VOLUME_MAX);
+    }
+
+    /**
+     * Gets the route's volume handling.
+     */
+    public int getVolumeHandling() {
+        return mBundle.getInt(KEY_VOLUME_HANDLING,
+                MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
+    }
+
+    /**
+     * Gets the route's presentation display id, or -1 if none.
+     */
+    public int getPresentationDisplayId() {
+        return mBundle.getInt(KEY_PRESENTATION_DISPLAY_ID, -1);
+    }
+
+    /**
+     * Gets a bundle of extras for this route descriptor.
+     * The extras will be ignored by the media router but they may be used
+     * by applications.
+     */
+    public Bundle getExtras() {
+        return mBundle.getBundle(KEY_EXTRAS);
+    }
+
+    /**
+     * Returns true if the route descriptor has all of the required fields.
+     */
+    public boolean isValid() {
+        ensureControlFilters();
+        if (TextUtils.isEmpty(getId())
+                || TextUtils.isEmpty(getName())
+                || mControlFilters.contains(null)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append("MediaRouteDescriptor{ ");
+        result.append("id=").append(getId());
+        result.append(", name=").append(getName());
+        result.append(", status=").append(getStatus());
+        result.append(", isEnabled=").append(isEnabled());
+        result.append(", isConnecting=").append(isConnecting());
+        result.append(", controlFilters=").append(Arrays.toString(getControlFilters().toArray()));
+        result.append(", iconDrawable=").append(getIconDrawable());
+        result.append(", iconResource=").append(getIconResource());
+        result.append(", playbackType=").append(getPlaybackType());
+        result.append(", playbackStream=").append(getPlaybackStream());
+        result.append(", volume=").append(getVolume());
+        result.append(", volumeMax=").append(getVolumeMax());
+        result.append(", volumeHandling=").append(getVolumeHandling());
+        result.append(", presentationDisplayId=").append(getPresentationDisplayId());
+        result.append(", extras=").append(getExtras());
+        result.append(", isValid=").append(isValid());
+        result.append("}");
+        return result.toString();
+    }
+
+    /**
+     * Converts this object to a bundle for serialization.
+     *
+     * @return The contents of the object represented as a bundle.
+     */
+    public Bundle asBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Creates an instance from a bundle.
+     *
+     * @param bundle The bundle, or null if none.
+     * @return The new instance, or null if the bundle was null.
+     */
+    public static MediaRouteDescriptor fromBundle(Bundle bundle) {
+        return bundle != null ? new MediaRouteDescriptor(bundle, null, null) : null;
+    }
+
+    /**
+     * Builder for {@link MediaRouteDescriptor media route descriptors}.
+     */
+    public static final class Builder {
+        private final Bundle mBundle;
+        private Drawable mIconDrawable;
+        private ArrayList<IntentFilter> mControlFilters;
+
+        /**
+         * Creates a media route descriptor builder.
+         *
+         * @param id The unique id of the route.
+         * @param name The user-friendly name of the route.
+         */
+        public Builder(String id, String name) {
+            mBundle = new Bundle();
+            setId(id);
+            setName(name);
+        }
+
+        /**
+         * Creates a media route descriptor builder whose initial contents are
+         * copied from an existing descriptor.
+         */
+        public Builder(MediaRouteDescriptor descriptor) {
+            if (descriptor == null) {
+                throw new IllegalArgumentException("descriptor must not be null");
+            }
+
+            mBundle = new Bundle(descriptor.mBundle);
+            mIconDrawable = descriptor.mIconDrawable;
+
+            descriptor.ensureControlFilters();
+            if (!descriptor.mControlFilters.isEmpty()) {
+                mControlFilters = new ArrayList<IntentFilter>(descriptor.mControlFilters);
+            }
+        }
+
+        /**
+         * Sets the unique id of the route.
+         */
+        public Builder setId(String id) {
+            mBundle.putString(KEY_ID, id);
+            return this;
+        }
+
+        /**
+         * Sets the user-friendly name of the route.
+         */
+        public Builder setName(String name) {
+            mBundle.putString(KEY_NAME, name);
+            return this;
+        }
+
+        /**
+         * Sets the user-friendly status of the route.
+         */
+        public Builder setStatus(String status) {
+            mBundle.putString(KEY_STATUS, status);
+            return this;
+        }
+
+        /**
+         * Sets a drawable to display as the route's icon.
+         * <p>
+         * Because drawables cannot be transferred to other processes, this method may
+         * only be used by media route providers that reside in the same process
+         * as the application.  When implementing a media route provider service, use
+         * {@link #setIconResource} instead.
+         * </p>
+         */
+        public Builder setIconDrawable(Drawable drawable) {
+            mIconDrawable = drawable;
+            return this;
+        }
+
+        /**
+         * Sets the id of a drawable resource to display as the route's icon.
+         * <p>
+         * The specified drawable resource id will be loaded from the media route
+         * provider's package.
+         * </p>
+         */
+        public Builder setIconResource(int id) {
+            mBundle.putInt(KEY_ICON_RESOURCE, id);
+            return this;
+        }
+
+        /**
+         * Sets whether the route is enabled.
+         * <p>
+         * Disabled routes represent routes that a route provider knows about, such as paired
+         * Wifi Display receivers, but that are not currently available for use.
+         * </p>
+         */
+        public Builder setEnabled(boolean enabled) {
+            mBundle.putBoolean(KEY_ENABLED, enabled);
+            return this;
+        }
+
+        /**
+         * Sets whether the route is in the process of connecting and is not yet
+         * ready for use.
+         */
+        public Builder setConnecting(boolean connecting) {
+            mBundle.putBoolean(KEY_CONNECTING, connecting);
+            return this;
+        }
+
+        /**
+         * Adds a {@link MediaControlIntent media control intent} filter for the route.
+         */
+        public Builder addControlFilter(IntentFilter filter) {
+            if (filter == null) {
+                throw new IllegalArgumentException("filter must not be null");
+            }
+
+            if (mControlFilters == null) {
+                mControlFilters = new ArrayList<IntentFilter>();
+            }
+            if (!mControlFilters.contains(filter)) {
+                mControlFilters.add(filter);
+            }
+            return this;
+        }
+
+        /**
+         * Adds a list of {@link MediaControlIntent media control intent} filters for the route.
+         */
+        public Builder addControlFilters(List<IntentFilter> filters) {
+            if (filters == null) {
+                throw new IllegalArgumentException("filters must not be null");
+            }
+
+            final int count = filters.size();
+            for (int i = 0; i < count; i++) {
+                addControlFilter(filters.get(i));
+            }
+            return this;
+        }
+
+        /**
+         * Sets the route's playback type.
+         */
+        public Builder setPlaybackType(int playbackType) {
+            mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
+            return this;
+        }
+
+        /**
+         * Sets the route's playback stream.
+         */
+        public Builder setPlaybackStream(int playbackStream) {
+            mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
+            return this;
+        }
+
+        /**
+         * Sets the route's current volume, or 0 if unknown.
+         */
+        public Builder setVolume(int volume) {
+            mBundle.putInt(KEY_VOLUME, volume);
+            return this;
+        }
+
+        /**
+         * Sets the route's maximum volume, or 0 if unknown.
+         */
+        public Builder setVolumeMax(int volumeMax) {
+            mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
+            return this;
+        }
+
+        /**
+         * Sets the route's volume handling.
+         */
+        public Builder setVolumeHandling(int volumeHandling) {
+            mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
+            return this;
+        }
+
+        /**
+         * Sets the route's presentation display id, or -1 if none.
+         */
+        public Builder setPresentationDisplayId(int presentationDisplayId) {
+            mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
+            return this;
+        }
+
+        /**
+         * Sets a bundle of extras for this route descriptor.
+         * The extras will be ignored by the media router but they may be used
+         * by applications.
+         */
+        public Builder setExtras(Bundle extras) {
+            mBundle.putBundle(KEY_EXTRAS, extras);
+            return this;
+        }
+
+        /**
+         * Builds the {@link MediaRouteDescriptor media route descriptor}.
+         */
+        public MediaRouteDescriptor build() {
+            if (mControlFilters != null) {
+                mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, mControlFilters);
+            }
+            return new MediaRouteDescriptor(mBundle, mIconDrawable, mControlFilters);
+        }
+    }
+}
\ No newline at end of file
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteDiscoveryRequest.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteDiscoveryRequest.java
new file mode 100644
index 0000000..f4d2f5f
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteDiscoveryRequest.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.support.v7.media;
+
+import android.os.Bundle;
+
+/**
+ * Describes the kinds of routes that the media router would like to discover
+ * and whether to perform active scanning.
+ * <p>
+ * This object is immutable once created.
+ * </p>
+ */
+public final class MediaRouteDiscoveryRequest {
+    private static final String KEY_SELECTOR = "selector";
+    private static final String KEY_ACTIVE_SCAN = "activeScan";
+
+    private final Bundle mBundle;
+    private MediaRouteSelector mSelector;
+
+    /**
+     * Creates a media route discovery request.
+     *
+     * @param selector The route selector that specifies the kinds of routes to discover.
+     * @param activeScan True if active scanning should be performed.
+     */
+    public MediaRouteDiscoveryRequest(MediaRouteSelector selector, boolean activeScan) {
+        if (selector == null) {
+            throw new IllegalArgumentException("selector must not be null");
+        }
+
+        mBundle = new Bundle();
+        mSelector = selector;
+        mBundle.putBundle(KEY_SELECTOR, selector.asBundle());
+        mBundle.putBoolean(KEY_ACTIVE_SCAN, activeScan);
+    }
+
+    private MediaRouteDiscoveryRequest(Bundle bundle) {
+        mBundle = bundle;
+    }
+
+    /**
+     * Gets the route selector that specifies the kinds of routes to discover.
+     */
+    public MediaRouteSelector getSelector() {
+        ensureSelector();
+        return mSelector;
+    }
+
+    private void ensureSelector() {
+        if (mSelector == null) {
+            mSelector = MediaRouteSelector.fromBundle(mBundle.getBundle(KEY_SELECTOR));
+            if (mSelector == null) {
+                mSelector = MediaRouteSelector.EMPTY;
+            }
+        }
+    }
+
+    /**
+     * Returns true if active scanning should be performed.
+     *
+     * @see MediaRouter#CALLBACK_FLAG_ACTIVE_SCAN
+     */
+    public boolean isActiveScan() {
+        return mBundle.getBoolean(KEY_ACTIVE_SCAN);
+    }
+
+    /**
+     * Returns true if the discovery request has all of the required fields.
+     */
+    public boolean isValid() {
+        ensureSelector();
+        return mSelector.isValid();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof MediaRouteDiscoveryRequest) {
+            MediaRouteDiscoveryRequest other = (MediaRouteDiscoveryRequest)o;
+            return getSelector().equals(other.getSelector())
+                    && isActiveScan() == other.isActiveScan();
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return getSelector().hashCode() ^ (isActiveScan() ? 1 : 0);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append("DiscoveryRequest{ selector=").append(getSelector());
+        result.append(", activeScan=").append(isActiveScan());
+        result.append(", isValid=").append(isValid());
+        result.append(" }");
+        return result.toString();
+    }
+
+    /**
+     * Converts this object to a bundle for serialization.
+     *
+     * @return The contents of the object represented as a bundle.
+     */
+    public Bundle asBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Creates an instance from a bundle.
+     *
+     * @param bundle The bundle, or null if none.
+     * @return The new instance, or null if the bundle was null.
+     */
+    public static MediaRouteDiscoveryRequest fromBundle(Bundle bundle) {
+        return bundle != null ? new MediaRouteDiscoveryRequest(bundle) : null;
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
index c78171d..aec7b2b 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
@@ -18,42 +18,54 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
-import android.os.Parcelable;
 import android.support.v7.media.MediaRouter.ControlRequestCallback;
 import android.text.TextUtils;
 
-import java.util.concurrent.CopyOnWriteArrayList;
-
 /**
  * Media route providers are used to publish additional media routes for
  * use within an application.  Media route providers may also be declared
  * as a service to publish additional media routes to all applications
  * in the system.
  * <p>
- * Applications and services should extend this class to publish additional media routes
- * to the {@link MediaRouter}.  To make additional media routes available within
- * your application, call {@link MediaRouter#addProvider} to add your provider to
- * the media router.  To make additional media routes available to all applications
- * in the system, register a media route provider service in your manifest.
+ * The purpose of a media route provider is to discover media routes that satisfy
+ * the criteria specified by the current {@link MediaRouteDiscoveryRequest} and publish a
+ * {@link MediaRouteProviderDescriptor} with information about each route by calling
+ * {@link #setDescriptor} to notify the currently registered {@link Callback}.
+ * </p><p>
+ * The provider should watch for changes to the discovery request by implementing
+ * {@link #onDiscoveryRequestChanged} and updating the set of routes that it is
+ * attempting to discover.  It should also handle route control requests such
+ * as volume changes or {@link MediaControlIntent media control intents}
+ * by implementing {@link #onCreateRouteController} to return a {@link RouteController}
+ * for a particular route.
+ * </p><p>
+ * A media route provider may be used privately within the scope of a single
+ * application process by calling {@link MediaRouter#addProvider MediaRouter.addProvider}
+ * to add it to the local {@link MediaRouter}.  A media route provider may also be made
+ * available globally to all applications by registering a {@link MediaRouteProviderService}
+ * in the provider's manifest.  When the media route provider is registered
+ * as a service, all applications that use the media router API will be able to
+ * discover and used the provider's routes without having to install anything else.
  * </p><p>
  * This object must only be accessed on the main thread.
  * </p>
  */
 public abstract class MediaRouteProvider {
     private static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
+    private static final int MSG_DELIVER_DISCOVERY_REQUEST_CHANGED = 2;
 
     private final Context mContext;
     private final ProviderMetadata mMetadata;
     private final ProviderHandler mHandler = new ProviderHandler();
-    private final CopyOnWriteArrayList<Callback> mCallbacks =
-            new CopyOnWriteArrayList<Callback>();
 
-    private ProviderDescriptor mDescriptor;
+    private Callback mCallback;
+
+    private MediaRouteDiscoveryRequest mDiscoveryRequest;
+    private boolean mPendingDiscoveryRequestChange;
+
+    private MediaRouteProviderDescriptor mDescriptor;
     private boolean mPendingDescriptorChange;
 
     /**
@@ -85,37 +97,124 @@
         return mContext;
     }
 
-    final ProviderMetadata getMetadata() {
+    /**
+     * Gets the provider's handler which is associated with the main thread.
+     */
+    public final Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * Gets some metadata about the provider's implementation.
+     */
+    public final ProviderMetadata getMetadata() {
         return mMetadata;
     }
 
     /**
+     * Sets a callback to invoke when the provider's descriptor changes.
+     *
+     * @param callback The callback to use, or null if none.
+     */
+    public final void setCallback(Callback callback) {
+        MediaRouter.checkCallingThread();
+        mCallback = callback;
+    }
+
+    /**
+     * Gets the current discovery request which informs the provider about the
+     * kinds of routes to discover and whether to perform active scanning.
+     *
+     * @return The current discovery request, or null if no discovery is needed at this time.
+     *
+     * @see #onDiscoveryRequestChanged
+     */
+    public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
+        return mDiscoveryRequest;
+    }
+
+    /**
+     * Sets a discovery request to inform the provider about the kinds of
+     * routes that its clients would like to discover and whether to perform active scanning.
+     *
+     * @param request The discovery request, or null if no discovery is needed at this time.
+     *
+     * @see #onDiscoveryRequestChanged
+     */
+    public final void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+        MediaRouter.checkCallingThread();
+
+        if (mDiscoveryRequest == request
+                || (mDiscoveryRequest != null && mDiscoveryRequest.equals(request))) {
+            return;
+        }
+
+        mDiscoveryRequest = request;
+        if (!mPendingDiscoveryRequestChange) {
+            mPendingDiscoveryRequestChange = true;
+            mHandler.sendEmptyMessage(MSG_DELIVER_DISCOVERY_REQUEST_CHANGED);
+        }
+    }
+
+    private void deliverDiscoveryRequestChanged() {
+        mPendingDiscoveryRequestChange = false;
+        onDiscoveryRequestChanged(mDiscoveryRequest);
+    }
+
+    /**
+     * Called by the media router when the {@link MediaRouteDiscoveryRequest discovery request}
+     * has changed.
+     * <p>
+     * Whenever an applications calls {@link MediaRouter#addCallback} to register
+     * a callback, it also provides a selector to specify the kinds of routes that
+     * it is interested in.  The media router combines all of these selectors together
+     * to generate a {@link MediaRouteDiscoveryRequest} and notifies each provider when a change
+     * occurs by calling {@link #setDiscoveryRequest} which posts a message to invoke
+     * this method asynchronously.
+     * </p><p>
+     * The provider should examine the {@link MediaControlIntent media control categories}
+     * in the discovery request's {@link MediaRouteSelector selector} to determine what
+     * kinds of routes it should try to discover and whether it should perform active
+     * or passive scans.  In many cases, the provider may be able to save power by
+     * determining that the selector does not contain any categories that it supports
+     * and it can therefore avoid performing any scans at all.
+     * </p>
+     *
+     * @param request The new discovery request, or null if no discovery is needed at this time.
+     *
+     * @see MediaRouter#addCallback
+     */
+    public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+    }
+
+    /**
      * Gets the provider's descriptor.
      * <p>
      * The descriptor describes the state of the media route provider and
      * the routes that it publishes.  Watch for changes to the descriptor
-     * by registering a {@link Callback callback} with {@link #addCallback}.
+     * by registering a {@link Callback callback} with {@link #setCallback}.
      * </p>
      *
-     * @return The media route provider descriptor, or null if none.  This object
-     * and all of its contents should be treated as if it were immutable so that it is
-     * safe for clients to cache it.
+     * @return The media route provider descriptor, or null if none.
+     *
+     * @see Callback#onDescriptorChanged
      */
-    public final ProviderDescriptor getDescriptor() {
+    public final MediaRouteProviderDescriptor getDescriptor() {
         return mDescriptor;
     }
 
     /**
      * Sets the provider's descriptor.
      * <p>
-     * Asynchronously notifies all registered {@link Callback callbacks} about the change.
+     * The provider must call this method to notify the currently registered
+     * {@link Callback callback} about the change to the provider's descriptor.
      * </p>
      *
      * @param descriptor The updated route provider descriptor, or null if none.
-     * This object and all of its contents should be treated as if it were immutable
-     * so that it is safe for clients to cache it.
+     *
+     * @see Callback#onDescriptorChanged
      */
-    public final void setDescriptor(ProviderDescriptor descriptor) {
+    public final void setDescriptor(MediaRouteProviderDescriptor descriptor) {
         MediaRouter.checkCallingThread();
 
         if (mDescriptor != descriptor) {
@@ -130,44 +229,12 @@
     private void deliverDescriptorChanged() {
         mPendingDescriptorChange = false;
 
-        if (!mCallbacks.isEmpty()) {
-            final ProviderDescriptor currentDescriptor = mDescriptor;
-            for (Callback callback : mCallbacks) {
-                callback.onDescriptorChanged(this, currentDescriptor);
-            }
+        if (mCallback != null) {
+            mCallback.onDescriptorChanged(this, mDescriptor);
         }
     }
 
     /**
-     * Adds a callback to be invoked on the main thread when information about a route
-     * provider and its routes changes.
-     *
-     * @param callback The callback to add.
-     */
-    public final void addCallback(Callback callback) {
-        if (callback == null) {
-            throw new IllegalArgumentException("callback");
-        }
-
-        if (!mCallbacks.contains(callback)) {
-            mCallbacks.add(callback);
-        }
-    }
-
-    /**
-     * Removes a callback.
-     *
-     * @param callback The callback to remove.
-     */
-    public final void removeCallback(Callback callback) {
-        if (callback == null) {
-            throw new IllegalArgumentException("callback");
-        }
-
-        mCallbacks.remove(callback);
-    }
-
-    /**
      * Called by the media router to obtain a route controller for a particular route.
      * <p>
      * The media router will invoke the {@link RouteController#onRelease} method of the route
@@ -183,38 +250,19 @@
     }
 
     /**
-     * Called by the media router when the provider should start actively scanning
-     * for changes to routes.
+     * Describes properties of the route provider's implementation.
      * <p>
-     * Typically this callback is invoked when the media route picker dialog has been
-     * opened by the user to ensure that the set of known routes is fresh and up to date.
+     * This object is immutable once created.
      * </p>
-     *
-     * @see ProviderDescriptor#setActiveScanRequired
      */
-    public void onStartActiveScan() {
-    }
-
-    /**
-     * Called by the media router when the provider should stop actively scanning
-     * for changes to routes.  The provider may continue passively scanning for changes
-     * to routes as long as its scans are low power and non-intrusive.
-     * <p>
-     * Typically this callback is invoked when the media route picker dialog has been
-     * closed by the user.
-     * </p>
-     *
-     * @see ProviderDescriptor#setActiveScanRequired
-     */
-    public void onStopActiveScan() {
-    }
-
-    /**
-     * Describes immutable properties of the route provider itself.
-     */
-    static final class ProviderMetadata {
+    public static final class ProviderMetadata {
         private final String mPackageName;
 
+        /**
+         * Creates a provider metadata object.
+         *
+         * @param packageName The provider application's package name.
+         */
         public ProviderMetadata(String packageName) {
             if (TextUtils.isEmpty(packageName)) {
                 throw new IllegalArgumentException("packageName must not be null or empty");
@@ -222,456 +270,16 @@
             mPackageName = packageName;
         }
 
+        /**
+         * Gets the provider application's package name.
+         */
         public String getPackageName() {
             return mPackageName;
         }
-    }
-
-    /**
-     * Describes the state of a media route provider and the routes that it publishes.
-     */
-    public static final class ProviderDescriptor {
-        private static final String KEY_ROUTES = "routes";
-        private static final String KEY_ACTIVE_SCAN_REQUIRED = "activeScanRequired";
-
-        private final Bundle mBundle;
-        private RouteDescriptor[] mRoutes;
-
-        /**
-         * Creates a route provider descriptor.
-         */
-        public ProviderDescriptor() {
-            mBundle = new Bundle();
-        }
-
-        /**
-         * Creates a copy of another route provider descriptor.
-         */
-        public ProviderDescriptor(ProviderDescriptor other) {
-            mBundle = new Bundle(other.mBundle);
-        }
-
-        ProviderDescriptor(Bundle bundle) {
-            mBundle = bundle;
-        }
-
-        /**
-         * Gets the list of all routes that this provider has published.
-         */
-        public RouteDescriptor[] getRoutes() {
-            if (mRoutes == null) {
-                mRoutes = RouteDescriptor.fromParcelableArray(
-                        mBundle.getParcelableArray(KEY_ROUTES));
-            }
-            return mRoutes;
-        }
-
-        /**
-         * Sets the list of all routes that this provider has published.
-         */
-        public void setRoutes(RouteDescriptor[] routes) {
-            if (routes == null) {
-                throw new IllegalArgumentException("routes must not be null");
-            }
-            mRoutes = routes;
-            mBundle.putParcelableArray(KEY_ROUTES, RouteDescriptor.toParcelableArray(routes));
-        }
-
-        /**
-         * Returns true if the provider requires active scans to discover routes.
-         */
-        public boolean isActiveScanRequired() {
-            return mBundle.getBoolean(KEY_ACTIVE_SCAN_REQUIRED, false);
-        }
-
-        /**
-         * Sets whether the provider requires active scans to discover routes.
-         * <p>
-         * To provide the best user experience, a media route provider should passively
-         * discover and publish changes to route descriptors in the background.
-         * However, for some providers, scanning for routes may use a significant
-         * amount of power or may interfere with wireless network connectivity.
-         * If this is the case, then the provider should indicate that it requires
-         * active scans by setting this flag.
-         * </p><p>
-         * Even if this flag is not set, the provider will be given an opportunity
-         * to perform a scan to update the route descriptors that it has published
-         * when the route picker dialog is opened.  The provider should only set
-         * this flag if it is unable to discover routes without active scans at all.
-         * </p>
-         */
-        public void setActiveScanRequired(boolean required) {
-            mBundle.putBoolean(KEY_ACTIVE_SCAN_REQUIRED, required);
-        }
-
-        /**
-         * Returns true if the route provider descriptor and all of the routes that
-         * it contains have all of the required fields.
-         * <p>
-         * This verification is deep.  If the provider descriptor is known to be
-         * valid then it is not necessary to call {@link #isValid} on each of its routes.
-         * </p>
-         */
-        public boolean isValid() {
-            for (RouteDescriptor route : getRoutes()) {
-                if (route == null || !route.isValid()) {
-                    return false;
-                }
-            }
-            return true;
-        }
 
         @Override
         public String toString() {
-            return "RouteProviderDescriptor{" + mBundle.toString() + "}";
-        }
-
-        Bundle asBundle() {
-            return mBundle;
-        }
-
-        static ProviderDescriptor fromBundle(Bundle bundle) {
-            return bundle != null ? new ProviderDescriptor(bundle) : null;
-        }
-    }
-
-    /**
-     * Describes the properties of a route.
-     * <p>
-     * Each route is uniquely identified by an opaque id string.  This token
-     * may take any form as long as it is unique within the media route provider.
-     * </p>
-     */
-    public static final class RouteDescriptor {
-        static final RouteDescriptor[] EMPTY_ROUTE_ARRAY = new RouteDescriptor[0];
-        static final IntentFilter[] EMTPY_FILTER_ARRAY = new IntentFilter[0];
-
-        private static final String KEY_ID = "id";
-        private static final String KEY_NAME = "name";
-        private static final String KEY_STATUS = "status";
-        private static final String KEY_ICON_RESOURCE = "iconId";
-        private static final String KEY_ENABLED = "enabled";
-        private static final String KEY_CONTROL_FILTERS = "controlFilters";
-        private static final String KEY_PLAYBACK_TYPE = "playbackType";
-        private static final String KEY_PLAYBACK_STREAM = "playbackStream";
-        private static final String KEY_VOLUME = "volume";
-        private static final String KEY_VOLUME_MAX = "volumeMax";
-        private static final String KEY_VOLUME_HANDLING = "volumeHandling";
-        private static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
-        private static final String KEY_EXTRAS = "extras";
-
-        private final Bundle mBundle;
-        private IntentFilter[] mControlFilters;
-        private Drawable mIconDrawable;
-
-        /**
-         * Creates a route descriptor.
-         *
-         * @param id The unique id of the route.
-         * @param name The user-friendly name of the route.
-         */
-        public RouteDescriptor(String id, String name) {
-            mBundle = new Bundle();
-            setId(id);
-            setName(name);
-        }
-
-        /**
-         * Creates a copy of another route descriptor.
-         */
-        public RouteDescriptor(RouteDescriptor other) {
-            mBundle = new Bundle(other.mBundle);
-        }
-
-        RouteDescriptor(Bundle bundle) {
-            mBundle = bundle;
-        }
-
-        /**
-         * Gets the unique id of the route.
-         */
-        public String getId() {
-            return mBundle.getString(KEY_ID);
-        }
-
-        /**
-         * Sets the unique id of the route.
-         */
-        public void setId(String id) {
-            mBundle.putString(KEY_ID, id);
-        }
-
-        /**
-         * Gets the user-friendly name of the route.
-         */
-        public String getName() {
-            return mBundle.getString(KEY_NAME);
-        }
-
-        /**
-         * Sets the user-friendly name of the route.
-         */
-        public void setName(String name) {
-            mBundle.putString(KEY_NAME, name);
-        }
-
-        /**
-         * Gets the user-friendly status of the route.
-         */
-        public String getStatus() {
-            return mBundle.getString(KEY_STATUS);
-        }
-
-        /**
-         * Sets the user-friendly status of the route.
-         */
-        public void setStatus(String status) {
-            mBundle.putString(KEY_STATUS, status);
-        }
-
-        /**
-         * Gets a drawable to display as the route's icon.
-         * <p>
-         * Because drawables cannot be transferred to other processes, this method may
-         * only be used by media route providers that reside in the same process
-         * as the application.  When implementing a media route provider service, use
-         * {@link #getIconResource} instead.
-         * </p>
-         */
-        public Drawable getIconDrawable() {
-            return mIconDrawable;
-        }
-
-        /**
-         * Sets a drawable to display as the route's icon.
-         * <p>
-         * Because drawables cannot be transferred to other processes, this method may
-         * only be used by media route providers that reside in the same process
-         * as the application.  When implementing a media route provider service, use
-         * {@link #setIconResource} instead.
-         * </p>
-         */
-        public void setIconDrawable(Drawable drawable) {
-            mIconDrawable = drawable;
-        }
-
-        /**
-         * Gets the id of a drawable resource to display as the route's icon.
-         * <p>
-         * The specified drawable resource id will be loaded from the media route
-         * provider's package.
-         * </p>
-         */
-        public int getIconResource() {
-            return mBundle.getInt(KEY_ICON_RESOURCE);
-        }
-
-        /**
-         * Sets the id of a drawable resource to display as the route's icon.
-         * <p>
-         * The specified drawable resource id will be loaded from the media route
-         * provider's package.
-         * </p>
-         */
-        public void setIconResource(int id) {
-            mBundle.putInt(KEY_ICON_RESOURCE, id);
-        }
-
-        /**
-         * Gets whether the route is enabled.
-         */
-        public boolean isEnabled() {
-            return mBundle.getBoolean(KEY_ENABLED, true);
-        }
-
-        /**
-         * Sets whether the route is enabled.
-         * <p>
-         * Disabled routes represent routes that a route provider knows about, such as paired
-         * Wifi Display receivers, but that are not currently available for use.
-         * </p>
-         */
-        public void setEnabled(boolean enabled) {
-            mBundle.putBoolean(KEY_ENABLED, enabled);
-        }
-
-        /**
-         * Gets the route's {@link MediaControlIntent media control intent} filters.
-         */
-        public IntentFilter[] getControlFilters() {
-            if (mControlFilters == null) {
-                Parcelable[] filters = mBundle.getParcelableArray(KEY_CONTROL_FILTERS);
-                if (filters instanceof IntentFilter[]) {
-                    mControlFilters = (IntentFilter[])filters;
-                } else if (filters != null && filters.length > 0) {
-                    mControlFilters = new IntentFilter[filters.length];
-                    System.arraycopy(filters, 0, mControlFilters, 0, filters.length);
-                } else {
-                    mControlFilters = EMTPY_FILTER_ARRAY;
-                }
-            }
-            return mControlFilters;
-        }
-
-        /**
-         * Sets the route's {@link MediaControlIntent media control intent} filters.
-         */
-        public void setControlFilters(IntentFilter[] controlFilters) {
-            if (controlFilters == null) {
-                throw new IllegalArgumentException("controlFilters must not be null");
-            }
-            mControlFilters = controlFilters;
-            mBundle.putParcelableArray(KEY_CONTROL_FILTERS, controlFilters);
-        }
-
-        /**
-         * Gets the route's playback type.
-         */
-        public int getPlaybackType() {
-            return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
-        }
-
-        /**
-         * Sets the route's playback type.
-         */
-        public void setPlaybackType(int playbackType) {
-            mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
-        }
-
-        /**
-         * Gets the route's playback stream.
-         */
-        public int getPlaybackStream() {
-            return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
-        }
-
-        /**
-         * Sets the route's playback stream.
-         */
-        public void setPlaybackStream(int playbackStream) {
-            mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
-        }
-
-        /**
-         * Gets the route's current volume, or 0 if unknown.
-         */
-        public int getVolume() {
-            return mBundle.getInt(KEY_VOLUME);
-        }
-
-        /**
-         * Sets the route's current volume, or 0 if unknown.
-         */
-        public void setVolume(int volume) {
-            mBundle.putInt(KEY_VOLUME, volume);
-        }
-
-        /**
-         * Gets the route's maximum volume, or 0 if unknown.
-         */
-        public int getVolumeMax() {
-            return mBundle.getInt(KEY_VOLUME_MAX);
-        }
-
-        /**
-         * Sets the route's maximum volume, or 0 if unknown.
-         */
-        public void setVolumeMax(int volumeMax) {
-            mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
-        }
-
-        /**
-         * Gets the route's volume handling.
-         */
-        public int getVolumeHandling() {
-            return mBundle.getInt(KEY_VOLUME_HANDLING,
-                    MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
-        }
-
-        /**
-         * Sets the route's volume handling.
-         */
-        public void setVolumeHandling(int volumeHandling) {
-            mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
-        }
-
-        /**
-         * Gets the route's presentation display id, or -1 if none.
-         */
-        public int getPresentationDisplayId() {
-            return mBundle.getInt(KEY_PRESENTATION_DISPLAY_ID, -1);
-        }
-
-        /**
-         * Sets the route's presentation display id, or -1 if none.
-         */
-        public void setPresentationDisplayId(int presentationDisplayId) {
-            mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
-        }
-
-        /**
-         * Gets a bundle of extras for this route descriptor.
-         * The extras will be ignored by the media router but they may be used
-         * by applications.
-         */
-        public Bundle getExtras() {
-            return mBundle.getBundle(KEY_EXTRAS);
-        }
-
-        /**
-         * Sets a bundle of extras for this route descriptor.
-         * The extras will be ignored by the media router but they may be used
-         * by applications.
-         */
-        public void setExtras(Bundle extras) {
-            mBundle.putBundle(KEY_EXTRAS, extras);
-        }
-
-        /**
-         * Returns true if the route descriptor has all of the required fields.
-         */
-        public boolean isValid() {
-            if (TextUtils.isEmpty(getId())
-                    || TextUtils.isEmpty(getName())) {
-                return false;
-            }
-            for (IntentFilter filter : getControlFilters()) {
-                if (filter == null) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        @Override
-        public String toString() {
-            return "RouteDescriptor{" + mBundle.toString() + "}";
-        }
-
-        Bundle asBundle() {
-            return mBundle;
-        }
-
-        static Parcelable[] toParcelableArray(RouteDescriptor[] descriptors) {
-            if (descriptors != null && descriptors.length > 0) {
-                Parcelable[] bundles = new Parcelable[descriptors.length];
-                for (int i = 0; i < descriptors.length; i++) {
-                    bundles[i] = descriptors[i].asBundle();
-                }
-                return bundles;
-            }
-            return null;
-        }
-
-        static RouteDescriptor[] fromParcelableArray(Parcelable[] bundles) {
-            if (bundles != null && bundles.length > 0) {
-                RouteDescriptor[] descriptors = new RouteDescriptor[bundles.length];
-                for (int i = 0; i < bundles.length; i++) {
-                    descriptors[i] = new RouteDescriptor((Bundle)bundles[i]);
-                }
-                return descriptors;
-            }
-            return EMPTY_ROUTE_ARRAY;
+            return "ProviderMetadata{ packageName=" + mPackageName + " }";
         }
     }
 
@@ -717,7 +325,7 @@
         /**
          * Requests to set the volume of the route.
          *
-         * @param volume The new volume value between 0 and {@link RouteDescriptor#getVolumeMax}.
+         * @param volume The new volume value between 0 and {@link MediaRouteDescriptor#getVolumeMax}.
          */
         public void onSetVolume(int volume) {
         }
@@ -738,7 +346,7 @@
          * @param callback A {@link ControlRequestCallback} to invoke with the result
          * of the request, or null if no result is required.
          * @return True if the controller intends to handle the request and will
-         * invoke the callback when finished.  False if the contorller will not
+         * invoke the callback when finished.  False if the controller will not
          * handle the request and will not invoke the callback.
          *
          * @see MediaControlIntent
@@ -755,12 +363,11 @@
         /**
          * Called when information about a route provider and its routes changes.
          *
-         * @param provider The media route provider that changed.
-         * and all of its contents should be treated as if it were immutable so that it is
-         * safe for clients to cache it.
+         * @param provider The media route provider that changed, never null.
+         * @param descriptor The new media route provider descriptor, or null if none.
          */
         public void onDescriptorChanged(MediaRouteProvider provider,
-                ProviderDescriptor descriptor) {
+                MediaRouteProviderDescriptor descriptor) {
         }
     }
 
@@ -771,6 +378,9 @@
                 case MSG_DELIVER_DESCRIPTOR_CHANGED:
                     deliverDescriptorChanged();
                     break;
+                case MSG_DELIVER_DISCOVERY_REQUEST_CHANGED:
+                    deliverDiscoveryRequestChanged();
+                    break;
             }
         }
     }
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
new file mode 100644
index 0000000..d82bc51
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
@@ -0,0 +1,291 @@
+/*
+ * 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 android.support.v7.media;
+
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the state of a media route provider and the routes that it publishes.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteProviderDescriptor {
+    private static final String KEY_ROUTES = "routes";
+    private static final String KEY_ACTIVE_SCAN_REQUIRED = "activeScanRequired";
+    private static final String KEY_DISCOVERABLE_CONTROL_FILTERS =
+            "discoverableControlFilters";
+
+    private final Bundle mBundle;
+    private List<MediaRouteDescriptor> mRoutes;
+    private List<IntentFilter> mDiscoverableControlFilters;
+
+    private MediaRouteProviderDescriptor(Bundle bundle,
+            List<MediaRouteDescriptor> routes, List<IntentFilter> discoverableControlFilters) {
+        mBundle = bundle;
+        mRoutes = routes;
+        mDiscoverableControlFilters = discoverableControlFilters;
+    }
+
+    /**
+     * Gets the list of all routes that this provider has published.
+     */
+    public List<MediaRouteDescriptor> getRoutes() {
+        ensureRoutes();
+        return mRoutes;
+    }
+
+    private void ensureRoutes() {
+        if (mRoutes == null) {
+            ArrayList<Bundle> routeBundles = mBundle.<Bundle>getParcelableArrayList(KEY_ROUTES);
+            if (routeBundles == null || routeBundles.isEmpty()) {
+                mRoutes = Collections.<MediaRouteDescriptor>emptyList();
+            } else {
+                final int count = routeBundles.size();
+                mRoutes = new ArrayList<MediaRouteDescriptor>(count);
+                for (int i = 0; i < count; i++) {
+                    mRoutes.add(MediaRouteDescriptor.fromBundle(routeBundles.get(i)));
+                }
+            }
+        }
+    }
+
+    /**
+     * Gets a list of {@link MediaControlIntent media route control filters} that
+     * describe the union of capabilities of all routes that this provider can
+     * possibly discover.
+     *
+     * @see MediaRouter.ProviderInfo#getDiscoverableControlFilters
+     */
+    public List<IntentFilter> getDiscoverableControlFilters() {
+        ensureDiscoverableControlFilters();
+        return mDiscoverableControlFilters;
+    }
+
+    private void ensureDiscoverableControlFilters() {
+        if (mDiscoverableControlFilters == null) {
+            mDiscoverableControlFilters =
+                    mBundle.<IntentFilter>getParcelableArrayList(KEY_DISCOVERABLE_CONTROL_FILTERS);
+            if (mDiscoverableControlFilters == null || mDiscoverableControlFilters.isEmpty()) {
+                mDiscoverableControlFilters = Collections.<IntentFilter>emptyList();
+            }
+        }
+    }
+
+    /**
+     * Returns true if the provider requires active scans to discover routes.
+     *
+     * @see MediaRouter.ProviderInfo#isActiveScanRequired
+     * @see MediaRouter#CALLBACK_FLAG_ACTIVE_SCAN
+     */
+    public boolean isActiveScanRequired() {
+        return mBundle.getBoolean(KEY_ACTIVE_SCAN_REQUIRED, false);
+    }
+
+    /**
+     * Returns true if the route provider descriptor and all of the routes that
+     * it contains have all of the required fields.
+     * <p>
+     * This verification is deep.  If the provider descriptor is known to be
+     * valid then it is not necessary to call {@link #isValid} on each of its routes.
+     * </p>
+     */
+    public boolean isValid() {
+        ensureDiscoverableControlFilters();
+        if (mDiscoverableControlFilters.contains(null)) {
+            return false;
+        }
+        ensureRoutes();
+        final int routeCount = mRoutes.size();
+        for (int i = 0; i < routeCount; i++) {
+            MediaRouteDescriptor route = mRoutes.get(i);
+            if (route == null || !route.isValid()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append("MediaRouteProviderDescriptor{ ");
+        result.append("isActiveScanRequired=").append(isActiveScanRequired());
+        result.append(", discoverableControlFilters=").append(
+                Arrays.toString(getDiscoverableControlFilters().toArray()));
+        result.append(", routes=").append(
+                Arrays.toString(getRoutes().toArray()));
+        result.append(", isValid=").append(isValid());
+        result.append("}");
+        return result.toString();
+    }
+
+    /**
+     * Converts this object to a bundle for serialization.
+     *
+     * @return The contents of the object represented as a bundle.
+     */
+    public Bundle asBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Creates an instance from a bundle.
+     *
+     * @param bundle The bundle, or null if none.
+     * @return The new instance, or null if the bundle was null.
+     */
+    public static MediaRouteProviderDescriptor fromBundle(Bundle bundle) {
+        return bundle != null ? new MediaRouteProviderDescriptor(bundle, null, null) : null;
+    }
+
+    /**
+     * Builder for {@link MediaRouteProviderDescriptor media route provider descriptors}.
+     */
+    public static final class Builder {
+        private final Bundle mBundle;
+        private ArrayList<MediaRouteDescriptor> mRoutes;
+        private ArrayList<IntentFilter> mDiscoverableControlFilters;
+
+        /**
+         * Creates an empty media route provider descriptor builder.
+         */
+        public Builder() {
+            mBundle = new Bundle();
+        }
+
+        /**
+         * Creates a media route provider descriptor builder whose initial contents are
+         * copied from an existing descriptor.
+         */
+        public Builder(MediaRouteProviderDescriptor descriptor) {
+            if (descriptor == null) {
+                throw new IllegalArgumentException("descriptor must not be null");
+            }
+
+            mBundle = new Bundle(descriptor.mBundle);
+
+            descriptor.ensureRoutes();
+            if (!descriptor.mRoutes.isEmpty()) {
+                mRoutes = new ArrayList<MediaRouteDescriptor>(descriptor.mRoutes);
+            }
+
+            descriptor.ensureDiscoverableControlFilters();
+            if (!descriptor.mDiscoverableControlFilters.isEmpty()) {
+                mDiscoverableControlFilters = new ArrayList<IntentFilter>(
+                        descriptor.mDiscoverableControlFilters);
+            }
+        }
+
+        /**
+         * Adds a route.
+         */
+        public Builder addRoute(MediaRouteDescriptor route) {
+            if (route == null) {
+                throw new IllegalArgumentException("route must not be null");
+            }
+
+            if (mRoutes == null) {
+                mRoutes = new ArrayList<MediaRouteDescriptor>();
+            } else if (mRoutes.contains(route)) {
+                throw new IllegalArgumentException("route descriptor already added");
+            }
+            mRoutes.add(route);
+            return this;
+        }
+
+        /**
+         * Adds a list of routes.
+         */
+        public Builder addRoutes(List<MediaRouteDescriptor> routes) {
+            if (routes == null) {
+                throw new IllegalArgumentException("routes must not be null");
+            }
+
+            final int count = routes.size();
+            for (int i = 0; i < count; i++) {
+                addRoute(routes.get(i));
+            }
+            return this;
+        }
+
+        /**
+         * Adds a {@link MediaControlIntent media control intent} filter.
+         */
+        public Builder addDiscoverableControlFilter(IntentFilter filter) {
+            if (filter == null) {
+                throw new IllegalArgumentException("filter must not be null");
+            }
+
+            if (mDiscoverableControlFilters == null) {
+                mDiscoverableControlFilters = new ArrayList<IntentFilter>();
+            }
+            if (!mDiscoverableControlFilters.contains(filter)) {
+                mDiscoverableControlFilters.add(filter);
+            }
+            return this;
+        }
+
+        /**
+         * Adds a list of {@link MediaControlIntent media control intent} filters.
+         */
+        public Builder addDiscoverableControlFilters(List<IntentFilter> filters) {
+            if (filters == null) {
+                throw new IllegalArgumentException("filters must not be null");
+            }
+
+            final int count = filters.size();
+            for (int i = 0; i < count; i++) {
+                addDiscoverableControlFilter(filters.get(i));
+            }
+            return this;
+        }
+
+        /**
+         * Sets whether the provider requires active scans to discover routes.
+         */
+        public Builder setActiveScanRequired(boolean required) {
+            mBundle.putBoolean(KEY_ACTIVE_SCAN_REQUIRED, required);
+            return this;
+        }
+
+        /**
+         * Builds the {@link MediaRouteProviderDescriptor media route provider descriptor}.
+         */
+        public MediaRouteProviderDescriptor build() {
+            if (mRoutes != null) {
+                final int count = mRoutes.size();
+                ArrayList<Bundle> routeBundles = new ArrayList<Bundle>(count);
+                for (int i = 0; i < count; i++) {
+                    routeBundles.add(mRoutes.get(i).asBundle());
+                }
+                mBundle.putParcelableArrayList(KEY_ROUTES, routeBundles);
+            }
+            if (mDiscoverableControlFilters != null) {
+                mBundle.putParcelableArrayList(KEY_DISCOVERABLE_CONTROL_FILTERS,
+                        mDiscoverableControlFilters);
+            }
+            return new MediaRouteProviderDescriptor(mBundle,
+                    mRoutes, mDiscoverableControlFilters);
+        }
+    }
+}
\ No newline at end of file
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
index 539f7a6..1c23997 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
@@ -26,7 +26,6 @@
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
-import android.support.v7.media.MediaRouteProvider.ProviderDescriptor;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -54,7 +53,7 @@
  */
 public abstract class MediaRouteProviderService extends Service {
     private static final String TAG = "MediaRouteProviderService";
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     private final ArrayList<ClientRecord> mClients = new ArrayList<ClientRecord>();
     private final ReceiveHandler mReceiveHandler;
@@ -63,7 +62,7 @@
     private final ProviderCallback mProviderCallback;
 
     private MediaRouteProvider mProvider;
-    private int mActiveScanClientCount;
+    private MediaRouteDiscoveryRequest mCompositeDiscoveryRequest;
 
     /**
      * The {@link Intent} that must be declared as handled by the service.
@@ -153,18 +152,12 @@
     static final int CLIENT_MSG_ROUTE_CONTROL_REQUEST = 9;
 
     /** (client v1)
-     * Start active scan.
+     * Sets the discovery request.
      * - replyTo : client messenger
      * - arg1    : request id
+     * - obj     : discovery request bundle, or null if none
      */
-    static final int CLIENT_MSG_START_ACTIVE_SCAN = 10;
-
-    /** (client v1)
-     * Stop active scan.
-     * - replyTo : client messenger
-     * - arg1    : request id
-     */
-    static final int CLIENT_MSG_STOP_ACTIVE_SCAN = 11;
+    static final int CLIENT_MSG_SET_DISCOVERY_REQUEST = 10;
 
     static final String CLIENT_DATA_ROUTE_ID = "routeId";
     static final String CLIENT_DATA_VOLUME = "volume";
@@ -277,7 +270,7 @@
                                 + ".  Service package name: " + getPackageName() + ".");
                     }
                     mProvider = provider;
-                    mProvider.addCallback(mProviderCallback);
+                    mProvider.setCallback(mProviderCallback);
                 }
             }
             if (mProvider != null) {
@@ -298,7 +291,7 @@
                         Log.d(TAG, client + ": Registered, version=" + version);
                     }
                     if (requestId != 0) {
-                        ProviderDescriptor descriptor = mProvider.getDescriptor();
+                        MediaRouteProviderDescriptor descriptor = mProvider.getDescriptor();
                         sendReply(messenger, SERVICE_MSG_REGISTERED,
                                 requestId, SERVICE_VERSION_CURRENT,
                                 descriptor != null ? descriptor.asBundle() : null, null);
@@ -480,14 +473,15 @@
         return false;
     }
 
-    private boolean onStartActiveScan(Messenger messenger, int requestId) {
+    private boolean onSetDiscoveryRequest(Messenger messenger, int requestId,
+            MediaRouteDiscoveryRequest request) {
         ClientRecord client = getClient(messenger);
         if (client != null) {
-            boolean actuallyStarted = client.startActiveScan();
+            boolean actuallyChanged = client.setDiscoveryRequest(request);
             if (DEBUG) {
-                Log.d(TAG, client + ": Start active scan"
-                        + ", actuallyStarted=" + actuallyStarted
-                        + ", activeScanClientCount=" + mActiveScanClientCount);
+                Log.d(TAG, client + ": Set discovery request, request=" + request
+                        + ", actuallyChanged=" + actuallyChanged
+                        + ", compositeDiscoveryRequest=" + mCompositeDiscoveryRequest);
             }
             sendGenericSuccess(messenger, requestId);
             return true;
@@ -495,22 +489,7 @@
         return false;
     }
 
-    private boolean onStopActiveScan(Messenger messenger, int requestId) {
-        ClientRecord client = getClient(messenger);
-        if (client != null) {
-            boolean actuallyStopped = client.stopActiveScan();
-            if (DEBUG) {
-                Log.d(TAG, client + ": Stop active scan"
-                        + ", actuallyStopped= " + actuallyStopped
-                        + ", activeScanClientCount=" + mActiveScanClientCount);
-            }
-            sendGenericSuccess(messenger, requestId);
-            return true;
-        }
-        return false;
-    }
-
-    private void sendDescriptorChanged(MediaRouteProvider.ProviderDescriptor descriptor) {
+    private void sendDescriptorChanged(MediaRouteProviderDescriptor descriptor) {
         Bundle descriptorBundle = descriptor != null ? descriptor.asBundle() : null;
         final int count = mClients.size();
         for (int i = 0; i < count; i++) {
@@ -523,6 +502,39 @@
         }
     }
 
+    private boolean updateCompositeDiscoveryRequest() {
+        MediaRouteDiscoveryRequest composite = null;
+        MediaRouteSelector.Builder selectorBuilder = null;
+        boolean activeScan = false;
+        final int count = mClients.size();
+        for (int i = 0; i < count; i++) {
+            MediaRouteDiscoveryRequest request = mClients.get(i).mDiscoveryRequest;
+            if (request != null
+                    && (!request.getSelector().isEmpty() || request.isActiveScan())) {
+                activeScan |= request.isActiveScan();
+                if (composite == null) {
+                    composite = request;
+                } else {
+                    if (selectorBuilder == null) {
+                        selectorBuilder = new MediaRouteSelector.Builder(composite.getSelector());
+                    }
+                    selectorBuilder.addSelector(request.getSelector());
+                }
+            }
+        }
+        if (selectorBuilder != null) {
+            composite = new MediaRouteDiscoveryRequest(selectorBuilder.build(), activeScan);
+        }
+        if (mCompositeDiscoveryRequest != composite
+                && (mCompositeDiscoveryRequest == null
+                        || !mCompositeDiscoveryRequest.equals(composite))) {
+            mCompositeDiscoveryRequest = composite;
+            mProvider.setDiscoveryRequest(composite);
+            return true;
+        }
+        return false;
+    }
+
     private ClientRecord getClient(Messenger messenger) {
         int index = findClient(messenger);
         return index >= 0 ? mClients.get(index) : null;
@@ -575,7 +587,7 @@
     /**
      * Returns true if the messenger object is valid.
      * <p>
-     * The messenger contructor and unparceling code does not check whether the
+     * The messenger constructor and unparceling code does not check whether the
      * provided IBinder is a valid IMessenger object.  As a result, it's possible
      * for a peer to send an invalid IBinder that will result in crashes downstream.
      * This method checks that the messenger is in a valid state.
@@ -605,7 +617,7 @@
     private final class ProviderCallback extends MediaRouteProvider.Callback {
         @Override
         public void onDescriptorChanged(MediaRouteProvider provider,
-                ProviderDescriptor descriptor) {
+                MediaRouteProviderDescriptor descriptor) {
             sendDescriptorChanged(descriptor);
         }
     }
@@ -613,7 +625,7 @@
     private final class ClientRecord implements DeathRecipient {
         public final Messenger mMessenger;
         public final int mVersion;
-        public boolean mActiveScanRequested;
+        public MediaRouteDiscoveryRequest mDiscoveryRequest;
 
         private final SparseArray<MediaRouteProvider.RouteController> mControllers =
                 new SparseArray<MediaRouteProvider.RouteController>();
@@ -642,7 +654,7 @@
 
             mMessenger.getBinder().unlinkToDeath(this, 0);
 
-            stopActiveScan();
+            setDiscoveryRequest(null);
         }
 
         public boolean hasMessenger(Messenger other) {
@@ -673,24 +685,11 @@
             return mControllers.get(controllerId);
         }
 
-        public boolean startActiveScan() {
-            if (!mActiveScanRequested) {
-                mActiveScanRequested = true;
-                mActiveScanClientCount += 1;
-                if (mActiveScanClientCount == 1) {
-                    mProvider.onStartActiveScan();
-                }
-            }
-            return false;
-        }
-
-        public boolean stopActiveScan() {
-            if (mActiveScanRequested) {
-                mActiveScanRequested = false;
-                mActiveScanClientCount -= 1;
-                if (mActiveScanClientCount == 0) {
-                    mProvider.onStopActiveScan();
-                }
+        public boolean setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+            if (mDiscoveryRequest != request
+                    && (mDiscoveryRequest == null || !mDiscoveryRequest.equals(request))) {
+                mDiscoveryRequest = request;
+                return updateCompositeDiscoveryRequest();
             }
             return false;
         }
@@ -803,11 +802,14 @@
                         }
                         break;
 
-                    case CLIENT_MSG_START_ACTIVE_SCAN:
-                        return service.onStartActiveScan(messenger, requestId);
-
-                    case CLIENT_MSG_STOP_ACTIVE_SCAN:
-                        return service.onStopActiveScan(messenger, requestId);
+                    case CLIENT_MSG_SET_DISCOVERY_REQUEST: {
+                        if (obj instanceof Bundle) {
+                            MediaRouteDiscoveryRequest request =
+                                    MediaRouteDiscoveryRequest.fromBundle((Bundle)obj);
+                            return service.onSetDiscoveryRequest(
+                                    messenger, requestId, request.isValid() ? request : null);
+                        }
+                    }
                 }
             }
             return false;
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
new file mode 100644
index 0000000..f1a076b
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
@@ -0,0 +1,300 @@
+/*
+ * 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 android.support.v7.media;
+
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the capabilities of routes that applications would like to discover and use.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
+ *         .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
+ *         .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ *         .build();
+ *
+ * MediaRouter router = MediaRouter.getInstance(context);
+ * router.addCallback(selector, callback);
+ * </pre>
+ */
+public final class MediaRouteSelector {
+    private static final String KEY_CONTROL_CATEGORIES = "controlCategories";
+
+    private final Bundle mBundle;
+    private List<String> mControlCategories;
+
+    /**
+     * An empty media route selector that will not match any routes.
+     */
+    public static final MediaRouteSelector EMPTY = new MediaRouteSelector(new Bundle(), null);
+
+    private MediaRouteSelector(Bundle bundle, List<String> controlCategories) {
+        mBundle = bundle;
+        mControlCategories = controlCategories;
+    }
+
+    /**
+     * Gets the list of {@link MediaControlIntent media control categories} in the selector.
+     *
+     * @return The list of categories.
+     */
+    public List<String> getControlCategories() {
+        ensureControlCategories();
+        return mControlCategories;
+    }
+
+    private void ensureControlCategories() {
+        if (mControlCategories == null) {
+            mControlCategories = mBundle.getStringArrayList(KEY_CONTROL_CATEGORIES);
+            if (mControlCategories == null || mControlCategories.isEmpty()) {
+                mControlCategories = Collections.<String>emptyList();
+            }
+        }
+    }
+
+    /**
+     * Returns true if the selector contains the specified category.
+     *
+     * @param category The category to check.
+     * @return True if the category is present.
+     */
+    public boolean hasControlCategory(String category) {
+        if (category != null) {
+            ensureControlCategories();
+            final int categoryCount = mControlCategories.size();
+            for (int i = 0; i < categoryCount; i++) {
+                if (mControlCategories.get(i).equals(category)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the selector matches at least one of the specified control filters.
+     *
+     * @param filters The list of control filters to consider.
+     * @return True if a match is found.
+     */
+    public boolean matchesControlFilters(List<IntentFilter> filters) {
+        if (filters != null) {
+            ensureControlCategories();
+            final int categoryCount = mControlCategories.size();
+            if (categoryCount != 0) {
+                final int filterCount = filters.size();
+                for (int i = 0; i < filterCount; i++) {
+                    final IntentFilter filter = filters.get(i);
+                    if (filter != null) {
+                        for (int j = 0; j < categoryCount; j++) {
+                            if (filter.hasCategory(mControlCategories.get(j))) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this selector contains all of the capabilities described
+     * by the specified selector.
+     *
+     * @param selector The selector to be examined.
+     * @return True if this selector contains all of the capabilities described
+     * by the specified selector.
+     */
+    public boolean contains(MediaRouteSelector selector) {
+        if (selector != null) {
+            ensureControlCategories();
+            selector.ensureControlCategories();
+            return mControlCategories.containsAll(selector.mControlCategories);
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the selector does not specify any capabilities.
+     */
+    public boolean isEmpty() {
+        ensureControlCategories();
+        return mControlCategories.isEmpty();
+    }
+
+    /**
+     * Returns true if the selector has all of the required fields.
+     */
+    public boolean isValid() {
+        ensureControlCategories();
+        if (mControlCategories.contains(null)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof MediaRouteSelector) {
+            MediaRouteSelector other = (MediaRouteSelector)o;
+            ensureControlCategories();
+            other.ensureControlCategories();
+            return mControlCategories.equals(other.mControlCategories);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        ensureControlCategories();
+        return mControlCategories.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append("MediaRouteSelector{ ");
+        result.append("controlCategories=").append(
+                Arrays.toString(getControlCategories().toArray()));
+        result.append(" }");
+        return result.toString();
+    }
+
+    /**
+     * Converts this object to a bundle for serialization.
+     *
+     * @return The contents of the object represented as a bundle.
+     */
+    public Bundle asBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Creates an instance from a bundle.
+     *
+     * @param bundle The bundle, or null if none.
+     * @return The new instance, or null if the bundle was null.
+     */
+    public static MediaRouteSelector fromBundle(Bundle bundle) {
+        return bundle != null ? new MediaRouteSelector(bundle, null) : null;
+    }
+
+    /**
+     * Builder for {@link MediaRouteSelector media route selectors}.
+     */
+    public static final class Builder {
+        private ArrayList<String> mControlCategories;
+
+        /**
+         * Creates an empty media route selector builder.
+         */
+        public Builder() {
+        }
+
+        /**
+         * Creates a media route selector descriptor builder whose initial contents are
+         * copied from an existing selector.
+         */
+        public Builder(MediaRouteSelector selector) {
+            if (selector == null) {
+                throw new IllegalArgumentException("selector must not be null");
+            }
+
+            selector.ensureControlCategories();
+            if (!selector.mControlCategories.isEmpty()) {
+                mControlCategories = new ArrayList<String>(selector.mControlCategories);
+            }
+        }
+
+        /**
+         * Adds a {@link MediaControlIntent media control category} to the builder.
+         *
+         * @param category The category to add to the set of desired capabilities, such as
+         * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+         * @return The builder instance for chaining.
+         */
+        public Builder addControlCategory(String category) {
+            if (category == null) {
+                throw new IllegalArgumentException("category must not be null");
+            }
+
+            if (mControlCategories == null) {
+                mControlCategories = new ArrayList<String>();
+            }
+            if (!mControlCategories.contains(category)) {
+                mControlCategories.add(category);
+            }
+            return this;
+        }
+
+        /**
+         * Adds a list of {@link MediaControlIntent media control categories} to the builder.
+         *
+         * @param categories The list categories to add to the set of desired capabilities,
+         * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+         * @return The builder instance for chaining.
+         */
+        public Builder addControlCategories(List<String> categories) {
+            if (categories == null) {
+                throw new IllegalArgumentException("categories must not be null");
+            }
+
+            final int count = categories.size();
+            for (int i = 0; i < count; i++) {
+                addControlCategory(categories.get(i));
+            }
+            return this;
+        }
+
+        /**
+         * Adds the contents of an existing media route selector to the builder.
+         *
+         * @param selector The media route selector whose contents are to be added.
+         * @return The builder instance for chaining.
+         */
+        public Builder addSelector(MediaRouteSelector selector) {
+            if (selector == null) {
+                throw new IllegalArgumentException("selector must not be null");
+            }
+
+            addControlCategories(selector.getControlCategories());
+            return this;
+        }
+
+        /**
+         * Builds the {@link MediaRouteSelector media route selector}.
+         */
+        public MediaRouteSelector build() {
+            if (mControlCategories == null) {
+                return EMPTY;
+            }
+            Bundle bundle = new Bundle();
+            bundle.putStringArrayList(KEY_CONTROL_CATEGORIES, mControlCategories);
+            return new MediaRouteSelector(bundle, mControlCategories);
+        }
+    }
+}
\ No newline at end of file
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index f4f646a..47c8a69 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -28,8 +28,6 @@
 import android.os.Looper;
 import android.os.Message;
 import android.support.v4.hardware.display.DisplayManagerCompat;
-import android.support.v7.media.MediaRouteProvider.RouteDescriptor;
-import android.support.v7.media.MediaRouteProvider.ProviderDescriptor;
 import android.support.v7.media.MediaRouteProvider.ProviderMetadata;
 import android.util.Log;
 import android.view.Display;
@@ -60,6 +58,7 @@
  */
 public final class MediaRouter {
     private static final String TAG = "MediaRouter";
+    private static final boolean DEBUG = false;
 
     // Maintains global media router state for the process.
     // This field is initialized in MediaRouter.getInstance() before any
@@ -69,8 +68,61 @@
 
     // Context-bound state of the media router.
     final Context mContext;
-    final CopyOnWriteArrayList<Callback> mCallbacks = new CopyOnWriteArrayList<Callback>();
-    final ArrayList<Selector> mSelectors = new ArrayList<Selector>();
+    final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords =
+            new CopyOnWriteArrayList<CallbackRecord>();
+
+    /**
+     * Flag for {@link #addCallback}: Actively scan for routes while this callback
+     * is registered.
+     * <p>
+     * When this flag is specified, the media router will actively scan for new
+     * routes.  Certain routes, such as wifi display routes, may not be discoverable
+     * except when actively scanning.  This flag is typically used when the route picker
+     * dialog has been opened by the user to ensure that the route information is
+     * up to date.
+     * </p><p>
+     * Active scanning may consume a significant amount of power and may have intrusive
+     * effects on wireless connectivity.  Therefore it is important that active scanning
+     * only be requested when it is actually needed to satisfy a user request to
+     * discover and select a new route.
+     * </p>
+     */
+    public static final int CALLBACK_FLAG_ACTIVE_SCAN = 1 << 0;
+
+    /**
+     * Flag for {@link #addCallback}: Do not filter route events.
+     * <p>
+     * When this flag is specified, the callback will be invoked for events that affect any
+     * route event if they do not match the callback's associated media route selector.
+     * </p>
+     */
+    public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
+
+    /**
+     * Flag for {@link #isRouteAvailable}: Ignore the default route.
+     * <p>
+     * This flag is used to determine whether a matching non-default route is available.
+     * This constraint may be used to decide whether to offer the route chooser dialog
+     * to the user.  There is no point offering the chooser if there are no
+     * non-default choices.
+     * </p>
+     */
+    public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
+
+    /**
+     * Flag for {@link #isRouteAvailable}: Consider whether matching routes
+     * might be discovered if an active scan were performed.
+     * <p>
+     * If no existing routes match the route selector, then this flag is used to
+     * determine whether to consider whether any route providers that require active
+     * scans might discover matching routes if an active scan were actually performed.
+     * </p><p>
+     * This flag may be used to decide whether to offer the route chooser dialog to the user.
+     * When the dialog is opened, an active scan will be performed which may cause
+     * additional routes to be discovered by any providers that require active scans.
+     * </p>
+     */
+    public static final int AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN = 1 << 1;
 
     MediaRouter(Context context) {
         mContext = context;
@@ -181,18 +233,21 @@
      * @return The previously selected route if it matched the selector, otherwise the
      * newly selected default route which is guaranteed to never be null.
      *
-     * @see Selector
+     * @see MediaRouteSelector
      * @see RouteInfo#matchesSelector
      * @see RouteInfo#isDefault
      */
-    public RouteInfo updateSelectedRoute(Selector selector) {
+    public RouteInfo updateSelectedRoute(MediaRouteSelector selector) {
         if (selector == null) {
             throw new IllegalArgumentException("selector must not be null");
         }
         checkCallingThread();
 
+        if (DEBUG) {
+            Log.d(TAG, "updateSelectedRoute: " + selector);
+        }
         RouteInfo route = sGlobal.getSelectedRoute();
-        if (!route.isDefault() && route.matchesSelector(selector)) {
+        if (!route.isDefault() && !route.matchesSelector(selector)) {
             route = sGlobal.getDefaultRoute();
             sGlobal.selectRoute(route);
         }
@@ -210,28 +265,166 @@
         }
         checkCallingThread();
 
+        if (DEBUG) {
+            Log.d(TAG, "selectRoute: " + route);
+        }
         sGlobal.selectRoute(route);
     }
 
     /**
-     * Adds a callback to listen to changes to media routes.
+     * Returns true if there is a route that matches the specified selector
+     * or, depending on the specified availability flags, if it is possible to discover one.
+     * <p>
+     * This method first considers whether there are any available
+     * routes that match the selector regardless of whether they are enabled or
+     * disabled.  If not and the {@link #AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN} flag
+     * was specifies, then it considers whether any of the route providers
+     * could discover a matching route if an active scan were performed.
+     * </p>
      *
+     * @param selector The selector to match.
+     * @param flags Flags to control the determination of whether a route may be available.
+     * May be zero or a combination of
+     * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} and
+     * {@link #AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN}.
+     * @return True if a matching route may be available.
+     */
+    public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
+        if (selector == null) {
+            throw new IllegalArgumentException("selector must not be null");
+        }
+        checkCallingThread();
+
+        return sGlobal.isRouteAvailable(selector, flags);
+    }
+
+    /**
+     * Registers a callback to discover routes that match the selector and to receive
+     * events when they change.
+     * <p>
+     * This is a convenience method that has the same effect as calling
+     * {@link #addCallback(MediaRouteSelector, Callback, int)} without flags.
+     * </p>
+     *
+     * @param selector A route selector that indicates the kinds of routes that the
+     * callback would like to discover.
      * @param callback The callback to add.
      * @see #removeCallback
      */
-    public void addCallback(Callback callback) {
+    public void addCallback(MediaRouteSelector selector, Callback callback) {
+        addCallback(selector, callback, 0);
+    }
+
+    /**
+     * Registers a callback to discover routes that match the selector and to receive
+     * events when they change.
+     * <p>
+     * The selector describes the kinds of routes that the application wants to
+     * discover.  For example, if the application wants to use
+     * live audio routes then it should include the
+     * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO live audio media control intent category}
+     * in its selector when it adds a callback to the media router.
+     * The selector may include any number of categories.
+     * </p><p>
+     * If the callback has already been registered, then the selector is added to
+     * the set of selectors being monitored by the callback.
+     * </p><p>
+     * By default, the callback will only be invoked for events that affect routes
+     * that match the specified selector.  Event filtering may be disabled by specifying
+     * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag when the callback is registered.
+     * </p>
+     *
+     * <h3>Example</h3>
+     * <pre>
+     * public class MyActivity extends Activity {
+     *     private MediaRouter mRouter;
+     *     private MediaRouter.Callback mCallback;
+     *     private MediaRouteSelector mSelector;
+     *
+     *     protected void onCreate(Bundle savedInstanceState) {
+     *         super.onCreate(savedInstanceState);
+     *
+     *         mRouter = Mediarouter.getInstance(this);
+     *         mCallback = new MyCallback();
+     *         mSelector = new MediaRouteSelector.Builder()
+     *                 .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+     *                 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+     *                 .build();
+     *     }
+     *
+     *     // Add the callback on resume to tell the media router what kinds of routes
+     *     // the application is interested in so that it can try to discover suitable ones.
+     *     public void onResume() {
+     *         super.onResume();
+     *
+     *         mediaRouter.addCallback(mSelector, mCallback);
+     *
+     *         MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
+     *         // do something with the route...
+     *     }
+     *
+     *     // Remove the selector on pause to tell the media router that it no longer
+     *     // needs to invest effort trying to discover routes of these kinds for now.
+     *     public void onPause() {
+     *         super.onPause();
+     *
+     *         mediaRouter.removeCallback(mCallback);
+     *     }
+     *
+     *     private final class MyCallback extends MediaRouter.Callback {
+     *         // Implement callback methods as needed.
+     *     }
+     * }
+     * </pre>
+     *
+     * @param selector A route selector that indicates the kinds of routes that the
+     * callback would like to discover.
+     * @param callback The callback to add.
+     * @param flags Flags to control the behavior of the callback.
+     * May be zero or a combination of {@link #CALLBACK_FLAG_ACTIVE_SCAN} and
+     * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
+     * @see #removeCallback
+     */
+    public void addCallback(MediaRouteSelector selector, Callback callback, int flags) {
+        if (selector == null) {
+            throw new IllegalArgumentException("selector must not be null");
+        }
         if (callback == null) {
             throw new IllegalArgumentException("callback must not be null");
         }
         checkCallingThread();
 
-        if (!mCallbacks.contains(callback)) {
-            mCallbacks.add(callback);
+        if (DEBUG) {
+            Log.d(TAG, "addCallback: selector=" + selector
+                    + ", callback=" + callback + ", flags=" + Integer.toHexString(flags));
+        }
+
+        CallbackRecord record;
+        int index = findCallbackRecord(callback);
+        if (index < 0) {
+            record = new CallbackRecord(callback);
+            mCallbackRecords.add(record);
+        } else {
+            record = mCallbackRecords.get(index);
+        }
+        boolean updateNeeded = false;
+        if ((flags & ~record.mFlags) != 0) {
+            record.mFlags |= flags;
+            updateNeeded = true;
+        }
+        if (!record.mSelector.contains(selector)) {
+            record.mSelector = new MediaRouteSelector.Builder(record.mSelector)
+                    .addSelector(selector)
+                    .build();
+            updateNeeded = true;
+        }
+        if (updateNeeded) {
+            sGlobal.updateDiscoveryRequest();
         }
     }
 
     /**
-     * Removes the specified callback.  It will no longer receive information about
+     * Removes the specified callback.  It will no longer receive events about
      * changes to media routes.
      *
      * @param callback The callback to remove.
@@ -243,82 +436,25 @@
         }
         checkCallingThread();
 
-        mCallbacks.remove(callback);
-    }
-
-    /**
-     * Adds a selector that provides hints about the kinds of routes that the application
-     * is interested in.  The selector may provide additional information that
-     * route providers need in order to discover routes with the specified capabilities.
-     * <p>
-     * Adding a selector only increases the possible set of routes that an application
-     * can discover using the media router; adding a selector does not restrict or
-     * filter the available routes in any way.
-     * </p><p>
-     * The application should not modify the selector object after it has been added
-     * to the media router.  To make changes, first remove the selector, then modify
-     * it, then add the selector back.
-     * </p>
-     *
-     * <h3>Example</h3>
-     * <pre>
-     * private MediaRouter.Selector mSelector;
-     *
-     * // Add the selector on resume to tell the media router what kinds of routes
-     * // the application is interested in so that it can try to discover suitable ones.
-     * public void onResume() {
-     *     super.onResume();
-     *     MediaRouter mediaRouter = MediaRouter.getInstance(context);
-     *     if (mSelector == null) {
-     *         mSelector = new MediaRouter.Selector();
-     *         mSelector.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
-     *         mSelector.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-     *     }
-     *     mediaRouter.addSelector(mSelector);
-     *     mediaRouter.updateSelectedRoute(mSelector);
-     * }
-     *
-     * // Remove the selector on pause to tell the media router that it no longer
-     * // needs to invest effort trying to discover routes of these kinds for now.
-     * public void onPause() {
-     *     super.onPause();
-     *     MediaRouter mediaRouter = MediaRouter.getInstance(context);
-     *     if (mSelector != null) {
-     *         mediaRouter.removeSelector(mSelector);
-     *     }
-     * }
-     * </pre>
-     *
-     * @param selector The selector to add.
-     * @see #removeSelector
-     */
-    public void addSelector(Selector selector) {
-        if (selector == null) {
-            throw new IllegalArgumentException("selector must not be null");
+        if (DEBUG) {
+            Log.d(TAG, "removeCallback: callback=" + callback);
         }
-        checkCallingThread();
 
-        if (!mSelectors.contains(selector)) {
-            mSelectors.add(selector);
-            sGlobal.updateCompositeSelector();
+        int index = findCallbackRecord(callback);
+        if (index >= 0) {
+            mCallbackRecords.remove(index);
+            sGlobal.updateDiscoveryRequest();
         }
     }
 
-    /**
-     * Removes a route selector.
-     *
-     * @param selector The selector to remove.
-     * @see #addSelector
-     */
-    public void removeSelector(Selector selector) {
-        if (selector == null) {
-            throw new IllegalArgumentException("selector must not be null");
+    private int findCallbackRecord(Callback callback) {
+        final int count = mCallbackRecords.size();
+        for (int i = 0; i < count; i++) {
+            if (mCallbackRecords.get(i).mCallback == callback) {
+                return i;
+            }
         }
-        checkCallingThread();
-
-        if (mSelectors.remove(selector)) {
-            sGlobal.updateCompositeSelector();
-        }
+        return -1;
     }
 
     /**
@@ -335,6 +471,9 @@
         }
         checkCallingThread();
 
+        if (DEBUG) {
+            Log.d(TAG, "addProvider: " + providerInstance);
+        }
         sGlobal.addProvider(providerInstance);
     }
 
@@ -352,52 +491,10 @@
         }
         checkCallingThread();
 
-        sGlobal.removeProvider(providerInstance);
-    }
-
-    /**
-     * Starts actively scanning for route changes.
-     * <p>
-     * This method should typically be invoked when the route picker dialog has been
-     * opened by the user to ensure that the set of known routes is fresh and up to date.
-     * </p><p>
-     * Active scanning may consume a significant amount of power and may have intrusive
-     * effects on wireless connectivity.  Therefore it is important that active scanning
-     * only be requested when it is actually needed by the application.
-     * </p><p>
-     * Calls to this method nest and must be matched by an equal number of calls
-     * to {@link #stopActiveScan}.
-     * </p>
-     *
-     * @see #stopActiveScan
-     */
-    public void startActiveScan() {
-        checkCallingThread();
-        sGlobal.startActiveScan();
-    }
-
-    /**
-     * Stops actively scanning for route changes.
-     * <p>
-     * This method should typically be invoked when the route picker dialog has been
-     * closed by the user.  Media route providers may continue passively scanning
-     * for routes.
-     * </p><p>
-     * This method must be called once for each matching call to {@link #startActiveScan}.
-     * </p>
-     *
-     * @see #startActiveScan
-     */
-    public void stopActiveScan() {
-        checkCallingThread();
-        sGlobal.stopActiveScan();
-    }
-
-    void combineSelectors(Selector result) {
-        final int count = mSelectors.size();
-        for (int i = 0; i < count; i++) {
-            result.add(mSelectors.get(i));
+        if (DEBUG) {
+            Log.d(TAG, "removeProvider: " + providerInstance);
         }
+        sGlobal.removeProvider(providerInstance);
     }
 
     /**
@@ -431,6 +528,7 @@
         private Drawable mIconDrawable;
         private int mIconResource;
         private boolean mEnabled;
+        private boolean mConnecting;
         private final ArrayList<IntentFilter> mControlFilters = new ArrayList<IntentFilter>();
         private int mPlaybackType;
         private int mPlaybackStream;
@@ -440,7 +538,7 @@
         private Display mPresentationDisplay;
         private int mPresentationDisplayId = -1;
         private Bundle mExtras;
-        private RouteDescriptor mDescriptor;
+        private MediaRouteDescriptor mDescriptor;
 
         /**
          * The default playback type, "local", indicating the presentation of the media
@@ -540,16 +638,26 @@
         /**
          * Returns true if this route is enabled and may be selected.
          *
-         * @return true if this route is enabled and may be selected.
+         * @return True if this route is enabled.
          */
         public boolean isEnabled() {
             return mEnabled;
         }
 
         /**
+         * Returns true if the route is in the process of connecting and is not
+         * yet ready for use.
+         *
+         * @return True if this route is in the process of connecting.
+         */
+        public boolean isConnecting() {
+            return mConnecting;
+        }
+
+        /**
          * Returns true if this route is currently selected.
          *
-         * @return true if this route is currently selected.
+         * @return True if this route is currently selected.
          *
          * @see MediaRouter#getSelectedRoute
          */
@@ -561,7 +669,7 @@
         /**
          * Returns true if this route is the default route.
          *
-         * @return true if this route is the default route.
+         * @return True if this route is the default route.
          *
          * @see MediaRouter#getDefaultRoute
          */
@@ -594,24 +702,12 @@
          * @return True if the route supports at least one of the capabilities
          * described in the media route selector.
          */
-        public boolean matchesSelector(Selector selector) {
+        public boolean matchesSelector(MediaRouteSelector selector) {
             if (selector == null) {
                 throw new IllegalArgumentException("selector must not be null");
             }
             checkCallingThread();
-
-            final int filterCount = mControlFilters.size();
-            final List<String> categories = selector.getControlCategories();
-            final int categoryCount = categories.size();
-            for (int i = 0; i < filterCount; i++) {
-                for (int j = 0; j < categoryCount; j++) {
-                    String category = categories.get(j);
-                    if (mControlFilters.get(i).hasCategory(category)) {
-                        return true;
-                    }
-                }
-            }
-            return false;
+            return selector.matchesControlFilters(mControlFilters);
         }
 
         /**
@@ -840,6 +936,7 @@
             return "MediaRouter.RouteInfo{ name=" + mName
                     + ", status=" + mStatus
                     + ", enabled=" + mEnabled
+                    + ", connecting=" + mConnecting
                     + ", playbackType=" + mPlaybackType
                     + ", playbackStream=" + mPlaybackStream
                     + ", volumeHandling=" + mVolumeHandling
@@ -851,7 +948,7 @@
                     + " }";
         }
 
-        int updateDescriptor(RouteDescriptor descriptor) {
+        int updateDescriptor(MediaRouteDescriptor descriptor) {
             int changes = 0;
             if (mDescriptor != descriptor) {
                 mDescriptor = descriptor;
@@ -878,12 +975,13 @@
                         mEnabled = descriptor.isEnabled();
                         changes |= CHANGE_GENERAL;
                     }
-                    IntentFilter[] descriptorControlFilters = descriptor.getControlFilters();
-                    if (!hasSameControlFilters(descriptorControlFilters)) {
+                    if (mConnecting != descriptor.isConnecting()) {
+                        mConnecting = descriptor.isConnecting();
+                        changes |= CHANGE_GENERAL;
+                    }
+                    if (!mControlFilters.equals(descriptor.getControlFilters())) {
                         mControlFilters.clear();
-                        for (IntentFilter f : descriptorControlFilters) {
-                            mControlFilters.add(f);
-                        }
+                        mControlFilters.addAll(descriptor.getControlFilters());
                         changes |= CHANGE_GENERAL;
                     }
                     if (mPlaybackType != descriptor.getPlaybackType()) {
@@ -920,19 +1018,6 @@
             return changes;
         }
 
-        boolean hasSameControlFilters(IntentFilter[] controlFilters) {
-            final int count = mControlFilters.size();
-            if (count != controlFilters.length) {
-                return false;
-            }
-            for (int i = 0; i < count; i++) {
-                if (!mControlFilters.get(i).equals(controlFilters[i])) {
-                    return false;
-                }
-            }
-            return true;
-        }
-
         String getDescriptorId() {
             return mDescriptorId;
         }
@@ -952,9 +1037,11 @@
     public static final class ProviderInfo {
         private final MediaRouteProvider mProviderInstance;
         private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
+        private final ArrayList<IntentFilter> mDiscoverableControlFilters =
+                new ArrayList<IntentFilter>();
 
         private final ProviderMetadata mMetadata;
-        private ProviderDescriptor mDescriptor;
+        private MediaRouteProviderDescriptor mDescriptor;
         private Resources mResources;
         private boolean mResourcesNotAvailable;
 
@@ -988,12 +1075,38 @@
 
         /**
          * Returns true if the provider requires active scans to discover routes.
+         * <p>
+         * To provide the best user experience, a media route provider should passively
+         * discover and publish changes to route descriptors in the background.
+         * However, for some providers, scanning for routes may use a significant
+         * amount of power or may interfere with wireless network connectivity.
+         * If this is the case, then the provider will indicate that it requires
+         * active scans to discover routes by setting this flag.  Active scans
+         * will be performed when the user opens the route chooser dialog.
+         * </p>
          */
         public boolean isActiveScanRequired() {
             checkCallingThread();
             return mDescriptor != null && mDescriptor.isActiveScanRequired();
         }
 
+        /**
+         * Gets a list of {@link MediaControlIntent media route control filters} that
+         * describe the union of capabilities of all routes that this provider can
+         * possibly discover.
+         * <p>
+         * Because a route provider may not know what to look for until an
+         * application actually asks for it, the contents of the discoverable control
+         * filter list may change depending on the route selectors that applications have
+         * actually specified when {@link MediaRouter#addCallback registering callbacks}
+         * on the media router to discover routes.
+         * </p>
+         */
+        public List<IntentFilter> getDiscoverableControlFilters() {
+            checkCallingThread();
+            return mDiscoverableControlFilters;
+        }
+
         Resources getResources() {
             if (mResources == null && !mResourcesNotAvailable) {
                 String packageName = getPackageName();
@@ -1009,9 +1122,15 @@
             return mResources;
         }
 
-        boolean updateDescriptor(ProviderDescriptor descriptor) {
+        boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) {
             if (mDescriptor != descriptor) {
                 mDescriptor = descriptor;
+
+                if (!mDiscoverableControlFilters.equals(
+                        descriptor.getDiscoverableControlFilters())) {
+                    mDiscoverableControlFilters.clear();
+                    mDiscoverableControlFilters.addAll(descriptor.getDiscoverableControlFilters());
+                }
                 return true;
             }
             return false;
@@ -1030,99 +1149,21 @@
         @Override
         public String toString() {
             return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName()
+                    + ", isActiveScanRequired=" + isActiveScanRequired()
                     + " }";
         }
     }
 
     /**
-     * A media route selector describes the capabilities of routes that applications
-     * would like to discover and use.
-     * <p>
-     * Selector objects are used in two ways.  First, the application may add a selector
-     * to the media router using the {@link MediaRouter#addSelector MediaRouter.addSelector}
-     * method to indicate interest in routes that support the capabilities expressed by the
-     * selector.  Second, the application may use a selector to determine whether a given
-     * route supports these capabilities.
-     * </p>
-     *
-     * <h3>Example</h3>
-     * <pre>
-     * MediaRouter.Selector selector = new MediaRouter.Selector();
-     * selector.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
-     * selector.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
-     *
-     * MediaRouter router = MediaRouter.getInstance(context);
-     * router.addSelector(selector);
-     * </pre>
-     */
-    public static final class Selector {
-        private final ArrayList<String> mControlCategories;
-
-        /**
-         * Creates an empty selector.
-         */
-        public Selector() {
-            mControlCategories = new ArrayList<String>();
-        }
-
-        /**
-         * Creates a copy of another selector.
-         *
-         * @param other The selector to copy.
-         */
-        public Selector(Selector other) {
-            if (other == null) {
-                throw new IllegalArgumentException("other must not be null");
-            }
-            mControlCategories = new ArrayList<String>(other.mControlCategories);
-        }
-
-        /**
-         * Clears the contents of the selector.
-         */
-        public void clear() {
-            mControlCategories.clear();
-        }
-
-        /**
-         * Adds a {@link MediaControlIntent media control category} to the selector.
-         *
-         * @param category The category to add to the set of desired capabilities, such as
-         * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
-         */
-        public void addControlCategory(String category) {
-            if (category == null) {
-                throw new IllegalArgumentException("category must not be null");
-            }
-            if (!mControlCategories.contains(category)) {
-                mControlCategories.add(category);
-            }
-        }
-
-        void add(Selector other) {
-            final int count = other.mControlCategories.size();
-            for (int i = 0; i < count; i++) {
-                String category = other.mControlCategories.get(i);
-                if (!mControlCategories.contains(category)) {
-                    mControlCategories.add(category);
-                }
-            }
-        }
-
-        List<String> getControlCategories() {
-            return mControlCategories;
-        }
-    }
-
-    /**
      * Interface for receiving events about media routing changes.
      * All methods of this interface will be called from the application's main thread.
      * <p>
      * A Callback will only receive events relevant to routes that the callback
-     * was registered for.
+     * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
+     * flag was specified in {@link MediaRouter#addCallback(MediaRouteSelector, Callback, int)}.
      * </p>
      *
-     * @see MediaRouter#addCallback(Callback)
+     * @see MediaRouter#addCallback(MediaRouteSelector, Callback, int)
      * @see MediaRouter#removeCallback(Callback)
      */
     public static abstract class Callback {
@@ -1212,6 +1253,15 @@
          */
         public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
         }
+
+        /**
+         * Called when a property of the indicated media route provider has changed.
+         *
+         * @param router The media router reporting the event.
+         * @param provider The provider that was changed.
+         */
+        public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
+        }
     }
 
     /**
@@ -1240,6 +1290,22 @@
         }
     }
 
+    private static final class CallbackRecord {
+        public final Callback mCallback;
+        public MediaRouteSelector mSelector;
+        public int mFlags;
+
+        public CallbackRecord(Callback callback) {
+            mCallback = callback;
+            mSelector = MediaRouteSelector.EMPTY;
+        }
+
+        public boolean filterRouteEvent(RouteInfo route) {
+            return (mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
+                    || route.matchesSelector(mSelector);
+        }
+    }
+
     /**
      * Global state for the media router.
      * <p>
@@ -1259,13 +1325,12 @@
         private final CallbackHandler mCallbackHandler = new CallbackHandler();
         private final DisplayManagerCompat mDisplayManager;
         private final SystemMediaRouteProvider mSystemProvider;
-        private final Selector mCompositeSelector = new Selector();
 
         private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
         private RouteInfo mDefaultRoute;
         private RouteInfo mSelectedRoute;
         private MediaRouteProvider.RouteController mSelectedRouteController;
-        private int mActiveScanRequestCount;
+        private MediaRouteDiscoveryRequest mDiscoveryRequest;
 
         GlobalMediaRouter(Context applicationContext) {
             mApplicationContext = applicationContext;
@@ -1383,10 +1448,77 @@
             setSelectedRouteInternal(route);
         }
 
-        public void updateCompositeSelector() {
-            mCompositeSelector.clear();
+        public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
+            // Check whether any existing routes match the selector.
+            final int routeCount = mRoutes.size();
+            for (int i = 0; i < routeCount; i++) {
+                RouteInfo route = mRoutes.get(i);
+                if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0
+                        && route.isDefault()) {
+                    continue;
+                }
+                if (route.matchesSelector(selector)) {
+                    return true;
+                }
+            }
+
+            // Check whether any provider could possibly discover a matching route
+            // if a required active scan were performed.
+            if ((flags & AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN) != 0) {
+                final int providerCount = mProviders.size();
+                for (int i = 0; i < providerCount; i++) {
+                    ProviderInfo provider = mProviders.get(i);
+                    if (provider.isActiveScanRequired() && selector.matchesControlFilters(
+                            provider.getDiscoverableControlFilters())) {
+                        return true;
+                    }
+                }
+            }
+
+            // It doesn't look like we can find a matching route right now.
+            return false;
+        }
+
+        public void updateDiscoveryRequest() {
+            // Combine all of the callback selectors and active scan flags.
+            boolean activeScan = false;
+            MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
             for (MediaRouter router : mRouters.values()) {
-                router.combineSelectors(mCompositeSelector);
+                final int count = router.mCallbackRecords.size();
+                for (int i = 0; i < count; i++) {
+                    CallbackRecord callback = router.mCallbackRecords.get(i);
+                    builder.addSelector(callback.mSelector);
+                    if ((callback.mFlags & CALLBACK_FLAG_ACTIVE_SCAN) != 0) {
+                        activeScan = true;
+                    }
+                }
+            }
+            MediaRouteSelector selector = builder.build();
+
+            // Create a new discovery request.
+            if (mDiscoveryRequest != null
+                    && mDiscoveryRequest.getSelector().equals(selector)
+                    && mDiscoveryRequest.isActiveScan() == activeScan) {
+                return; // no change
+            }
+            if (selector.isEmpty() && !activeScan) {
+                // Discovery is not needed.
+                if (mDiscoveryRequest == null) {
+                    return; // no change
+                }
+                mDiscoveryRequest = null;
+            } else {
+                // Discovery is needed.
+                mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan);
+            }
+            if (DEBUG) {
+                Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest);
+            }
+
+            // Notify providers.
+            final int providerCount = mProviders.size();
+            for (int i = 0; i < providerCount; i++) {
+                mProviders.get(i).mProviderInstance.setDiscoveryRequest(mDiscoveryRequest);
             }
         }
 
@@ -1396,58 +1528,40 @@
                 // 1. Add the provider to the list.
                 ProviderInfo provider = new ProviderInfo(providerInstance);
                 mProviders.add(provider);
+                if (DEBUG) {
+                    Log.d(TAG, "Provider added: " + provider);
+                }
                 mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider);
                 // 2. Create the provider's contents.
                 updateProviderContents(provider, providerInstance.getDescriptor());
                 // 3. Register the provider callback.
-                providerInstance.addCallback(mProviderCallback);
-                // 4. Start active scans if needed.
-                if (mActiveScanRequestCount != 0) {
-                    providerInstance.onStartActiveScan();
-                }
+                providerInstance.setCallback(mProviderCallback);
+                // 4. Set the discovery request.
+                providerInstance.setDiscoveryRequest(mDiscoveryRequest);
             }
         }
 
         public void removeProvider(MediaRouteProvider providerInstance) {
             int index = findProviderInfo(providerInstance);
             if (index >= 0) {
-                // 1. Stop active scans if needed.
-                if (mActiveScanRequestCount != 0) {
-                    providerInstance.onStopActiveScan();
-                }
-                // 2. Unregister the provider callback.
-                providerInstance.removeCallback(mProviderCallback);
+                // 1. Unregister the provider callback.
+                providerInstance.setCallback(null);
+                // 2. Clear the discovery request.
+                providerInstance.setDiscoveryRequest(null);
                 // 3. Delete the provider's contents.
                 ProviderInfo provider = mProviders.get(index);
                 updateProviderContents(provider, null);
                 // 4. Remove the provider from the list.
+                if (DEBUG) {
+                    Log.d(TAG, "Provider removed: " + provider);
+                }
                 mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider);
                 mProviders.remove(index);
             }
         }
 
-        public void startActiveScan() {
-            mActiveScanRequestCount += 1;
-            if (mActiveScanRequestCount == 1) {
-                final int count = mProviders.size();
-                for (int i = 0; i < count; i++) {
-                    mProviders.get(i).mProviderInstance.onStartActiveScan();
-                }
-            }
-        }
-
-        public void stopActiveScan() {
-            mActiveScanRequestCount -= 1;
-            if (mActiveScanRequestCount == 0) {
-                final int count = mProviders.size();
-                for (int i = 0; i < count; i++) {
-                    mProviders.get(i).mProviderInstance.onStopActiveScan();
-                }
-            }
-        }
-
         private void updateProviderDescriptor(MediaRouteProvider providerInstance,
-                ProviderDescriptor descriptor) {
+                MediaRouteProviderDescriptor descriptor) {
             int index = findProviderInfo(providerInstance);
             if (index >= 0) {
                 // Update the provider's contents.
@@ -1467,16 +1581,18 @@
         }
 
         private void updateProviderContents(ProviderInfo provider,
-                ProviderDescriptor providerDescriptor) {
+                MediaRouteProviderDescriptor providerDescriptor) {
             if (provider.updateDescriptor(providerDescriptor)) {
                 // Update all existing routes and reorder them to match
                 // the order of their descriptors.
                 int targetIndex = 0;
                 if (providerDescriptor != null) {
                     if (providerDescriptor.isValid()) {
-                        final RouteDescriptor[] routeDescriptors = providerDescriptor.getRoutes();
-                        for (int i = 0; i < routeDescriptors.length; i++) {
-                            final RouteDescriptor routeDescriptor = routeDescriptors[i];
+                        final List<MediaRouteDescriptor> routeDescriptors =
+                                providerDescriptor.getRoutes();
+                        final int routeCount = routeDescriptors.size();
+                        for (int i = 0; i < routeCount; i++) {
+                            final MediaRouteDescriptor routeDescriptor = routeDescriptors.get(i);
                             final String id = routeDescriptor.getId();
                             final int sourceIndex = provider.findRouteByDescriptorId(id);
                             if (sourceIndex < 0) {
@@ -1487,6 +1603,9 @@
                                 // 2. Create the route's contents.
                                 route.updateDescriptor(routeDescriptor);
                                 // 3. Notify clients about addition.
+                                if (DEBUG) {
+                                    Log.d(TAG, "Route added: " + route);
+                                }
                                 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
                             } else if (sourceIndex < targetIndex) {
                                 Log.w(TAG, "Ignoring route descriptor with duplicate id: "
@@ -1502,14 +1621,24 @@
                                 unselectRouteIfNeeded(route);
                                 // 4. Notify clients about changes.
                                 if ((changes & RouteInfo.CHANGE_GENERAL) != 0) {
+                                    if (DEBUG) {
+                                        Log.d(TAG, "Route changed: " + route);
+                                    }
                                     mCallbackHandler.post(
                                             CallbackHandler.MSG_ROUTE_CHANGED, route);
                                 }
                                 if ((changes & RouteInfo.CHANGE_VOLUME) != 0) {
+                                    if (DEBUG) {
+                                        Log.d(TAG, "Route volume changed: " + route);
+                                    }
                                     mCallbackHandler.post(
                                             CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route);
                                 }
                                 if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) {
+                                    if (DEBUG) {
+                                        Log.d(TAG, "Route presentation display changed: "
+                                                + route);
+                                    }
                                     mCallbackHandler.post(CallbackHandler.
                                             MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route);
                                 }
@@ -1526,14 +1655,23 @@
                     RouteInfo route = provider.mRoutes.get(i);
                     route.updateDescriptor(null);
                     // 2. Remove the route from the list.
-                    mRoutes.remove(provider);
+                    mRoutes.remove(route);
                     provider.mRoutes.remove(i);
                     // 3. Unselect route if needed before notifying about removal.
                     unselectRouteIfNeeded(route);
                     // 4. Notify clients about removal.
+                    if (DEBUG) {
+                        Log.d(TAG, "Route removed: " + route);
+                    }
                     mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route);
                 }
 
+                // Notify provider changed.
+                if (DEBUG) {
+                    Log.d(TAG, "Provider changed: " + provider);
+                }
+                mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider);
+
                 // Choose a new selected route if needed.
                 selectRouteIfNeeded();
             }
@@ -1581,6 +1719,9 @@
         private void setSelectedRouteInternal(RouteInfo route) {
             if (mSelectedRoute != route) {
                 if (mSelectedRoute != null) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Route unselected: " + mSelectedRoute);
+                    }
                     mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute);
                     if (mSelectedRouteController != null) {
                         mSelectedRouteController.onUnselect();
@@ -1597,6 +1738,9 @@
                     if (mSelectedRouteController != null) {
                         mSelectedRouteController.onSelect();
                     }
+                    if (DEBUG) {
+                        Log.d(TAG, "Route selected: " + mSelectedRoute);
+                    }
                     mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute);
                 }
             }
@@ -1618,7 +1762,7 @@
         private final class ProviderCallback extends MediaRouteProvider.Callback {
             @Override
             public void onDescriptorChanged(MediaRouteProvider provider,
-                    ProviderDescriptor descriptor) {
+                    MediaRouteProviderDescriptor descriptor) {
                 updateProviderDescriptor(provider, descriptor);
             }
         }
@@ -1627,15 +1771,21 @@
             private final ArrayList<MediaRouter> mTempMediaRouters =
                     new ArrayList<MediaRouter>();
 
-            public static final int MSG_ROUTE_ADDED = 1;
-            public static final int MSG_ROUTE_REMOVED = 2;
-            public static final int MSG_ROUTE_CHANGED = 3;
-            public static final int MSG_ROUTE_VOLUME_CHANGED = 4;
-            public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = 5;
-            public static final int MSG_ROUTE_SELECTED = 6;
-            public static final int MSG_ROUTE_UNSELECTED = 7;
-            public static final int MSG_PROVIDER_ADDED = 8;
-            public static final int MSG_PROVIDER_REMOVED = 9;
+            private static final int MSG_TYPE_MASK = 0xff00;
+            private static final int MSG_TYPE_ROUTE = 0x0100;
+            private static final int MSG_TYPE_PROVIDER = 0x0200;
+
+            public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1;
+            public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2;
+            public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3;
+            public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4;
+            public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5;
+            public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6;
+            public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7;
+
+            public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1;
+            public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2;
+            public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3;
 
             public void post(int msg, Object obj) {
                 obtainMessage(msg, obj).sendToTarget();
@@ -1655,9 +1805,9 @@
                     final int routerCount = mTempMediaRouters.size();
                     for (int i = 0; i < routerCount; i++) {
                         final MediaRouter router = mTempMediaRouters.get(i);
-                        if (!router.mCallbacks.isEmpty()) {
-                            for (MediaRouter.Callback callback : router.mCallbacks) {
-                                invokeCallback(router, callback, what, obj);
+                        if (!router.mCallbackRecords.isEmpty()) {
+                            for (CallbackRecord record : router.mCallbackRecords) {
+                                invokeCallback(router, record, what, obj);
                             }
                         }
                     }
@@ -1683,36 +1833,54 @@
                 }
             }
 
-            private void invokeCallback(MediaRouter router, MediaRouter.Callback callback,
+            private void invokeCallback(MediaRouter router, CallbackRecord record,
                     int what, Object obj) {
-                switch (what) {
-                    case MSG_ROUTE_ADDED:
-                        callback.onRouteAdded(router, (RouteInfo)obj);
+                final MediaRouter.Callback callback = record.mCallback;
+                switch (what & MSG_TYPE_MASK) {
+                    case MSG_TYPE_ROUTE: {
+                        final RouteInfo route = (RouteInfo)obj;
+                        if (!record.filterRouteEvent(route)) {
+                            break;
+                        }
+                        switch (what) {
+                            case MSG_ROUTE_ADDED:
+                                callback.onRouteAdded(router, route);
+                                break;
+                            case MSG_ROUTE_REMOVED:
+                                callback.onRouteRemoved(router, route);
+                                break;
+                            case MSG_ROUTE_CHANGED:
+                                callback.onRouteChanged(router, route);
+                                break;
+                            case MSG_ROUTE_VOLUME_CHANGED:
+                                callback.onRouteVolumeChanged(router, route);
+                                break;
+                            case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
+                                callback.onRoutePresentationDisplayChanged(router, route);
+                                break;
+                            case MSG_ROUTE_SELECTED:
+                                callback.onRouteSelected(router, route);
+                                break;
+                            case MSG_ROUTE_UNSELECTED:
+                                callback.onRouteUnselected(router, route);
+                                break;
+                        }
                         break;
-                    case MSG_ROUTE_REMOVED:
-                        callback.onRouteRemoved(router, (RouteInfo)obj);
-                        break;
-                    case MSG_ROUTE_CHANGED:
-                        callback.onRouteChanged(router, (RouteInfo)obj);
-                        break;
-                    case MSG_ROUTE_VOLUME_CHANGED:
-                        callback.onRouteVolumeChanged(router, (RouteInfo)obj);
-                        break;
-                    case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
-                        callback.onRoutePresentationDisplayChanged(router, (RouteInfo)obj);
-                        break;
-                    case MSG_ROUTE_SELECTED:
-                        callback.onRouteSelected(router, (RouteInfo)obj);
-                        break;
-                    case MSG_ROUTE_UNSELECTED:
-                        callback.onRouteUnselected(router, (RouteInfo)obj);
-                        break;
-                    case MSG_PROVIDER_ADDED:
-                        callback.onProviderAdded(router, (ProviderInfo)obj);
-                        break;
-                    case MSG_PROVIDER_REMOVED:
-                        callback.onProviderRemoved(router, (ProviderInfo)obj);
-                        break;
+                    }
+                    case MSG_TYPE_PROVIDER: {
+                        final ProviderInfo provider = (ProviderInfo)obj;
+                        switch (what) {
+                            case MSG_PROVIDER_ADDED:
+                                callback.onProviderAdded(router, provider);
+                                break;
+                            case MSG_PROVIDER_REMOVED:
+                                callback.onProviderRemoved(router, provider);
+                                break;
+                            case MSG_PROVIDER_CHANGED:
+                                callback.onProviderChanged(router, provider);
+                                break;
+                        }
+                    }
                 }
             }
         }
diff --git a/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java
index 0c619b7..90ca4b7 100644
--- a/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java
@@ -34,6 +34,7 @@
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Maintains a connection to a particular media route provider service.
@@ -41,7 +42,7 @@
 final class RegisteredMediaRouteProvider extends MediaRouteProvider
         implements ServiceConnection {
     private static final String TAG = "RegisteredMediaRouteProvider";
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     private final ComponentName mComponentName;
     private final PrivateHandler mPrivateHandler;
@@ -50,7 +51,6 @@
     private boolean mBound;
     private Connection mActiveConnection;
     private boolean mConnectionReady;
-    private boolean mActiveScanRequested;
 
     public RegisteredMediaRouteProvider(Context context, ComponentName componentName) {
         super(context, new ProviderMetadata(componentName.getPackageName()));
@@ -61,11 +61,13 @@
 
     @Override
     public RouteController onCreateRouteController(String routeId) {
-        ProviderDescriptor descriptor = getDescriptor();
+        MediaRouteProviderDescriptor descriptor = getDescriptor();
         if (descriptor != null) {
-            RouteDescriptor[] routes = descriptor.getRoutes();
-            for (int i = 0; i < routes.length; i++) {
-                if (routes[i].getId().equals(routeId)) {
+            List<MediaRouteDescriptor> routes = descriptor.getRoutes();
+            final int count = routes.size();
+            for (int i = 0; i < count; i++) {
+                final MediaRouteDescriptor route = routes.get(i);
+                if (route.getId().equals(routeId)) {
                     Controller controller = new Controller(routeId);
                     mControllers.add(controller);
                     if (mConnectionReady) {
@@ -79,18 +81,9 @@
     }
 
     @Override
-    public void onStartActiveScan() {
+    public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
         if (mConnectionReady) {
-            mActiveScanRequested = true;
-            mActiveConnection.startActiveScan();
-        }
-    }
-
-    @Override
-    public void onStopActiveScan() {
-        if (mConnectionReady) {
-            mActiveScanRequested = false;
-            mActiveConnection.stopActiveScan();
+            mActiveConnection.setDiscoveryRequest(request);
         }
     }
 
@@ -174,8 +167,10 @@
         if (mActiveConnection == connection) {
             mConnectionReady = true;
             attachControllersToConnection();
-            if (mActiveScanRequested) {
-                mActiveConnection.startActiveScan();
+
+            MediaRouteDiscoveryRequest request = getDiscoveryRequest();
+            if (request != null) {
+                mActiveConnection.setDiscoveryRequest(request);
             }
         }
     }
@@ -199,7 +194,7 @@
     }
 
     private void onConnectionDescriptorChanged(Connection connection,
-            ProviderDescriptor descriptor) {
+            MediaRouteProviderDescriptor descriptor) {
         if (mActiveConnection == connection) {
             if (DEBUG) {
                 Log.d(TAG, this + ": Descriptor changed, descriptor=" + descriptor);
@@ -411,7 +406,7 @@
                 mPendingRegisterRequestId = 0;
                 mServiceVersion = serviceVersion;
                 onConnectionDescriptorChanged(this,
-                        ProviderDescriptor.fromBundle(descriptorBundle));
+                        MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
                 onConnectionReady(this);
                 return true;
             }
@@ -421,7 +416,7 @@
         public boolean onDescriptorChanged(Bundle descriptorBundle) {
             if (mServiceVersion != 0) {
                 onConnectionDescriptorChanged(this,
-                        ProviderDescriptor.fromBundle(descriptorBundle));
+                        MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
                 return true;
             }
             return false;
@@ -499,14 +494,9 @@
             return false;
         }
 
-        public void startActiveScan() {
-            sendRequest(MediaRouteProviderService.CLIENT_MSG_START_ACTIVE_SCAN,
-                    mNextRequestId++, 0, null, null);
-        }
-
-        public void stopActiveScan() {
-            sendRequest(MediaRouteProviderService.CLIENT_MSG_STOP_ACTIVE_SCAN,
-                    mNextRequestId++, 0, null, null);
+        public void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+            sendRequest(MediaRouteProviderService.CLIENT_MSG_SET_DISCOVERY_REQUEST,
+                    mNextRequestId++, 0, request != null ? request.asBundle() : null, null);
         }
 
         private boolean sendRequest(int what, int requestId, int arg, Object obj, Bundle data) {
diff --git a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
index 1246e96..989519c 100644
--- a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
@@ -24,12 +24,10 @@
 import android.media.AudioManager;
 import android.os.Build;
 import android.support.v7.mediarouter.R;
-import android.util.Log;
 import android.view.Display;
 
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Provides routes for built-in system destinations such as the local display
@@ -101,12 +99,14 @@
     static class LegacyImpl extends SystemMediaRouteProvider {
         private static final int PLAYBACK_STREAM = AudioManager.STREAM_MUSIC;
 
-        private static final IntentFilter[] CONTROL_FILTERS;
+        private static final ArrayList<IntentFilter> CONTROL_FILTERS;
         static {
-            CONTROL_FILTERS = new IntentFilter[1];
-            CONTROL_FILTERS[0] = new IntentFilter();
-            CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
-            CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+            IntentFilter f = new IntentFilter();
+            f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+            f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+
+            CONTROL_FILTERS = new ArrayList<IntentFilter>();
+            CONTROL_FILTERS.add(f);
         }
 
         private final AudioManager mAudioManager;
@@ -125,18 +125,23 @@
 
         private void publishRoutes() {
             Resources r = getContext().getResources();
-            RouteDescriptor defaultRoute = new RouteDescriptor(
-                    DEFAULT_ROUTE_ID, r.getString(R.string.system_route_name));
-            defaultRoute.setControlFilters(CONTROL_FILTERS);
-            defaultRoute.setPlaybackStream(PLAYBACK_STREAM);
-            defaultRoute.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL);
-            defaultRoute.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
-            defaultRoute.setVolumeMax(mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM));
+            int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
             mLastReportedVolume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
-            defaultRoute.setVolume(mLastReportedVolume);
+            MediaRouteDescriptor defaultRoute = new MediaRouteDescriptor.Builder(
+                    DEFAULT_ROUTE_ID, r.getString(R.string.system_route_name))
+                    .addControlFilters(CONTROL_FILTERS)
+                    .setPlaybackStream(PLAYBACK_STREAM)
+                    .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL)
+                    .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+                    .setVolumeMax(maxVolume)
+                    .setVolume(mLastReportedVolume)
+                    .build();
 
-            ProviderDescriptor providerDescriptor = new ProviderDescriptor();
-            providerDescriptor.setRoutes(new RouteDescriptor[] { defaultRoute });
+            MediaRouteProviderDescriptor providerDescriptor =
+                    new MediaRouteProviderDescriptor.Builder()
+                    .addDiscoverableControlFilters(CONTROL_FILTERS)
+                    .addRoute(defaultRoute)
+                    .build();
             setDescriptor(providerDescriptor);
         }
 
@@ -196,41 +201,33 @@
      */
     static class JellybeanImpl extends SystemMediaRouteProvider
             implements MediaRouterJellybean.Callback, MediaRouterJellybean.VolumeCallback {
-        protected static final int ALL_ROUTE_TYPES =
-                MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
-                | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
-                | MediaRouterJellybean.ROUTE_TYPE_USER;
-
-        private static final IntentFilter[] LIVE_AUDIO_CONTROL_FILTERS;
+        private static final ArrayList<IntentFilter> LIVE_AUDIO_CONTROL_FILTERS;
         static {
-            LIVE_AUDIO_CONTROL_FILTERS = new IntentFilter[1];
-            LIVE_AUDIO_CONTROL_FILTERS[0] = new IntentFilter();
-            LIVE_AUDIO_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+            IntentFilter f = new IntentFilter();
+            f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+
+            LIVE_AUDIO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+            LIVE_AUDIO_CONTROL_FILTERS.add(f);
         }
 
-        private static final IntentFilter[] LIVE_VIDEO_CONTROL_FILTERS;
+        private static final ArrayList<IntentFilter> LIVE_VIDEO_CONTROL_FILTERS;
         static {
-            LIVE_VIDEO_CONTROL_FILTERS = new IntentFilter[1];
-            LIVE_VIDEO_CONTROL_FILTERS[0] = new IntentFilter();
-            LIVE_VIDEO_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
-        }
+            IntentFilter f = new IntentFilter();
+            f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
 
-        private static final IntentFilter[] ALL_CONTROL_FILTERS;
-        static {
-            ALL_CONTROL_FILTERS = new IntentFilter[1];
-            ALL_CONTROL_FILTERS[0] = new IntentFilter();
-            ALL_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
-            ALL_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+            LIVE_VIDEO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+            LIVE_VIDEO_CONTROL_FILTERS.add(f);
         }
 
         private final SyncCallback mSyncCallback;
-        private Method mSelectRouteIntMethod;
-        private Method mGetSystemAudioRouteMethod;
 
         protected final Object mRouterObj;
         protected final Object mCallbackObj;
         protected final Object mVolumeCallbackObj;
         protected final Object mUserRouteCategoryObj;
+        protected int mRouteTypes;
+        protected boolean mActiveScan;
+        protected boolean mCallbackRegistered;
 
         // Maintains an association from framework routes to support library routes.
         // Note that we cannot use the tag field for this because an application may
@@ -243,6 +240,9 @@
         protected final ArrayList<UserRouteRecord> mUserRouteRecords =
                 new ArrayList<UserRouteRecord>();
 
+        private MediaRouterJellybean.SelectRouteWorkaround mSelectRouteWorkaround;
+        private MediaRouterJellybean.GetDefaultRouteWorkaround mGetDefaultRouteWorkaround;
+
         public JellybeanImpl(Context context, SyncCallback syncCallback) {
             super(context);
             mSyncCallback = syncCallback;
@@ -254,35 +254,65 @@
             mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
                     mRouterObj, r.getString(R.string.user_route_category_name), false);
 
-            addInitialSystemRoutes();
-            MediaRouterJellybean.addCallback(mRouterObj, ALL_ROUTE_TYPES, mCallbackObj);
+            updateSystemRoutes();
+        }
+
+        @Override
+        public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+            int newRouteTypes = 0;
+            boolean newActiveScan = false;
+            if (request != null) {
+                final MediaRouteSelector selector = request.getSelector();
+                final List<String> categories = selector.getControlCategories();
+                final int count = categories.size();
+                for (int i = 0; i < count; i++) {
+                    String category = categories.get(i);
+                    if (category.equals(MediaControlIntent.CATEGORY_LIVE_AUDIO)) {
+                        newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO;
+                    } else if (category.equals(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
+                        newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO;
+                    } else {
+                        newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_USER;
+                    }
+                }
+                newActiveScan = request.isActiveScan();
+            }
+
+            if (mRouteTypes != newRouteTypes || mActiveScan != newActiveScan) {
+                mRouteTypes = newRouteTypes;
+                mActiveScan = newActiveScan;
+                updateCallback();
+                updateSystemRoutes();
+            }
         }
 
         @Override
         public void onRouteAdded(Object routeObj) {
-            if (getUserRouteRecord(routeObj) == null) {
-                int index = findSystemRouteRecord(routeObj);
-                if (index < 0) {
-                    addSystemRouteNoPublish(routeObj);
-                    publishRoutes();
-                }
+            if (addSystemRouteNoPublish(routeObj)) {
+                publishRoutes();
             }
         }
 
-        private void addInitialSystemRoutes() {
+        private void updateSystemRoutes() {
+            boolean changed = false;
             for (Object routeObj : MediaRouterJellybean.getRoutes(mRouterObj)) {
-                addSystemRouteNoPublish(routeObj);
+                changed |= addSystemRouteNoPublish(routeObj);
             }
-            publishRoutes();
+            if (changed) {
+                publishRoutes();
+            }
         }
 
-        private void addSystemRouteNoPublish(Object routeObj) {
-            if (getUserRouteRecord(routeObj) == null) {
+        private boolean addSystemRouteNoPublish(Object routeObj) {
+            if (getUserRouteRecord(routeObj) == null
+                    && findSystemRouteRecord(routeObj) < 0) {
                 boolean isDefault = (getDefaultRoute() == routeObj);
                 SystemRouteRecord record = new SystemRouteRecord(routeObj, isDefault);
                 updateSystemRouteDescriptor(record);
                 mSystemRouteRecords.add(record);
+                return true;
             }
+            return false;
         }
 
         @Override
@@ -316,8 +346,10 @@
                     SystemRouteRecord record = mSystemRouteRecords.get(index);
                     int newVolume = MediaRouterJellybean.RouteInfo.getVolume(routeObj);
                     if (newVolume != record.mRouteDescriptor.getVolume()) {
-                        record.mRouteDescriptor = new RouteDescriptor(record.mRouteDescriptor);
-                        record.mRouteDescriptor.setVolume(newVolume);
+                        record.mRouteDescriptor =
+                                new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+                                .setVolume(newVolume)
+                                .build();
                         publishRoutes();
                     }
                 }
@@ -326,7 +358,8 @@
 
         @Override
         public void onRouteSelected(int type, Object routeObj) {
-            if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj, ALL_ROUTE_TYPES)) {
+            if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj,
+                    MediaRouterJellybean.ALL_ROUTE_TYPES)) {
                 // The currently selected route has already changed so this callback
                 // is stale.  Drop it to prevent getting into sync loops.
                 return;
@@ -398,7 +431,7 @@
                 // route in the framework media router then ensure it is selected in
                 // the compat media router.
                 Object routeObj = MediaRouterJellybean.getSelectedRoute(
-                        mRouterObj, ALL_ROUTE_TYPES);
+                        mRouterObj, MediaRouterJellybean.ALL_ROUTE_TYPES);
                 int index = findSystemRouteRecord(routeObj);
                 if (index >= 0) {
                     SystemRouteRecord record = mSystemRouteRecords.get(index);
@@ -457,15 +490,17 @@
         }
 
         protected void publishRoutes() {
+            MediaRouteProviderDescriptor.Builder builder =
+                    new MediaRouteProviderDescriptor.Builder()
+                    .addDiscoverableControlFilters(LIVE_AUDIO_CONTROL_FILTERS)
+                    .addDiscoverableControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
+
             int count = mSystemRouteRecords.size();
-            RouteDescriptor[] routeDescriptors = new RouteDescriptor[count];
             for (int i = 0; i < count; i++) {
-                routeDescriptors[i] = mSystemRouteRecords.get(i).mRouteDescriptor;
+                builder.addRoute(mSystemRouteRecords.get(i).mRouteDescriptor);
             }
 
-            ProviderDescriptor providerDescriptor = new ProviderDescriptor();
-            providerDescriptor.setRoutes(routeDescriptors);
-            setDescriptor(providerDescriptor);
+            setDescriptor(builder.build());
         }
 
         protected int findSystemRouteRecord(Object routeObj) {
@@ -513,36 +548,38 @@
             // (with a log message so we can track down the problem).
             CharSequence name = MediaRouterJellybean.RouteInfo.getName(
                     record.mRouteObj, getContext());
-            record.mRouteDescriptor = new RouteDescriptor(
+            MediaRouteDescriptor.Builder builder = new MediaRouteDescriptor.Builder(
                     record.mRouteDescriptorId, name != null ? name.toString() : "");
+            onBuildSystemRouteDescriptor(record, builder);
+            record.mRouteDescriptor = builder.build();
+        }
 
+        protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+                MediaRouteDescriptor.Builder builder) {
             int supportedTypes = MediaRouterJellybean.RouteInfo.getSupportedTypes(
                     record.mRouteObj);
             if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO) != 0) {
-                if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
-                    record.mRouteDescriptor.setControlFilters(ALL_CONTROL_FILTERS);
-                } else {
-                    record.mRouteDescriptor.setControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
-                }
-            } else if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
-                record.mRouteDescriptor.setControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
+                builder.addControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
+            }
+            if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+                builder.addControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
             }
 
             CharSequence status = MediaRouterJellybean.RouteInfo.getStatus(record.mRouteObj);
             if (status != null) {
-                record.mRouteDescriptor.setStatus(status.toString());
+                builder.setStatus(status.toString());
             }
-            record.mRouteDescriptor.setIconDrawable(
+            builder.setIconDrawable(
                     MediaRouterJellybean.RouteInfo.getIconDrawable(record.mRouteObj));
-            record.mRouteDescriptor.setPlaybackType(
+            builder.setPlaybackType(
                     MediaRouterJellybean.RouteInfo.getPlaybackType(record.mRouteObj));
-            record.mRouteDescriptor.setPlaybackStream(
+            builder.setPlaybackStream(
                     MediaRouterJellybean.RouteInfo.getPlaybackStream(record.mRouteObj));
-            record.mRouteDescriptor.setVolume(
+            builder.setVolume(
                     MediaRouterJellybean.RouteInfo.getVolume(record.mRouteObj));
-            record.mRouteDescriptor.setVolumeMax(
+            builder.setVolumeMax(
                     MediaRouterJellybean.RouteInfo.getVolumeMax(record.mRouteObj));
-            record.mRouteDescriptor.setVolumeHandling(
+            builder.setVolumeHandling(
                     MediaRouterJellybean.RouteInfo.getVolumeHandling(record.mRouteObj));
         }
 
@@ -565,6 +602,18 @@
                     record.mRouteObj, record.mRoute.getVolumeHandling());
         }
 
+        protected void updateCallback() {
+            if (mCallbackRegistered) {
+                mCallbackRegistered = false;
+                MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+            }
+
+            if (mRouteTypes != 0) {
+                mCallbackRegistered = true;
+                MediaRouterJellybean.addCallback(mRouterObj, mRouteTypes, mCallbackObj);
+            }
+        }
+
         // The framework MediaRouter crashes if we set a null status even though
         // RouteInfo.getStatus() may return null.  So we need to use a different
         // value instead.
@@ -581,63 +630,18 @@
         }
 
         protected void selectRoute(Object routeObj) {
-            int types = MediaRouterJellybean.RouteInfo.getSupportedTypes(routeObj);
-            if ((types & MediaRouterJellybean.ROUTE_TYPE_USER) == 0) {
-                // Handle non-user routes.
-                // On JB and JB MR1, the selectRoute() API only supports programmatically
-                // selecting user routes.  So instead we rely on the hidden selectRouteInt()
-                // method on these versions of the platform.  This limitation was removed
-                // in JB MR2.  See also the JellybeanMr2Impl implementation of this method.
-                if (mSelectRouteIntMethod == null) {
-                    try {
-                        mSelectRouteIntMethod = mRouterObj.getClass().getMethod(
-                                "selectRouteInt", int.class, MediaRouterJellybean.RouteInfo.clazz);
-                    } catch (NoSuchMethodException ex) {
-                        Log.w(TAG, "Cannot programmatically select non-user route "
-                                + "because the platform is missing the selectRouteInt() "
-                                + "method.  Media routing may not work.", ex);
-                        return;
-                    }
-                }
-                try {
-                    mSelectRouteIntMethod.invoke(mRouterObj, ALL_ROUTE_TYPES, routeObj);
-                } catch (IllegalAccessException ex) {
-                    Log.w(TAG, "Cannot programmatically select non-user route.  "
-                            + "Media routing may not work.", ex);
-                } catch (InvocationTargetException ex) {
-                    Log.w(TAG, "Cannot programmatically select non-user route.  "
-                            + "Media routing may not work.", ex);
-                }
-            } else {
-                // Handle user routes.
-                MediaRouterJellybean.selectRoute(mRouterObj, ALL_ROUTE_TYPES, routeObj);
+            if (mSelectRouteWorkaround == null) {
+                mSelectRouteWorkaround = new MediaRouterJellybean.SelectRouteWorkaround();
             }
+            mSelectRouteWorkaround.selectRoute(mRouterObj,
+                    MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
         }
 
         protected Object getDefaultRoute() {
-            // On JB and JB MR1, the getDefaultRoute() API does not exist.
-            // Instead there is a hidden getSystemAudioRoute() that does the same thing.
-            // See also the JellybeanMr2Impl implementation of this method.
-            if (mGetSystemAudioRouteMethod == null) {
-                try {
-                    mGetSystemAudioRouteMethod = mRouterObj.getClass().getMethod(
-                            "getSystemAudioRoute");
-                } catch (NoSuchMethodException ex) {
-                    // Fall through.
-                }
+            if (mGetDefaultRouteWorkaround == null) {
+                mGetDefaultRouteWorkaround = new MediaRouterJellybean.GetDefaultRouteWorkaround();
             }
-            if (mGetSystemAudioRouteMethod != null) {
-                try {
-                    return mGetSystemAudioRouteMethod.invoke(mRouterObj);
-                } catch (IllegalAccessException ex) {
-                    // Fall through.
-                } catch (InvocationTargetException ex) {
-                    // Fall through.
-                }
-            }
-            // Could not find the method or it does not work.
-            // Return the first route and hope for the best.
-            return MediaRouterJellybean.getRoutes(mRouterObj).get(0);
+            return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj);
         }
 
         /**
@@ -649,7 +653,7 @@
 
             public final Object mRouteObj;
             public final String mRouteDescriptorId;
-            public RouteDescriptor mRouteDescriptor; // assigned immediately after creation
+            public MediaRouteDescriptor mRouteDescriptor; // assigned immediately after creation
 
             public SystemRouteRecord(Object routeObj, boolean isDefault) {
                 mRouteObj = routeObj;
@@ -677,6 +681,9 @@
      */
     private static class JellybeanMr1Impl extends JellybeanImpl
             implements MediaRouterJellybeanMr1.Callback {
+        private MediaRouterJellybeanMr1.ActiveScanWorkaround mActiveScanWorkaround;
+        private MediaRouterJellybeanMr1.IsConnectingWorkaround mIsConnectingWorkaround;
+
         public JellybeanMr1Impl(Context context, SyncCallback syncCallback) {
             super(context, syncCallback);
         }
@@ -692,32 +699,57 @@
                         ? newPresentationDisplay.getDisplayId() : -1);
                 if (newPresentationDisplayId
                         != record.mRouteDescriptor.getPresentationDisplayId()) {
-                    record.mRouteDescriptor = new RouteDescriptor(record.mRouteDescriptor);
-                    record.mRouteDescriptor.setPresentationDisplayId(newPresentationDisplayId);
+                    record.mRouteDescriptor =
+                            new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+                            .setPresentationDisplayId(newPresentationDisplayId)
+                            .build();
                     publishRoutes();
                 }
             }
         }
 
         @Override
-        protected void updateSystemRouteDescriptor(SystemRouteRecord record) {
-            super.updateSystemRouteDescriptor(record);
+        protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+                MediaRouteDescriptor.Builder builder) {
+            super.onBuildSystemRouteDescriptor(record, builder);
 
             if (!MediaRouterJellybeanMr1.RouteInfo.isEnabled(record.mRouteObj)) {
-                record.mRouteDescriptor.setEnabled(false);
+                builder.setEnabled(false);
             }
+
+            if (isConnecting(record)) {
+                builder.setConnecting(true);
+            }
+
             Display presentationDisplay =
                     MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(record.mRouteObj);
             if (presentationDisplay != null) {
-                record.mRouteDescriptor.setPresentationDisplayId(
-                        presentationDisplay.getDisplayId());
+                builder.setPresentationDisplayId(presentationDisplay.getDisplayId());
             }
         }
 
         @Override
+        protected void updateCallback() {
+            super.updateCallback();
+
+            if (mActiveScanWorkaround == null) {
+                mActiveScanWorkaround = new MediaRouterJellybeanMr1.ActiveScanWorkaround(
+                        getContext(), getHandler());
+            }
+            mActiveScanWorkaround.setActiveScanRouteTypes(mActiveScan ? mRouteTypes : 0);
+        }
+
+        @Override
         protected Object createCallbackObj() {
             return MediaRouterJellybeanMr1.createCallback(this);
         }
+
+        protected boolean isConnecting(SystemRouteRecord record) {
+            if (mIsConnectingWorkaround == null) {
+                mIsConnectingWorkaround = new MediaRouterJellybeanMr1.IsConnectingWorkaround();
+            }
+            return mIsConnectingWorkaround.isConnecting(record.mRouteObj);
+        }
     }
 
     /**
@@ -730,12 +762,30 @@
 
         @Override
         protected void selectRoute(Object routeObj) {
-            MediaRouterJellybean.selectRoute(mRouterObj, ALL_ROUTE_TYPES, routeObj);
+            MediaRouterJellybean.selectRoute(mRouterObj,
+                    MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
         }
 
         @Override
         protected Object getDefaultRoute() {
             return MediaRouterJellybeanMr2.getDefaultRoute(mRouterObj);
         }
+
+        @Override
+        protected void updateCallback() {
+            if (mCallbackRegistered) {
+                MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+            }
+
+            mCallbackRegistered = true;
+            MediaRouterJellybeanMr2.addCallback(mRouterObj, mRouteTypes, mCallbackObj,
+                    MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS
+                    | (mActiveScan ? MediaRouter.CALLBACK_FLAG_ACTIVE_SCAN : 0));
+        }
+
+        @Override
+        protected boolean isConnecting(SystemRouteRecord record) {
+            return MediaRouterJellybeanMr2.RouteInfo.isConnecting(record.mRouteObj);
+        }
     }
 }