Merge "Fix Context MenuInflater is created with" 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/app/ActionBarDrawerToggle.java b/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
index fa58e21..11eea28 100644
--- a/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
+++ b/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
@@ -55,6 +55,42 @@
  */
 public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener {
 
+    /**
+     * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use
+     * with ActionBarDrawerToggle.
+     */
+    public interface DelegateProvider {
+
+        /**
+         * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity
+         *         does not wish to override the default behavior.
+         */
+        Delegate getDrawerToggleDelegate();
+    }
+
+    public interface Delegate {
+        /**
+         * @return Up indicator drawable as defined in the Activity's theme, or null if one is not
+         *         defined.
+         */
+        Drawable getThemeUpIndicator();
+
+        /**
+         * Set the Action Bar's up indicator drawable and content description.
+         *
+         * @param upDrawable     - Drawable to set as up indicator
+         * @param contentDescRes - Content description to set
+         */
+        void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes);
+
+        /**
+         * Set the Action Bar's up indicator content description.
+         *
+         * @param contentDescRes - Content description to set
+         */
+        void setActionBarDescription(int contentDescRes);
+    }
+
     private interface ActionBarDrawerToggleImpl {
         Drawable getThemeUpIndicator(Activity activity);
         Object setActionBarUpIndicator(Object info, Activity activity,
@@ -117,6 +153,7 @@
     private static final int ID_HOME = 0x0102002c;
 
     private final Activity mActivity;
+    private final Delegate mActivityImpl;
     private final DrawerLayout mDrawerLayout;
     private boolean mDrawerIndicatorEnabled = true;
 
@@ -156,10 +193,17 @@
         mOpenDrawerContentDescRes = openDrawerContentDescRes;
         mCloseDrawerContentDescRes = closeDrawerContentDescRes;
 
-        mThemeImage = IMPL.getThemeUpIndicator(activity);
+        mThemeImage = getThemeUpIndicator();
         mDrawerImage = activity.getResources().getDrawable(drawerImageRes);
         mSlider = new SlideDrawable(mDrawerImage);
         mSlider.setOffsetBy(1.f / 3);
+
+        // Allow the Activity to provide an impl
+        if (activity instanceof DelegateProvider) {
+            mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate();
+        } else {
+            mActivityImpl = null;
+        }
     }
 
     /**
@@ -179,8 +223,7 @@
         }
 
         if (mDrawerIndicatorEnabled) {
-            mSetIndicatorInfo = IMPL.setActionBarUpIndicator(mSetIndicatorInfo, mActivity,
-                    mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
+            setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
                     mOpenDrawerContentDescRes : mCloseDrawerContentDescRes);
         }
     }
@@ -198,12 +241,10 @@
     public void setDrawerIndicatorEnabled(boolean enable) {
         if (enable != mDrawerIndicatorEnabled) {
             if (enable) {
-                mSetIndicatorInfo = IMPL.setActionBarUpIndicator(mSetIndicatorInfo,
-                        mActivity, mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
-                        mOpenDrawerContentDescRes : mCloseDrawerContentDescRes);
+                setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
+                                mOpenDrawerContentDescRes : mCloseDrawerContentDescRes);
             } else {
-                mSetIndicatorInfo = IMPL.setActionBarUpIndicator(mSetIndicatorInfo,
-                        mActivity, mThemeImage, 0);
+                setActionBarUpIndicator(mThemeImage, 0);
             }
             mDrawerIndicatorEnabled = enable;
         }
@@ -226,7 +267,7 @@
      */
     public void onConfigurationChanged(Configuration newConfig) {
         // Reload drawables that can change with configuration
-        mThemeImage = IMPL.getThemeUpIndicator(mActivity);
+        mThemeImage = getThemeUpIndicator();
         mDrawerImage = mActivity.getResources().getDrawable(mDrawerImageResource);
         syncState();
     }
@@ -247,6 +288,7 @@
             } else {
                 mDrawerLayout.openDrawer(GravityCompat.START);
             }
+            return true;
         }
         return false;
     }
@@ -281,8 +323,7 @@
     public void onDrawerOpened(View drawerView) {
         mSlider.setOffset(1.f);
         if (mDrawerIndicatorEnabled) {
-            mSetIndicatorInfo = IMPL.setActionBarDescription(mSetIndicatorInfo, mActivity,
-                    mOpenDrawerContentDescRes);
+            setActionBarDescription(mOpenDrawerContentDescRes);
         }
     }
 
@@ -297,8 +338,7 @@
     public void onDrawerClosed(View drawerView) {
         mSlider.setOffset(0.f);
         if (mDrawerIndicatorEnabled) {
-            mSetIndicatorInfo = IMPL.setActionBarDescription(mSetIndicatorInfo, mActivity,
-                    mCloseDrawerContentDescRes);
+            setActionBarDescription(mCloseDrawerContentDescRes);
         }
     }
 
@@ -313,6 +353,31 @@
     public void onDrawerStateChanged(int newState) {
     }
 
+    Drawable getThemeUpIndicator() {
+        if (mActivityImpl != null) {
+            return mActivityImpl.getThemeUpIndicator();
+        }
+        return IMPL.getThemeUpIndicator(mActivity);
+    }
+
+    void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
+        if (mActivityImpl != null) {
+            mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes);
+            return;
+        }
+        mSetIndicatorInfo = IMPL
+                .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes);
+    }
+
+    void setActionBarDescription(int contentDescRes) {
+        if (mActivityImpl != null) {
+            mActivityImpl.setActionBarDescription(contentDescRes);
+            return;
+        }
+        mSetIndicatorInfo = IMPL
+                .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes);
+    }
+
     private static class SlideDrawable extends Drawable implements Drawable.Callback {
         private Drawable mWrapped;
         private float mOffset;
diff --git a/v4/java/android/support/v4/app/FragmentActivity.java b/v4/java/android/support/v4/app/FragmentActivity.java
index acb8181..9f111f7 100644
--- a/v4/java/android/support/v4/app/FragmentActivity.java
+++ b/v4/java/android/support/v4/app/FragmentActivity.java
@@ -468,7 +468,7 @@
             }
             boolean goforit = super.onPreparePanel(featureId, view, menu);
             goforit |= mFragments.dispatchPrepareOptionsMenu(menu);
-            return goforit && menu.hasVisibleItems();
+            return goforit;
         }
         return super.onPreparePanel(featureId, view, menu);
     }
diff --git a/v4/java/android/support/v4/app/TaskStackBuilder.java b/v4/java/android/support/v4/app/TaskStackBuilder.java
index 0f12a33..9146872 100644
--- a/v4/java/android/support/v4/app/TaskStackBuilder.java
+++ b/v4/java/android/support/v4/app/TaskStackBuilder.java
@@ -69,6 +69,10 @@
 public class TaskStackBuilder implements Iterable<Intent> {
     private static final String TAG = "TaskStackBuilder";
 
+    public interface SupportParentable {
+        Intent getSupportParentActivityIntent();
+    }
+
     interface TaskStackBuilderImpl {
         PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode,
                 int flags, Bundle options);
@@ -190,7 +194,14 @@
      * @return This TaskStackBuilder for method chaining
      */
     public TaskStackBuilder addParentStack(Activity sourceActivity) {
-        final Intent parent = NavUtils.getParentActivityIntent(sourceActivity);
+        Intent parent = null;
+        if (sourceActivity instanceof SupportParentable) {
+            parent = ((SupportParentable) sourceActivity).getSupportParentActivityIntent();
+        }
+        if (parent == null) {
+            parent = NavUtils.getParentActivityIntent(sourceActivity);
+        }
+
         if (parent != null) {
             // We have the actual parent intent, build the rest from static metadata
             // then add the direct parent intent to the end.
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..afe27d4 100644
--- a/v4/java/android/support/v4/widget/DrawerLayout.java
+++ b/v4/java/android/support/v4/widget/DrawerLayout.java
@@ -582,14 +582,34 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
-        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
-        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
-        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
 
         if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
-            throw new IllegalArgumentException(
-                    "DrawerLayout must be measured with MeasureSpec.EXACTLY.");
+            if (isInEditMode()) {
+                // Don't crash the layout editor. Consume all of the space if specified
+                // or pick a magic number from thin air otherwise.
+                // TODO Better communication with tools of this bogus state.
+                // It will crash on a real device.
+                if (widthMode == MeasureSpec.AT_MOST) {
+                    widthMode = MeasureSpec.EXACTLY;
+                } else if (widthMode == MeasureSpec.UNSPECIFIED) {
+                    widthMode = MeasureSpec.EXACTLY;
+                    widthSize = 300;
+                }
+                if (heightMode == MeasureSpec.AT_MOST) {
+                    heightMode = MeasureSpec.EXACTLY;
+                }
+                else if (heightMode == MeasureSpec.UNSPECIFIED) {
+                    heightMode = MeasureSpec.EXACTLY;
+                    heightSize = 300;
+                }
+            } else {
+                throw new IllegalArgumentException(
+                        "DrawerLayout must be measured with MeasureSpec.EXACTLY.");
+            }
         }
 
         setMeasuredDimension(widthSize, heightSize);
@@ -639,6 +659,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 +679,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 +725,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/v4/java/android/support/v4/widget/SlidingPaneLayout.java b/v4/java/android/support/v4/widget/SlidingPaneLayout.java
index 591d827..aaed38f 100644
--- a/v4/java/android/support/v4/widget/SlidingPaneLayout.java
+++ b/v4/java/android/support/v4/widget/SlidingPaneLayout.java
@@ -84,8 +84,6 @@
  * sized to fill all available space in the closed state. Weight on a pane that becomes covered
  * indicates that the pane should be sized to fill all available space except a small minimum strip
  * that the user may use to grab the slideable view and pull it back over into a closed state.</p>
- *
- * <p>Experimental. This class may be removed.</p>
  */
 public class SlidingPaneLayout extends ViewGroup {
     private static final String TAG = "SlidingPaneLayout";
@@ -424,15 +422,38 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
-        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
-        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
-        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
 
         if (widthMode != MeasureSpec.EXACTLY) {
-            throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
+            if (isInEditMode()) {
+                // Don't crash the layout editor. Consume all of the space if specified
+                // or pick a magic number from thin air otherwise.
+                // TODO Better communication with tools of this bogus state.
+                // It will crash on a real device.
+                if (widthMode == MeasureSpec.AT_MOST) {
+                    widthMode = MeasureSpec.EXACTLY;
+                } else if (widthMode == MeasureSpec.UNSPECIFIED) {
+                    widthMode = MeasureSpec.EXACTLY;
+                    widthSize = 300;
+                }
+            } else {
+                throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
+            }
         } else if (heightMode == MeasureSpec.UNSPECIFIED) {
-            throw new IllegalStateException("Height must not be UNSPECIFIED");
+            if (isInEditMode()) {
+                // Don't crash the layout editor. Pick a magic number from thin air instead.
+                // TODO Better communication with tools of this bogus state.
+                // It will crash on a real device.
+                if (heightMode == MeasureSpec.UNSPECIFIED) {
+                    heightMode = MeasureSpec.AT_MOST;
+                    heightSize = 300;
+                }
+            } else {
+                throw new IllegalStateException("Height must not be UNSPECIFIED");
+            }
         }
 
         int layoutHeight = 0;
@@ -1371,26 +1392,39 @@
         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
             final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info);
             super.onInitializeAccessibilityNodeInfo(host, superNode);
+            copyNodeInfoNoChildren(info, superNode);
+            superNode.recycle();
 
+            info.setClassName(SlidingPaneLayout.class.getName());
             info.setSource(host);
+
             final ViewParent parent = ViewCompat.getParentForAccessibility(host);
             if (parent instanceof View) {
                 info.setParent((View) parent);
             }
-            copyNodeInfoNoChildren(info, superNode);
 
-            superNode.recycle();
-
+            // This is a best-approximation of addChildrenForAccessibility()
+            // that accounts for filtering.
             final int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
                 final View child = getChildAt(i);
-                if (!filter(child)) {
+                if (!filter(child) && (child.getVisibility() == View.VISIBLE)) {
+                    // Force importance to "yes" since we can't read the value.
+                    ViewCompat.setImportantForAccessibility(
+                            child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
                     info.addChild(child);
                 }
             }
         }
 
         @Override
+        public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+            super.onInitializeAccessibilityEvent(host, event);
+
+            event.setClassName(SlidingPaneLayout.class.getName());
+        }
+
+        @Override
         public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
                 AccessibilityEvent event) {
             if (!filter(child)) {
@@ -1432,6 +1466,8 @@
             dest.setLongClickable(src.isLongClickable());
 
             dest.addAction(src.getActions());
+
+            dest.setMovementGranularities(src.getMovementGranularities());
         }
     }
 
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/appcompat/src/android/support/v7/app/ActionBarActivity.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
index 1968d3c..b6f1568 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
@@ -21,6 +21,7 @@
 import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.NavUtils;
@@ -37,7 +38,8 @@
 /**
  * Base class for activities that use the support library action bar features.
  */
-public class ActionBarActivity extends FragmentActivity implements ActionBar.Callback {
+public class ActionBarActivity extends FragmentActivity implements ActionBar.Callback,
+        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
     ActionBarActivityDelegate mImpl;
 
     /**
@@ -328,11 +330,34 @@
     public void onPrepareSupportNavigateUpTaskStack(TaskStackBuilder builder) {
     }
 
+    /**
+     * This method is called whenever the user chooses to navigate Up within your application's
+     * activity hierarchy from the action bar.
+     *
+     * <p>If a parent was specified in the manifest for this activity or an activity-alias to it,
+     * default Up navigation will be handled automatically. See
+     * {@link #getSupportParentActivityIntent()} for how to specify the parent. If any activity
+     * along the parent chain requires extra Intent arguments, the Activity subclass
+     * should override the method {@link #onPrepareSupportNavigateUpTaskStack(TaskStackBuilder)}
+     * to supply those arguments.</p>
+     *
+     * <p>See <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and
+     * Back Stack</a> from the developer guide and
+     * <a href="{@docRoot}design/patterns/navigation.html">Navigation</a> from the design guide
+     * for more information about navigating within your app.</p>
+     *
+     * <p>See the {@link TaskStackBuilder} class and the Activity methods
+     * {@link #getSupportParentActivityIntent()}, {@link #supportShouldUpRecreateTask(Intent)}, and
+     * {@link #supportNavigateUpTo(Intent)} for help implementing custom Up navigation.</p>
+     *
+     * @return true if Up navigation completed successfully and this Activity was finished,
+     *         false otherwise.
+     */
     public boolean onSupportNavigateUp() {
-        Intent upIntent = NavUtils.getParentActivityIntent(this);
+        Intent upIntent = getSupportParentActivityIntent();
 
         if (upIntent != null) {
-            if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
+            if (supportShouldUpRecreateTask(upIntent)) {
                 TaskStackBuilder b = TaskStackBuilder.create(this);
                 onCreateSupportNavigateUpTaskStack(b);
                 onPrepareSupportNavigateUpTaskStack(b);
@@ -348,11 +373,61 @@
             } else {
                 // This activity is part of the application's task, so simply
                 // navigate up to the hierarchical parent activity.
-                NavUtils.navigateUpTo(this, upIntent);
+                supportNavigateUpTo(upIntent);
             }
             return true;
         }
         return false;
     }
 
+    /**
+     * Obtain an {@link Intent} that will launch an explicit target activity
+     * specified by sourceActivity's {@link NavUtils#PARENT_ACTIVITY} &lt;meta-data&gt;
+     * element in the application's manifest. If the device is running
+     * Jellybean or newer, the android:parentActivityName attribute will be preferred
+     * if it is present.
+     *
+     * @return a new Intent targeting the defined parent activity of sourceActivity
+     */
+    public Intent getSupportParentActivityIntent() {
+        return NavUtils.getParentActivityIntent(this);
+    }
+
+    /**
+     * Returns true if sourceActivity should recreate the task when navigating 'up'
+     * by using targetIntent.
+     *
+     * <p>If this method returns false the app can trivially call
+     * {@link #supportNavigateUpTo(Intent)} using the same parameters to correctly perform
+     * up navigation. If this method returns false, the app should synthesize a new task stack
+     * by using {@link TaskStackBuilder} or another similar mechanism to perform up navigation.</p>
+     *
+     * @param targetIntent An intent representing the target destination for up navigation
+     * @return true if navigating up should recreate a new task stack, false if the same task
+     *         should be used for the destination
+     */
+    public boolean supportShouldUpRecreateTask(Intent targetIntent) {
+        return NavUtils.shouldUpRecreateTask(this, targetIntent);
+    }
+
+    /**
+     * Navigate from sourceActivity to the activity specified by upIntent, finishing sourceActivity
+     * in the process. upIntent will have the flag {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set
+     * by this method, along with any others required for proper up navigation as outlined
+     * in the Android Design Guide.
+     *
+     * <p>This method should be used when performing up navigation from within the same task
+     * as the destination. If up navigation should cross tasks in some cases, see
+     * {@link #supportShouldUpRecreateTask(Intent)}.</p>
+     *
+     * @param upIntent An intent representing the target destination for up navigation
+     */
+    public void supportNavigateUpTo(Intent upIntent) {
+        NavUtils.navigateUpTo(this, upIntent);
+    }
+
+    @Override
+    public final ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
+        return mImpl.getDrawerToggleDelegate();
+    }
 }
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
index b93116a..c46f698 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
@@ -23,6 +23,7 @@
 import android.content.res.TypedArray;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
 import android.support.v7.appcompat.R;
 import android.support.v7.internal.view.SupportMenuInflater;
 import android.support.v7.view.ActionMode;
@@ -150,6 +151,8 @@
 
     abstract void setSupportProgress(int progress);
 
+    abstract ActionBarDrawerToggle.Delegate getDrawerToggleDelegate();
+
     protected final String getUiOptionsFromMetadata() {
         try {
             PackageManager pm = mActivity.getPackageManager();
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java
index 62a2c74..93923cf 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java
@@ -19,6 +19,8 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.v4.app.ActionBarDrawerToggle;
 import android.support.v4.view.WindowCompat;
 import android.support.v7.appcompat.R;
 import android.support.v7.internal.view.menu.ListMenuPresenter;
@@ -31,7 +33,6 @@
 import android.support.v7.internal.widget.ActionBarView;
 import android.support.v7.internal.widget.ProgressBarICS;
 import android.support.v7.view.ActionMode;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -44,6 +45,10 @@
         MenuPresenter.Callback, MenuBuilder.Callback {
     private static final String TAG = "ActionBarActivityDelegateBase";
 
+    private static final int[] ACTION_BAR_DRAWABLE_TOGGLE_ATTRS = new int[] {
+            R.attr.homeAsUpIndicator
+    };
+
     private ActionBarView mActionBarView;
     private ListMenuPresenter mListMenuPresenter;
     private MenuBuilder mMenu;
@@ -471,6 +476,11 @@
         updateProgressBars(Window.PROGRESS_START + progress);
     }
 
+    @Override
+    ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
+        return new ActionBarDrawableToggleImpl();
+    }
+
     /**
      * Progress Bar function. Mostly extracted from PhoneWindow.java
      */
@@ -579,4 +589,28 @@
         }
     }
 
+    private class ActionBarDrawableToggleImpl
+            implements ActionBarDrawerToggle.Delegate {
+
+        @Override
+        public Drawable getThemeUpIndicator() {
+            final TypedArray a = mActivity.obtainStyledAttributes(ACTION_BAR_DRAWABLE_TOGGLE_ATTRS);
+            final Drawable result = a.getDrawable(0);
+            a.recycle();
+            return result;
+        }
+
+        @Override
+        public void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
+            if (mActionBarView != null) {
+                mActionBarView.setHomeAsUpIndicator(upDrawable);
+            }
+        }
+
+        @Override
+        public void setActionBarDescription(int contentDescRes) {
+            // No support for setting Action Bar content description
+        }
+    }
+
 }
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java
index 4610c1a..7540cc4 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
 import android.support.v4.view.WindowCompat;
 import android.support.v7.internal.view.ActionModeWrapper;
 import android.support.v7.internal.view.menu.MenuWrapperFactory;
@@ -219,6 +220,12 @@
         return false;
     }
 
+    @Override
+    public ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
+        // Return null so that ActionBarDrawableToggle uses it's standard impl
+        return null;
+    }
+
     class WindowCallbackWrapper implements Window.Callback {
         final Window.Callback mWrapped;
 
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java
index 4856df1..276a0f5 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java
@@ -495,6 +495,9 @@
 
     @Override
     public boolean isVisible() {
+        if (mActionProvider != null && mActionProvider.overridesItemVisibility()) {
+            return (mFlags & HIDDEN) == 0 && mActionProvider.isVisible();
+        }
         return (mFlags & HIDDEN) == 0;
     }
 
@@ -659,7 +662,7 @@
             actionProvider.setVisibilityListener(new ActionProvider.VisibilityListener() {
                 @Override
                 public void onActionProviderVisibilityChanged(boolean isVisible) {
-                    setVisible(isVisible);
+                    mMenu.onItemVisibleChanged(MenuItemImpl.this);
                 }
             });
         }
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java
index 95144b9..93702bb 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java
@@ -18,8 +18,8 @@
 
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
-import android.support.v4.view.ActionProvider;
 import android.support.v4.internal.view.SupportMenuItem;
+import android.support.v4.view.ActionProvider;
 import android.support.v4.view.MenuItemCompat;
 import android.view.ContextMenu;
 import android.view.MenuItem;
@@ -226,16 +226,14 @@
 
     @Override
     public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider) {
-        mWrappedObject.setActionProvider(actionProvider != null ?
-                createActionProviderWrapper(actionProvider) : null);
+        // APIv14+ API
         return this;
     }
 
     @Override
     public ActionProvider getSupportActionProvider() {
-        ActionProviderWrapper providerWrapper =
-                (ActionProviderWrapper)mWrappedObject.getActionProvider();
-        return providerWrapper.mInner;
+        // APIv14+ API
+        return null;
     }
 
     @Override
@@ -281,10 +279,6 @@
         return null;
     }
 
-    ActionProviderWrapper createActionProviderWrapper(ActionProvider provider) {
-        return new ActionProviderWrapper(provider);
-    }
-
     private class OnMenuItemClickListenerWrapper extends BaseWrapper<OnMenuItemClickListener>
             implements android.view.MenuItem.OnMenuItemClickListener {
 
@@ -297,34 +291,4 @@
             return mWrappedObject.onMenuItemClick(getMenuItemWrapper(item));
         }
     }
-
-    class ActionProviderWrapper extends android.view.ActionProvider {
-        final ActionProvider mInner;
-
-        public ActionProviderWrapper(ActionProvider inner) {
-            super(inner.getContext());
-            mInner = inner;
-        }
-
-        @Override
-        @SuppressWarnings("deprecation")
-        public View onCreateActionView() {
-            return mInner.onCreateActionView(mWrappedObject);
-        }
-
-        @Override
-        public boolean onPerformDefaultAction() {
-            return mInner.onPerformDefaultAction();
-        }
-
-        @Override
-        public boolean hasSubMenu() {
-            return mInner.hasSubMenu();
-        }
-
-        @Override
-        public void onPrepareSubMenu(android.view.SubMenu subMenu) {
-            mInner.onPrepareSubMenu(getSubMenuWrapper(subMenu));
-        }
-    }
 }
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java
index d84f8fc..9277b12 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java
@@ -16,7 +16,10 @@
 
 package android.support.v7.internal.view.menu;
 
+import android.support.v4.internal.view.SupportMenuItem;
+import android.support.v4.view.ActionProvider;
 import android.view.MenuItem;
+import android.view.View;
 
 class MenuItemWrapperICS extends MenuItemWrapperHC {
     MenuItemWrapperICS(android.view.MenuItem object) {
@@ -56,6 +59,24 @@
         return this;
     }
 
+    @Override
+    public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider) {
+        mWrappedObject.setActionProvider(actionProvider != null ?
+                createActionProviderWrapper(actionProvider) : null);
+        return this;
+    }
+
+    @Override
+    public ActionProvider getSupportActionProvider() {
+        ActionProviderWrapper providerWrapper =
+                (ActionProviderWrapper)mWrappedObject.getActionProvider();
+        return providerWrapper.mInner;
+    }
+
+    ActionProviderWrapper createActionProviderWrapper(ActionProvider provider) {
+        return new ActionProviderWrapper(provider);
+    }
+
     private class OnActionExpandListenerWrapper extends BaseWrapper<MenuItem.OnActionExpandListener>
             implements android.view.MenuItem.OnActionExpandListener {
 
@@ -73,4 +94,33 @@
             return mWrappedObject.onMenuItemActionCollapse(getMenuItemWrapper(item));
         }
     }
+
+    class ActionProviderWrapper extends android.view.ActionProvider {
+        final ActionProvider mInner;
+
+        public ActionProviderWrapper(ActionProvider inner) {
+            super(inner.getContext());
+            mInner = inner;
+        }
+
+        @Override
+        public View onCreateActionView() {
+            return mInner.onCreateActionView();
+        }
+
+        @Override
+        public boolean onPerformDefaultAction() {
+            return mInner.onPerformDefaultAction();
+        }
+
+        @Override
+        public boolean hasSubMenu() {
+            return mInner.hasSubMenu();
+        }
+
+        @Override
+        public void onPrepareSubMenu(android.view.SubMenu subMenu) {
+            mInner.onPrepareSubMenu(getSubMenuWrapper(subMenu));
+        }
+    }
 }
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java
index 3288d37..eb2cc5f 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java
@@ -18,6 +18,7 @@
 
 import android.support.v4.view.ActionProvider;
 import android.view.MenuItem;
+import android.view.View;
 
 class MenuItemWrapperJB extends MenuItemWrapperICS {
     MenuItemWrapperJB(android.view.MenuItem object) {
@@ -25,14 +26,8 @@
     }
 
     @Override
-    public MenuItem setActionProvider(android.view.ActionProvider provider) {
-        mWrappedObject.setActionProvider(provider);
-        return this;
-    }
-
-    @Override
-    public android.view.ActionProvider getActionProvider() {
-        return mWrappedObject.getActionProvider();
+    ActionProviderWrapper createActionProviderWrapper(ActionProvider provider) {
+        return new ActionProviderWrapperJB(provider);
     }
 
     class ActionProviderWrapperJB extends ActionProviderWrapper
@@ -44,6 +39,11 @@
         }
 
         @Override
+        public View onCreateActionView(MenuItem forItem) {
+            return mInner.onCreateActionView(forItem);
+        }
+
+        @Override
         public boolean overridesItemVisibility() {
             return mInner.overridesItemVisibility();
         }
diff --git a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java
index 25776d7..c08615d 100644
--- a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java
+++ b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java
@@ -80,20 +80,76 @@
     }
 
     public void setPrimaryBackground(Drawable bg) {
+        if (mBackground != null) {
+            mBackground.setCallback(null);
+            unscheduleDrawable(mBackground);
+        }
         mBackground = bg;
+        if (bg != null) {
+            bg.setCallback(this);
+        }
+        setWillNotDraw(mIsSplit ? mSplitBackground == null :
+                mBackground == null && mStackedBackground == null);
         invalidate();
     }
 
     public void setStackedBackground(Drawable bg) {
+        if (mStackedBackground != null) {
+            mStackedBackground.setCallback(null);
+            unscheduleDrawable(mStackedBackground);
+        }
         mStackedBackground = bg;
+        if (bg != null) {
+            bg.setCallback(this);
+        }
+        setWillNotDraw(mIsSplit ? mSplitBackground == null :
+                mBackground == null && mStackedBackground == null);
         invalidate();
     }
 
     public void setSplitBackground(Drawable bg) {
+        if (mSplitBackground != null) {
+            mSplitBackground.setCallback(null);
+            unscheduleDrawable(mSplitBackground);
+        }
         mSplitBackground = bg;
+        if (bg != null) {
+            bg.setCallback(this);
+        }
+        setWillNotDraw(mIsSplit ? mSplitBackground == null :
+                mBackground == null && mStackedBackground == null);
         invalidate();
     }
 
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        final boolean isVisible = visibility == VISIBLE;
+        if (mBackground != null) mBackground.setVisible(isVisible, false);
+        if (mStackedBackground != null) mStackedBackground.setVisible(isVisible, false);
+        if (mSplitBackground != null) mSplitBackground.setVisible(isVisible, false);
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return (who == mBackground && !mIsSplit) || (who == mStackedBackground && mIsStacked) ||
+                (who == mSplitBackground && mIsSplit) || super.verifyDrawable(who);
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        if (mBackground != null && mBackground.isStateful()) {
+            mBackground.setState(getDrawableState());
+        }
+        if (mStackedBackground != null && mStackedBackground.isStateful()) {
+            mStackedBackground.setState(getDrawableState());
+        }
+        if (mSplitBackground != null && mSplitBackground.isStateful()) {
+            mSplitBackground.setState(getDrawableState());
+        }
+    }
+
     /**
      * Set the action bar into a "transitioning" state. While transitioning the bar will block focus
      * and touch from all of its descendants. This prevents the user from interacting with the bar
diff --git a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java
index d30e2ad..8232081 100644
--- a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java
+++ b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java
@@ -1191,6 +1191,14 @@
         }
     }
 
+    public void setHomeAsUpIndicator(Drawable indicator) {
+        mHomeLayout.setUpIndicator(indicator);
+    }
+
+    public void setHomeAsUpIndicator(int resId) {
+        mHomeLayout.setUpIndicator(resId);
+    }
+
     static class SavedState extends BaseSavedState {
 
         int expandedMenuItemId;
@@ -1226,12 +1234,11 @@
     }
 
     private static class HomeView extends FrameLayout {
-
-        private View mUpView;
-
+        private ImageView mUpView;
         private ImageView mIconView;
-
         private int mUpWidth;
+        private int mUpIndicatorRes;
+        private Drawable mDefaultUpIndicator;
 
         public HomeView(Context context) {
             this(context, null);
@@ -1249,6 +1256,25 @@
             mIconView.setImageDrawable(icon);
         }
 
+        public void setUpIndicator(Drawable d) {
+            mUpView.setImageDrawable(d != null ? d : mDefaultUpIndicator);
+            mUpIndicatorRes = 0;
+        }
+
+        public void setUpIndicator(int resId) {
+            mUpIndicatorRes = resId;
+            mUpView.setImageDrawable(resId != 0 ? getResources().getDrawable(resId) : null);
+        }
+
+        @Override
+        protected void onConfigurationChanged(Configuration newConfig) {
+            super.onConfigurationChanged(newConfig);
+            if (mUpIndicatorRes != 0) {
+                // Reload for config change
+                setUpIndicator(mUpIndicatorRes);
+            }
+        }
+
         @Override
         public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
             final CharSequence cdesc = getContentDescription();
@@ -1260,8 +1286,9 @@
 
         @Override
         protected void onFinishInflate() {
-            mUpView = findViewById(R.id.up);
+            mUpView = (ImageView) findViewById(R.id.up);
             mIconView = (ImageView) findViewById(R.id.home);
+            mDefaultUpIndicator = mUpView.getDrawable();
         }
 
         public int getLeftOffset() {
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..f3c2966 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,25 @@
     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 CharSequence getDescription(Object routeObj) {
+            return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription();
+        }
+
+        public static boolean isConnecting(Object routeObj) {
+            return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting();
+        }
+    }
+
+    public static final class UserRouteInfo {
+        public static void setDescription(Object routeObj, CharSequence description) {
+            ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description);
+        }
+    }
 }
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/mr_ic_audio_vol.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_audio_vol.png
new file mode 100644
index 0000000..6ea2693
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..b47d666
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..03b0d2a
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..13d803c
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..3ae436b
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..24824fc
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..af3819b
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..83dc251
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..8d9d592
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..1310ec9
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..1705074
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..7027b88
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..7027b88
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_audio_vol.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_audio_vol.png
new file mode 100644
index 0000000..c32fdbc
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..fa22d82
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..a686cd1
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..6764598
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..94e0bb6
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..5ce2f20
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..5105e90
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..68c06ed
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..6e9b144
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..45dc56f
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..46e743a
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..e384691
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..e384691
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_audio_vol.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_audio_vol.png
new file mode 100644
index 0000000..4e2e20e
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..1d48e12
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..2c8d1ec
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..00b2043
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..ce1d939
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..3064b46
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..4316686
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..25c4e31
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..8e32bd2
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..aeaa78f
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..85277fa
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..b01dbe8
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..c19a2ad
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_dark.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_dark.xml
new file mode 100644
index 0000000..6b27536
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_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/mr_ic_media_route_on_0_holo_dark" android:duration="500" />
+    <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_dark" android:duration="500" />
+    <item android:drawable="@drawable/mr_ic_media_route_on_2_holo_dark" android:duration="500" />
+    <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_dark" android:duration="500" />
+</animation-list>
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_light.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_light.xml
new file mode 100644
index 0000000..aaa6473
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_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/mr_ic_media_route_on_0_holo_light" android:duration="500" />
+    <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_light" android:duration="500" />
+    <item android:drawable="@drawable/mr_ic_media_route_on_2_holo_light" android:duration="500" />
+    <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_light" android:duration="500" />
+</animation-list>
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_holo_dark.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_holo_dark.xml
new file mode 100644
index 0000000..6870591
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_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/mr_ic_media_route_on_holo_dark" />
+    <item android:state_checkable="true" android:state_enabled="true"
+            android:drawable="@drawable/mr_ic_media_route_connecting_holo_dark" />
+    <item android:state_enabled="true"
+            android:drawable="@drawable/mr_ic_media_route_off_holo_dark" />
+    <item android:drawable="@drawable/mr_ic_media_route_disabled_holo_dark" />
+</selector>
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_holo_light.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_holo_light.xml
new file mode 100644
index 0000000..0e4a065
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_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/mr_ic_media_route_on_holo_light" />
+    <item android:state_checkable="true" android:state_enabled="true"
+            android:drawable="@drawable/mr_ic_media_route_connecting_holo_light" />
+    <item android:state_enabled="true"
+            android:drawable="@drawable/mr_ic_media_route_off_holo_light" />
+    <item android:drawable="@drawable/mr_ic_media_route_disabled_holo_light" />
+</selector>
diff --git a/v7/mediarouter/res/layout-v17/mr_media_route_list_item.xml b/v7/mediarouter/res/layout-v17/mr_media_route_list_item.xml
new file mode 100644
index 0000000..1b798ee
--- /dev/null
+++ b/v7/mediarouter/res/layout-v17/mr_media_route_list_item.xml
@@ -0,0 +1,48 @@
+<?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">
+
+    <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/mr_media_route_chooser_dialog.xml b/v7/mediarouter/res/layout/mr_media_route_chooser_dialog.xml
new file mode 100644
index 0000000..d853b37
--- /dev/null
+++ b/v7/mediarouter/res/layout/mr_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/mr_media_route_controller_dialog.xml b/v7/mediarouter/res/layout/mr_media_route_controller_dialog.xml
new file mode 100644
index 0000000..c17321b
--- /dev/null
+++ b/v7/mediarouter/res/layout/mr_media_route_controller_dialog.xml
@@ -0,0 +1,66 @@
+<?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/mr_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"
+            style="?android:attr/buttonBarButtonStyle"
+            android:gravity="center"
+            android:text="@string/mr_media_route_controller_disconnect" />
+</LinearLayout>
diff --git a/v7/mediarouter/res/layout/mr_media_route_list_item.xml b/v7/mediarouter/res/layout/mr_media_route_list_item.xml
new file mode 100644
index 0000000..6c63a12
--- /dev/null
+++ b/v7/mediarouter/res/layout/mr_media_route_list_item.xml
@@ -0,0 +1,48 @@
+<?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">
+
+    <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..6dd71e1 100644
--- a/v7/mediarouter/res/values/strings.xml
+++ b/v7/mediarouter/res/values/strings.xml
@@ -16,8 +16,18 @@
 
 <resources>
     <!-- Name for the default system route prior to Jellybean. [CHAR LIMIT=30] -->
-    <string name="system_route_name">System</string>
+    <string name="mr_system_route_name">System</string>
 
     <!-- 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>
+    <string name="mr_user_route_category_name">Devices</string>
+
+    <!-- Content description of a MediaRouteButton for accessibility support. [CHAR LIMIT=50] -->
+    <string name="mr_media_route_button_content_description">Media output</string>
+
+    <!-- Title of the media route chooser dialog. [CHAR LIMIT=30] -->
+    <string name="mr_media_route_chooser_title">Connect to device</string>
+
+    <!-- Button to disconnect from a media route.  [CHAR LIMIT=30] -->
+    <string name="mr_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..981ff15
--- /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/mr_media_route_button_content_description</item>
+        <item name="externalRouteEnabledDrawable">@drawable/mr_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/mr_media_route_button_content_description</item>
+        <item name="externalRouteEnabledDrawable">@drawable/mr_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..879188d
--- /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/mr_ic_media_route_off_holo_dark</item>
+        <item name="mediaRouteConnectingDrawable">@drawable/mr_ic_media_route_connecting_holo_dark</item>
+        <item name="mediaRouteOnDrawable">@drawable/mr_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/mr_ic_media_route_off_holo_light</item>
+        <item name="mediaRouteConnectingDrawable">@drawable/mr_ic_media_route_connecting_holo_light</item>
+        <item name="mediaRouteOnDrawable">@drawable/mr_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..bcf1dab
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
@@ -0,0 +1,237 @@
+/*
+ * 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.text.TextUtils;
+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.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 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();
+    }
+
+    /**
+     * 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.mr_media_route_chooser_dialog);
+        setTitle(R.string.mr_media_route_chooser_title);
+
+        // Must be called after setContentView.
+        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
+                MediaRouterThemeHelper.getThemeResource(
+                        getContext(), R.attr.mediaRouteOffDrawable));
+
+        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.mr_media_route_list_item, parent, false);
+            }
+            MediaRouter.RouteInfo route = getItem(position);
+            TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+            TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+            text1.setText(route.getName());
+            String description = route.getDescription();
+            if (TextUtils.isEmpty(description)) {
+                text2.setVisibility(View.GONE);
+                text2.setText("");
+            } else {
+                text2.setVisibility(View.VISIBLE);
+                text2.setText(description);
+            }
+            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..c0a0c05
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -0,0 +1,273 @@
+/*
+ * 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.graphics.drawable.Drawable;
+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 Drawable mMediaRouteConnectingDrawable;
+    private Drawable mMediaRouteOnDrawable;
+    private Drawable mCurrentIconDrawable;
+
+    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();
+    }
+
+    /**
+     * 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.mr_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();
+
+        Drawable icon = getIconDrawable();
+        if (icon != mCurrentIconDrawable) {
+            mCurrentIconDrawable = icon;
+
+            // There seems to be a bug in the framework where feature drawables
+            // will not start animating unless they experience a transition from
+            // invisible to visible.  So we force the drawable to be invisible here.
+            // The window will make the drawable visible when attached.
+            icon.setVisible(false, true);
+            getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, icon);
+        }
+        return true;
+    }
+
+    private Drawable getIconDrawable() {
+        if (mRoute.isConnecting()) {
+            if (mMediaRouteConnectingDrawable == null) {
+                mMediaRouteConnectingDrawable = MediaRouterThemeHelper.getThemeDrawable(
+                        getContext(), R.attr.mediaRouteConnectingDrawable);
+            }
+            return mMediaRouteConnectingDrawable;
+        } else {
+            if (mMediaRouteOnDrawable == null) {
+                mMediaRouteOnDrawable = MediaRouterThemeHelper.getThemeDrawable(
+                        getContext(), R.attr.mediaRouteOnDrawable);
+            }
+            return mMediaRouteOnDrawable;
+        }
+    }
+
+    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..9b35da3
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.graphics.drawable.Drawable;
+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;
+    }
+
+    public static Drawable getThemeDrawable(Context context, int attr) {
+        int res = getThemeResource(context, attr);
+        return res != 0 ? context.getResources().getDrawable(res) : null;
+    }
+}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java b/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java
index 5d2711c..27a2c27 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java
@@ -66,8 +66,7 @@
  * A media item id is an opaque token that represents the playback request.
  * The application must supply the media item id when sending control requests to
  * {@link #ACTION_PAUSE pause}, {@link #ACTION_RESUME resume}, {@link #ACTION_SEEK seek},
- * {@link #ACTION_GET_STATUS get status}, {@link #ACTION_GET_PROGRESS get progress},
- * or perform other actions to affect playback.
+ * {@link #ACTION_GET_STATUS get status}, or perform other actions to affect playback.
  * </p><p>
  * Each remote playback action is bound to a specific media item.  If a
  * media item has finished, been canceled or encountered an error, then most
@@ -171,16 +170,16 @@
      * If the data uri specifies an HTTP or HTTPS scheme, then the destination is
      * responsible for following HTTP redirects to a reasonable depth of at least 3
      * levels as might typically be handled by a web browser.  If an HTTP error
-     * occurs, then the destination should send a status update back to the client
-     * indicating the {@link MediaItemStatus#PLAYBACK_STATE_ERROR error}
-     * {@link MediaItemStatus#KEY_PLAYBACK_STATE state}
-     * and include the {@link MediaItemStatus#KEY_HTTP_STATUS_CODE HTTP status code}.
+     * occurs, then the destination should send a {@link MediaItemStatus status update}
+     * back to the client indicating the {@link MediaItemStatus#PLAYBACK_STATE_ERROR error}
+     * {@link MediaItemStatus#getPlaybackState() playback state}
+     * and include the {@link MediaItemStatus#getHttpStatusCode() HTTP status code}.
      * </p>
      *
      * <h3>Request parameters</h3>
      * <ul>
      * <li>{@link #EXTRA_ITEM_QUEUE_BEHAVIOR}: specifies when the content should be played.
-     * <li>{@link #EXTRA_ITEM_POSITION}: specifies the initial start position of the content.
+     * <li>{@link #EXTRA_ITEM_CONTENT_POSITION}: specifies the initial content playback position.
      * <li>{@link #EXTRA_ITEM_METADATA}: specifies metadata associated with the
      * content such as the title of a song.
      * <li>{@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER}: specifies a {@link PendingIntent}
@@ -193,13 +192,20 @@
      * <ul>
      * <li>{@link #EXTRA_ITEM_ID}: specifies an opaque string identifier to use to refer
      * to the media item in subsequent requests such as {@link #ACTION_PAUSE}.
+     * <li>{@link #EXTRA_ITEM_STATUS}: specifies the initial status of the item
+     * that has been enqueued.
      * </ul>
      *
      * <h3>Status updates</h3>
      * <p>
      * If the client supplies a {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER status update receiver}
      * then the media route provider is responsible for sending status updates to the receiver
-     * when significant media item state changes occur.
+     * when significant media item state changes occur such as when playback starts or
+     * stops.  The receiver will not be invoked for content playback position changes.
+     * The application may retrieve the current playback position when necessary
+     * using the {@link #ACTION_GET_STATUS} request.
+     * </p><p>
+     * Refer to {@link MediaItemStatus} for details.
      * </p>
      *
      * <h3>Example</h3>
@@ -230,7 +236,6 @@
      * @see #ACTION_STOP
      * @see #ACTION_PAUSE
      * @see #ACTION_RESUME
-     * @see #ACTION_GET_PROGRESS
      * @see #ACTION_GET_STATUS
      */
     public static final String ACTION_PLAY = "android.media.intent.action.PLAY";
@@ -257,12 +262,12 @@
      * <li>{@link #EXTRA_ITEM_ID}: specifies the media item id of the playback to be
      * controlled.  This value was returned as a result from the
      * {@link #ACTION_PLAY play} action.
-     * <li>{@link #EXTRA_ITEM_POSITION}: specifies the new position of the content.
+     * <li>{@link #EXTRA_ITEM_CONTENT_POSITION}: specifies the new position of the content.
      * </ul>
      *
      * <h3>Result data</h3>
      * <ul>
-     * <li>(none)
+     * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
      * </ul>
      *
      * @see MediaRouter.RouteInfo#sendControlRequest
@@ -292,7 +297,7 @@
      *
      * <h3>Result data</h3>
      * <ul>
-     * <li>(none)
+     * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
      * </ul>
      *
      * @see MediaRouter.RouteInfo#sendControlRequest
@@ -323,7 +328,7 @@
      *
      * <h3>Result data</h3>
      * <ul>
-     * <li>(none)
+     * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
      * </ul>
      *
      * @see MediaRouter.RouteInfo#sendControlRequest
@@ -351,7 +356,7 @@
      *
      * <h3>Result data</h3>
      * <ul>
-     * <li>(none)
+     * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
      * </ul>
      *
      * @see MediaRouter.RouteInfo#sendControlRequest
@@ -361,13 +366,13 @@
     public static final String ACTION_RESUME = "android.media.intent.action.RESUME";
 
     /**
-     * Media control action: Get media item status.
+     * Media control action: Get media item playback status and progress information.
      * <p>
      * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
      * media control.
      * </p><p>
-     * This action asks a remote playback route to provide updated status information
-     * about playback of the specified media item.
+     * This action asks a remote playback route to provide updated playback status and progress
+     * information about the specified media item.
      * </p>
      *
      * <h3>Request parameters</h3>
@@ -389,35 +394,6 @@
     public static final String ACTION_GET_STATUS = "android.media.intent.action.GET_STATUS";
 
     /**
-     * Media control action: Get media item progress.
-     * <p>
-     * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
-     * media control.
-     * </p><p>
-     * This action asks a remote playback route to provide media item playback
-     * progress information.  The client may use this information to provide feedback
-     * to the user about the current playback position and overall duration.
-     * </p>
-     *
-     * <h3>Request parameters</h3>
-     * <ul>
-     * <li>{@link #EXTRA_ITEM_ID}: specifies the media item id of the playback to be
-     * controlled.  This value was returned as a result from the
-     * {@link #ACTION_PLAY play} action.
-     * </ul>
-     *
-     * <h3>Result data</h3>
-     * <ul>
-     * <li>{@link #EXTRA_ITEM_POSITION}: specifies the content playback position.
-     * <li>{@link #EXTRA_ITEM_DURATION}: specifies the overall duration of the content.
-     * </ul>
-     *
-     * @see MediaRouter.RouteInfo#sendControlRequest
-     * @see #CATEGORY_REMOTE_PLAYBACK
-     */
-    public static final String ACTION_GET_PROGRESS = "android.media.intent.action.GET_PROGRESS";
-
-    /**
      * Integer extra: Media item queue behavior.
      * <p>
      * Used with {@link #ACTION_PLAY} to specify when the requested  should be
@@ -473,39 +449,23 @@
     public static final int ITEM_QUEUE_BEHAVIOR_PLAY_LATER = 2;
 
     /**
-     * Integer extra: Media item content position.
+     * Double extra: Media item content position.
      * <p>
      * Used with {@link #ACTION_PLAY} to specify the starting playback position.
      * </p><p>
      * Used with {@link #ACTION_SEEK} to set a new playback position.
      * </p><p>
-     * Used with {@link #ACTION_GET_PROGRESS} to report the current playback position.
-     * </p><p>
-     * The value is an integer number of seconds from the beginning of the content.
+     * The value is a double-precision floating point number of seconds
+     * from the beginning of the content.
      * <p>
      *
      * @see #ACTION_PLAY
      * @see #ACTION_SEEK
-     * @see #ACTION_GET_PROGRESS
      */
-    public static final String EXTRA_ITEM_POSITION =
+    public static final String EXTRA_ITEM_CONTENT_POSITION =
             "android.media.intent.extra.ITEM_POSITION";
 
     /**
-     * Integer extra: Media item content duration.
-     * <p>
-     * Used with {@link #ACTION_GET_PROGRESS} to report the overall duration of the
-     * media item content.
-     * </p><p>
-     * The value is an integer number of seconds from the beginning of the content.
-     * <p>
-     *
-     * @see #ACTION_GET_PROGRESS
-     */
-    public static final String EXTRA_ITEM_DURATION =
-            "android.media.intent.extra.ITEM_DURATION";
-
-    /**
      * Bundle extra: Media item metadata.
      * <p>
      * Used with {@link #ACTION_PLAY} to specify metadata associated with the content
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java b/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java
index 6458ae2..2b8bfff 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java
@@ -16,11 +16,18 @@
 
 package android.support.v7.media;
 
-import android.graphics.Bitmap;
 import android.os.Bundle;
 
 /**
  * Constants for specifying metadata about a media item as a {@link Bundle}.
+ * <p>
+ * Media item metadata is described as a bundle of key/value pairs as defined
+ * in this class.  The documentation specifies the type of value associated
+ * with each key.
+ * </p><p>
+ * An application may specify additional custom metadata keys but there is no guarantee
+ * that they will be recognized by the destination.
+ * </p>
  */
 public final class MediaItemMetadata {
     /*
@@ -33,99 +40,83 @@
     }
 
     /**
-     * Metadata key: Album artist name.
+     * String key: Album artist name.
      * <p>
      * The value is a string suitable for display.
      * </p>
      */
-    public static final String KEY_ALBUM_ARTIST = "ALBUM_ARTIST";
+    public static final String KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
 
     /**
-     * Metadata key: Album title.
+     * String key: Album title.
      * <p>
      * The value is a string suitable for display.
      * </p>
      */
-    public static final String KEY_ALBUM_TITLE = "ALBUM_TITLE";
+    public static final String KEY_ALBUM_TITLE = "android.media.metadata.ALBUM_TITLE";
 
     /**
-     * Metadata key: Artwork Uri.
+     * String key: Artwork Uri.
      * <p>
      * The value is a string URI for an image file associated with the media item,
      * such as album or cover art.
      * </p>
-     *
-     * @see #KEY_ARTWORK_BITMAP
      */
-    public static final String KEY_ARTWORK_URI = "ARTWORK_URI";
+    public static final String KEY_ARTWORK_URI = "android.media.metadata.ARTWORK_URI";
 
     /**
-     * Metadata key: Artwork Bitmap.
-     * <p>
-     * The value is a {@link Bitmap} for an image file associated with the media item,
-     * such as album or cover art.
-     * </p><p>
-     * Because bitmaps can be large, use {@link #KEY_ARTWORK_URI} instead if the artwork can
-     * be downloaded from the network.
-     * </p>
-     *
-     * @see #KEY_ARTWORK_URI
-     */
-    public static final String KEY_ARTWORK_BITMAP = "ARTWORK_BITMAP";
-
-    /**
-     * Metadata key: Artist name.
+     * String key: Artist name.
      * <p>
      * The value is a string suitable for display.
      * </p>
      */
-    public static final String KEY_ARTIST = "ARTIST";
+    public static final String KEY_ARTIST = "android.media.metadata.ARTIST";
 
     /**
-     * Metadata key: Author name.
+     * String key: Author name.
      * <p>
      * The value is a string suitable for display.
      * </p>
      */
-    public static final String KEY_AUTHOR = "AUTHOR";
+    public static final String KEY_AUTHOR = "android.media.metadata.AUTHOR";
 
     /**
-     * Metadata key: Composer name.
+     * String key: Composer name.
      * <p>
      * The value is a string suitable for display.
      * </p>
      */
-    public static final String KEY_COMPOSER = "COMPOSER";
+    public static final String KEY_COMPOSER = "android.media.metadata.COMPOSER";
 
     /**
-     * Metadata key: Track title.
+     * String key: Track title.
      * <p>
      * The value is a string suitable for display.
      * </p>
      */
-    public static final String KEY_TITLE = "TITLE";
+    public static final String KEY_TITLE = "android.media.metadata.TITLE";
 
     /**
-     * Metadata key: Year of publication.
+     * Integer key: Year of publication.
      * <p>
      * The value is an integer year number.
      * </p>
      */
-    public static final String KEY_YEAR = "YEAR";
+    public static final String KEY_YEAR = "android.media.metadata.YEAR";
 
     /**
-     * Metadata key: Track number (such as a track on a CD).
+     * Integer key: Track number (such as a track on a CD).
      * <p>
      * The value is a one-based integer track number.
      * </p>
      */
-    public static final String KEY_TRACK_NUMBER = "TRACK_NUMBER";
+    public static final String KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
 
     /**
-     * Metadata key: Disc number within a collection.
+     * Integer key: Disc number within a collection.
      * <p>
      * The value is a one-based integer disc number.
      * </p>
      */
-    public static final String KEY_DISC_NUMBER = "DISC_NUMBER";
+    public static final String KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
 }
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java b/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java
index 4a53113..fe49b30 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java
@@ -16,26 +16,52 @@
 
 package android.support.v7.media;
 
+import android.app.PendingIntent;
 import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.util.TimeUtils;
 
 /**
- * Constants for specifying status about a media item as a {@link Bundle}.
+ * Describes the playback status of a media item.
+ * <p>
+ * As a media item is played, it transitions through a sequence of states including:
+ * {@link #PLAYBACK_STATE_QUEUED queued}, {@link #PLAYBACK_STATE_BUFFERING buffering},
+ * {@link #PLAYBACK_STATE_PLAYING playing}, {@link #PLAYBACK_STATE_PAUSED paused},
+ * {@link #PLAYBACK_STATE_STOPPED stopped}, {@link #PLAYBACK_STATE_CANCELED canceled},
+ * {@link #PLAYBACK_STATE_ERROR error}.  Refer to the documentation of each state
+ * for an explanation of its meaning.
+ * </p><p>
+ * While the item is playing, the playback status may also include progress information
+ * about the {@link #getContentPosition content position} and
+ * {@link #getContentDuration content duration} although not all route destinations
+ * will report it.
+ * </p><p>
+ * To monitor playback status, the application should supply a {@link PendingIntent} to use
+ * as the {@link MediaControlIntent#EXTRA_ITEM_STATUS_UPDATE_RECEIVER status update receiver}
+ * for a given {@link MediaControlIntent#ACTION_PLAY playback request}.  Note that
+ * the status update receiver will only be invoked for major status changes such as a
+ * transition from playing to stopped.
+ * </p><p class="note">
+ * The status update receiver will not be invoked for minor progress updates such as
+ * changes to playback position or duration.  If the application wants to monitor
+ * playback progress, then it must use the
+ * {@link MediaControlIntent#ACTION_GET_STATUS get status request} to poll for changes
+ * periodically and estimate the playback position while playing.  Note that there may
+ * be a significant power impact to polling so the application is advised only
+ * to poll when the screen is on and never more than about once every 5 seconds or so.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
  */
 public final class MediaItemStatus {
-    private MediaItemStatus() {
-    }
+    private static final String KEY_TIMESTAMP = "timestamp";
+    private static final String KEY_PLAYBACK_STATE = "playbackState";
+    private static final String KEY_CONTENT_POSITION = "contentPosition";
+    private static final String KEY_CONTENT_DURATION = "contentDuration";
+    private static final String KEY_HTTP_STATUS_CODE = "httpStatusCode";
+    private static final String KEY_EXTRAS = "extras";
 
-    /**
-     * Status key: Playback state.
-     * <p>
-     * Specifies the playback state of a media item.
-     * </p><p>
-     * Value is one of {@link #PLAYBACK_STATE_QUEUED}, {@link #PLAYBACK_STATE_PLAYING},
-     * {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_BUFFERING},
-     * {@link #PLAYBACK_STATE_CANCELED}, or {@link #PLAYBACK_STATE_ERROR}.
-     * </p>
-     */
-    public static final String KEY_PLAYBACK_STATE = "PLAYBACK_STATE";
+    private final Bundle mBundle;
 
     /**
      * Playback state: Queued.
@@ -113,18 +139,208 @@
      */
     public static final int PLAYBACK_STATE_ERROR = 6;
 
+    private MediaItemStatus(Bundle bundle) {
+        mBundle = bundle;
+    }
+
     /**
-     * Status key: HTTP status code.
+     * Gets the timestamp associated with the status information in
+     * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+     *
+     * @return The status timestamp in the {@link SystemClock#elapsedRealtime()} time base.
+     */
+    public long getTimestamp() {
+        return mBundle.getLong(KEY_TIMESTAMP);
+    }
+
+    /**
+     * Gets the playback state of the media item.
+     *
+     * @return The playback state.  One of {@link #PLAYBACK_STATE_QUEUED},
+     * {@link #PLAYBACK_STATE_PLAYING},
+     * {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_BUFFERING},
+     * {@link #PLAYBACK_STATE_CANCELED}, or {@link #PLAYBACK_STATE_ERROR}.
+     */
+    public int getPlaybackState() {
+        return mBundle.getInt(KEY_PLAYBACK_STATE, PLAYBACK_STATE_CANCELED);
+    }
+
+    /**
+     * Gets the content playback position as a floating point number of seconds
+     * from the beginning of the content.
+     *
+     * @return The content playback position in seconds, or -1 if unknown.
+     */
+    public double getContentPosition() {
+        return mBundle.getDouble(KEY_CONTENT_POSITION, -1);
+    }
+
+    /**
+     * Gets the total duration of the content to be played as a floating point number
+     * of seconds.
+     *
+     * @return The content duration in seconds, or -1 if unknown.
+     */
+    public double getContentDuration() {
+        return mBundle.getDouble(KEY_CONTENT_DURATION, -1);
+    }
+
+    /**
+     * Gets the associated HTTP status code.
      * <p>
      * Specifies the HTTP status code that was encountered when the content
      * was requested after all redirects were followed.  This key only needs to
      * specified when the content uri uses the HTTP or HTTPS scheme and an error
      * occurred.  This key may be omitted if the content was able to be played
      * successfully; there is no need to report a 200 (OK) status code.
-     * </p><p>
-     * Value is an integer HTTP status code such as 401 (Unauthorized), 404 (Not Found),
-     * or 500 (Server Error).
      * </p>
+     *
+     * @return The HTTP status code from playback such as 401 (Unauthorized), 404 (Not Found),
+     * or 500 (Server Error), or 0 if none.
      */
-    public static final String KEY_HTTP_STATUS_CODE = "HTTP_STATUS_CODE";
+    public int getHttpStatusCode() {
+        return mBundle.getInt(KEY_HTTP_STATUS_CODE, 0);
+    }
+
+    /**
+     * Gets a bundle of extras for this status object.
+     * The extras will be ignored by the media router but they may be used
+     * by applications.
+     */
+    public Bundle getExtras() {
+        return mBundle.getBundle(KEY_EXTRAS);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append("MediaItemStatus{ ");
+        result.append("timestamp=");
+        TimeUtils.formatDuration(SystemClock.elapsedRealtime() - getTimestamp(), result);
+        result.append(" ms ago");
+        result.append(", playbackState=").append(getPlaybackState());
+        result.append(", contentPosition=").append(getContentPosition());
+        result.append(", contentDuration=").append(getContentDuration());
+        result.append(", httpStatusCode=").append(getHttpStatusCode());
+        result.append(", extras=").append(getExtras());
+        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 MediaItemStatus fromBundle(Bundle bundle) {
+        return bundle != null ? new MediaItemStatus(bundle) : null;
+    }
+
+    /**
+     * Builder for {@link MediaItemStatus media item status objects}.
+     */
+    public static final class Builder {
+        private final Bundle mBundle;
+
+        /**
+         * Creates a media item status builder using the current time as the
+         * reference timestamp.
+         *
+         * @param playbackState The item playback state.
+         */
+        public Builder(int playbackState) {
+            mBundle = new Bundle();
+            setTimestamp(SystemClock.elapsedRealtime());
+            setPlaybackState(playbackState);
+        }
+
+        /**
+         * Creates a media item status builder whose initial contents are
+         * copied from an existing status.
+         */
+        public Builder(MediaItemStatus status) {
+            if (status == null) {
+                throw new IllegalArgumentException("status must not be null");
+            }
+
+            mBundle = new Bundle(status.mBundle);
+        }
+
+        /**
+         * Sets the timestamp associated with the status information in
+         * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+         */
+        public Builder setTimestamp(long elapsedRealtimeTimestamp) {
+            mBundle.putLong(KEY_TIMESTAMP, elapsedRealtimeTimestamp);
+            return this;
+        }
+
+        /**
+         * Sets the playback state of the media item.
+         */
+        public Builder setPlaybackState(int playbackState) {
+            mBundle.putInt(KEY_PLAYBACK_STATE, playbackState);
+            return this;
+        }
+
+        /**
+         * Sets the content playback position as a floating point number of seconds
+         * from the beginning of the content.
+         */
+        public Builder setContentPosition(double positionSeconds) {
+            mBundle.putDouble(KEY_CONTENT_POSITION, positionSeconds);
+            return this;
+        }
+
+        /**
+         * Sets the total duration of the content to be played as a floating point number
+         * of seconds.
+         */
+        public Builder setContentDuration(double durationSeconds) {
+            mBundle.putDouble(KEY_CONTENT_DURATION, durationSeconds);
+            return this;
+        }
+
+        /**
+         * Sets the associated HTTP status code.
+         * <p>
+         * Specifies the HTTP status code that was encountered when the content
+         * was requested after all redirects were followed.  This key only needs to
+         * specified when the content uri uses the HTTP or HTTPS scheme and an error
+         * occurred.  This key may be omitted if the content was able to be played
+         * successfully; there is no need to report a 200 (OK) status code.
+         * </p>
+         */
+        public Builder setHttpStatusCode(int httpStatusCode) {
+            mBundle.putInt(KEY_HTTP_STATUS_CODE, httpStatusCode);
+            return this;
+        }
+
+        /**
+         * Sets a bundle of extras for this status object.
+         * 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 MediaItemStatus media item status object}.
+         */
+        public MediaItemStatus build() {
+            return new MediaItemStatus(mBundle);
+        }
+    }
 }
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..af267c0
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteDescriptor.java
@@ -0,0 +1,416 @@
+/*
+ * 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 android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+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_DESCRIPTION = "status";
+    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 List<IntentFilter> mControlFilters;
+
+    private MediaRouteDescriptor(Bundle bundle, List<IntentFilter> controlFilters) {
+        mBundle = bundle;
+        mControlFilters = controlFilters;
+    }
+
+    /**
+     * Gets the unique id of the route.
+     */
+    public String getId() {
+        return mBundle.getString(KEY_ID);
+    }
+
+    /**
+     * Gets the user-visible name of the route.
+     * <p>
+     * The route name identifies the destination represented by the route.
+     * It may be a user-supplied name, an alias, or device serial number.
+     * </p>
+     */
+    public String getName() {
+        return mBundle.getString(KEY_NAME);
+    }
+
+    /**
+     * Gets the user-visible description of the route.
+     * <p>
+     * The route description describes the kind of destination represented by the route.
+     * It may be a user-supplied string, a model number or brand of device.
+     * </p>
+     */
+    public String getDescription() {
+        return mBundle.getString(KEY_DESCRIPTION);
+    }
+
+    /**
+     * 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(", description=").append(getDescription());
+        result.append(", isEnabled=").append(isEnabled());
+        result.append(", isConnecting=").append(isConnecting());
+        result.append(", controlFilters=").append(Arrays.toString(getControlFilters().toArray()));
+        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;
+    }
+
+    /**
+     * Builder for {@link MediaRouteDescriptor media route descriptors}.
+     */
+    public static final class Builder {
+        private final Bundle mBundle;
+        private ArrayList<IntentFilter> mControlFilters;
+
+        /**
+         * Creates a media route descriptor builder.
+         *
+         * @param id The unique id of the route.
+         * @param name The user-visible 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);
+
+            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-visible name of the route.
+         * <p>
+         * The route name identifies the destination represented by the route.
+         * It may be a user-supplied name, an alias, or device serial number.
+         * </p>
+         */
+        public Builder setName(String name) {
+            mBundle.putString(KEY_NAME, name);
+            return this;
+        }
+
+        /**
+         * Sets the user-visible description of the route.
+         * <p>
+         * The route description describes the kind of destination represented by the route.
+         * It may be a user-supplied string, a model number or brand of device.
+         * </p>
+         */
+        public Builder setDescription(String description) {
+            mBundle.putString(KEY_DESCRIPTION, description);
+            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(Collection<IntentFilter> filters) {
+            if (filters == null) {
+                throw new IllegalArgumentException("filters must not be null");
+            }
+
+            if (!filters.isEmpty()) {
+                for (IntentFilter filter : filters) {
+                    addControlFilter(filter);
+                }
+            }
+            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, 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..9f95d2e
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
@@ -0,0 +1,294 @@
+/*
+ * 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.Collection;
+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(Collection<MediaRouteDescriptor> routes) {
+            if (routes == null) {
+                throw new IllegalArgumentException("routes must not be null");
+            }
+
+            if (!routes.isEmpty()) {
+                for (MediaRouteDescriptor route : routes) {
+                    addRoute(route);
+                }
+            }
+            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(Collection<IntentFilter> filters) {
+            if (filters == null) {
+                throw new IllegalArgumentException("filters must not be null");
+            }
+
+            if (!filters.isEmpty()) {
+                for (IntentFilter filter : filters) {
+                    addDiscoverableControlFilter(filter);
+                }
+            }
+            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..e82d7ab 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";
@@ -195,19 +188,28 @@
     static final int SERVICE_MSG_REGISTERED = 2;
 
     /** (service v1)
-     * Route control request result.
+     * Route control request success result.
      * - arg1    : request id
-     * - arg2    : result code
      * - obj     : result data bundle, or null
      */
-    static final int SERVICE_MSG_CONTROL_RESULT = 3;
+    static final int SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED = 3;
+
+    /** (service v1)
+     * Route control request failure result.
+     * - arg1    : request id
+     * - obj     : result data bundle, or null
+     * - SERVICE_DATA_ERROR: error message
+     */
+    static final int SERVICE_MSG_CONTROL_REQUEST_FAILED = 4;
 
     /** (service v1)
      * Route provider descriptor changed.  (unsolicited event)
      * - arg1    : reserved (0)
      * - obj     : route provider descriptor bundle, or null
      */
-    static final int SERVICE_MSG_DESCRIPTOR_CHANGED = 4;
+    static final int SERVICE_MSG_DESCRIPTOR_CHANGED = 5;
+
+    static final String SERVICE_DATA_ERROR = "error";
 
     /*
      * Recognized client version numbers.  (Reserved for future use.)
@@ -277,7 +279,7 @@
                                 + ".  Service package name: " + getPackageName() + ".");
                     }
                     mProvider = provider;
-                    mProvider.addCallback(mProviderCallback);
+                    mProvider.setCallback(mProviderCallback);
                 }
             }
             if (mProvider != null) {
@@ -298,7 +300,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);
@@ -454,16 +456,37 @@
                 if (requestId != 0) {
                     callback = new MediaRouter.ControlRequestCallback() {
                         @Override
-                        public void onResult(int result, Bundle data) {
+                        public void onResult(Bundle data) {
                             if (DEBUG) {
-                                Log.d(TAG, client + ": Route control request finished"
+                                Log.d(TAG, client + ": Route control request succeeded"
                                         + ", controllerId=" + controllerId
                                         + ", intent=" + intent
-                                        + ", result=" + result + ", data=" + data);
+                                        + ", data=" + data);
                             }
                             if (findClient(messenger) >= 0) {
-                                sendReply(messenger, SERVICE_MSG_CONTROL_RESULT,
-                                        requestId, result, data, null);
+                                sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED,
+                                        requestId, 0, data, null);
+                            }
+                        }
+
+                        @Override
+                        public void onError(String error, Bundle data) {
+                            if (DEBUG) {
+                                Log.d(TAG, client + ": Route control request failed"
+                                        + ", controllerId=" + controllerId
+                                        + ", intent=" + intent
+                                        + ", error=" + error + ", data=" + data);
+                            }
+                            if (findClient(messenger) >= 0) {
+                                if (error != null) {
+                                    Bundle bundle = new Bundle();
+                                    bundle.putString(SERVICE_DATA_ERROR, error);
+                                    sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+                                            requestId, 0, data, bundle);
+                                } else {
+                                    sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+                                            requestId, 0, data, null);
+                                }
                             }
                         }
                     };
@@ -480,14 +503,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 +519,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 +532,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 +617,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 +647,7 @@
     private final class ProviderCallback extends MediaRouteProvider.Callback {
         @Override
         public void onDescriptorChanged(MediaRouteProvider provider,
-                ProviderDescriptor descriptor) {
+                MediaRouteProviderDescriptor descriptor) {
             sendDescriptorChanged(descriptor);
         }
     }
@@ -613,7 +655,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 +684,7 @@
 
             mMessenger.getBinder().unlinkToDeath(this, 0);
 
-            stopActiveScan();
+            setDiscoveryRequest(null);
         }
 
         public boolean hasMessenger(Messenger other) {
@@ -673,24 +715,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 +832,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..7dba8b8
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
@@ -0,0 +1,302 @@
+/*
+ * 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.Collection;
+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(Collection<String> categories) {
+            if (categories == null) {
+                throw new IllegalArgumentException("categories must not be null");
+            }
+
+            if (!categories.isEmpty()) {
+                for (String category : categories) {
+                    addControlCategory(category);
+                }
+            }
+            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..3dd1298 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -22,14 +22,11 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
 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 +57,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 +67,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 +232,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 +264,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 +435,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 +470,9 @@
         }
         checkCallingThread();
 
+        if (DEBUG) {
+            Log.d(TAG, "addProvider: " + providerInstance);
+        }
         sGlobal.addProvider(providerInstance);
     }
 
@@ -352,52 +490,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);
     }
 
     /**
@@ -427,10 +523,9 @@
         private final ProviderInfo mProvider;
         private final String mDescriptorId;
         private String mName;
-        private String mStatus;
-        private Drawable mIconDrawable;
-        private int mIconResource;
+        private String mDescription;
         private boolean mEnabled;
+        private boolean mConnecting;
         private final ArrayList<IntentFilter> mControlFilters = new ArrayList<IntentFilter>();
         private int mPlaybackType;
         private int mPlaybackStream;
@@ -440,7 +535,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
@@ -494,9 +589,13 @@
         }
 
         /**
-         * Gets the name of this route.
+         * Gets the user-visible name of the route.
+         * <p>
+         * The route name identifies the destination represented by the route.
+         * It may be a user-supplied name, an alias, or device serial number.
+         * </p>
          *
-         * @return The user-friendly name of a media route. This is the string presented
+         * @return The user-visible name of a media route.  This is the string presented
          * to users who may select this as the active route.
          */
         public String getName() {
@@ -504,52 +603,41 @@
         }
 
         /**
-         * Gets the status of this route.
+         * Gets the user-visible description of the route.
+         * <p>
+         * The route description describes the kind of destination represented by the route.
+         * It may be a user-supplied string, a model number or brand of device.
+         * </p>
          *
-         * @return The user-friendly status for a media route. This may include a description
-         * of the currently playing media, if available.
+         * @return The description of the route, or null if none.
          */
-        public String getStatus() {
-            return mStatus;
-        }
-
-        /**
-         * Get the icon representing this route.
-         * This icon will be used in picker UIs if available.
-         *
-         * @return The icon representing this route or null if no icon is available.
-         */
-        public Drawable getIconDrawable() {
-            checkCallingThread();
-            if (mIconDrawable == null) {
-                if (mIconResource != 0) {
-                    Resources resources = mProvider.getResources();
-                    if (resources != null) {
-                        try {
-                            mIconDrawable = resources.getDrawable(mIconResource);
-                        } catch (Resources.NotFoundException ex) {
-                            Log.w(TAG, "Unable to load media route icon drawable resource "
-                                    + "from provider.", ex);
-                        }
-                    }
-                }
-            }
-            return mIconDrawable;
+        public String getDescription() {
+            return mDescription;
         }
 
         /**
          * 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 +649,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 +682,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);
         }
 
         /**
@@ -838,8 +914,9 @@
         @Override
         public String toString() {
             return "MediaRouter.RouteInfo{ name=" + mName
-                    + ", status=" + mStatus
+                    + ", description=" + mDescription
                     + ", enabled=" + mEnabled
+                    + ", connecting=" + mConnecting
                     + ", playbackType=" + mPlaybackType
                     + ", playbackStream=" + mPlaybackStream
                     + ", volumeHandling=" + mVolumeHandling
@@ -851,7 +928,7 @@
                     + " }";
         }
 
-        int updateDescriptor(RouteDescriptor descriptor) {
+        int updateDescriptor(MediaRouteDescriptor descriptor) {
             int changes = 0;
             if (mDescriptor != descriptor) {
                 mDescriptor = descriptor;
@@ -860,30 +937,21 @@
                         mName = descriptor.getName();
                         changes |= CHANGE_GENERAL;
                     }
-                    if (!equal(mStatus, descriptor.getStatus())) {
-                        mStatus = descriptor.getStatus();
-                        changes |= CHANGE_GENERAL;
-                    }
-                    if (mIconResource != descriptor.getIconResource()) {
-                        mIconResource = descriptor.getIconResource();
-                        mIconDrawable = null;
-                        changes |= CHANGE_GENERAL;
-                    }
-                    if (mIconResource == 0
-                            && mIconDrawable != descriptor.getIconDrawable()) {
-                        mIconDrawable = descriptor.getIconDrawable();
+                    if (!equal(mDescription, descriptor.getDescription())) {
+                        mDescription = descriptor.getDescription();
                         changes |= CHANGE_GENERAL;
                     }
                     if (mEnabled != descriptor.isEnabled()) {
                         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 +988,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 +1007,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 +1045,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 +1092,17 @@
             return mResources;
         }
 
-        boolean updateDescriptor(ProviderDescriptor descriptor) {
+        boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) {
             if (mDescriptor != descriptor) {
                 mDescriptor = descriptor;
+                if (descriptor != null) {
+                    if (!mDiscoverableControlFilters.equals(
+                            descriptor.getDiscoverableControlFilters())) {
+                        mDiscoverableControlFilters.clear();
+                        mDiscoverableControlFilters.addAll(
+                                descriptor.getDiscoverableControlFilters());
+                    }
+                }
                 return true;
             }
             return false;
@@ -1030,99 +1121,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 +1225,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) {
+        }
     }
 
     /**
@@ -1221,22 +1243,39 @@
      */
     public static abstract class ControlRequestCallback {
         /**
-         * Result code: The media control action succeeded.
-         */
-        public static final int REQUEST_SUCCEEDED = 0;
-
-        /**
-         * Result code: The media control action failed.
-         */
-        public static final int REQUEST_FAILED = -1;
-
-        /**
-         * Called with the result of the media control request.
+         * Called when a media control request succeeds.
          *
-         * @param result The result code: {@link #REQUEST_SUCCEEDED}, or {@link #REQUEST_FAILED}.
-         * @param data Additional result data.  Contents depend on the media control action.
+         * @param data Result data, or null if none.
+         * Contents depend on the {@link MediaControlIntent media control action}.
          */
-        public void onResult(int result, Bundle data) {
+        public void onResult(Bundle data) {
+        }
+
+        /**
+         * Called when a media control request fails.
+         *
+         * @param error A localized error message which may be shown to the user, or null
+         * if the cause of the error is unclear.
+         * @param data Error data, or null if none.
+         * Contents depend on the {@link MediaControlIntent media control action}.
+         */
+        public void onError(String error, Bundle data) {
+        }
+    }
+
+    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);
         }
     }
 
@@ -1259,13 +1298,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;
@@ -1324,7 +1362,7 @@
                 }
             }
             if (callback != null) {
-                callback.onResult(ControlRequestCallback.REQUEST_FAILED, null);
+                callback.onError(null, null);
             }
         }
 
@@ -1383,10 +1421,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 +1501,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 +1554,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 +1576,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 +1594,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 +1628,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 +1692,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 +1711,9 @@
                     if (mSelectedRouteController != null) {
                         mSelectedRouteController.onSelect();
                     }
+                    if (DEBUG) {
+                        Log.d(TAG, "Route selected: " + mSelectedRoute);
+                    }
                     mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute);
                 }
             }
@@ -1618,7 +1735,7 @@
         private final class ProviderCallback extends MediaRouteProvider.Callback {
             @Override
             public void onDescriptorChanged(MediaRouteProvider provider,
-                    ProviderDescriptor descriptor) {
+                    MediaRouteProviderDescriptor descriptor) {
                 updateProviderDescriptor(provider, descriptor);
             }
         }
@@ -1627,15 +1744,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 +1778,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 +1806,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..b8d2f0e 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);
@@ -381,7 +376,7 @@
         private void failPendingCallbacks() {
             int count = 0;
             for (int i = 0; i < mPendingCallbacks.size(); i++) {
-                mPendingCallbacks.get(i).onResult(ControlRequestCallback.REQUEST_FAILED, null);
+                mPendingCallbacks.get(i).onError(null, null);
             }
             mPendingCallbacks.clear();
         }
@@ -394,7 +389,7 @@
             ControlRequestCallback callback = mPendingCallbacks.get(requestId);
             if (callback != null) {
                 mPendingCallbacks.remove(requestId);
-                callback.onResult(ControlRequestCallback.REQUEST_FAILED, null);
+                callback.onError(null, null);
             }
             return true;
         }
@@ -411,7 +406,7 @@
                 mPendingRegisterRequestId = 0;
                 mServiceVersion = serviceVersion;
                 onConnectionDescriptorChanged(this,
-                        ProviderDescriptor.fromBundle(descriptorBundle));
+                        MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
                 onConnectionReady(this);
                 return true;
             }
@@ -421,18 +416,27 @@
         public boolean onDescriptorChanged(Bundle descriptorBundle) {
             if (mServiceVersion != 0) {
                 onConnectionDescriptorChanged(this,
-                        ProviderDescriptor.fromBundle(descriptorBundle));
+                        MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
                 return true;
             }
             return false;
         }
 
-        public boolean onControlRequestResult(int requestId, int resultCode,
-                Bundle resultData) {
+        public boolean onControlRequestSucceeded(int requestId, Bundle data) {
             ControlRequestCallback callback = mPendingCallbacks.get(requestId);
             if (callback != null) {
                 mPendingCallbacks.remove(requestId);
-                callback.onResult(resultCode, resultData);
+                callback.onResult(data);
+                return true;
+            }
+            return false;
+        }
+
+        public boolean onControlRequestFailed(int requestId, String error, Bundle data) {
+            ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+            if (callback != null) {
+                mPendingCallbacks.remove(requestId);
+                callback.onError(error, data);
                 return true;
             }
             return false;
@@ -499,14 +503,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) {
@@ -562,14 +561,15 @@
             final int requestId = msg.arg1;
             final int arg = msg.arg2;
             final Object obj = msg.obj;
-            if (!processMessage(what, requestId, arg, obj)) {
+            final Bundle data = msg.peekData();
+            if (!processMessage(what, requestId, arg, obj, data)) {
                 if (DEBUG) {
                     Log.d(TAG, "Unhandled message from server: " + msg);
                 }
             }
         }
 
-        private boolean processMessage(int what, int requestId, int arg, Object obj) {
+        private boolean processMessage(int what, int requestId, int arg, Object obj, Bundle data) {
             Connection connection = mConnectionRef.get();
             if (connection != null) {
                 switch (what) {
@@ -593,13 +593,22 @@
                         }
                         break;
 
-                    case MediaRouteProviderService.SERVICE_MSG_CONTROL_RESULT:
+                    case MediaRouteProviderService.SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED:
                         if (obj == null || obj instanceof Bundle) {
-                            return connection.onControlRequestResult(
-                                    requestId, arg, (Bundle)obj);
+                            return connection.onControlRequestSucceeded(
+                                    requestId, (Bundle)obj);
                         }
                         break;
-                }
+
+                    case MediaRouteProviderService.SERVICE_MSG_CONTROL_REQUEST_FAILED:
+                        if (obj == null || obj instanceof Bundle) {
+                            String error =
+                                    data.getString(MediaRouteProviderService.SERVICE_DATA_ERROR);
+                            return connection.onControlRequestFailed(
+                                    requestId, error, (Bundle)obj);
+                        }
+                        break;
+                 }
             }
             return false;
         }
diff --git a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
index 1246e96..bf84033 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.mr_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;
@@ -252,37 +252,67 @@
 
             Resources r = context.getResources();
             mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
-                    mRouterObj, r.getString(R.string.user_route_category_name), false);
+                    mRouterObj, r.getString(R.string.mr_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,46 +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());
-            }
-            record.mRouteDescriptor.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));
         }
 
         protected void updateUserRouteProperties(UserRouteRecord record) {
             MediaRouterJellybean.UserRouteInfo.setName(
                     record.mRouteObj, record.mRoute.getName());
-            MediaRouterJellybean.UserRouteInfo.setStatus(
-                    record.mRouteObj, normalizeStatus(record.mRoute.getStatus()));
-            MediaRouterJellybean.UserRouteInfo.setIconDrawable(
-                    record.mRouteObj, record.mRoute.getIconDrawable());
             MediaRouterJellybean.UserRouteInfo.setPlaybackType(
                     record.mRouteObj, record.mRoute.getPlaybackType());
             MediaRouterJellybean.UserRouteInfo.setPlaybackStream(
@@ -565,11 +592,16 @@
                     record.mRouteObj, record.mRoute.getVolumeHandling());
         }
 
-        // 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.
-        private static String normalizeStatus(String status) {
-            return status != null ? status : "";
+        protected void updateCallback() {
+            if (mCallbackRegistered) {
+                mCallbackRegistered = false;
+                MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+            }
+
+            if (mRouteTypes != 0) {
+                mCallbackRegistered = true;
+                MediaRouterJellybean.addCallback(mRouterObj, mRouteTypes, mCallbackObj);
+            }
         }
 
         protected Object createCallbackObj() {
@@ -581,63 +613,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 +636,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 +664,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 +682,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);
+        }
     }
 
     /**
@@ -729,13 +744,51 @@
         }
 
         @Override
+        protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+                MediaRouteDescriptor.Builder builder) {
+            super.onBuildSystemRouteDescriptor(record, builder);
+
+            CharSequence description =
+                    MediaRouterJellybeanMr2.RouteInfo.getDescription(record.mRouteObj);
+            if (description != null) {
+                builder.setDescription(description.toString());
+            }
+        }
+
+        @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 updateUserRouteProperties(UserRouteRecord record) {
+            super.updateUserRouteProperties(record);
+
+            MediaRouterJellybeanMr2.UserRouteInfo.setDescription(
+                    record.mRouteObj, record.mRoute.getDescription());
+        }
+
+        @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);
+        }
     }
 }