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