Merge "Tidy up Up Navigate for ActionBarActivity" into jb-mr2-dev
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/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java b/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java
index 6ea47c5..750b6f4 100644
--- a/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java
+++ b/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java
@@ -57,22 +57,9 @@
      */
     @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
     static boolean sInitialized;
+    static Object lock = new Object();
     native static void _nInit();
 
-
-    static {
-        sInitialized = false;
-        try {
-            System.loadLibrary("RSSupport");
-            System.loadLibrary("rsjni");
-            _nInit();
-            sInitialized = true;
-        } catch (UnsatisfiedLinkError e) {
-            Log.e(LOG_TAG, "Error loading RS jni library: " + e);
-            throw new RSRuntimeException("Error loading RS jni library: " + e);
-        }
-    }
-
     // Non-threadsafe functions.
     native int  nDeviceCreate();
     native void nDeviceDestroy(int dev);
@@ -888,6 +875,19 @@
             android.util.Log.v(LOG_TAG, "RS native mode");
             return RenderScriptThunker.create(ctx, sdkVersion);
         }
+        synchronized(lock) {
+            if (sInitialized == false) {
+                try {
+                    System.loadLibrary("RSSupport");
+                    System.loadLibrary("rsjni");
+                    _nInit();
+                    sInitialized = true;
+                } catch (UnsatisfiedLinkError e) {
+                    Log.e(LOG_TAG, "Error loading RS jni library: " + e);
+                    throw new RSRuntimeException("Error loading RS jni library: " + e);
+                }
+            }
+        }
 
         android.util.Log.v(LOG_TAG, "RS compat mode");
         rs.mDev = rs.nDeviceCreate();
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/v4/java/android/support/v4/widget/DrawerLayout.java b/v4/java/android/support/v4/widget/DrawerLayout.java
index cc55b82..e5455b4 100644
--- a/v4/java/android/support/v4/widget/DrawerLayout.java
+++ b/v4/java/android/support/v4/widget/DrawerLayout.java
@@ -639,6 +639,7 @@
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         mInLayout = true;
+        final int width = r - l;
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             final View child = getChildAt(i);
@@ -658,12 +659,17 @@
                 final int childHeight = child.getMeasuredHeight();
                 int childLeft;
 
+                final float newOffset;
                 if (checkDrawerViewGravity(child, Gravity.LEFT)) {
                     childLeft = -childWidth + (int) (childWidth * lp.onScreen);
+                    newOffset = (float) (childWidth + childLeft) / childWidth;
                 } else { // Right; onMeasure checked for us.
-                    childLeft = r - l - (int) (childWidth * lp.onScreen);
+                    childLeft = width - (int) (childWidth * lp.onScreen);
+                    newOffset = (float) (width - childLeft) / childWidth;
                 }
 
+                final boolean changeOffset = newOffset != lp.onScreen;
+
                 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
 
                 switch (vgrav) {
@@ -699,8 +705,13 @@
                     }
                 }
 
-                if (lp.onScreen == 0) {
-                    child.setVisibility(INVISIBLE);
+                if (changeOffset) {
+                    setDrawerViewOffset(child, newOffset);
+                }
+
+                final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE;
+                if (child.getVisibility() != newVisibility) {
+                    child.setVisibility(newVisibility);
                 }
             }
         }
diff --git a/v7/appcompat/res/values/attrs.xml b/v7/appcompat/res/values/attrs.xml
index 499b904..e9caedf 100644
--- a/v7/appcompat/res/values/attrs.xml
+++ b/v7/appcompat/res/values/attrs.xml
@@ -474,5 +474,6 @@
     <!-- Default Spinner style. -->
     <attr name="spinnerDropDownItemStyle" format="reference" />
 
-
+    <!-- Specifies whether the theme is light, otherwise it is dark. -->
+    <attr name="isLightTheme" format="boolean" />
 </resources>
diff --git a/v7/appcompat/res/values/themes.xml b/v7/appcompat/res/values/themes.xml
index 9e4ee54..91199f4 100644
--- a/v7/appcompat/res/values/themes.xml
+++ b/v7/appcompat/res/values/themes.xml
@@ -27,6 +27,7 @@
 
     <!-- Platform-independent theme providing an action bar in a dark-themed activity. -->
     <style name="Theme.AppCompat" parent="Theme.Base.AppCompat">
+        <item name="isLightTheme">false</item>
 
         <!-- Required for use of support_simple_spinner_dropdown_item.xml -->
         <item name="spinnerDropDownItemStyle">
@@ -46,6 +47,7 @@
 
     <!-- Platform-independent theme providing an action bar in a light-themed activity. -->
     <style name="Theme.AppCompat.Light" parent="Theme.Base.AppCompat.Light">
+        <item name="isLightTheme">true</item>
 
         <!-- Required for use of support_simple_spinner_dropdown_item.xml -->
         <item name="spinnerDropDownItemStyle">
@@ -66,6 +68,7 @@
     <!-- Platform-independent theme providing an action bar in a dark-themed activity. -->
     <style name="Theme.AppCompat.Light.DarkActionBar"
            parent="Theme.Base.AppCompat.Light.DarkActionBar">
+        <item name="isLightTheme">true</item>
 
         <!-- Required for use of support_simple_spinner_dropdown_item.xml -->
         <item name="spinnerDropDownItemStyle">
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);
+        }
     }
 }