Merge "Fix Context MenuInflater is created with" into jb-mr2-dev
diff --git a/CleanSpec.mk b/CleanSpec.mk
index 0805120..ea4358f 100644
--- a/CleanSpec.mk
+++ b/CleanSpec.mk
@@ -46,6 +46,8 @@
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
# ************************************************
# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
diff --git a/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java b/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java
index 6ea47c5..750b6f4 100644
--- a/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java
+++ b/renderscript/v8/java/src/android/support/v8/renderscript/RenderScript.java
@@ -57,22 +57,9 @@
*/
@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
static boolean sInitialized;
+ static Object lock = new Object();
native static void _nInit();
-
- static {
- sInitialized = false;
- try {
- System.loadLibrary("RSSupport");
- System.loadLibrary("rsjni");
- _nInit();
- sInitialized = true;
- } catch (UnsatisfiedLinkError e) {
- Log.e(LOG_TAG, "Error loading RS jni library: " + e);
- throw new RSRuntimeException("Error loading RS jni library: " + e);
- }
- }
-
// Non-threadsafe functions.
native int nDeviceCreate();
native void nDeviceDestroy(int dev);
@@ -888,6 +875,19 @@
android.util.Log.v(LOG_TAG, "RS native mode");
return RenderScriptThunker.create(ctx, sdkVersion);
}
+ synchronized(lock) {
+ if (sInitialized == false) {
+ try {
+ System.loadLibrary("RSSupport");
+ System.loadLibrary("rsjni");
+ _nInit();
+ sInitialized = true;
+ } catch (UnsatisfiedLinkError e) {
+ Log.e(LOG_TAG, "Error loading RS jni library: " + e);
+ throw new RSRuntimeException("Error loading RS jni library: " + e);
+ }
+ }
+ }
android.util.Log.v(LOG_TAG, "RS compat mode");
rs.mDev = rs.nDeviceCreate();
diff --git a/v4/honeycomb/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java b/v4/honeycomb/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java
new file mode 100644
index 0000000..4c5d48b
--- /dev/null
+++ b/v4/honeycomb/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.graphics.drawable;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Implementation of drawable compatibility that can call Honeycomb APIs.
+ */
+class DrawableCompatHoneycomb {
+ public static void jumpToCurrentState(Drawable drawable) {
+ drawable.jumpToCurrentState();
+ }
+}
diff --git a/v4/java/android/support/v4/app/ActionBarDrawerToggle.java b/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
index fa58e21..11eea28 100644
--- a/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
+++ b/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
@@ -55,6 +55,42 @@
*/
public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener {
+ /**
+ * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use
+ * with ActionBarDrawerToggle.
+ */
+ public interface DelegateProvider {
+
+ /**
+ * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity
+ * does not wish to override the default behavior.
+ */
+ Delegate getDrawerToggleDelegate();
+ }
+
+ public interface Delegate {
+ /**
+ * @return Up indicator drawable as defined in the Activity's theme, or null if one is not
+ * defined.
+ */
+ Drawable getThemeUpIndicator();
+
+ /**
+ * Set the Action Bar's up indicator drawable and content description.
+ *
+ * @param upDrawable - Drawable to set as up indicator
+ * @param contentDescRes - Content description to set
+ */
+ void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes);
+
+ /**
+ * Set the Action Bar's up indicator content description.
+ *
+ * @param contentDescRes - Content description to set
+ */
+ void setActionBarDescription(int contentDescRes);
+ }
+
private interface ActionBarDrawerToggleImpl {
Drawable getThemeUpIndicator(Activity activity);
Object setActionBarUpIndicator(Object info, Activity activity,
@@ -117,6 +153,7 @@
private static final int ID_HOME = 0x0102002c;
private final Activity mActivity;
+ private final Delegate mActivityImpl;
private final DrawerLayout mDrawerLayout;
private boolean mDrawerIndicatorEnabled = true;
@@ -156,10 +193,17 @@
mOpenDrawerContentDescRes = openDrawerContentDescRes;
mCloseDrawerContentDescRes = closeDrawerContentDescRes;
- mThemeImage = IMPL.getThemeUpIndicator(activity);
+ mThemeImage = getThemeUpIndicator();
mDrawerImage = activity.getResources().getDrawable(drawerImageRes);
mSlider = new SlideDrawable(mDrawerImage);
mSlider.setOffsetBy(1.f / 3);
+
+ // Allow the Activity to provide an impl
+ if (activity instanceof DelegateProvider) {
+ mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate();
+ } else {
+ mActivityImpl = null;
+ }
}
/**
@@ -179,8 +223,7 @@
}
if (mDrawerIndicatorEnabled) {
- mSetIndicatorInfo = IMPL.setActionBarUpIndicator(mSetIndicatorInfo, mActivity,
- mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
+ setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
mOpenDrawerContentDescRes : mCloseDrawerContentDescRes);
}
}
@@ -198,12 +241,10 @@
public void setDrawerIndicatorEnabled(boolean enable) {
if (enable != mDrawerIndicatorEnabled) {
if (enable) {
- mSetIndicatorInfo = IMPL.setActionBarUpIndicator(mSetIndicatorInfo,
- mActivity, mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
- mOpenDrawerContentDescRes : mCloseDrawerContentDescRes);
+ setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
+ mOpenDrawerContentDescRes : mCloseDrawerContentDescRes);
} else {
- mSetIndicatorInfo = IMPL.setActionBarUpIndicator(mSetIndicatorInfo,
- mActivity, mThemeImage, 0);
+ setActionBarUpIndicator(mThemeImage, 0);
}
mDrawerIndicatorEnabled = enable;
}
@@ -226,7 +267,7 @@
*/
public void onConfigurationChanged(Configuration newConfig) {
// Reload drawables that can change with configuration
- mThemeImage = IMPL.getThemeUpIndicator(mActivity);
+ mThemeImage = getThemeUpIndicator();
mDrawerImage = mActivity.getResources().getDrawable(mDrawerImageResource);
syncState();
}
@@ -247,6 +288,7 @@
} else {
mDrawerLayout.openDrawer(GravityCompat.START);
}
+ return true;
}
return false;
}
@@ -281,8 +323,7 @@
public void onDrawerOpened(View drawerView) {
mSlider.setOffset(1.f);
if (mDrawerIndicatorEnabled) {
- mSetIndicatorInfo = IMPL.setActionBarDescription(mSetIndicatorInfo, mActivity,
- mOpenDrawerContentDescRes);
+ setActionBarDescription(mOpenDrawerContentDescRes);
}
}
@@ -297,8 +338,7 @@
public void onDrawerClosed(View drawerView) {
mSlider.setOffset(0.f);
if (mDrawerIndicatorEnabled) {
- mSetIndicatorInfo = IMPL.setActionBarDescription(mSetIndicatorInfo, mActivity,
- mCloseDrawerContentDescRes);
+ setActionBarDescription(mCloseDrawerContentDescRes);
}
}
@@ -313,6 +353,31 @@
public void onDrawerStateChanged(int newState) {
}
+ Drawable getThemeUpIndicator() {
+ if (mActivityImpl != null) {
+ return mActivityImpl.getThemeUpIndicator();
+ }
+ return IMPL.getThemeUpIndicator(mActivity);
+ }
+
+ void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
+ if (mActivityImpl != null) {
+ mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes);
+ return;
+ }
+ mSetIndicatorInfo = IMPL
+ .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes);
+ }
+
+ void setActionBarDescription(int contentDescRes) {
+ if (mActivityImpl != null) {
+ mActivityImpl.setActionBarDescription(contentDescRes);
+ return;
+ }
+ mSetIndicatorInfo = IMPL
+ .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes);
+ }
+
private static class SlideDrawable extends Drawable implements Drawable.Callback {
private Drawable mWrapped;
private float mOffset;
diff --git a/v4/java/android/support/v4/app/FragmentActivity.java b/v4/java/android/support/v4/app/FragmentActivity.java
index acb8181..9f111f7 100644
--- a/v4/java/android/support/v4/app/FragmentActivity.java
+++ b/v4/java/android/support/v4/app/FragmentActivity.java
@@ -468,7 +468,7 @@
}
boolean goforit = super.onPreparePanel(featureId, view, menu);
goforit |= mFragments.dispatchPrepareOptionsMenu(menu);
- return goforit && menu.hasVisibleItems();
+ return goforit;
}
return super.onPreparePanel(featureId, view, menu);
}
diff --git a/v4/java/android/support/v4/app/TaskStackBuilder.java b/v4/java/android/support/v4/app/TaskStackBuilder.java
index 0f12a33..9146872 100644
--- a/v4/java/android/support/v4/app/TaskStackBuilder.java
+++ b/v4/java/android/support/v4/app/TaskStackBuilder.java
@@ -69,6 +69,10 @@
public class TaskStackBuilder implements Iterable<Intent> {
private static final String TAG = "TaskStackBuilder";
+ public interface SupportParentable {
+ Intent getSupportParentActivityIntent();
+ }
+
interface TaskStackBuilderImpl {
PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode,
int flags, Bundle options);
@@ -190,7 +194,14 @@
* @return This TaskStackBuilder for method chaining
*/
public TaskStackBuilder addParentStack(Activity sourceActivity) {
- final Intent parent = NavUtils.getParentActivityIntent(sourceActivity);
+ Intent parent = null;
+ if (sourceActivity instanceof SupportParentable) {
+ parent = ((SupportParentable) sourceActivity).getSupportParentActivityIntent();
+ }
+ if (parent == null) {
+ parent = NavUtils.getParentActivityIntent(sourceActivity);
+ }
+
if (parent != null) {
// We have the actual parent intent, build the rest from static metadata
// then add the direct parent intent to the end.
diff --git a/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java b/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
new file mode 100644
index 0000000..f9c1342
--- /dev/null
+++ b/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.graphics.drawable;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Helper for accessing features in {@link android.graphics.drawable.Drawable}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class DrawableCompat {
+ /**
+ * Interface for the full API.
+ */
+ interface DrawableImpl {
+ void jumpToCurrentState(Drawable drawable);
+ }
+
+ /**
+ * Interface implementation that doesn't use anything about v4 APIs.
+ */
+ static class BaseDrawableImpl implements DrawableImpl {
+ @Override
+ public void jumpToCurrentState(Drawable drawable) {
+ }
+ }
+
+ /**
+ * Interface implementation for devices with at least v11 APIs.
+ */
+ static class HoneycombDrawableImpl implements DrawableImpl {
+ @Override
+ public void jumpToCurrentState(Drawable drawable) {
+ DrawableCompatHoneycomb.jumpToCurrentState(drawable);
+ }
+ }
+
+ /**
+ * Select the correct implementation to use for the current platform.
+ */
+ static final DrawableImpl IMPL;
+ static {
+ final int version = android.os.Build.VERSION.SDK_INT;
+ if (version >= 11) {
+ IMPL = new HoneycombDrawableImpl();
+ } else {
+ IMPL = new BaseDrawableImpl();
+ }
+ }
+
+ /**
+ * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}.
+ * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device
+ * this method does nothing.
+ */
+ public static void jumpToCurrentState(Drawable drawable) {
+ IMPL.jumpToCurrentState(drawable);
+ }
+}
diff --git a/v4/java/android/support/v4/widget/DrawerLayout.java b/v4/java/android/support/v4/widget/DrawerLayout.java
index cc55b82..afe27d4 100644
--- a/v4/java/android/support/v4/widget/DrawerLayout.java
+++ b/v4/java/android/support/v4/widget/DrawerLayout.java
@@ -582,14 +582,34 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
- throw new IllegalArgumentException(
- "DrawerLayout must be measured with MeasureSpec.EXACTLY.");
+ if (isInEditMode()) {
+ // Don't crash the layout editor. Consume all of the space if specified
+ // or pick a magic number from thin air otherwise.
+ // TODO Better communication with tools of this bogus state.
+ // It will crash on a real device.
+ if (widthMode == MeasureSpec.AT_MOST) {
+ widthMode = MeasureSpec.EXACTLY;
+ } else if (widthMode == MeasureSpec.UNSPECIFIED) {
+ widthMode = MeasureSpec.EXACTLY;
+ widthSize = 300;
+ }
+ if (heightMode == MeasureSpec.AT_MOST) {
+ heightMode = MeasureSpec.EXACTLY;
+ }
+ else if (heightMode == MeasureSpec.UNSPECIFIED) {
+ heightMode = MeasureSpec.EXACTLY;
+ heightSize = 300;
+ }
+ } else {
+ throw new IllegalArgumentException(
+ "DrawerLayout must be measured with MeasureSpec.EXACTLY.");
+ }
}
setMeasuredDimension(widthSize, heightSize);
@@ -639,6 +659,7 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mInLayout = true;
+ final int width = r - l;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
@@ -658,12 +679,17 @@
final int childHeight = child.getMeasuredHeight();
int childLeft;
+ final float newOffset;
if (checkDrawerViewGravity(child, Gravity.LEFT)) {
childLeft = -childWidth + (int) (childWidth * lp.onScreen);
+ newOffset = (float) (childWidth + childLeft) / childWidth;
} else { // Right; onMeasure checked for us.
- childLeft = r - l - (int) (childWidth * lp.onScreen);
+ childLeft = width - (int) (childWidth * lp.onScreen);
+ newOffset = (float) (width - childLeft) / childWidth;
}
+ final boolean changeOffset = newOffset != lp.onScreen;
+
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (vgrav) {
@@ -699,8 +725,13 @@
}
}
- if (lp.onScreen == 0) {
- child.setVisibility(INVISIBLE);
+ if (changeOffset) {
+ setDrawerViewOffset(child, newOffset);
+ }
+
+ final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE;
+ if (child.getVisibility() != newVisibility) {
+ child.setVisibility(newVisibility);
}
}
}
diff --git a/v4/java/android/support/v4/widget/SlidingPaneLayout.java b/v4/java/android/support/v4/widget/SlidingPaneLayout.java
index 591d827..aaed38f 100644
--- a/v4/java/android/support/v4/widget/SlidingPaneLayout.java
+++ b/v4/java/android/support/v4/widget/SlidingPaneLayout.java
@@ -84,8 +84,6 @@
* sized to fill all available space in the closed state. Weight on a pane that becomes covered
* indicates that the pane should be sized to fill all available space except a small minimum strip
* that the user may use to grab the slideable view and pull it back over into a closed state.</p>
- *
- * <p>Experimental. This class may be removed.</p>
*/
public class SlidingPaneLayout extends ViewGroup {
private static final String TAG = "SlidingPaneLayout";
@@ -424,15 +422,38 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
- throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
+ if (isInEditMode()) {
+ // Don't crash the layout editor. Consume all of the space if specified
+ // or pick a magic number from thin air otherwise.
+ // TODO Better communication with tools of this bogus state.
+ // It will crash on a real device.
+ if (widthMode == MeasureSpec.AT_MOST) {
+ widthMode = MeasureSpec.EXACTLY;
+ } else if (widthMode == MeasureSpec.UNSPECIFIED) {
+ widthMode = MeasureSpec.EXACTLY;
+ widthSize = 300;
+ }
+ } else {
+ throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
+ }
} else if (heightMode == MeasureSpec.UNSPECIFIED) {
- throw new IllegalStateException("Height must not be UNSPECIFIED");
+ if (isInEditMode()) {
+ // Don't crash the layout editor. Pick a magic number from thin air instead.
+ // TODO Better communication with tools of this bogus state.
+ // It will crash on a real device.
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ heightMode = MeasureSpec.AT_MOST;
+ heightSize = 300;
+ }
+ } else {
+ throw new IllegalStateException("Height must not be UNSPECIFIED");
+ }
}
int layoutHeight = 0;
@@ -1371,26 +1392,39 @@
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info);
super.onInitializeAccessibilityNodeInfo(host, superNode);
+ copyNodeInfoNoChildren(info, superNode);
+ superNode.recycle();
+ info.setClassName(SlidingPaneLayout.class.getName());
info.setSource(host);
+
final ViewParent parent = ViewCompat.getParentForAccessibility(host);
if (parent instanceof View) {
info.setParent((View) parent);
}
- copyNodeInfoNoChildren(info, superNode);
- superNode.recycle();
-
+ // This is a best-approximation of addChildrenForAccessibility()
+ // that accounts for filtering.
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
- if (!filter(child)) {
+ if (!filter(child) && (child.getVisibility() == View.VISIBLE)) {
+ // Force importance to "yes" since we can't read the value.
+ ViewCompat.setImportantForAccessibility(
+ child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
info.addChild(child);
}
}
}
@Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+
+ event.setClassName(SlidingPaneLayout.class.getName());
+ }
+
+ @Override
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
AccessibilityEvent event) {
if (!filter(child)) {
@@ -1432,6 +1466,8 @@
dest.setLongClickable(src.isLongClickable());
dest.addAction(src.getActions());
+
+ dest.setMovementGranularities(src.getMovementGranularities());
}
}
diff --git a/v7/appcompat/res/values/attrs.xml b/v7/appcompat/res/values/attrs.xml
index 499b904..e9caedf 100644
--- a/v7/appcompat/res/values/attrs.xml
+++ b/v7/appcompat/res/values/attrs.xml
@@ -474,5 +474,6 @@
<!-- Default Spinner style. -->
<attr name="spinnerDropDownItemStyle" format="reference" />
-
+ <!-- Specifies whether the theme is light, otherwise it is dark. -->
+ <attr name="isLightTheme" format="boolean" />
</resources>
diff --git a/v7/appcompat/res/values/themes.xml b/v7/appcompat/res/values/themes.xml
index 9e4ee54..91199f4 100644
--- a/v7/appcompat/res/values/themes.xml
+++ b/v7/appcompat/res/values/themes.xml
@@ -27,6 +27,7 @@
<!-- Platform-independent theme providing an action bar in a dark-themed activity. -->
<style name="Theme.AppCompat" parent="Theme.Base.AppCompat">
+ <item name="isLightTheme">false</item>
<!-- Required for use of support_simple_spinner_dropdown_item.xml -->
<item name="spinnerDropDownItemStyle">
@@ -46,6 +47,7 @@
<!-- Platform-independent theme providing an action bar in a light-themed activity. -->
<style name="Theme.AppCompat.Light" parent="Theme.Base.AppCompat.Light">
+ <item name="isLightTheme">true</item>
<!-- Required for use of support_simple_spinner_dropdown_item.xml -->
<item name="spinnerDropDownItemStyle">
@@ -66,6 +68,7 @@
<!-- Platform-independent theme providing an action bar in a dark-themed activity. -->
<style name="Theme.AppCompat.Light.DarkActionBar"
parent="Theme.Base.AppCompat.Light.DarkActionBar">
+ <item name="isLightTheme">true</item>
<!-- Required for use of support_simple_spinner_dropdown_item.xml -->
<item name="spinnerDropDownItemStyle">
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
index 1968d3c..b6f1568 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
@@ -21,6 +21,7 @@
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.NavUtils;
@@ -37,7 +38,8 @@
/**
* Base class for activities that use the support library action bar features.
*/
-public class ActionBarActivity extends FragmentActivity implements ActionBar.Callback {
+public class ActionBarActivity extends FragmentActivity implements ActionBar.Callback,
+ TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
ActionBarActivityDelegate mImpl;
/**
@@ -328,11 +330,34 @@
public void onPrepareSupportNavigateUpTaskStack(TaskStackBuilder builder) {
}
+ /**
+ * This method is called whenever the user chooses to navigate Up within your application's
+ * activity hierarchy from the action bar.
+ *
+ * <p>If a parent was specified in the manifest for this activity or an activity-alias to it,
+ * default Up navigation will be handled automatically. See
+ * {@link #getSupportParentActivityIntent()} for how to specify the parent. If any activity
+ * along the parent chain requires extra Intent arguments, the Activity subclass
+ * should override the method {@link #onPrepareSupportNavigateUpTaskStack(TaskStackBuilder)}
+ * to supply those arguments.</p>
+ *
+ * <p>See <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and
+ * Back Stack</a> from the developer guide and
+ * <a href="{@docRoot}design/patterns/navigation.html">Navigation</a> from the design guide
+ * for more information about navigating within your app.</p>
+ *
+ * <p>See the {@link TaskStackBuilder} class and the Activity methods
+ * {@link #getSupportParentActivityIntent()}, {@link #supportShouldUpRecreateTask(Intent)}, and
+ * {@link #supportNavigateUpTo(Intent)} for help implementing custom Up navigation.</p>
+ *
+ * @return true if Up navigation completed successfully and this Activity was finished,
+ * false otherwise.
+ */
public boolean onSupportNavigateUp() {
- Intent upIntent = NavUtils.getParentActivityIntent(this);
+ Intent upIntent = getSupportParentActivityIntent();
if (upIntent != null) {
- if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
+ if (supportShouldUpRecreateTask(upIntent)) {
TaskStackBuilder b = TaskStackBuilder.create(this);
onCreateSupportNavigateUpTaskStack(b);
onPrepareSupportNavigateUpTaskStack(b);
@@ -348,11 +373,61 @@
} else {
// This activity is part of the application's task, so simply
// navigate up to the hierarchical parent activity.
- NavUtils.navigateUpTo(this, upIntent);
+ supportNavigateUpTo(upIntent);
}
return true;
}
return false;
}
+ /**
+ * Obtain an {@link Intent} that will launch an explicit target activity
+ * specified by sourceActivity's {@link NavUtils#PARENT_ACTIVITY} <meta-data>
+ * element in the application's manifest. If the device is running
+ * Jellybean or newer, the android:parentActivityName attribute will be preferred
+ * if it is present.
+ *
+ * @return a new Intent targeting the defined parent activity of sourceActivity
+ */
+ public Intent getSupportParentActivityIntent() {
+ return NavUtils.getParentActivityIntent(this);
+ }
+
+ /**
+ * Returns true if sourceActivity should recreate the task when navigating 'up'
+ * by using targetIntent.
+ *
+ * <p>If this method returns false the app can trivially call
+ * {@link #supportNavigateUpTo(Intent)} using the same parameters to correctly perform
+ * up navigation. If this method returns false, the app should synthesize a new task stack
+ * by using {@link TaskStackBuilder} or another similar mechanism to perform up navigation.</p>
+ *
+ * @param targetIntent An intent representing the target destination for up navigation
+ * @return true if navigating up should recreate a new task stack, false if the same task
+ * should be used for the destination
+ */
+ public boolean supportShouldUpRecreateTask(Intent targetIntent) {
+ return NavUtils.shouldUpRecreateTask(this, targetIntent);
+ }
+
+ /**
+ * Navigate from sourceActivity to the activity specified by upIntent, finishing sourceActivity
+ * in the process. upIntent will have the flag {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set
+ * by this method, along with any others required for proper up navigation as outlined
+ * in the Android Design Guide.
+ *
+ * <p>This method should be used when performing up navigation from within the same task
+ * as the destination. If up navigation should cross tasks in some cases, see
+ * {@link #supportShouldUpRecreateTask(Intent)}.</p>
+ *
+ * @param upIntent An intent representing the target destination for up navigation
+ */
+ public void supportNavigateUpTo(Intent upIntent) {
+ NavUtils.navigateUpTo(this, upIntent);
+ }
+
+ @Override
+ public final ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
+ return mImpl.getDrawerToggleDelegate();
+ }
}
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
index b93116a..c46f698 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
@@ -23,6 +23,7 @@
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v7.appcompat.R;
import android.support.v7.internal.view.SupportMenuInflater;
import android.support.v7.view.ActionMode;
@@ -150,6 +151,8 @@
abstract void setSupportProgress(int progress);
+ abstract ActionBarDrawerToggle.Delegate getDrawerToggleDelegate();
+
protected final String getUiOptionsFromMetadata() {
try {
PackageManager pm = mActivity.getPackageManager();
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java
index 62a2c74..93923cf 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateBase.java
@@ -19,6 +19,8 @@
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.WindowCompat;
import android.support.v7.appcompat.R;
import android.support.v7.internal.view.menu.ListMenuPresenter;
@@ -31,7 +33,6 @@
import android.support.v7.internal.widget.ActionBarView;
import android.support.v7.internal.widget.ProgressBarICS;
import android.support.v7.view.ActionMode;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@@ -44,6 +45,10 @@
MenuPresenter.Callback, MenuBuilder.Callback {
private static final String TAG = "ActionBarActivityDelegateBase";
+ private static final int[] ACTION_BAR_DRAWABLE_TOGGLE_ATTRS = new int[] {
+ R.attr.homeAsUpIndicator
+ };
+
private ActionBarView mActionBarView;
private ListMenuPresenter mListMenuPresenter;
private MenuBuilder mMenu;
@@ -471,6 +476,11 @@
updateProgressBars(Window.PROGRESS_START + progress);
}
+ @Override
+ ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
+ return new ActionBarDrawableToggleImpl();
+ }
+
/**
* Progress Bar function. Mostly extracted from PhoneWindow.java
*/
@@ -579,4 +589,28 @@
}
}
+ private class ActionBarDrawableToggleImpl
+ implements ActionBarDrawerToggle.Delegate {
+
+ @Override
+ public Drawable getThemeUpIndicator() {
+ final TypedArray a = mActivity.obtainStyledAttributes(ACTION_BAR_DRAWABLE_TOGGLE_ATTRS);
+ final Drawable result = a.getDrawable(0);
+ a.recycle();
+ return result;
+ }
+
+ @Override
+ public void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
+ if (mActionBarView != null) {
+ mActionBarView.setHomeAsUpIndicator(upDrawable);
+ }
+ }
+
+ @Override
+ public void setActionBarDescription(int contentDescRes) {
+ // No support for setting Action Bar content description
+ }
+ }
+
}
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java
index 4610c1a..7540cc4 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateHC.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.WindowCompat;
import android.support.v7.internal.view.ActionModeWrapper;
import android.support.v7.internal.view.menu.MenuWrapperFactory;
@@ -219,6 +220,12 @@
return false;
}
+ @Override
+ public ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
+ // Return null so that ActionBarDrawableToggle uses it's standard impl
+ return null;
+ }
+
class WindowCallbackWrapper implements Window.Callback {
final Window.Callback mWrapped;
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java
index 4856df1..276a0f5 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemImpl.java
@@ -495,6 +495,9 @@
@Override
public boolean isVisible() {
+ if (mActionProvider != null && mActionProvider.overridesItemVisibility()) {
+ return (mFlags & HIDDEN) == 0 && mActionProvider.isVisible();
+ }
return (mFlags & HIDDEN) == 0;
}
@@ -659,7 +662,7 @@
actionProvider.setVisibilityListener(new ActionProvider.VisibilityListener() {
@Override
public void onActionProviderVisibilityChanged(boolean isVisible) {
- setVisible(isVisible);
+ mMenu.onItemVisibleChanged(MenuItemImpl.this);
}
});
}
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java
index 95144b9..93702bb 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperHC.java
@@ -18,8 +18,8 @@
import android.content.Intent;
import android.graphics.drawable.Drawable;
-import android.support.v4.view.ActionProvider;
import android.support.v4.internal.view.SupportMenuItem;
+import android.support.v4.view.ActionProvider;
import android.support.v4.view.MenuItemCompat;
import android.view.ContextMenu;
import android.view.MenuItem;
@@ -226,16 +226,14 @@
@Override
public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider) {
- mWrappedObject.setActionProvider(actionProvider != null ?
- createActionProviderWrapper(actionProvider) : null);
+ // APIv14+ API
return this;
}
@Override
public ActionProvider getSupportActionProvider() {
- ActionProviderWrapper providerWrapper =
- (ActionProviderWrapper)mWrappedObject.getActionProvider();
- return providerWrapper.mInner;
+ // APIv14+ API
+ return null;
}
@Override
@@ -281,10 +279,6 @@
return null;
}
- ActionProviderWrapper createActionProviderWrapper(ActionProvider provider) {
- return new ActionProviderWrapper(provider);
- }
-
private class OnMenuItemClickListenerWrapper extends BaseWrapper<OnMenuItemClickListener>
implements android.view.MenuItem.OnMenuItemClickListener {
@@ -297,34 +291,4 @@
return mWrappedObject.onMenuItemClick(getMenuItemWrapper(item));
}
}
-
- class ActionProviderWrapper extends android.view.ActionProvider {
- final ActionProvider mInner;
-
- public ActionProviderWrapper(ActionProvider inner) {
- super(inner.getContext());
- mInner = inner;
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public View onCreateActionView() {
- return mInner.onCreateActionView(mWrappedObject);
- }
-
- @Override
- public boolean onPerformDefaultAction() {
- return mInner.onPerformDefaultAction();
- }
-
- @Override
- public boolean hasSubMenu() {
- return mInner.hasSubMenu();
- }
-
- @Override
- public void onPrepareSubMenu(android.view.SubMenu subMenu) {
- mInner.onPrepareSubMenu(getSubMenuWrapper(subMenu));
- }
- }
}
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java
index d84f8fc..9277b12 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperICS.java
@@ -16,7 +16,10 @@
package android.support.v7.internal.view.menu;
+import android.support.v4.internal.view.SupportMenuItem;
+import android.support.v4.view.ActionProvider;
import android.view.MenuItem;
+import android.view.View;
class MenuItemWrapperICS extends MenuItemWrapperHC {
MenuItemWrapperICS(android.view.MenuItem object) {
@@ -56,6 +59,24 @@
return this;
}
+ @Override
+ public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider) {
+ mWrappedObject.setActionProvider(actionProvider != null ?
+ createActionProviderWrapper(actionProvider) : null);
+ return this;
+ }
+
+ @Override
+ public ActionProvider getSupportActionProvider() {
+ ActionProviderWrapper providerWrapper =
+ (ActionProviderWrapper)mWrappedObject.getActionProvider();
+ return providerWrapper.mInner;
+ }
+
+ ActionProviderWrapper createActionProviderWrapper(ActionProvider provider) {
+ return new ActionProviderWrapper(provider);
+ }
+
private class OnActionExpandListenerWrapper extends BaseWrapper<MenuItem.OnActionExpandListener>
implements android.view.MenuItem.OnActionExpandListener {
@@ -73,4 +94,33 @@
return mWrappedObject.onMenuItemActionCollapse(getMenuItemWrapper(item));
}
}
+
+ class ActionProviderWrapper extends android.view.ActionProvider {
+ final ActionProvider mInner;
+
+ public ActionProviderWrapper(ActionProvider inner) {
+ super(inner.getContext());
+ mInner = inner;
+ }
+
+ @Override
+ public View onCreateActionView() {
+ return mInner.onCreateActionView();
+ }
+
+ @Override
+ public boolean onPerformDefaultAction() {
+ return mInner.onPerformDefaultAction();
+ }
+
+ @Override
+ public boolean hasSubMenu() {
+ return mInner.hasSubMenu();
+ }
+
+ @Override
+ public void onPrepareSubMenu(android.view.SubMenu subMenu) {
+ mInner.onPrepareSubMenu(getSubMenuWrapper(subMenu));
+ }
+ }
}
diff --git a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java
index 3288d37..eb2cc5f 100644
--- a/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java
+++ b/v7/appcompat/src/android/support/v7/internal/view/menu/MenuItemWrapperJB.java
@@ -18,6 +18,7 @@
import android.support.v4.view.ActionProvider;
import android.view.MenuItem;
+import android.view.View;
class MenuItemWrapperJB extends MenuItemWrapperICS {
MenuItemWrapperJB(android.view.MenuItem object) {
@@ -25,14 +26,8 @@
}
@Override
- public MenuItem setActionProvider(android.view.ActionProvider provider) {
- mWrappedObject.setActionProvider(provider);
- return this;
- }
-
- @Override
- public android.view.ActionProvider getActionProvider() {
- return mWrappedObject.getActionProvider();
+ ActionProviderWrapper createActionProviderWrapper(ActionProvider provider) {
+ return new ActionProviderWrapperJB(provider);
}
class ActionProviderWrapperJB extends ActionProviderWrapper
@@ -44,6 +39,11 @@
}
@Override
+ public View onCreateActionView(MenuItem forItem) {
+ return mInner.onCreateActionView(forItem);
+ }
+
+ @Override
public boolean overridesItemVisibility() {
return mInner.overridesItemVisibility();
}
diff --git a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java
index 25776d7..c08615d 100644
--- a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java
+++ b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarContainer.java
@@ -80,20 +80,76 @@
}
public void setPrimaryBackground(Drawable bg) {
+ if (mBackground != null) {
+ mBackground.setCallback(null);
+ unscheduleDrawable(mBackground);
+ }
mBackground = bg;
+ if (bg != null) {
+ bg.setCallback(this);
+ }
+ setWillNotDraw(mIsSplit ? mSplitBackground == null :
+ mBackground == null && mStackedBackground == null);
invalidate();
}
public void setStackedBackground(Drawable bg) {
+ if (mStackedBackground != null) {
+ mStackedBackground.setCallback(null);
+ unscheduleDrawable(mStackedBackground);
+ }
mStackedBackground = bg;
+ if (bg != null) {
+ bg.setCallback(this);
+ }
+ setWillNotDraw(mIsSplit ? mSplitBackground == null :
+ mBackground == null && mStackedBackground == null);
invalidate();
}
public void setSplitBackground(Drawable bg) {
+ if (mSplitBackground != null) {
+ mSplitBackground.setCallback(null);
+ unscheduleDrawable(mSplitBackground);
+ }
mSplitBackground = bg;
+ if (bg != null) {
+ bg.setCallback(this);
+ }
+ setWillNotDraw(mIsSplit ? mSplitBackground == null :
+ mBackground == null && mStackedBackground == null);
invalidate();
}
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ final boolean isVisible = visibility == VISIBLE;
+ if (mBackground != null) mBackground.setVisible(isVisible, false);
+ if (mStackedBackground != null) mStackedBackground.setVisible(isVisible, false);
+ if (mSplitBackground != null) mSplitBackground.setVisible(isVisible, false);
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return (who == mBackground && !mIsSplit) || (who == mStackedBackground && mIsStacked) ||
+ (who == mSplitBackground && mIsSplit) || super.verifyDrawable(who);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mBackground != null && mBackground.isStateful()) {
+ mBackground.setState(getDrawableState());
+ }
+ if (mStackedBackground != null && mStackedBackground.isStateful()) {
+ mStackedBackground.setState(getDrawableState());
+ }
+ if (mSplitBackground != null && mSplitBackground.isStateful()) {
+ mSplitBackground.setState(getDrawableState());
+ }
+ }
+
/**
* Set the action bar into a "transitioning" state. While transitioning the bar will block focus
* and touch from all of its descendants. This prevents the user from interacting with the bar
diff --git a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java
index d30e2ad..8232081 100644
--- a/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java
+++ b/v7/appcompat/src/android/support/v7/internal/widget/ActionBarView.java
@@ -1191,6 +1191,14 @@
}
}
+ public void setHomeAsUpIndicator(Drawable indicator) {
+ mHomeLayout.setUpIndicator(indicator);
+ }
+
+ public void setHomeAsUpIndicator(int resId) {
+ mHomeLayout.setUpIndicator(resId);
+ }
+
static class SavedState extends BaseSavedState {
int expandedMenuItemId;
@@ -1226,12 +1234,11 @@
}
private static class HomeView extends FrameLayout {
-
- private View mUpView;
-
+ private ImageView mUpView;
private ImageView mIconView;
-
private int mUpWidth;
+ private int mUpIndicatorRes;
+ private Drawable mDefaultUpIndicator;
public HomeView(Context context) {
this(context, null);
@@ -1249,6 +1256,25 @@
mIconView.setImageDrawable(icon);
}
+ public void setUpIndicator(Drawable d) {
+ mUpView.setImageDrawable(d != null ? d : mDefaultUpIndicator);
+ mUpIndicatorRes = 0;
+ }
+
+ public void setUpIndicator(int resId) {
+ mUpIndicatorRes = resId;
+ mUpView.setImageDrawable(resId != 0 ? getResources().getDrawable(resId) : null);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mUpIndicatorRes != 0) {
+ // Reload for config change
+ setUpIndicator(mUpIndicatorRes);
+ }
+ }
+
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
final CharSequence cdesc = getContentDescription();
@@ -1260,8 +1286,9 @@
@Override
protected void onFinishInflate() {
- mUpView = findViewById(R.id.up);
+ mUpView = (ImageView) findViewById(R.id.up);
mIconView = (ImageView) findViewById(R.id.home);
+ mDefaultUpIndicator = mUpView.getDrawable();
}
public int getLeftOffset() {
diff --git a/v7/mediarouter/Android.mk b/v7/mediarouter/Android.mk
index b3153a2..995e2bb 100644
--- a/v7/mediarouter/Android.mk
+++ b/v7/mediarouter/Android.mk
@@ -14,12 +14,27 @@
LOCAL_PATH := $(call my-dir)
+# Build the resources using the current SDK version.
+# We do this here because the final static library must be compiled with an older
+# SDK version than the resources. The resources library and the R class that it
+# contains will not be linked into the final static library.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-mediarouter-res
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, dummy)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \
+ frameworks/support/v7/appcompat/res
+LOCAL_AAPT_FLAGS := \
+ --auto-add-overlay \
+ --extra-packages android.support.v7.appcompat
+LOCAL_JAR_EXCLUDE_FILES := none
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
# A helper sub-library that makes direct use of JellyBean APIs.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v7-mediarouter-jellybean
LOCAL_SDK_VERSION := 16
LOCAL_SRC_FILES := $(call all-java-files-under, jellybean)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
include $(BUILD_STATIC_JAVA_LIBRARY)
# A helper sub-library that makes direct use of JellyBean MR1 APIs.
@@ -27,7 +42,6 @@
LOCAL_MODULE := android-support-v7-mediarouter-jellybean-mr1
LOCAL_SDK_VERSION := 17
LOCAL_SRC_FILES := $(call all-java-files-under, jellybean-mr1)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-mediarouter-jellybean
include $(BUILD_STATIC_JAVA_LIBRARY)
@@ -36,7 +50,6 @@
LOCAL_MODULE := android-support-v7-mediarouter-jellybean-mr2
LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := $(call all-java-files-under, jellybean-mr2)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-mediarouter-jellybean-mr1
include $(BUILD_STATIC_JAVA_LIBRARY)
@@ -46,11 +59,10 @@
# in their makefiles to include the resources in their package.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v7-mediarouter
-LOCAL_SDK_VERSION := 4
+LOCAL_SDK_VERSION := 7
LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-mediarouter-jellybean-mr2
-LOCAL_JAVA_LIBRARIES := android-support-v4
+LOCAL_JAVA_LIBRARIES := android-support-v4 android-support-v7-mediarouter-res
include $(BUILD_STATIC_JAVA_LIBRARY)
# Include this library in the build server's output directory
diff --git a/v7/mediarouter/dummy/Dummy.java b/v7/mediarouter/dummy/Dummy.java
new file mode 100644
index 0000000..be16dc7
--- /dev/null
+++ b/v7/mediarouter/dummy/Dummy.java
@@ -0,0 +1 @@
+// Dummy java file used to build the resource library.
diff --git a/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java b/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java
index a6b2afd..9837fb2 100644
--- a/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java
+++ b/v7/mediarouter/jellybean-mr1/android/support/v7/media/MediaRouterJellybeanMr1.java
@@ -16,9 +16,20 @@
package android.support.v7.media;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Handler;
+import android.util.Log;
import android.view.Display;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
final class MediaRouterJellybeanMr1 {
+ private static final String TAG = "MediaRouterJellybeanMr1";
+
public static Object createCallback(Callback callback) {
return new CallbackProxy<Callback>(callback);
}
@@ -37,6 +48,119 @@
public void onRoutePresentationDisplayChanged(Object routeObj);
}
+ /**
+ * Workaround the fact that the version of MediaRouter.addCallback() that accepts a
+ * flag to perform an active scan does not exist in JB MR1 so we need to force
+ * wifi display scans directly through the DisplayManager.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class ActiveScanWorkaround implements Runnable {
+ // Time between wifi display scans when actively scanning in milliseconds.
+ private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000;
+
+ private final DisplayManager mDisplayManager;
+ private final Handler mHandler;
+ private Method mScanWifiDisplaysMethod;
+
+ private boolean mActivelyScanningWifiDisplays;
+
+ public ActiveScanWorkaround(Context context, Handler handler) {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ mHandler = handler;
+ try {
+ mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void setActiveScanRouteTypes(int routeTypes) {
+ // On JB MR1, there is no API to scan wifi display routes.
+ // Instead we must make a direct call into the DisplayManager to scan
+ // wifi displays on this version but only when live video routes are requested.
+ // See also the JellybeanMr2Impl implementation of this method.
+ // This was fixed in JB MR2 by adding a new overload of addCallback() to
+ // enable active scanning on request.
+ if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+ if (!mActivelyScanningWifiDisplays) {
+ if (mScanWifiDisplaysMethod != null) {
+ mActivelyScanningWifiDisplays = true;
+ mHandler.post(this);
+ } else {
+ Log.w(TAG, "Cannot scan for wifi displays because the "
+ + "DisplayManager.scanWifiDisplays() method is "
+ + "not available on this device.");
+ }
+ }
+ } else {
+ if (mActivelyScanningWifiDisplays) {
+ mActivelyScanningWifiDisplays = false;
+ mHandler.removeCallbacks(this);
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ if (mActivelyScanningWifiDisplays) {
+ try {
+ mScanWifiDisplaysMethod.invoke(mDisplayManager);
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ }
+ mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL);
+ }
+ }
+ }
+
+ /**
+ * Workaround the fact that the isConnecting() method does not exist in JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class IsConnectingWorkaround {
+ private Method mGetStatusCodeMethod;
+ private int mStatusConnecting;
+
+ public IsConnectingWorkaround() {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ try {
+ Field statusConnectingField =
+ android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING");
+ mStatusConnecting = statusConnectingField.getInt(null);
+ mGetStatusCodeMethod =
+ android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode");
+ } catch (NoSuchFieldException ex) {
+ } catch (NoSuchMethodException ex) {
+ } catch (IllegalAccessException ex) {
+ }
+ }
+
+ public boolean isConnecting(Object routeObj) {
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ if (mGetStatusCodeMethod != null) {
+ try {
+ int statusCode = (Integer)mGetStatusCodeMethod.invoke(route);
+ return statusCode == mStatusConnecting;
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Assume not connecting.
+ return false;
+ }
+ }
+
static class CallbackProxy<T extends Callback>
extends MediaRouterJellybean.CallbackProxy<T> {
public CallbackProxy(T callback) {
diff --git a/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java b/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java
index a78b9eb..f3c2966 100644
--- a/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java
+++ b/v7/mediarouter/jellybean-mr2/android/support/v7/media/MediaRouterJellybeanMr2.java
@@ -20,4 +20,25 @@
public static Object getDefaultRoute(Object routerObj) {
return ((android.media.MediaRouter)routerObj).getDefaultRoute();
}
+
+ public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) {
+ ((android.media.MediaRouter)routerObj).addCallback(types,
+ (android.media.MediaRouter.Callback)callbackObj, flags);
+ }
+
+ public static final class RouteInfo {
+ public static CharSequence getDescription(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription();
+ }
+
+ public static boolean isConnecting(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting();
+ }
+ }
+
+ public static final class UserRouteInfo {
+ public static void setDescription(Object routeObj, CharSequence description) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description);
+ }
+ }
}
diff --git a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
index 9b8cc72..275ffa5 100644
--- a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
+++ b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
@@ -18,15 +18,26 @@
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.Log;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
final class MediaRouterJellybean {
+ private static final String TAG = "MediaRouterJellybean";
+
public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
public static final int ROUTE_TYPE_USER = 0x00800000;
+ public static final int ALL_ROUTE_TYPES =
+ MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
+ | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
+ | MediaRouterJellybean.ROUTE_TYPE_USER;
+
public static Object getMediaRouter(Context context) {
return context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
}
@@ -101,8 +112,6 @@
}
public static final class RouteInfo {
- public static final Class<?> clazz = android.media.MediaRouter.RouteInfo.class;
-
public static CharSequence getName(Object routeObj, Context context) {
return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context);
}
@@ -258,6 +267,94 @@
public void onVolumeUpdateRequest(Object routeObj, int direction);
}
+ /**
+ * Workaround for limitations of selectRoute() on JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class SelectRouteWorkaround {
+ private Method mSelectRouteIntMethod;
+
+ public SelectRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod(
+ "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class);
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void selectRoute(Object routerObj, int types, Object routeObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ int routeTypes = route.getSupportedTypes();
+ if ((routeTypes & ROUTE_TYPE_USER) == 0) {
+ // Handle non-user routes.
+ // On JB and JB MR1, the selectRoute() API only supports programmatically
+ // selecting user routes. So instead we rely on the hidden selectRouteInt()
+ // method on these versions of the platform.
+ // This limitation was removed in JB MR2.
+ if (mSelectRouteIntMethod != null) {
+ try {
+ mSelectRouteIntMethod.invoke(router, types, route);
+ return; // success!
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ }
+ } else {
+ Log.w(TAG, "Cannot programmatically select non-user route "
+ + "because the platform is missing the selectRouteInt() "
+ + "method. Media routing may not work.");
+ }
+ }
+
+ // Default handling.
+ router.selectRoute(types, route);
+ }
+ }
+
+ /**
+ * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class GetDefaultRouteWorkaround {
+ private Method mGetSystemAudioRouteMethod;
+
+ public GetDefaultRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mGetSystemAudioRouteMethod =
+ android.media.MediaRouter.class.getMethod("getSystemAudioRoute");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public Object getDefaultRoute(Object routerObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+
+ if (mGetSystemAudioRouteMethod != null) {
+ try {
+ return mGetSystemAudioRouteMethod.invoke(router);
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Could not find the method or it does not work.
+ // Return the first route and hope for the best.
+ return router.getRouteAt(0);
+ }
+ }
+
static class CallbackProxy<T extends Callback>
extends android.media.MediaRouter.Callback {
protected final T mCallback;
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_audio_vol.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_audio_vol.png
new file mode 100644
index 0000000..6ea2693
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..b47d666
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..03b0d2a
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..13d803c
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..3ae436b
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..24824fc
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..af3819b
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..83dc251
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..8d9d592
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..1310ec9
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..1705074
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..7027b88
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..7027b88
--- /dev/null
+++ b/v7/mediarouter/res/drawable-hdpi/mr_ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_audio_vol.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_audio_vol.png
new file mode 100644
index 0000000..c32fdbc
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..fa22d82
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..a686cd1
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..6764598
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..94e0bb6
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..5ce2f20
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..5105e90
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..68c06ed
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..6e9b144
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..45dc56f
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..46e743a
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..e384691
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..e384691
--- /dev/null
+++ b/v7/mediarouter/res/drawable-mdpi/mr_ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_audio_vol.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_audio_vol.png
new file mode 100644
index 0000000..4e2e20e
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_audio_vol.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_dark.png
new file mode 100644
index 0000000..1d48e12
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_light.png
new file mode 100644
index 0000000..2c8d1ec
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_disabled_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_dark.png
new file mode 100644
index 0000000..00b2043
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_light.png
new file mode 100644
index 0000000..ce1d939
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_off_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_dark.png
new file mode 100644
index 0000000..3064b46
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_light.png
new file mode 100644
index 0000000..4316686
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_0_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_dark.png
new file mode 100644
index 0000000..25c4e31
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_light.png
new file mode 100644
index 0000000..8e32bd2
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_1_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_dark.png
new file mode 100644
index 0000000..aeaa78f
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_light.png
new file mode 100644
index 0000000..85277fa
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_2_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_dark.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_dark.png
new file mode 100644
index 0000000..b01dbe8
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_dark.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_light.png b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_light.png
new file mode 100644
index 0000000..c19a2ad
--- /dev/null
+++ b/v7/mediarouter/res/drawable-xhdpi/mr_ic_media_route_on_holo_light.png
Binary files differ
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_dark.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_dark.xml
new file mode 100644
index 0000000..6b27536
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_dark.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<animation-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="false">
+ <item android:drawable="@drawable/mr_ic_media_route_on_0_holo_dark" android:duration="500" />
+ <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_dark" android:duration="500" />
+ <item android:drawable="@drawable/mr_ic_media_route_on_2_holo_dark" android:duration="500" />
+ <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_dark" android:duration="500" />
+</animation-list>
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_light.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_light.xml
new file mode 100644
index 0000000..aaa6473
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_ic_media_route_connecting_holo_light.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<animation-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="false">
+ <item android:drawable="@drawable/mr_ic_media_route_on_0_holo_light" android:duration="500" />
+ <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_light" android:duration="500" />
+ <item android:drawable="@drawable/mr_ic_media_route_on_2_holo_light" android:duration="500" />
+ <item android:drawable="@drawable/mr_ic_media_route_on_1_holo_light" android:duration="500" />
+</animation-list>
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_holo_dark.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_holo_dark.xml
new file mode 100644
index 0000000..6870591
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_ic_media_route_holo_dark.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_ic_media_route_on_holo_dark" />
+ <item android:state_checkable="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_ic_media_route_connecting_holo_dark" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/mr_ic_media_route_off_holo_dark" />
+ <item android:drawable="@drawable/mr_ic_media_route_disabled_holo_dark" />
+</selector>
diff --git a/v7/mediarouter/res/drawable/mr_ic_media_route_holo_light.xml b/v7/mediarouter/res/drawable/mr_ic_media_route_holo_light.xml
new file mode 100644
index 0000000..0e4a065
--- /dev/null
+++ b/v7/mediarouter/res/drawable/mr_ic_media_route_holo_light.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_ic_media_route_on_holo_light" />
+ <item android:state_checkable="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_ic_media_route_connecting_holo_light" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/mr_ic_media_route_off_holo_light" />
+ <item android:drawable="@drawable/mr_ic_media_route_disabled_holo_light" />
+</selector>
diff --git a/v7/mediarouter/res/layout-v17/mr_media_route_list_item.xml b/v7/mediarouter/res/layout-v17/mr_media_route_list_item.xml
new file mode 100644
index 0000000..1b798ee
--- /dev/null
+++ b/v7/mediarouter/res/layout-v17/mr_media_route_list_item.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical">
+
+ <LinearLayout android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="start|center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:duplicateParentState="true">
+
+ <TextView android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:duplicateParentState="true" />
+
+ <TextView android:id="@android:id/text2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:duplicateParentState="true" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/v7/mediarouter/res/layout/mr_media_route_chooser_dialog.xml b/v7/mediarouter/res/layout/mr_media_route_chooser_dialog.xml
new file mode 100644
index 0000000..d853b37
--- /dev/null
+++ b/v7/mediarouter/res/layout/mr_media_route_chooser_dialog.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <ListView android:id="@+id/media_route_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+</LinearLayout>
diff --git a/v7/mediarouter/res/layout/mr_media_route_controller_dialog.xml b/v7/mediarouter/res/layout/mr_media_route_controller_dialog.xml
new file mode 100644
index 0000000..c17321b
--- /dev/null
+++ b/v7/mediarouter/res/layout/mr_media_route_controller_dialog.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <!-- Optional volume slider section. -->
+ <LinearLayout android:id="@+id/media_route_volume_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ android:gravity="center_vertical"
+ android:padding="8dp"
+ android:visibility="gone">
+ <ImageView android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:src="@drawable/mr_ic_audio_vol"
+ android:gravity="center"
+ android:scaleType="center" />
+ <SeekBar android:id="@+id/media_route_volume_slider"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_marginRight="8dp" />
+ </LinearLayout>
+ <ImageView android:id="@+id/media_route_volume_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:src="?android:attr/listDivider" />
+ android:scaleType="fitXY"
+ android:visibility="gone" />
+
+ <!-- Optional content view section. -->
+ <FrameLayout android:id="@+id/media_route_control_frame"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+ <ImageView android:id="@+id/media_route_control_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:src="?android:attr/listDivider" />
+ android:scaleType="fitXY"
+ android:visibility="gone" />
+
+ <!-- Disconnect button. -->
+ <Button android:id="@+id/media_route_disconnect_button"
+ android:layout_width="fill_parent"
+ android:layout_height="56dp"
+ style="?android:attr/buttonBarButtonStyle"
+ android:gravity="center"
+ android:text="@string/mr_media_route_controller_disconnect" />
+</LinearLayout>
diff --git a/v7/mediarouter/res/layout/mr_media_route_list_item.xml b/v7/mediarouter/res/layout/mr_media_route_list_item.xml
new file mode 100644
index 0000000..6c63a12
--- /dev/null
+++ b/v7/mediarouter/res/layout/mr_media_route_list_item.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="64dp"
+ android:gravity="center_vertical">
+
+ <LinearLayout android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="left|center_vertical"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:duplicateParentState="true">
+
+ <TextView android:id="@android:id/text1"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:duplicateParentState="true" />
+
+ <TextView android:id="@android:id/text2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:duplicateParentState="true" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/v7/mediarouter/res/values/attrs.xml b/v7/mediarouter/res/values/attrs.xml
new file mode 100644
index 0000000..2272e7a
--- /dev/null
+++ b/v7/mediarouter/res/values/attrs.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <declare-styleable name="MediaRouteButton">
+ <!-- This drawable is a state list where the "checked" state
+ indicates active media routing. Checkable indicates connecting
+ and non-checked / non-checkable indicates
+ that media is playing to the local device only. -->
+ <attr name="externalRouteEnabledDrawable" format="reference" />
+
+ <attr name="android:minWidth" />
+ <attr name="android:minHeight" />
+ </declare-styleable>
+
+ <attr name="mediaRouteButtonStyle" format="reference" />
+ <attr name="mediaRouteOffDrawable" format="reference" />
+ <attr name="mediaRouteConnectingDrawable" format="reference" />
+ <attr name="mediaRouteOnDrawable" format="reference" />
+</resources>
\ No newline at end of file
diff --git a/v7/mediarouter/res/values/strings.xml b/v7/mediarouter/res/values/strings.xml
index a2caff3..6dd71e1 100644
--- a/v7/mediarouter/res/values/strings.xml
+++ b/v7/mediarouter/res/values/strings.xml
@@ -16,8 +16,18 @@
<resources>
<!-- Name for the default system route prior to Jellybean. [CHAR LIMIT=30] -->
- <string name="system_route_name">System</string>
+ <string name="mr_system_route_name">System</string>
<!-- Name for the user route category created when publishing routes to the system in Jellybean and above. [CHAR LIMIT=30] -->
- <string name="user_route_category_name">Devices</string>
+ <string name="mr_user_route_category_name">Devices</string>
+
+ <!-- Content description of a MediaRouteButton for accessibility support. [CHAR LIMIT=50] -->
+ <string name="mr_media_route_button_content_description">Media output</string>
+
+ <!-- Title of the media route chooser dialog. [CHAR LIMIT=30] -->
+ <string name="mr_media_route_chooser_title">Connect to device</string>
+
+ <!-- Button to disconnect from a media route. [CHAR LIMIT=30] -->
+ <string name="mr_media_route_controller_disconnect">Disconnect</string>
+
</resources>
diff --git a/v7/mediarouter/res/values/styles.xml b/v7/mediarouter/res/values/styles.xml
new file mode 100644
index 0000000..981ff15
--- /dev/null
+++ b/v7/mediarouter/res/values/styles.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <style name="Widget.MediaRouter.MediaRouteButton"
+ parent="Widget.AppCompat.ActionButton">
+ <item name="android:minWidth">56dp</item>
+ <item name="android:minHeight">48dp</item>
+ <item name="android:focusable">true</item>
+ <item name="android:contentDescription">@string/mr_media_route_button_content_description</item>
+ <item name="externalRouteEnabledDrawable">@drawable/mr_ic_media_route_holo_dark</item>
+ </style>
+
+ <style name="Widget.MediaRouter.Light.MediaRouteButton"
+ parent="Widget.AppCompat.Light.ActionButton">
+ <item name="android:minWidth">56dp</item>
+ <item name="android:minHeight">48dp</item>
+ <item name="android:focusable">true</item>
+ <item name="android:contentDescription">@string/mr_media_route_button_content_description</item>
+ <item name="externalRouteEnabledDrawable">@drawable/mr_ic_media_route_holo_light</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/v7/mediarouter/res/values/themes.xml b/v7/mediarouter/res/values/themes.xml
new file mode 100644
index 0000000..879188d
--- /dev/null
+++ b/v7/mediarouter/res/values/themes.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <style name="Theme.MediaRouter" parent="">
+ <item name="mediaRouteButtonStyle">@style/Widget.MediaRouter.MediaRouteButton</item>
+
+ <item name="mediaRouteOffDrawable">@drawable/mr_ic_media_route_off_holo_dark</item>
+ <item name="mediaRouteConnectingDrawable">@drawable/mr_ic_media_route_connecting_holo_dark</item>
+ <item name="mediaRouteOnDrawable">@drawable/mr_ic_media_route_on_holo_dark</item>
+ </style>
+
+ <style name="Theme.MediaRouter.Light" parent="">
+ <item name="mediaRouteButtonStyle">@style/Widget.MediaRouter.Light.MediaRouteButton</item>
+
+ <item name="mediaRouteOffDrawable">@drawable/mr_ic_media_route_off_holo_light</item>
+ <item name="mediaRouteConnectingDrawable">@drawable/mr_ic_media_route_connecting_holo_light</item>
+ <item name="mediaRouteOnDrawable">@drawable/mr_ic_media_route_on_holo_light</item>
+ </style>
+
+</resources>
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java
new file mode 100644
index 0000000..c1775b8
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app;
+
+import android.content.Context;
+import android.support.v4.view.ActionProvider;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouteSelector;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * The media route action provider displays a {@link MediaRouteButton media route button}
+ * in the application's {@link ActionBar} to allow the user to select routes and
+ * to control the currently selected route.
+ * <p>
+ * The application must specify the kinds of routes that the user should be allowed
+ * to select by specifying a {@link MediaRouteSelector selector} with the
+ * {@link #setRouteSelector} method.
+ * </p>
+ *
+ * <h3>Prerequisites</h3>
+ * <p>
+ * To use the media route action provider, the activity must be a subclass of
+ * {@link ActionBarActivity} from the <code>android.support.v7.appcompat</code>
+ * support library. Refer to support library documentation for details.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <p>
+ * </p><p>
+ * The application should define a menu resource to include the provider in the
+ * action bar options menu. Note that the support library action bar uses attributes
+ * that are defined in the application's resource namespace rather than the framework's
+ * resource namespace to configure each item.
+ * </p><pre>
+ * <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..bcf1dab
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.mediarouter.R;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * This class implements the route chooser dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to choose a route that matches a given selector.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteChooserDialog extends Dialog {
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+ private RouteAdapter mAdapter;
+ private ListView mListView;
+ private boolean mAttachedToWindow;
+
+ public MediaRouteChooserDialog(Context context) {
+ this(context, 0);
+ }
+
+ public MediaRouteChooserDialog(Context context, int theme) {
+ super(MediaRouterThemeHelper.createThemedContext(context), theme);
+ context = getContext();
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback();
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can select.
+ *
+ * @return The selector, never null.
+ */
+ public MediaRouteSelector getRouteSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can select.
+ *
+ * @param selector The selector, must not be null.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ if (mAttachedToWindow) {
+ mRouter.removeCallback(mCallback);
+ mRouter.addCallback(selector, mCallback);
+ }
+
+ refreshRoutes();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+
+ setContentView(R.layout.mr_media_route_chooser_dialog);
+ setTitle(R.string.mr_media_route_chooser_title);
+
+ // Must be called after setContentView.
+ getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
+ MediaRouterThemeHelper.getThemeResource(
+ getContext(), R.attr.mediaRouteOffDrawable));
+
+ mAdapter = new RouteAdapter(getContext());
+ mListView = (ListView)findViewById(R.id.media_route_list);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(mAdapter);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mAttachedToWindow = true;
+ mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_ACTIVE_SCAN);
+ refreshRoutes();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mAttachedToWindow = false;
+ mRouter.removeCallback(mCallback);
+
+ super.onDetachedFromWindow();
+ }
+
+ private void refreshRoutes() {
+ if (mAttachedToWindow) {
+ mAdapter.update();
+ }
+ }
+
+ private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo>
+ implements ListView.OnItemClickListener {
+ private final LayoutInflater mInflater;
+
+ public RouteAdapter(Context context) {
+ super(context, 0);
+ mInflater = LayoutInflater.from(context);
+ }
+
+ public void update() {
+ clear();
+ final List<MediaRouter.RouteInfo> routes = mRouter.getRoutes();
+ final int count = routes.size();
+ for (int i = 0; i < count; i++) {
+ MediaRouter.RouteInfo route = routes.get(i);
+ if (!route.isDefault() && route.matchesSelector(mSelector)) {
+ add(route);
+ }
+ }
+ sort(RouteComparator.sInstance);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return getItem(position).isEnabled();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = mInflater.inflate(R.layout.mr_media_route_list_item, parent, false);
+ }
+ MediaRouter.RouteInfo route = getItem(position);
+ TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+ TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+ text1.setText(route.getName());
+ String description = route.getDescription();
+ if (TextUtils.isEmpty(description)) {
+ text2.setVisibility(View.GONE);
+ text2.setText("");
+ } else {
+ text2.setVisibility(View.VISIBLE);
+ text2.setText(description);
+ }
+ view.setEnabled(route.isEnabled());
+ return view;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ MediaRouter.RouteInfo route = getItem(position);
+ if (route.isEnabled()) {
+ route.select();
+ dismiss();
+ }
+ }
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+ dismiss();
+ }
+ }
+
+ private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
+ public static final RouteComparator sInstance = new RouteComparator();
+
+ @Override
+ public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
+ return lhs.getName().compareTo(rhs.getName());
+ }
+ }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialogFragment.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialogFragment.java
new file mode 100644
index 0000000..efb7b3e
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialogFragment.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.media.MediaRouteSelector;
+
+/**
+ * Media route chooser dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteChooserDialog}. The application may subclass this
+ * dialog fragment to customize the dialog.
+ * </p>
+ */
+public class MediaRouteChooserDialogFragment extends DialogFragment {
+ private final String ARGUMENT_SELECTOR = "selector";
+
+ private MediaRouteSelector mSelector;
+
+ public MediaRouteChooserDialogFragment() {
+ setCancelable(true);
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can select.
+ *
+ * @return The selector, never null.
+ */
+ public MediaRouteSelector getRouteSelector() {
+ ensureRouteSelector();
+ return mSelector;
+ }
+
+ private void ensureRouteSelector() {
+ if (mSelector == null) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mSelector = MediaRouteSelector.fromBundle(args.getBundle(ARGUMENT_SELECTOR));
+ }
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can select.
+ * This method must be called before the fragment is added.
+ *
+ * @param selector The selector to set.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ ensureRouteSelector();
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ Bundle args = getArguments();
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putBundle(ARGUMENT_SELECTOR, selector.asBundle());
+ setArguments(args);
+
+ MediaRouteChooserDialog dialog = (MediaRouteChooserDialog)getDialog();
+ if (dialog != null) {
+ dialog.setRouteSelector(selector);
+ }
+ }
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ MediaRouteChooserDialog dialog = new MediaRouteChooserDialog(getActivity());
+ dialog.setRouteSelector(getRouteSelector());
+ return dialog;
+ }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
new file mode 100644
index 0000000..c0a0c05
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.support.v7.mediarouter.R;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+/**
+ * This class implements the route controller dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to control or disconnect from the currently selected route.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteControllerDialog extends Dialog {
+ private static final String TAG = "MediaRouteControllerDialog";
+
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+ private final MediaRouter.RouteInfo mRoute;
+
+ private Drawable mMediaRouteConnectingDrawable;
+ private Drawable mMediaRouteOnDrawable;
+ private Drawable mCurrentIconDrawable;
+
+ private LinearLayout mVolumeLayout;
+ private ImageView mVolumeDivider;
+ private SeekBar mVolumeSlider;
+ private boolean mVolumeSliderTouched;
+
+ private View mControlView;
+
+ private Button mDisconnectButton;
+
+ public MediaRouteControllerDialog(Context context) {
+ this(context, 0);
+ }
+
+ public MediaRouteControllerDialog(Context context, int theme) {
+ super(MediaRouterThemeHelper.createThemedContext(context), theme);
+ context = getContext();
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback();
+ mRoute = mRouter.getSelectedRoute();
+ }
+
+ /**
+ * Gets the route that this dialog is controlling.
+ */
+ public MediaRouter.RouteInfo getRoute() {
+ return mRoute;
+ }
+
+ /**
+ * Provides the subclass an opportunity to create a view that will
+ * be included within the body of the dialog to offer additional media controls
+ * for the currently playing content.
+ *
+ * @param savedInstanceState The dialog's saved instance state.
+ * @return The media control view, or null if none.
+ */
+ public View onCreateMediaControlView(Bundle savedInstanceState) {
+ return null;
+ }
+
+ /**
+ * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
+ *
+ * @return The media control view, or null if none.
+ */
+ public View getMediaControlView() {
+ return mControlView;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().requestFeature(Window.FEATURE_LEFT_ICON);
+
+ setContentView(R.layout.mr_media_route_controller_dialog);
+
+ mVolumeLayout = (LinearLayout)findViewById(R.id.media_route_volume_layout);
+ mVolumeSlider = (SeekBar)findViewById(R.id.media_route_volume_slider);
+ mVolumeDivider = (ImageView)findViewById(R.id.media_route_volume_divider);
+ mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mVolumeSliderTouched = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mVolumeSliderTouched = false;
+ updateVolume();
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ mRoute.requestSetVolume(progress);
+ }
+ }
+ });
+
+ mDisconnectButton = (Button)findViewById(R.id.media_route_disconnect_button);
+ mDisconnectButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mRoute.isSelected()) {
+ mRouter.getDefaultRoute().select();
+ }
+ dismiss();
+ }
+ });
+
+ if (update()) {
+ mControlView = onCreateMediaControlView(savedInstanceState);
+ if (mControlView != null) {
+ FrameLayout controlFrame =
+ (FrameLayout)findViewById(R.id.media_route_control_frame);
+ ImageView controlDivider =
+ (ImageView)findViewById(R.id.media_route_control_divider);
+ controlFrame.addView(controlFrame);
+ controlFrame.setVisibility(View.VISIBLE);
+ controlDivider.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback,
+ MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
+ update();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mRouter.removeCallback(mCallback);
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
+ mRoute.requestUpdateVolume(-1);
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ mRoute.requestUpdateVolume(1);
+ return true;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ return true;
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private boolean update() {
+ if (!mRoute.isSelected() || mRoute.isDefault()) {
+ dismiss();
+ return false;
+ }
+
+ setTitle(mRoute.getName());
+ updateVolume();
+
+ Drawable icon = getIconDrawable();
+ if (icon != mCurrentIconDrawable) {
+ mCurrentIconDrawable = icon;
+
+ // There seems to be a bug in the framework where feature drawables
+ // will not start animating unless they experience a transition from
+ // invisible to visible. So we force the drawable to be invisible here.
+ // The window will make the drawable visible when attached.
+ icon.setVisible(false, true);
+ getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, icon);
+ }
+ return true;
+ }
+
+ private Drawable getIconDrawable() {
+ if (mRoute.isConnecting()) {
+ if (mMediaRouteConnectingDrawable == null) {
+ mMediaRouteConnectingDrawable = MediaRouterThemeHelper.getThemeDrawable(
+ getContext(), R.attr.mediaRouteConnectingDrawable);
+ }
+ return mMediaRouteConnectingDrawable;
+ } else {
+ if (mMediaRouteOnDrawable == null) {
+ mMediaRouteOnDrawable = MediaRouterThemeHelper.getThemeDrawable(
+ getContext(), R.attr.mediaRouteOnDrawable);
+ }
+ return mMediaRouteOnDrawable;
+ }
+ }
+
+ private void updateVolume() {
+ if (!mVolumeSliderTouched) {
+ if (mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+ mVolumeLayout.setVisibility(View.VISIBLE);
+ mVolumeDivider.setVisibility(View.VISIBLE);
+ mVolumeSlider.setMax(mRoute.getVolumeMax());
+ mVolumeSlider.setProgress(mRoute.getVolume());
+ } else {
+ mVolumeLayout.setVisibility(View.GONE);
+ mVolumeDivider.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
+ update();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+ update();
+ }
+
+ @Override
+ public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+ if (route == mRoute) {
+ updateVolume();
+ }
+ }
+ }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java
new file mode 100644
index 0000000..a6dc205
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+/**
+ * Media route controller dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteControllerDialog}. The application may subclass this
+ * dialog fragment to customize the dialog.
+ * </p>
+ */
+public class MediaRouteControllerDialogFragment extends DialogFragment {
+ public MediaRouteControllerDialogFragment() {
+ setCancelable(true);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new MediaRouteControllerDialog(getActivity());
+ }
+}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java b/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
new file mode 100644
index 0000000..9b35da3
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouterThemeHelper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.app;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v7.mediarouter.R;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+
+final class MediaRouterThemeHelper {
+ private MediaRouterThemeHelper() {
+ }
+
+ public static Context createThemedContext(Context context) {
+ TypedValue value = new TypedValue();
+ boolean isLightTheme =
+ context.getTheme().resolveAttribute(R.attr.isLightTheme, value, true)
+ && value.data != 0;
+ return new ContextThemeWrapper(context,
+ isLightTheme ? R.style.Theme_MediaRouter_Light : R.style.Theme_MediaRouter);
+ }
+
+ public static int getThemeResource(Context context, int attr) {
+ TypedValue value = new TypedValue();
+ return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
+ }
+
+ public static Drawable getThemeDrawable(Context context, int attr) {
+ int res = getThemeResource(context, attr);
+ return res != 0 ? context.getResources().getDrawable(res) : null;
+ }
+}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java b/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java
index 5d2711c..27a2c27 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaControlIntent.java
@@ -66,8 +66,7 @@
* A media item id is an opaque token that represents the playback request.
* The application must supply the media item id when sending control requests to
* {@link #ACTION_PAUSE pause}, {@link #ACTION_RESUME resume}, {@link #ACTION_SEEK seek},
- * {@link #ACTION_GET_STATUS get status}, {@link #ACTION_GET_PROGRESS get progress},
- * or perform other actions to affect playback.
+ * {@link #ACTION_GET_STATUS get status}, or perform other actions to affect playback.
* </p><p>
* Each remote playback action is bound to a specific media item. If a
* media item has finished, been canceled or encountered an error, then most
@@ -171,16 +170,16 @@
* If the data uri specifies an HTTP or HTTPS scheme, then the destination is
* responsible for following HTTP redirects to a reasonable depth of at least 3
* levels as might typically be handled by a web browser. If an HTTP error
- * occurs, then the destination should send a status update back to the client
- * indicating the {@link MediaItemStatus#PLAYBACK_STATE_ERROR error}
- * {@link MediaItemStatus#KEY_PLAYBACK_STATE state}
- * and include the {@link MediaItemStatus#KEY_HTTP_STATUS_CODE HTTP status code}.
+ * occurs, then the destination should send a {@link MediaItemStatus status update}
+ * back to the client indicating the {@link MediaItemStatus#PLAYBACK_STATE_ERROR error}
+ * {@link MediaItemStatus#getPlaybackState() playback state}
+ * and include the {@link MediaItemStatus#getHttpStatusCode() HTTP status code}.
* </p>
*
* <h3>Request parameters</h3>
* <ul>
* <li>{@link #EXTRA_ITEM_QUEUE_BEHAVIOR}: specifies when the content should be played.
- * <li>{@link #EXTRA_ITEM_POSITION}: specifies the initial start position of the content.
+ * <li>{@link #EXTRA_ITEM_CONTENT_POSITION}: specifies the initial content playback position.
* <li>{@link #EXTRA_ITEM_METADATA}: specifies metadata associated with the
* content such as the title of a song.
* <li>{@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER}: specifies a {@link PendingIntent}
@@ -193,13 +192,20 @@
* <ul>
* <li>{@link #EXTRA_ITEM_ID}: specifies an opaque string identifier to use to refer
* to the media item in subsequent requests such as {@link #ACTION_PAUSE}.
+ * <li>{@link #EXTRA_ITEM_STATUS}: specifies the initial status of the item
+ * that has been enqueued.
* </ul>
*
* <h3>Status updates</h3>
* <p>
* If the client supplies a {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER status update receiver}
* then the media route provider is responsible for sending status updates to the receiver
- * when significant media item state changes occur.
+ * when significant media item state changes occur such as when playback starts or
+ * stops. The receiver will not be invoked for content playback position changes.
+ * The application may retrieve the current playback position when necessary
+ * using the {@link #ACTION_GET_STATUS} request.
+ * </p><p>
+ * Refer to {@link MediaItemStatus} for details.
* </p>
*
* <h3>Example</h3>
@@ -230,7 +236,6 @@
* @see #ACTION_STOP
* @see #ACTION_PAUSE
* @see #ACTION_RESUME
- * @see #ACTION_GET_PROGRESS
* @see #ACTION_GET_STATUS
*/
public static final String ACTION_PLAY = "android.media.intent.action.PLAY";
@@ -257,12 +262,12 @@
* <li>{@link #EXTRA_ITEM_ID}: specifies the media item id of the playback to be
* controlled. This value was returned as a result from the
* {@link #ACTION_PLAY play} action.
- * <li>{@link #EXTRA_ITEM_POSITION}: specifies the new position of the content.
+ * <li>{@link #EXTRA_ITEM_CONTENT_POSITION}: specifies the new position of the content.
* </ul>
*
* <h3>Result data</h3>
* <ul>
- * <li>(none)
+ * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
* </ul>
*
* @see MediaRouter.RouteInfo#sendControlRequest
@@ -292,7 +297,7 @@
*
* <h3>Result data</h3>
* <ul>
- * <li>(none)
+ * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
* </ul>
*
* @see MediaRouter.RouteInfo#sendControlRequest
@@ -323,7 +328,7 @@
*
* <h3>Result data</h3>
* <ul>
- * <li>(none)
+ * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
* </ul>
*
* @see MediaRouter.RouteInfo#sendControlRequest
@@ -351,7 +356,7 @@
*
* <h3>Result data</h3>
* <ul>
- * <li>(none)
+ * <li>{@link #EXTRA_ITEM_STATUS}: specifies the status of the stream.
* </ul>
*
* @see MediaRouter.RouteInfo#sendControlRequest
@@ -361,13 +366,13 @@
public static final String ACTION_RESUME = "android.media.intent.action.RESUME";
/**
- * Media control action: Get media item status.
+ * Media control action: Get media item playback status and progress information.
* <p>
* Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
* media control.
* </p><p>
- * This action asks a remote playback route to provide updated status information
- * about playback of the specified media item.
+ * This action asks a remote playback route to provide updated playback status and progress
+ * information about the specified media item.
* </p>
*
* <h3>Request parameters</h3>
@@ -389,35 +394,6 @@
public static final String ACTION_GET_STATUS = "android.media.intent.action.GET_STATUS";
/**
- * Media control action: Get media item progress.
- * <p>
- * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
- * media control.
- * </p><p>
- * This action asks a remote playback route to provide media item playback
- * progress information. The client may use this information to provide feedback
- * to the user about the current playback position and overall duration.
- * </p>
- *
- * <h3>Request parameters</h3>
- * <ul>
- * <li>{@link #EXTRA_ITEM_ID}: specifies the media item id of the playback to be
- * controlled. This value was returned as a result from the
- * {@link #ACTION_PLAY play} action.
- * </ul>
- *
- * <h3>Result data</h3>
- * <ul>
- * <li>{@link #EXTRA_ITEM_POSITION}: specifies the content playback position.
- * <li>{@link #EXTRA_ITEM_DURATION}: specifies the overall duration of the content.
- * </ul>
- *
- * @see MediaRouter.RouteInfo#sendControlRequest
- * @see #CATEGORY_REMOTE_PLAYBACK
- */
- public static final String ACTION_GET_PROGRESS = "android.media.intent.action.GET_PROGRESS";
-
- /**
* Integer extra: Media item queue behavior.
* <p>
* Used with {@link #ACTION_PLAY} to specify when the requested should be
@@ -473,39 +449,23 @@
public static final int ITEM_QUEUE_BEHAVIOR_PLAY_LATER = 2;
/**
- * Integer extra: Media item content position.
+ * Double extra: Media item content position.
* <p>
* Used with {@link #ACTION_PLAY} to specify the starting playback position.
* </p><p>
* Used with {@link #ACTION_SEEK} to set a new playback position.
* </p><p>
- * Used with {@link #ACTION_GET_PROGRESS} to report the current playback position.
- * </p><p>
- * The value is an integer number of seconds from the beginning of the content.
+ * The value is a double-precision floating point number of seconds
+ * from the beginning of the content.
* <p>
*
* @see #ACTION_PLAY
* @see #ACTION_SEEK
- * @see #ACTION_GET_PROGRESS
*/
- public static final String EXTRA_ITEM_POSITION =
+ public static final String EXTRA_ITEM_CONTENT_POSITION =
"android.media.intent.extra.ITEM_POSITION";
/**
- * Integer extra: Media item content duration.
- * <p>
- * Used with {@link #ACTION_GET_PROGRESS} to report the overall duration of the
- * media item content.
- * </p><p>
- * The value is an integer number of seconds from the beginning of the content.
- * <p>
- *
- * @see #ACTION_GET_PROGRESS
- */
- public static final String EXTRA_ITEM_DURATION =
- "android.media.intent.extra.ITEM_DURATION";
-
- /**
* Bundle extra: Media item metadata.
* <p>
* Used with {@link #ACTION_PLAY} to specify metadata associated with the content
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java b/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java
index 6458ae2..2b8bfff 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaItemMetadata.java
@@ -16,11 +16,18 @@
package android.support.v7.media;
-import android.graphics.Bitmap;
import android.os.Bundle;
/**
* Constants for specifying metadata about a media item as a {@link Bundle}.
+ * <p>
+ * Media item metadata is described as a bundle of key/value pairs as defined
+ * in this class. The documentation specifies the type of value associated
+ * with each key.
+ * </p><p>
+ * An application may specify additional custom metadata keys but there is no guarantee
+ * that they will be recognized by the destination.
+ * </p>
*/
public final class MediaItemMetadata {
/*
@@ -33,99 +40,83 @@
}
/**
- * Metadata key: Album artist name.
+ * String key: Album artist name.
* <p>
* The value is a string suitable for display.
* </p>
*/
- public static final String KEY_ALBUM_ARTIST = "ALBUM_ARTIST";
+ public static final String KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
/**
- * Metadata key: Album title.
+ * String key: Album title.
* <p>
* The value is a string suitable for display.
* </p>
*/
- public static final String KEY_ALBUM_TITLE = "ALBUM_TITLE";
+ public static final String KEY_ALBUM_TITLE = "android.media.metadata.ALBUM_TITLE";
/**
- * Metadata key: Artwork Uri.
+ * String key: Artwork Uri.
* <p>
* The value is a string URI for an image file associated with the media item,
* such as album or cover art.
* </p>
- *
- * @see #KEY_ARTWORK_BITMAP
*/
- public static final String KEY_ARTWORK_URI = "ARTWORK_URI";
+ public static final String KEY_ARTWORK_URI = "android.media.metadata.ARTWORK_URI";
/**
- * Metadata key: Artwork Bitmap.
- * <p>
- * The value is a {@link Bitmap} for an image file associated with the media item,
- * such as album or cover art.
- * </p><p>
- * Because bitmaps can be large, use {@link #KEY_ARTWORK_URI} instead if the artwork can
- * be downloaded from the network.
- * </p>
- *
- * @see #KEY_ARTWORK_URI
- */
- public static final String KEY_ARTWORK_BITMAP = "ARTWORK_BITMAP";
-
- /**
- * Metadata key: Artist name.
+ * String key: Artist name.
* <p>
* The value is a string suitable for display.
* </p>
*/
- public static final String KEY_ARTIST = "ARTIST";
+ public static final String KEY_ARTIST = "android.media.metadata.ARTIST";
/**
- * Metadata key: Author name.
+ * String key: Author name.
* <p>
* The value is a string suitable for display.
* </p>
*/
- public static final String KEY_AUTHOR = "AUTHOR";
+ public static final String KEY_AUTHOR = "android.media.metadata.AUTHOR";
/**
- * Metadata key: Composer name.
+ * String key: Composer name.
* <p>
* The value is a string suitable for display.
* </p>
*/
- public static final String KEY_COMPOSER = "COMPOSER";
+ public static final String KEY_COMPOSER = "android.media.metadata.COMPOSER";
/**
- * Metadata key: Track title.
+ * String key: Track title.
* <p>
* The value is a string suitable for display.
* </p>
*/
- public static final String KEY_TITLE = "TITLE";
+ public static final String KEY_TITLE = "android.media.metadata.TITLE";
/**
- * Metadata key: Year of publication.
+ * Integer key: Year of publication.
* <p>
* The value is an integer year number.
* </p>
*/
- public static final String KEY_YEAR = "YEAR";
+ public static final String KEY_YEAR = "android.media.metadata.YEAR";
/**
- * Metadata key: Track number (such as a track on a CD).
+ * Integer key: Track number (such as a track on a CD).
* <p>
* The value is a one-based integer track number.
* </p>
*/
- public static final String KEY_TRACK_NUMBER = "TRACK_NUMBER";
+ public static final String KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
/**
- * Metadata key: Disc number within a collection.
+ * Integer key: Disc number within a collection.
* <p>
* The value is a one-based integer disc number.
* </p>
*/
- public static final String KEY_DISC_NUMBER = "DISC_NUMBER";
+ public static final String KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java b/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java
index 4a53113..fe49b30 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaItemStatus.java
@@ -16,26 +16,52 @@
package android.support.v7.media;
+import android.app.PendingIntent;
import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.util.TimeUtils;
/**
- * Constants for specifying status about a media item as a {@link Bundle}.
+ * Describes the playback status of a media item.
+ * <p>
+ * As a media item is played, it transitions through a sequence of states including:
+ * {@link #PLAYBACK_STATE_QUEUED queued}, {@link #PLAYBACK_STATE_BUFFERING buffering},
+ * {@link #PLAYBACK_STATE_PLAYING playing}, {@link #PLAYBACK_STATE_PAUSED paused},
+ * {@link #PLAYBACK_STATE_STOPPED stopped}, {@link #PLAYBACK_STATE_CANCELED canceled},
+ * {@link #PLAYBACK_STATE_ERROR error}. Refer to the documentation of each state
+ * for an explanation of its meaning.
+ * </p><p>
+ * While the item is playing, the playback status may also include progress information
+ * about the {@link #getContentPosition content position} and
+ * {@link #getContentDuration content duration} although not all route destinations
+ * will report it.
+ * </p><p>
+ * To monitor playback status, the application should supply a {@link PendingIntent} to use
+ * as the {@link MediaControlIntent#EXTRA_ITEM_STATUS_UPDATE_RECEIVER status update receiver}
+ * for a given {@link MediaControlIntent#ACTION_PLAY playback request}. Note that
+ * the status update receiver will only be invoked for major status changes such as a
+ * transition from playing to stopped.
+ * </p><p class="note">
+ * The status update receiver will not be invoked for minor progress updates such as
+ * changes to playback position or duration. If the application wants to monitor
+ * playback progress, then it must use the
+ * {@link MediaControlIntent#ACTION_GET_STATUS get status request} to poll for changes
+ * periodically and estimate the playback position while playing. Note that there may
+ * be a significant power impact to polling so the application is advised only
+ * to poll when the screen is on and never more than about once every 5 seconds or so.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
*/
public final class MediaItemStatus {
- private MediaItemStatus() {
- }
+ private static final String KEY_TIMESTAMP = "timestamp";
+ private static final String KEY_PLAYBACK_STATE = "playbackState";
+ private static final String KEY_CONTENT_POSITION = "contentPosition";
+ private static final String KEY_CONTENT_DURATION = "contentDuration";
+ private static final String KEY_HTTP_STATUS_CODE = "httpStatusCode";
+ private static final String KEY_EXTRAS = "extras";
- /**
- * Status key: Playback state.
- * <p>
- * Specifies the playback state of a media item.
- * </p><p>
- * Value is one of {@link #PLAYBACK_STATE_QUEUED}, {@link #PLAYBACK_STATE_PLAYING},
- * {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_BUFFERING},
- * {@link #PLAYBACK_STATE_CANCELED}, or {@link #PLAYBACK_STATE_ERROR}.
- * </p>
- */
- public static final String KEY_PLAYBACK_STATE = "PLAYBACK_STATE";
+ private final Bundle mBundle;
/**
* Playback state: Queued.
@@ -113,18 +139,208 @@
*/
public static final int PLAYBACK_STATE_ERROR = 6;
+ private MediaItemStatus(Bundle bundle) {
+ mBundle = bundle;
+ }
+
/**
- * Status key: HTTP status code.
+ * Gets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ *
+ * @return The status timestamp in the {@link SystemClock#elapsedRealtime()} time base.
+ */
+ public long getTimestamp() {
+ return mBundle.getLong(KEY_TIMESTAMP);
+ }
+
+ /**
+ * Gets the playback state of the media item.
+ *
+ * @return The playback state. One of {@link #PLAYBACK_STATE_QUEUED},
+ * {@link #PLAYBACK_STATE_PLAYING},
+ * {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_BUFFERING},
+ * {@link #PLAYBACK_STATE_CANCELED}, or {@link #PLAYBACK_STATE_ERROR}.
+ */
+ public int getPlaybackState() {
+ return mBundle.getInt(KEY_PLAYBACK_STATE, PLAYBACK_STATE_CANCELED);
+ }
+
+ /**
+ * Gets the content playback position as a floating point number of seconds
+ * from the beginning of the content.
+ *
+ * @return The content playback position in seconds, or -1 if unknown.
+ */
+ public double getContentPosition() {
+ return mBundle.getDouble(KEY_CONTENT_POSITION, -1);
+ }
+
+ /**
+ * Gets the total duration of the content to be played as a floating point number
+ * of seconds.
+ *
+ * @return The content duration in seconds, or -1 if unknown.
+ */
+ public double getContentDuration() {
+ return mBundle.getDouble(KEY_CONTENT_DURATION, -1);
+ }
+
+ /**
+ * Gets the associated HTTP status code.
* <p>
* Specifies the HTTP status code that was encountered when the content
* was requested after all redirects were followed. This key only needs to
* specified when the content uri uses the HTTP or HTTPS scheme and an error
* occurred. This key may be omitted if the content was able to be played
* successfully; there is no need to report a 200 (OK) status code.
- * </p><p>
- * Value is an integer HTTP status code such as 401 (Unauthorized), 404 (Not Found),
- * or 500 (Server Error).
* </p>
+ *
+ * @return The HTTP status code from playback such as 401 (Unauthorized), 404 (Not Found),
+ * or 500 (Server Error), or 0 if none.
*/
- public static final String KEY_HTTP_STATUS_CODE = "HTTP_STATUS_CODE";
+ public int getHttpStatusCode() {
+ return mBundle.getInt(KEY_HTTP_STATUS_CODE, 0);
+ }
+
+ /**
+ * Gets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaItemStatus{ ");
+ result.append("timestamp=");
+ TimeUtils.formatDuration(SystemClock.elapsedRealtime() - getTimestamp(), result);
+ result.append(" ms ago");
+ result.append(", playbackState=").append(getPlaybackState());
+ result.append(", contentPosition=").append(getContentPosition());
+ result.append(", contentDuration=").append(getContentDuration());
+ result.append(", httpStatusCode=").append(getHttpStatusCode());
+ result.append(", extras=").append(getExtras());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaItemStatus fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaItemStatus(bundle) : null;
+ }
+
+ /**
+ * Builder for {@link MediaItemStatus media item status objects}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Creates a media item status builder using the current time as the
+ * reference timestamp.
+ *
+ * @param playbackState The item playback state.
+ */
+ public Builder(int playbackState) {
+ mBundle = new Bundle();
+ setTimestamp(SystemClock.elapsedRealtime());
+ setPlaybackState(playbackState);
+ }
+
+ /**
+ * Creates a media item status builder whose initial contents are
+ * copied from an existing status.
+ */
+ public Builder(MediaItemStatus status) {
+ if (status == null) {
+ throw new IllegalArgumentException("status must not be null");
+ }
+
+ mBundle = new Bundle(status.mBundle);
+ }
+
+ /**
+ * Sets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ */
+ public Builder setTimestamp(long elapsedRealtimeTimestamp) {
+ mBundle.putLong(KEY_TIMESTAMP, elapsedRealtimeTimestamp);
+ return this;
+ }
+
+ /**
+ * Sets the playback state of the media item.
+ */
+ public Builder setPlaybackState(int playbackState) {
+ mBundle.putInt(KEY_PLAYBACK_STATE, playbackState);
+ return this;
+ }
+
+ /**
+ * Sets the content playback position as a floating point number of seconds
+ * from the beginning of the content.
+ */
+ public Builder setContentPosition(double positionSeconds) {
+ mBundle.putDouble(KEY_CONTENT_POSITION, positionSeconds);
+ return this;
+ }
+
+ /**
+ * Sets the total duration of the content to be played as a floating point number
+ * of seconds.
+ */
+ public Builder setContentDuration(double durationSeconds) {
+ mBundle.putDouble(KEY_CONTENT_DURATION, durationSeconds);
+ return this;
+ }
+
+ /**
+ * Sets the associated HTTP status code.
+ * <p>
+ * Specifies the HTTP status code that was encountered when the content
+ * was requested after all redirects were followed. This key only needs to
+ * specified when the content uri uses the HTTP or HTTPS scheme and an error
+ * occurred. This key may be omitted if the content was able to be played
+ * successfully; there is no need to report a 200 (OK) status code.
+ * </p>
+ */
+ public Builder setHttpStatusCode(int httpStatusCode) {
+ mBundle.putInt(KEY_HTTP_STATUS_CODE, httpStatusCode);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaItemStatus media item status object}.
+ */
+ public MediaItemStatus build() {
+ return new MediaItemStatus(mBundle);
+ }
+ }
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteDescriptor.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteDescriptor.java
new file mode 100644
index 0000000..af267c0
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteDescriptor.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.media;
+
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the properties of a route.
+ * <p>
+ * Each route is uniquely identified by an opaque id string. This token
+ * may take any form as long as it is unique within the media route provider.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteDescriptor {
+ private static final String KEY_ID = "id";
+ private static final String KEY_NAME = "name";
+ private static final String KEY_DESCRIPTION = "status";
+ private static final String KEY_ENABLED = "enabled";
+ private static final String KEY_CONNECTING = "connecting";
+ private static final String KEY_CONTROL_FILTERS = "controlFilters";
+ private static final String KEY_PLAYBACK_TYPE = "playbackType";
+ private static final String KEY_PLAYBACK_STREAM = "playbackStream";
+ private static final String KEY_VOLUME = "volume";
+ private static final String KEY_VOLUME_MAX = "volumeMax";
+ private static final String KEY_VOLUME_HANDLING = "volumeHandling";
+ private static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
+ private static final String KEY_EXTRAS = "extras";
+
+ private final Bundle mBundle;
+ private List<IntentFilter> mControlFilters;
+
+ private MediaRouteDescriptor(Bundle bundle, List<IntentFilter> controlFilters) {
+ mBundle = bundle;
+ mControlFilters = controlFilters;
+ }
+
+ /**
+ * Gets the unique id of the route.
+ */
+ public String getId() {
+ return mBundle.getString(KEY_ID);
+ }
+
+ /**
+ * Gets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ */
+ public String getName() {
+ return mBundle.getString(KEY_NAME);
+ }
+
+ /**
+ * Gets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ */
+ public String getDescription() {
+ return mBundle.getString(KEY_DESCRIPTION);
+ }
+
+ /**
+ * Gets whether the route is enabled.
+ */
+ public boolean isEnabled() {
+ return mBundle.getBoolean(KEY_ENABLED, true);
+ }
+
+ /**
+ * Gets whether the route is connecting.
+ */
+ public boolean isConnecting() {
+ return mBundle.getBoolean(KEY_CONNECTING, false);
+ }
+
+ /**
+ * Gets the route's {@link MediaControlIntent media control intent} filters.
+ */
+ public List<IntentFilter> getControlFilters() {
+ ensureControlFilters();
+ return mControlFilters;
+ }
+
+ private void ensureControlFilters() {
+ if (mControlFilters == null) {
+ mControlFilters = mBundle.<IntentFilter>getParcelableArrayList(KEY_CONTROL_FILTERS);
+ if (mControlFilters == null) {
+ mControlFilters = Collections.<IntentFilter>emptyList();
+ }
+ }
+ }
+
+ /**
+ * Gets the route's playback type.
+ */
+ public int getPlaybackType() {
+ return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
+ }
+
+ /**
+ * Gets the route's playback stream.
+ */
+ public int getPlaybackStream() {
+ return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
+ }
+
+ /**
+ * Gets the route's current volume, or 0 if unknown.
+ */
+ public int getVolume() {
+ return mBundle.getInt(KEY_VOLUME);
+ }
+
+ /**
+ * Gets the route's maximum volume, or 0 if unknown.
+ */
+ public int getVolumeMax() {
+ return mBundle.getInt(KEY_VOLUME_MAX);
+ }
+
+ /**
+ * Gets the route's volume handling.
+ */
+ public int getVolumeHandling() {
+ return mBundle.getInt(KEY_VOLUME_HANDLING,
+ MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
+ }
+
+ /**
+ * Gets the route's presentation display id, or -1 if none.
+ */
+ public int getPresentationDisplayId() {
+ return mBundle.getInt(KEY_PRESENTATION_DISPLAY_ID, -1);
+ }
+
+ /**
+ * Gets a bundle of extras for this route descriptor.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ /**
+ * Returns true if the route descriptor has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureControlFilters();
+ if (TextUtils.isEmpty(getId())
+ || TextUtils.isEmpty(getName())
+ || mControlFilters.contains(null)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteDescriptor{ ");
+ result.append("id=").append(getId());
+ result.append(", name=").append(getName());
+ result.append(", description=").append(getDescription());
+ result.append(", isEnabled=").append(isEnabled());
+ result.append(", isConnecting=").append(isConnecting());
+ result.append(", controlFilters=").append(Arrays.toString(getControlFilters().toArray()));
+ result.append(", playbackType=").append(getPlaybackType());
+ result.append(", playbackStream=").append(getPlaybackStream());
+ result.append(", volume=").append(getVolume());
+ result.append(", volumeMax=").append(getVolumeMax());
+ result.append(", volumeHandling=").append(getVolumeHandling());
+ result.append(", presentationDisplayId=").append(getPresentationDisplayId());
+ result.append(", extras=").append(getExtras());
+ result.append(", isValid=").append(isValid());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteDescriptor fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteDescriptor(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteDescriptor media route descriptors}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+ private ArrayList<IntentFilter> mControlFilters;
+
+ /**
+ * Creates a media route descriptor builder.
+ *
+ * @param id The unique id of the route.
+ * @param name The user-visible name of the route.
+ */
+ public Builder(String id, String name) {
+ mBundle = new Bundle();
+ setId(id);
+ setName(name);
+ }
+
+ /**
+ * Creates a media route descriptor builder whose initial contents are
+ * copied from an existing descriptor.
+ */
+ public Builder(MediaRouteDescriptor descriptor) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+
+ mBundle = new Bundle(descriptor.mBundle);
+
+ descriptor.ensureControlFilters();
+ if (!descriptor.mControlFilters.isEmpty()) {
+ mControlFilters = new ArrayList<IntentFilter>(descriptor.mControlFilters);
+ }
+ }
+
+ /**
+ * Sets the unique id of the route.
+ */
+ public Builder setId(String id) {
+ mBundle.putString(KEY_ID, id);
+ return this;
+ }
+
+ /**
+ * Sets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ */
+ public Builder setName(String name) {
+ mBundle.putString(KEY_NAME, name);
+ return this;
+ }
+
+ /**
+ * Sets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ */
+ public Builder setDescription(String description) {
+ mBundle.putString(KEY_DESCRIPTION, description);
+ return this;
+ }
+
+ /**
+ * Sets whether the route is enabled.
+ * <p>
+ * Disabled routes represent routes that a route provider knows about, such as paired
+ * Wifi Display receivers, but that are not currently available for use.
+ * </p>
+ */
+ public Builder setEnabled(boolean enabled) {
+ mBundle.putBoolean(KEY_ENABLED, enabled);
+ return this;
+ }
+
+ /**
+ * Sets whether the route is in the process of connecting and is not yet
+ * ready for use.
+ */
+ public Builder setConnecting(boolean connecting) {
+ mBundle.putBoolean(KEY_CONNECTING, connecting);
+ return this;
+ }
+
+ /**
+ * Adds a {@link MediaControlIntent media control intent} filter for the route.
+ */
+ public Builder addControlFilter(IntentFilter filter) {
+ if (filter == null) {
+ throw new IllegalArgumentException("filter must not be null");
+ }
+
+ if (mControlFilters == null) {
+ mControlFilters = new ArrayList<IntentFilter>();
+ }
+ if (!mControlFilters.contains(filter)) {
+ mControlFilters.add(filter);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of {@link MediaControlIntent media control intent} filters for the route.
+ */
+ public Builder addControlFilters(Collection<IntentFilter> filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException("filters must not be null");
+ }
+
+ if (!filters.isEmpty()) {
+ for (IntentFilter filter : filters) {
+ addControlFilter(filter);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the route's playback type.
+ */
+ public Builder setPlaybackType(int playbackType) {
+ mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
+ return this;
+ }
+
+ /**
+ * Sets the route's playback stream.
+ */
+ public Builder setPlaybackStream(int playbackStream) {
+ mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
+ return this;
+ }
+
+ /**
+ * Sets the route's current volume, or 0 if unknown.
+ */
+ public Builder setVolume(int volume) {
+ mBundle.putInt(KEY_VOLUME, volume);
+ return this;
+ }
+
+ /**
+ * Sets the route's maximum volume, or 0 if unknown.
+ */
+ public Builder setVolumeMax(int volumeMax) {
+ mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
+ return this;
+ }
+
+ /**
+ * Sets the route's volume handling.
+ */
+ public Builder setVolumeHandling(int volumeHandling) {
+ mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
+ return this;
+ }
+
+ /**
+ * Sets the route's presentation display id, or -1 if none.
+ */
+ public Builder setPresentationDisplayId(int presentationDisplayId) {
+ mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this route descriptor.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteDescriptor media route descriptor}.
+ */
+ public MediaRouteDescriptor build() {
+ if (mControlFilters != null) {
+ mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, mControlFilters);
+ }
+ return new MediaRouteDescriptor(mBundle, mControlFilters);
+ }
+ }
+}
\ No newline at end of file
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteDiscoveryRequest.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteDiscoveryRequest.java
new file mode 100644
index 0000000..f4d2f5f
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteDiscoveryRequest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.media;
+
+import android.os.Bundle;
+
+/**
+ * Describes the kinds of routes that the media router would like to discover
+ * and whether to perform active scanning.
+ * <p>
+ * This object is immutable once created.
+ * </p>
+ */
+public final class MediaRouteDiscoveryRequest {
+ private static final String KEY_SELECTOR = "selector";
+ private static final String KEY_ACTIVE_SCAN = "activeScan";
+
+ private final Bundle mBundle;
+ private MediaRouteSelector mSelector;
+
+ /**
+ * Creates a media route discovery request.
+ *
+ * @param selector The route selector that specifies the kinds of routes to discover.
+ * @param activeScan True if active scanning should be performed.
+ */
+ public MediaRouteDiscoveryRequest(MediaRouteSelector selector, boolean activeScan) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ mBundle = new Bundle();
+ mSelector = selector;
+ mBundle.putBundle(KEY_SELECTOR, selector.asBundle());
+ mBundle.putBoolean(KEY_ACTIVE_SCAN, activeScan);
+ }
+
+ private MediaRouteDiscoveryRequest(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets the route selector that specifies the kinds of routes to discover.
+ */
+ public MediaRouteSelector getSelector() {
+ ensureSelector();
+ return mSelector;
+ }
+
+ private void ensureSelector() {
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.fromBundle(mBundle.getBundle(KEY_SELECTOR));
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Returns true if active scanning should be performed.
+ *
+ * @see MediaRouter#CALLBACK_FLAG_ACTIVE_SCAN
+ */
+ public boolean isActiveScan() {
+ return mBundle.getBoolean(KEY_ACTIVE_SCAN);
+ }
+
+ /**
+ * Returns true if the discovery request has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureSelector();
+ return mSelector.isValid();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MediaRouteDiscoveryRequest) {
+ MediaRouteDiscoveryRequest other = (MediaRouteDiscoveryRequest)o;
+ return getSelector().equals(other.getSelector())
+ && isActiveScan() == other.isActiveScan();
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getSelector().hashCode() ^ (isActiveScan() ? 1 : 0);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("DiscoveryRequest{ selector=").append(getSelector());
+ result.append(", activeScan=").append(isActiveScan());
+ result.append(", isValid=").append(isValid());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteDiscoveryRequest fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteDiscoveryRequest(bundle) : null;
+ }
+}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
index c78171d..aec7b2b 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
@@ -18,42 +18,54 @@
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
-import android.os.Parcelable;
import android.support.v7.media.MediaRouter.ControlRequestCallback;
import android.text.TextUtils;
-import java.util.concurrent.CopyOnWriteArrayList;
-
/**
* Media route providers are used to publish additional media routes for
* use within an application. Media route providers may also be declared
* as a service to publish additional media routes to all applications
* in the system.
* <p>
- * Applications and services should extend this class to publish additional media routes
- * to the {@link MediaRouter}. To make additional media routes available within
- * your application, call {@link MediaRouter#addProvider} to add your provider to
- * the media router. To make additional media routes available to all applications
- * in the system, register a media route provider service in your manifest.
+ * The purpose of a media route provider is to discover media routes that satisfy
+ * the criteria specified by the current {@link MediaRouteDiscoveryRequest} and publish a
+ * {@link MediaRouteProviderDescriptor} with information about each route by calling
+ * {@link #setDescriptor} to notify the currently registered {@link Callback}.
+ * </p><p>
+ * The provider should watch for changes to the discovery request by implementing
+ * {@link #onDiscoveryRequestChanged} and updating the set of routes that it is
+ * attempting to discover. It should also handle route control requests such
+ * as volume changes or {@link MediaControlIntent media control intents}
+ * by implementing {@link #onCreateRouteController} to return a {@link RouteController}
+ * for a particular route.
+ * </p><p>
+ * A media route provider may be used privately within the scope of a single
+ * application process by calling {@link MediaRouter#addProvider MediaRouter.addProvider}
+ * to add it to the local {@link MediaRouter}. A media route provider may also be made
+ * available globally to all applications by registering a {@link MediaRouteProviderService}
+ * in the provider's manifest. When the media route provider is registered
+ * as a service, all applications that use the media router API will be able to
+ * discover and used the provider's routes without having to install anything else.
* </p><p>
* This object must only be accessed on the main thread.
* </p>
*/
public abstract class MediaRouteProvider {
private static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
+ private static final int MSG_DELIVER_DISCOVERY_REQUEST_CHANGED = 2;
private final Context mContext;
private final ProviderMetadata mMetadata;
private final ProviderHandler mHandler = new ProviderHandler();
- private final CopyOnWriteArrayList<Callback> mCallbacks =
- new CopyOnWriteArrayList<Callback>();
- private ProviderDescriptor mDescriptor;
+ private Callback mCallback;
+
+ private MediaRouteDiscoveryRequest mDiscoveryRequest;
+ private boolean mPendingDiscoveryRequestChange;
+
+ private MediaRouteProviderDescriptor mDescriptor;
private boolean mPendingDescriptorChange;
/**
@@ -85,37 +97,124 @@
return mContext;
}
- final ProviderMetadata getMetadata() {
+ /**
+ * Gets the provider's handler which is associated with the main thread.
+ */
+ public final Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * Gets some metadata about the provider's implementation.
+ */
+ public final ProviderMetadata getMetadata() {
return mMetadata;
}
/**
+ * Sets a callback to invoke when the provider's descriptor changes.
+ *
+ * @param callback The callback to use, or null if none.
+ */
+ public final void setCallback(Callback callback) {
+ MediaRouter.checkCallingThread();
+ mCallback = callback;
+ }
+
+ /**
+ * Gets the current discovery request which informs the provider about the
+ * kinds of routes to discover and whether to perform active scanning.
+ *
+ * @return The current discovery request, or null if no discovery is needed at this time.
+ *
+ * @see #onDiscoveryRequestChanged
+ */
+ public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
+ return mDiscoveryRequest;
+ }
+
+ /**
+ * Sets a discovery request to inform the provider about the kinds of
+ * routes that its clients would like to discover and whether to perform active scanning.
+ *
+ * @param request The discovery request, or null if no discovery is needed at this time.
+ *
+ * @see #onDiscoveryRequestChanged
+ */
+ public final void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ MediaRouter.checkCallingThread();
+
+ if (mDiscoveryRequest == request
+ || (mDiscoveryRequest != null && mDiscoveryRequest.equals(request))) {
+ return;
+ }
+
+ mDiscoveryRequest = request;
+ if (!mPendingDiscoveryRequestChange) {
+ mPendingDiscoveryRequestChange = true;
+ mHandler.sendEmptyMessage(MSG_DELIVER_DISCOVERY_REQUEST_CHANGED);
+ }
+ }
+
+ private void deliverDiscoveryRequestChanged() {
+ mPendingDiscoveryRequestChange = false;
+ onDiscoveryRequestChanged(mDiscoveryRequest);
+ }
+
+ /**
+ * Called by the media router when the {@link MediaRouteDiscoveryRequest discovery request}
+ * has changed.
+ * <p>
+ * Whenever an applications calls {@link MediaRouter#addCallback} to register
+ * a callback, it also provides a selector to specify the kinds of routes that
+ * it is interested in. The media router combines all of these selectors together
+ * to generate a {@link MediaRouteDiscoveryRequest} and notifies each provider when a change
+ * occurs by calling {@link #setDiscoveryRequest} which posts a message to invoke
+ * this method asynchronously.
+ * </p><p>
+ * The provider should examine the {@link MediaControlIntent media control categories}
+ * in the discovery request's {@link MediaRouteSelector selector} to determine what
+ * kinds of routes it should try to discover and whether it should perform active
+ * or passive scans. In many cases, the provider may be able to save power by
+ * determining that the selector does not contain any categories that it supports
+ * and it can therefore avoid performing any scans at all.
+ * </p>
+ *
+ * @param request The new discovery request, or null if no discovery is needed at this time.
+ *
+ * @see MediaRouter#addCallback
+ */
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ }
+
+ /**
* Gets the provider's descriptor.
* <p>
* The descriptor describes the state of the media route provider and
* the routes that it publishes. Watch for changes to the descriptor
- * by registering a {@link Callback callback} with {@link #addCallback}.
+ * by registering a {@link Callback callback} with {@link #setCallback}.
* </p>
*
- * @return The media route provider descriptor, or null if none. This object
- * and all of its contents should be treated as if it were immutable so that it is
- * safe for clients to cache it.
+ * @return The media route provider descriptor, or null if none.
+ *
+ * @see Callback#onDescriptorChanged
*/
- public final ProviderDescriptor getDescriptor() {
+ public final MediaRouteProviderDescriptor getDescriptor() {
return mDescriptor;
}
/**
* Sets the provider's descriptor.
* <p>
- * Asynchronously notifies all registered {@link Callback callbacks} about the change.
+ * The provider must call this method to notify the currently registered
+ * {@link Callback callback} about the change to the provider's descriptor.
* </p>
*
* @param descriptor The updated route provider descriptor, or null if none.
- * This object and all of its contents should be treated as if it were immutable
- * so that it is safe for clients to cache it.
+ *
+ * @see Callback#onDescriptorChanged
*/
- public final void setDescriptor(ProviderDescriptor descriptor) {
+ public final void setDescriptor(MediaRouteProviderDescriptor descriptor) {
MediaRouter.checkCallingThread();
if (mDescriptor != descriptor) {
@@ -130,44 +229,12 @@
private void deliverDescriptorChanged() {
mPendingDescriptorChange = false;
- if (!mCallbacks.isEmpty()) {
- final ProviderDescriptor currentDescriptor = mDescriptor;
- for (Callback callback : mCallbacks) {
- callback.onDescriptorChanged(this, currentDescriptor);
- }
+ if (mCallback != null) {
+ mCallback.onDescriptorChanged(this, mDescriptor);
}
}
/**
- * Adds a callback to be invoked on the main thread when information about a route
- * provider and its routes changes.
- *
- * @param callback The callback to add.
- */
- public final void addCallback(Callback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("callback");
- }
-
- if (!mCallbacks.contains(callback)) {
- mCallbacks.add(callback);
- }
- }
-
- /**
- * Removes a callback.
- *
- * @param callback The callback to remove.
- */
- public final void removeCallback(Callback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("callback");
- }
-
- mCallbacks.remove(callback);
- }
-
- /**
* Called by the media router to obtain a route controller for a particular route.
* <p>
* The media router will invoke the {@link RouteController#onRelease} method of the route
@@ -183,38 +250,19 @@
}
/**
- * Called by the media router when the provider should start actively scanning
- * for changes to routes.
+ * Describes properties of the route provider's implementation.
* <p>
- * Typically this callback is invoked when the media route picker dialog has been
- * opened by the user to ensure that the set of known routes is fresh and up to date.
+ * This object is immutable once created.
* </p>
- *
- * @see ProviderDescriptor#setActiveScanRequired
*/
- public void onStartActiveScan() {
- }
-
- /**
- * Called by the media router when the provider should stop actively scanning
- * for changes to routes. The provider may continue passively scanning for changes
- * to routes as long as its scans are low power and non-intrusive.
- * <p>
- * Typically this callback is invoked when the media route picker dialog has been
- * closed by the user.
- * </p>
- *
- * @see ProviderDescriptor#setActiveScanRequired
- */
- public void onStopActiveScan() {
- }
-
- /**
- * Describes immutable properties of the route provider itself.
- */
- static final class ProviderMetadata {
+ public static final class ProviderMetadata {
private final String mPackageName;
+ /**
+ * Creates a provider metadata object.
+ *
+ * @param packageName The provider application's package name.
+ */
public ProviderMetadata(String packageName) {
if (TextUtils.isEmpty(packageName)) {
throw new IllegalArgumentException("packageName must not be null or empty");
@@ -222,456 +270,16 @@
mPackageName = packageName;
}
+ /**
+ * Gets the provider application's package name.
+ */
public String getPackageName() {
return mPackageName;
}
- }
-
- /**
- * Describes the state of a media route provider and the routes that it publishes.
- */
- public static final class ProviderDescriptor {
- private static final String KEY_ROUTES = "routes";
- private static final String KEY_ACTIVE_SCAN_REQUIRED = "activeScanRequired";
-
- private final Bundle mBundle;
- private RouteDescriptor[] mRoutes;
-
- /**
- * Creates a route provider descriptor.
- */
- public ProviderDescriptor() {
- mBundle = new Bundle();
- }
-
- /**
- * Creates a copy of another route provider descriptor.
- */
- public ProviderDescriptor(ProviderDescriptor other) {
- mBundle = new Bundle(other.mBundle);
- }
-
- ProviderDescriptor(Bundle bundle) {
- mBundle = bundle;
- }
-
- /**
- * Gets the list of all routes that this provider has published.
- */
- public RouteDescriptor[] getRoutes() {
- if (mRoutes == null) {
- mRoutes = RouteDescriptor.fromParcelableArray(
- mBundle.getParcelableArray(KEY_ROUTES));
- }
- return mRoutes;
- }
-
- /**
- * Sets the list of all routes that this provider has published.
- */
- public void setRoutes(RouteDescriptor[] routes) {
- if (routes == null) {
- throw new IllegalArgumentException("routes must not be null");
- }
- mRoutes = routes;
- mBundle.putParcelableArray(KEY_ROUTES, RouteDescriptor.toParcelableArray(routes));
- }
-
- /**
- * Returns true if the provider requires active scans to discover routes.
- */
- public boolean isActiveScanRequired() {
- return mBundle.getBoolean(KEY_ACTIVE_SCAN_REQUIRED, false);
- }
-
- /**
- * Sets whether the provider requires active scans to discover routes.
- * <p>
- * To provide the best user experience, a media route provider should passively
- * discover and publish changes to route descriptors in the background.
- * However, for some providers, scanning for routes may use a significant
- * amount of power or may interfere with wireless network connectivity.
- * If this is the case, then the provider should indicate that it requires
- * active scans by setting this flag.
- * </p><p>
- * Even if this flag is not set, the provider will be given an opportunity
- * to perform a scan to update the route descriptors that it has published
- * when the route picker dialog is opened. The provider should only set
- * this flag if it is unable to discover routes without active scans at all.
- * </p>
- */
- public void setActiveScanRequired(boolean required) {
- mBundle.putBoolean(KEY_ACTIVE_SCAN_REQUIRED, required);
- }
-
- /**
- * Returns true if the route provider descriptor and all of the routes that
- * it contains have all of the required fields.
- * <p>
- * This verification is deep. If the provider descriptor is known to be
- * valid then it is not necessary to call {@link #isValid} on each of its routes.
- * </p>
- */
- public boolean isValid() {
- for (RouteDescriptor route : getRoutes()) {
- if (route == null || !route.isValid()) {
- return false;
- }
- }
- return true;
- }
@Override
public String toString() {
- return "RouteProviderDescriptor{" + mBundle.toString() + "}";
- }
-
- Bundle asBundle() {
- return mBundle;
- }
-
- static ProviderDescriptor fromBundle(Bundle bundle) {
- return bundle != null ? new ProviderDescriptor(bundle) : null;
- }
- }
-
- /**
- * Describes the properties of a route.
- * <p>
- * Each route is uniquely identified by an opaque id string. This token
- * may take any form as long as it is unique within the media route provider.
- * </p>
- */
- public static final class RouteDescriptor {
- static final RouteDescriptor[] EMPTY_ROUTE_ARRAY = new RouteDescriptor[0];
- static final IntentFilter[] EMTPY_FILTER_ARRAY = new IntentFilter[0];
-
- private static final String KEY_ID = "id";
- private static final String KEY_NAME = "name";
- private static final String KEY_STATUS = "status";
- private static final String KEY_ICON_RESOURCE = "iconId";
- private static final String KEY_ENABLED = "enabled";
- private static final String KEY_CONTROL_FILTERS = "controlFilters";
- private static final String KEY_PLAYBACK_TYPE = "playbackType";
- private static final String KEY_PLAYBACK_STREAM = "playbackStream";
- private static final String KEY_VOLUME = "volume";
- private static final String KEY_VOLUME_MAX = "volumeMax";
- private static final String KEY_VOLUME_HANDLING = "volumeHandling";
- private static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
- private static final String KEY_EXTRAS = "extras";
-
- private final Bundle mBundle;
- private IntentFilter[] mControlFilters;
- private Drawable mIconDrawable;
-
- /**
- * Creates a route descriptor.
- *
- * @param id The unique id of the route.
- * @param name The user-friendly name of the route.
- */
- public RouteDescriptor(String id, String name) {
- mBundle = new Bundle();
- setId(id);
- setName(name);
- }
-
- /**
- * Creates a copy of another route descriptor.
- */
- public RouteDescriptor(RouteDescriptor other) {
- mBundle = new Bundle(other.mBundle);
- }
-
- RouteDescriptor(Bundle bundle) {
- mBundle = bundle;
- }
-
- /**
- * Gets the unique id of the route.
- */
- public String getId() {
- return mBundle.getString(KEY_ID);
- }
-
- /**
- * Sets the unique id of the route.
- */
- public void setId(String id) {
- mBundle.putString(KEY_ID, id);
- }
-
- /**
- * Gets the user-friendly name of the route.
- */
- public String getName() {
- return mBundle.getString(KEY_NAME);
- }
-
- /**
- * Sets the user-friendly name of the route.
- */
- public void setName(String name) {
- mBundle.putString(KEY_NAME, name);
- }
-
- /**
- * Gets the user-friendly status of the route.
- */
- public String getStatus() {
- return mBundle.getString(KEY_STATUS);
- }
-
- /**
- * Sets the user-friendly status of the route.
- */
- public void setStatus(String status) {
- mBundle.putString(KEY_STATUS, status);
- }
-
- /**
- * Gets a drawable to display as the route's icon.
- * <p>
- * Because drawables cannot be transferred to other processes, this method may
- * only be used by media route providers that reside in the same process
- * as the application. When implementing a media route provider service, use
- * {@link #getIconResource} instead.
- * </p>
- */
- public Drawable getIconDrawable() {
- return mIconDrawable;
- }
-
- /**
- * Sets a drawable to display as the route's icon.
- * <p>
- * Because drawables cannot be transferred to other processes, this method may
- * only be used by media route providers that reside in the same process
- * as the application. When implementing a media route provider service, use
- * {@link #setIconResource} instead.
- * </p>
- */
- public void setIconDrawable(Drawable drawable) {
- mIconDrawable = drawable;
- }
-
- /**
- * Gets the id of a drawable resource to display as the route's icon.
- * <p>
- * The specified drawable resource id will be loaded from the media route
- * provider's package.
- * </p>
- */
- public int getIconResource() {
- return mBundle.getInt(KEY_ICON_RESOURCE);
- }
-
- /**
- * Sets the id of a drawable resource to display as the route's icon.
- * <p>
- * The specified drawable resource id will be loaded from the media route
- * provider's package.
- * </p>
- */
- public void setIconResource(int id) {
- mBundle.putInt(KEY_ICON_RESOURCE, id);
- }
-
- /**
- * Gets whether the route is enabled.
- */
- public boolean isEnabled() {
- return mBundle.getBoolean(KEY_ENABLED, true);
- }
-
- /**
- * Sets whether the route is enabled.
- * <p>
- * Disabled routes represent routes that a route provider knows about, such as paired
- * Wifi Display receivers, but that are not currently available for use.
- * </p>
- */
- public void setEnabled(boolean enabled) {
- mBundle.putBoolean(KEY_ENABLED, enabled);
- }
-
- /**
- * Gets the route's {@link MediaControlIntent media control intent} filters.
- */
- public IntentFilter[] getControlFilters() {
- if (mControlFilters == null) {
- Parcelable[] filters = mBundle.getParcelableArray(KEY_CONTROL_FILTERS);
- if (filters instanceof IntentFilter[]) {
- mControlFilters = (IntentFilter[])filters;
- } else if (filters != null && filters.length > 0) {
- mControlFilters = new IntentFilter[filters.length];
- System.arraycopy(filters, 0, mControlFilters, 0, filters.length);
- } else {
- mControlFilters = EMTPY_FILTER_ARRAY;
- }
- }
- return mControlFilters;
- }
-
- /**
- * Sets the route's {@link MediaControlIntent media control intent} filters.
- */
- public void setControlFilters(IntentFilter[] controlFilters) {
- if (controlFilters == null) {
- throw new IllegalArgumentException("controlFilters must not be null");
- }
- mControlFilters = controlFilters;
- mBundle.putParcelableArray(KEY_CONTROL_FILTERS, controlFilters);
- }
-
- /**
- * Gets the route's playback type.
- */
- public int getPlaybackType() {
- return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
- }
-
- /**
- * Sets the route's playback type.
- */
- public void setPlaybackType(int playbackType) {
- mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
- }
-
- /**
- * Gets the route's playback stream.
- */
- public int getPlaybackStream() {
- return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
- }
-
- /**
- * Sets the route's playback stream.
- */
- public void setPlaybackStream(int playbackStream) {
- mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
- }
-
- /**
- * Gets the route's current volume, or 0 if unknown.
- */
- public int getVolume() {
- return mBundle.getInt(KEY_VOLUME);
- }
-
- /**
- * Sets the route's current volume, or 0 if unknown.
- */
- public void setVolume(int volume) {
- mBundle.putInt(KEY_VOLUME, volume);
- }
-
- /**
- * Gets the route's maximum volume, or 0 if unknown.
- */
- public int getVolumeMax() {
- return mBundle.getInt(KEY_VOLUME_MAX);
- }
-
- /**
- * Sets the route's maximum volume, or 0 if unknown.
- */
- public void setVolumeMax(int volumeMax) {
- mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
- }
-
- /**
- * Gets the route's volume handling.
- */
- public int getVolumeHandling() {
- return mBundle.getInt(KEY_VOLUME_HANDLING,
- MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
- }
-
- /**
- * Sets the route's volume handling.
- */
- public void setVolumeHandling(int volumeHandling) {
- mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
- }
-
- /**
- * Gets the route's presentation display id, or -1 if none.
- */
- public int getPresentationDisplayId() {
- return mBundle.getInt(KEY_PRESENTATION_DISPLAY_ID, -1);
- }
-
- /**
- * Sets the route's presentation display id, or -1 if none.
- */
- public void setPresentationDisplayId(int presentationDisplayId) {
- mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
- }
-
- /**
- * Gets a bundle of extras for this route descriptor.
- * The extras will be ignored by the media router but they may be used
- * by applications.
- */
- public Bundle getExtras() {
- return mBundle.getBundle(KEY_EXTRAS);
- }
-
- /**
- * Sets a bundle of extras for this route descriptor.
- * The extras will be ignored by the media router but they may be used
- * by applications.
- */
- public void setExtras(Bundle extras) {
- mBundle.putBundle(KEY_EXTRAS, extras);
- }
-
- /**
- * Returns true if the route descriptor has all of the required fields.
- */
- public boolean isValid() {
- if (TextUtils.isEmpty(getId())
- || TextUtils.isEmpty(getName())) {
- return false;
- }
- for (IntentFilter filter : getControlFilters()) {
- if (filter == null) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public String toString() {
- return "RouteDescriptor{" + mBundle.toString() + "}";
- }
-
- Bundle asBundle() {
- return mBundle;
- }
-
- static Parcelable[] toParcelableArray(RouteDescriptor[] descriptors) {
- if (descriptors != null && descriptors.length > 0) {
- Parcelable[] bundles = new Parcelable[descriptors.length];
- for (int i = 0; i < descriptors.length; i++) {
- bundles[i] = descriptors[i].asBundle();
- }
- return bundles;
- }
- return null;
- }
-
- static RouteDescriptor[] fromParcelableArray(Parcelable[] bundles) {
- if (bundles != null && bundles.length > 0) {
- RouteDescriptor[] descriptors = new RouteDescriptor[bundles.length];
- for (int i = 0; i < bundles.length; i++) {
- descriptors[i] = new RouteDescriptor((Bundle)bundles[i]);
- }
- return descriptors;
- }
- return EMPTY_ROUTE_ARRAY;
+ return "ProviderMetadata{ packageName=" + mPackageName + " }";
}
}
@@ -717,7 +325,7 @@
/**
* Requests to set the volume of the route.
*
- * @param volume The new volume value between 0 and {@link RouteDescriptor#getVolumeMax}.
+ * @param volume The new volume value between 0 and {@link MediaRouteDescriptor#getVolumeMax}.
*/
public void onSetVolume(int volume) {
}
@@ -738,7 +346,7 @@
* @param callback A {@link ControlRequestCallback} to invoke with the result
* of the request, or null if no result is required.
* @return True if the controller intends to handle the request and will
- * invoke the callback when finished. False if the contorller will not
+ * invoke the callback when finished. False if the controller will not
* handle the request and will not invoke the callback.
*
* @see MediaControlIntent
@@ -755,12 +363,11 @@
/**
* Called when information about a route provider and its routes changes.
*
- * @param provider The media route provider that changed.
- * and all of its contents should be treated as if it were immutable so that it is
- * safe for clients to cache it.
+ * @param provider The media route provider that changed, never null.
+ * @param descriptor The new media route provider descriptor, or null if none.
*/
public void onDescriptorChanged(MediaRouteProvider provider,
- ProviderDescriptor descriptor) {
+ MediaRouteProviderDescriptor descriptor) {
}
}
@@ -771,6 +378,9 @@
case MSG_DELIVER_DESCRIPTOR_CHANGED:
deliverDescriptorChanged();
break;
+ case MSG_DELIVER_DISCOVERY_REQUEST_CHANGED:
+ deliverDiscoveryRequestChanged();
+ break;
}
}
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
new file mode 100644
index 0000000..9f95d2e
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderDescriptor.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.media;
+
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the state of a media route provider and the routes that it publishes.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteProviderDescriptor {
+ private static final String KEY_ROUTES = "routes";
+ private static final String KEY_ACTIVE_SCAN_REQUIRED = "activeScanRequired";
+ private static final String KEY_DISCOVERABLE_CONTROL_FILTERS =
+ "discoverableControlFilters";
+
+ private final Bundle mBundle;
+ private List<MediaRouteDescriptor> mRoutes;
+ private List<IntentFilter> mDiscoverableControlFilters;
+
+ private MediaRouteProviderDescriptor(Bundle bundle,
+ List<MediaRouteDescriptor> routes, List<IntentFilter> discoverableControlFilters) {
+ mBundle = bundle;
+ mRoutes = routes;
+ mDiscoverableControlFilters = discoverableControlFilters;
+ }
+
+ /**
+ * Gets the list of all routes that this provider has published.
+ */
+ public List<MediaRouteDescriptor> getRoutes() {
+ ensureRoutes();
+ return mRoutes;
+ }
+
+ private void ensureRoutes() {
+ if (mRoutes == null) {
+ ArrayList<Bundle> routeBundles = mBundle.<Bundle>getParcelableArrayList(KEY_ROUTES);
+ if (routeBundles == null || routeBundles.isEmpty()) {
+ mRoutes = Collections.<MediaRouteDescriptor>emptyList();
+ } else {
+ final int count = routeBundles.size();
+ mRoutes = new ArrayList<MediaRouteDescriptor>(count);
+ for (int i = 0; i < count; i++) {
+ mRoutes.add(MediaRouteDescriptor.fromBundle(routeBundles.get(i)));
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets a list of {@link MediaControlIntent media route control filters} that
+ * describe the union of capabilities of all routes that this provider can
+ * possibly discover.
+ *
+ * @see MediaRouter.ProviderInfo#getDiscoverableControlFilters
+ */
+ public List<IntentFilter> getDiscoverableControlFilters() {
+ ensureDiscoverableControlFilters();
+ return mDiscoverableControlFilters;
+ }
+
+ private void ensureDiscoverableControlFilters() {
+ if (mDiscoverableControlFilters == null) {
+ mDiscoverableControlFilters =
+ mBundle.<IntentFilter>getParcelableArrayList(KEY_DISCOVERABLE_CONTROL_FILTERS);
+ if (mDiscoverableControlFilters == null || mDiscoverableControlFilters.isEmpty()) {
+ mDiscoverableControlFilters = Collections.<IntentFilter>emptyList();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the provider requires active scans to discover routes.
+ *
+ * @see MediaRouter.ProviderInfo#isActiveScanRequired
+ * @see MediaRouter#CALLBACK_FLAG_ACTIVE_SCAN
+ */
+ public boolean isActiveScanRequired() {
+ return mBundle.getBoolean(KEY_ACTIVE_SCAN_REQUIRED, false);
+ }
+
+ /**
+ * Returns true if the route provider descriptor and all of the routes that
+ * it contains have all of the required fields.
+ * <p>
+ * This verification is deep. If the provider descriptor is known to be
+ * valid then it is not necessary to call {@link #isValid} on each of its routes.
+ * </p>
+ */
+ public boolean isValid() {
+ ensureDiscoverableControlFilters();
+ if (mDiscoverableControlFilters.contains(null)) {
+ return false;
+ }
+ ensureRoutes();
+ final int routeCount = mRoutes.size();
+ for (int i = 0; i < routeCount; i++) {
+ MediaRouteDescriptor route = mRoutes.get(i);
+ if (route == null || !route.isValid()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteProviderDescriptor{ ");
+ result.append("isActiveScanRequired=").append(isActiveScanRequired());
+ result.append(", discoverableControlFilters=").append(
+ Arrays.toString(getDiscoverableControlFilters().toArray()));
+ result.append(", routes=").append(
+ Arrays.toString(getRoutes().toArray()));
+ result.append(", isValid=").append(isValid());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteProviderDescriptor fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteProviderDescriptor(bundle, null, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteProviderDescriptor media route provider descriptors}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+ private ArrayList<MediaRouteDescriptor> mRoutes;
+ private ArrayList<IntentFilter> mDiscoverableControlFilters;
+
+ /**
+ * Creates an empty media route provider descriptor builder.
+ */
+ public Builder() {
+ mBundle = new Bundle();
+ }
+
+ /**
+ * Creates a media route provider descriptor builder whose initial contents are
+ * copied from an existing descriptor.
+ */
+ public Builder(MediaRouteProviderDescriptor descriptor) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+
+ mBundle = new Bundle(descriptor.mBundle);
+
+ descriptor.ensureRoutes();
+ if (!descriptor.mRoutes.isEmpty()) {
+ mRoutes = new ArrayList<MediaRouteDescriptor>(descriptor.mRoutes);
+ }
+
+ descriptor.ensureDiscoverableControlFilters();
+ if (!descriptor.mDiscoverableControlFilters.isEmpty()) {
+ mDiscoverableControlFilters = new ArrayList<IntentFilter>(
+ descriptor.mDiscoverableControlFilters);
+ }
+ }
+
+ /**
+ * Adds a route.
+ */
+ public Builder addRoute(MediaRouteDescriptor route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+
+ if (mRoutes == null) {
+ mRoutes = new ArrayList<MediaRouteDescriptor>();
+ } else if (mRoutes.contains(route)) {
+ throw new IllegalArgumentException("route descriptor already added");
+ }
+ mRoutes.add(route);
+ return this;
+ }
+
+ /**
+ * Adds a list of routes.
+ */
+ public Builder addRoutes(Collection<MediaRouteDescriptor> routes) {
+ if (routes == null) {
+ throw new IllegalArgumentException("routes must not be null");
+ }
+
+ if (!routes.isEmpty()) {
+ for (MediaRouteDescriptor route : routes) {
+ addRoute(route);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds a {@link MediaControlIntent media control intent} filter.
+ */
+ public Builder addDiscoverableControlFilter(IntentFilter filter) {
+ if (filter == null) {
+ throw new IllegalArgumentException("filter must not be null");
+ }
+
+ if (mDiscoverableControlFilters == null) {
+ mDiscoverableControlFilters = new ArrayList<IntentFilter>();
+ }
+ if (!mDiscoverableControlFilters.contains(filter)) {
+ mDiscoverableControlFilters.add(filter);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of {@link MediaControlIntent media control intent} filters.
+ */
+ public Builder addDiscoverableControlFilters(Collection<IntentFilter> filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException("filters must not be null");
+ }
+
+ if (!filters.isEmpty()) {
+ for (IntentFilter filter : filters) {
+ addDiscoverableControlFilter(filter);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the provider requires active scans to discover routes.
+ */
+ public Builder setActiveScanRequired(boolean required) {
+ mBundle.putBoolean(KEY_ACTIVE_SCAN_REQUIRED, required);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteProviderDescriptor media route provider descriptor}.
+ */
+ public MediaRouteProviderDescriptor build() {
+ if (mRoutes != null) {
+ final int count = mRoutes.size();
+ ArrayList<Bundle> routeBundles = new ArrayList<Bundle>(count);
+ for (int i = 0; i < count; i++) {
+ routeBundles.add(mRoutes.get(i).asBundle());
+ }
+ mBundle.putParcelableArrayList(KEY_ROUTES, routeBundles);
+ }
+ if (mDiscoverableControlFilters != null) {
+ mBundle.putParcelableArrayList(KEY_DISCOVERABLE_CONTROL_FILTERS,
+ mDiscoverableControlFilters);
+ }
+ return new MediaRouteProviderDescriptor(mBundle,
+ mRoutes, mDiscoverableControlFilters);
+ }
+ }
+}
\ No newline at end of file
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
index 539f7a6..e82d7ab 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProviderService.java
@@ -26,7 +26,6 @@
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
-import android.support.v7.media.MediaRouteProvider.ProviderDescriptor;
import android.util.Log;
import android.util.SparseArray;
@@ -54,7 +53,7 @@
*/
public abstract class MediaRouteProviderService extends Service {
private static final String TAG = "MediaRouteProviderService";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private final ArrayList<ClientRecord> mClients = new ArrayList<ClientRecord>();
private final ReceiveHandler mReceiveHandler;
@@ -63,7 +62,7 @@
private final ProviderCallback mProviderCallback;
private MediaRouteProvider mProvider;
- private int mActiveScanClientCount;
+ private MediaRouteDiscoveryRequest mCompositeDiscoveryRequest;
/**
* The {@link Intent} that must be declared as handled by the service.
@@ -153,18 +152,12 @@
static final int CLIENT_MSG_ROUTE_CONTROL_REQUEST = 9;
/** (client v1)
- * Start active scan.
+ * Sets the discovery request.
* - replyTo : client messenger
* - arg1 : request id
+ * - obj : discovery request bundle, or null if none
*/
- static final int CLIENT_MSG_START_ACTIVE_SCAN = 10;
-
- /** (client v1)
- * Stop active scan.
- * - replyTo : client messenger
- * - arg1 : request id
- */
- static final int CLIENT_MSG_STOP_ACTIVE_SCAN = 11;
+ static final int CLIENT_MSG_SET_DISCOVERY_REQUEST = 10;
static final String CLIENT_DATA_ROUTE_ID = "routeId";
static final String CLIENT_DATA_VOLUME = "volume";
@@ -195,19 +188,28 @@
static final int SERVICE_MSG_REGISTERED = 2;
/** (service v1)
- * Route control request result.
+ * Route control request success result.
* - arg1 : request id
- * - arg2 : result code
* - obj : result data bundle, or null
*/
- static final int SERVICE_MSG_CONTROL_RESULT = 3;
+ static final int SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED = 3;
+
+ /** (service v1)
+ * Route control request failure result.
+ * - arg1 : request id
+ * - obj : result data bundle, or null
+ * - SERVICE_DATA_ERROR: error message
+ */
+ static final int SERVICE_MSG_CONTROL_REQUEST_FAILED = 4;
/** (service v1)
* Route provider descriptor changed. (unsolicited event)
* - arg1 : reserved (0)
* - obj : route provider descriptor bundle, or null
*/
- static final int SERVICE_MSG_DESCRIPTOR_CHANGED = 4;
+ static final int SERVICE_MSG_DESCRIPTOR_CHANGED = 5;
+
+ static final String SERVICE_DATA_ERROR = "error";
/*
* Recognized client version numbers. (Reserved for future use.)
@@ -277,7 +279,7 @@
+ ". Service package name: " + getPackageName() + ".");
}
mProvider = provider;
- mProvider.addCallback(mProviderCallback);
+ mProvider.setCallback(mProviderCallback);
}
}
if (mProvider != null) {
@@ -298,7 +300,7 @@
Log.d(TAG, client + ": Registered, version=" + version);
}
if (requestId != 0) {
- ProviderDescriptor descriptor = mProvider.getDescriptor();
+ MediaRouteProviderDescriptor descriptor = mProvider.getDescriptor();
sendReply(messenger, SERVICE_MSG_REGISTERED,
requestId, SERVICE_VERSION_CURRENT,
descriptor != null ? descriptor.asBundle() : null, null);
@@ -454,16 +456,37 @@
if (requestId != 0) {
callback = new MediaRouter.ControlRequestCallback() {
@Override
- public void onResult(int result, Bundle data) {
+ public void onResult(Bundle data) {
if (DEBUG) {
- Log.d(TAG, client + ": Route control request finished"
+ Log.d(TAG, client + ": Route control request succeeded"
+ ", controllerId=" + controllerId
+ ", intent=" + intent
- + ", result=" + result + ", data=" + data);
+ + ", data=" + data);
}
if (findClient(messenger) >= 0) {
- sendReply(messenger, SERVICE_MSG_CONTROL_RESULT,
- requestId, result, data, null);
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED,
+ requestId, 0, data, null);
+ }
+ }
+
+ @Override
+ public void onError(String error, Bundle data) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route control request failed"
+ + ", controllerId=" + controllerId
+ + ", intent=" + intent
+ + ", error=" + error + ", data=" + data);
+ }
+ if (findClient(messenger) >= 0) {
+ if (error != null) {
+ Bundle bundle = new Bundle();
+ bundle.putString(SERVICE_DATA_ERROR, error);
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+ requestId, 0, data, bundle);
+ } else {
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+ requestId, 0, data, null);
+ }
}
}
};
@@ -480,14 +503,15 @@
return false;
}
- private boolean onStartActiveScan(Messenger messenger, int requestId) {
+ private boolean onSetDiscoveryRequest(Messenger messenger, int requestId,
+ MediaRouteDiscoveryRequest request) {
ClientRecord client = getClient(messenger);
if (client != null) {
- boolean actuallyStarted = client.startActiveScan();
+ boolean actuallyChanged = client.setDiscoveryRequest(request);
if (DEBUG) {
- Log.d(TAG, client + ": Start active scan"
- + ", actuallyStarted=" + actuallyStarted
- + ", activeScanClientCount=" + mActiveScanClientCount);
+ Log.d(TAG, client + ": Set discovery request, request=" + request
+ + ", actuallyChanged=" + actuallyChanged
+ + ", compositeDiscoveryRequest=" + mCompositeDiscoveryRequest);
}
sendGenericSuccess(messenger, requestId);
return true;
@@ -495,22 +519,7 @@
return false;
}
- private boolean onStopActiveScan(Messenger messenger, int requestId) {
- ClientRecord client = getClient(messenger);
- if (client != null) {
- boolean actuallyStopped = client.stopActiveScan();
- if (DEBUG) {
- Log.d(TAG, client + ": Stop active scan"
- + ", actuallyStopped= " + actuallyStopped
- + ", activeScanClientCount=" + mActiveScanClientCount);
- }
- sendGenericSuccess(messenger, requestId);
- return true;
- }
- return false;
- }
-
- private void sendDescriptorChanged(MediaRouteProvider.ProviderDescriptor descriptor) {
+ private void sendDescriptorChanged(MediaRouteProviderDescriptor descriptor) {
Bundle descriptorBundle = descriptor != null ? descriptor.asBundle() : null;
final int count = mClients.size();
for (int i = 0; i < count; i++) {
@@ -523,6 +532,39 @@
}
}
+ private boolean updateCompositeDiscoveryRequest() {
+ MediaRouteDiscoveryRequest composite = null;
+ MediaRouteSelector.Builder selectorBuilder = null;
+ boolean activeScan = false;
+ final int count = mClients.size();
+ for (int i = 0; i < count; i++) {
+ MediaRouteDiscoveryRequest request = mClients.get(i).mDiscoveryRequest;
+ if (request != null
+ && (!request.getSelector().isEmpty() || request.isActiveScan())) {
+ activeScan |= request.isActiveScan();
+ if (composite == null) {
+ composite = request;
+ } else {
+ if (selectorBuilder == null) {
+ selectorBuilder = new MediaRouteSelector.Builder(composite.getSelector());
+ }
+ selectorBuilder.addSelector(request.getSelector());
+ }
+ }
+ }
+ if (selectorBuilder != null) {
+ composite = new MediaRouteDiscoveryRequest(selectorBuilder.build(), activeScan);
+ }
+ if (mCompositeDiscoveryRequest != composite
+ && (mCompositeDiscoveryRequest == null
+ || !mCompositeDiscoveryRequest.equals(composite))) {
+ mCompositeDiscoveryRequest = composite;
+ mProvider.setDiscoveryRequest(composite);
+ return true;
+ }
+ return false;
+ }
+
private ClientRecord getClient(Messenger messenger) {
int index = findClient(messenger);
return index >= 0 ? mClients.get(index) : null;
@@ -575,7 +617,7 @@
/**
* Returns true if the messenger object is valid.
* <p>
- * The messenger contructor and unparceling code does not check whether the
+ * The messenger constructor and unparceling code does not check whether the
* provided IBinder is a valid IMessenger object. As a result, it's possible
* for a peer to send an invalid IBinder that will result in crashes downstream.
* This method checks that the messenger is in a valid state.
@@ -605,7 +647,7 @@
private final class ProviderCallback extends MediaRouteProvider.Callback {
@Override
public void onDescriptorChanged(MediaRouteProvider provider,
- ProviderDescriptor descriptor) {
+ MediaRouteProviderDescriptor descriptor) {
sendDescriptorChanged(descriptor);
}
}
@@ -613,7 +655,7 @@
private final class ClientRecord implements DeathRecipient {
public final Messenger mMessenger;
public final int mVersion;
- public boolean mActiveScanRequested;
+ public MediaRouteDiscoveryRequest mDiscoveryRequest;
private final SparseArray<MediaRouteProvider.RouteController> mControllers =
new SparseArray<MediaRouteProvider.RouteController>();
@@ -642,7 +684,7 @@
mMessenger.getBinder().unlinkToDeath(this, 0);
- stopActiveScan();
+ setDiscoveryRequest(null);
}
public boolean hasMessenger(Messenger other) {
@@ -673,24 +715,11 @@
return mControllers.get(controllerId);
}
- public boolean startActiveScan() {
- if (!mActiveScanRequested) {
- mActiveScanRequested = true;
- mActiveScanClientCount += 1;
- if (mActiveScanClientCount == 1) {
- mProvider.onStartActiveScan();
- }
- }
- return false;
- }
-
- public boolean stopActiveScan() {
- if (mActiveScanRequested) {
- mActiveScanRequested = false;
- mActiveScanClientCount -= 1;
- if (mActiveScanClientCount == 0) {
- mProvider.onStopActiveScan();
- }
+ public boolean setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ if (mDiscoveryRequest != request
+ && (mDiscoveryRequest == null || !mDiscoveryRequest.equals(request))) {
+ mDiscoveryRequest = request;
+ return updateCompositeDiscoveryRequest();
}
return false;
}
@@ -803,11 +832,14 @@
}
break;
- case CLIENT_MSG_START_ACTIVE_SCAN:
- return service.onStartActiveScan(messenger, requestId);
-
- case CLIENT_MSG_STOP_ACTIVE_SCAN:
- return service.onStopActiveScan(messenger, requestId);
+ case CLIENT_MSG_SET_DISCOVERY_REQUEST: {
+ if (obj instanceof Bundle) {
+ MediaRouteDiscoveryRequest request =
+ MediaRouteDiscoveryRequest.fromBundle((Bundle)obj);
+ return service.onSetDiscoveryRequest(
+ messenger, requestId, request.isValid() ? request : null);
+ }
+ }
}
}
return false;
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
new file mode 100644
index 0000000..7dba8b8
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.media;
+
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the capabilities of routes that applications would like to discover and use.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ *
+ * MediaRouter router = MediaRouter.getInstance(context);
+ * router.addCallback(selector, callback);
+ * </pre>
+ */
+public final class MediaRouteSelector {
+ private static final String KEY_CONTROL_CATEGORIES = "controlCategories";
+
+ private final Bundle mBundle;
+ private List<String> mControlCategories;
+
+ /**
+ * An empty media route selector that will not match any routes.
+ */
+ public static final MediaRouteSelector EMPTY = new MediaRouteSelector(new Bundle(), null);
+
+ private MediaRouteSelector(Bundle bundle, List<String> controlCategories) {
+ mBundle = bundle;
+ mControlCategories = controlCategories;
+ }
+
+ /**
+ * Gets the list of {@link MediaControlIntent media control categories} in the selector.
+ *
+ * @return The list of categories.
+ */
+ public List<String> getControlCategories() {
+ ensureControlCategories();
+ return mControlCategories;
+ }
+
+ private void ensureControlCategories() {
+ if (mControlCategories == null) {
+ mControlCategories = mBundle.getStringArrayList(KEY_CONTROL_CATEGORIES);
+ if (mControlCategories == null || mControlCategories.isEmpty()) {
+ mControlCategories = Collections.<String>emptyList();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the selector contains the specified category.
+ *
+ * @param category The category to check.
+ * @return True if the category is present.
+ */
+ public boolean hasControlCategory(String category) {
+ if (category != null) {
+ ensureControlCategories();
+ final int categoryCount = mControlCategories.size();
+ for (int i = 0; i < categoryCount; i++) {
+ if (mControlCategories.get(i).equals(category)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the selector matches at least one of the specified control filters.
+ *
+ * @param filters The list of control filters to consider.
+ * @return True if a match is found.
+ */
+ public boolean matchesControlFilters(List<IntentFilter> filters) {
+ if (filters != null) {
+ ensureControlCategories();
+ final int categoryCount = mControlCategories.size();
+ if (categoryCount != 0) {
+ final int filterCount = filters.size();
+ for (int i = 0; i < filterCount; i++) {
+ final IntentFilter filter = filters.get(i);
+ if (filter != null) {
+ for (int j = 0; j < categoryCount; j++) {
+ if (filter.hasCategory(mControlCategories.get(j))) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this selector contains all of the capabilities described
+ * by the specified selector.
+ *
+ * @param selector The selector to be examined.
+ * @return True if this selector contains all of the capabilities described
+ * by the specified selector.
+ */
+ public boolean contains(MediaRouteSelector selector) {
+ if (selector != null) {
+ ensureControlCategories();
+ selector.ensureControlCategories();
+ return mControlCategories.containsAll(selector.mControlCategories);
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the selector does not specify any capabilities.
+ */
+ public boolean isEmpty() {
+ ensureControlCategories();
+ return mControlCategories.isEmpty();
+ }
+
+ /**
+ * Returns true if the selector has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureControlCategories();
+ if (mControlCategories.contains(null)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MediaRouteSelector) {
+ MediaRouteSelector other = (MediaRouteSelector)o;
+ ensureControlCategories();
+ other.ensureControlCategories();
+ return mControlCategories.equals(other.mControlCategories);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ ensureControlCategories();
+ return mControlCategories.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteSelector{ ");
+ result.append("controlCategories=").append(
+ Arrays.toString(getControlCategories().toArray()));
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteSelector fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteSelector(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteSelector media route selectors}.
+ */
+ public static final class Builder {
+ private ArrayList<String> mControlCategories;
+
+ /**
+ * Creates an empty media route selector builder.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Creates a media route selector descriptor builder whose initial contents are
+ * copied from an existing selector.
+ */
+ public Builder(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ selector.ensureControlCategories();
+ if (!selector.mControlCategories.isEmpty()) {
+ mControlCategories = new ArrayList<String>(selector.mControlCategories);
+ }
+ }
+
+ /**
+ * Adds a {@link MediaControlIntent media control category} to the builder.
+ *
+ * @param category The category to add to the set of desired capabilities, such as
+ * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+ * @return The builder instance for chaining.
+ */
+ public Builder addControlCategory(String category) {
+ if (category == null) {
+ throw new IllegalArgumentException("category must not be null");
+ }
+
+ if (mControlCategories == null) {
+ mControlCategories = new ArrayList<String>();
+ }
+ if (!mControlCategories.contains(category)) {
+ mControlCategories.add(category);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of {@link MediaControlIntent media control categories} to the builder.
+ *
+ * @param categories The list categories to add to the set of desired capabilities,
+ * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+ * @return The builder instance for chaining.
+ */
+ public Builder addControlCategories(Collection<String> categories) {
+ if (categories == null) {
+ throw new IllegalArgumentException("categories must not be null");
+ }
+
+ if (!categories.isEmpty()) {
+ for (String category : categories) {
+ addControlCategory(category);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds the contents of an existing media route selector to the builder.
+ *
+ * @param selector The media route selector whose contents are to be added.
+ * @return The builder instance for chaining.
+ */
+ public Builder addSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ addControlCategories(selector.getControlCategories());
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteSelector media route selector}.
+ */
+ public MediaRouteSelector build() {
+ if (mControlCategories == null) {
+ return EMPTY;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putStringArrayList(KEY_CONTROL_CATEGORIES, mControlCategories);
+ return new MediaRouteSelector(bundle, mControlCategories);
+ }
+ }
+}
\ No newline at end of file
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index f4f646a..3dd1298 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -22,14 +22,11 @@
import android.content.IntentFilter;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v4.hardware.display.DisplayManagerCompat;
-import android.support.v7.media.MediaRouteProvider.RouteDescriptor;
-import android.support.v7.media.MediaRouteProvider.ProviderDescriptor;
import android.support.v7.media.MediaRouteProvider.ProviderMetadata;
import android.util.Log;
import android.view.Display;
@@ -60,6 +57,7 @@
*/
public final class MediaRouter {
private static final String TAG = "MediaRouter";
+ private static final boolean DEBUG = false;
// Maintains global media router state for the process.
// This field is initialized in MediaRouter.getInstance() before any
@@ -69,8 +67,61 @@
// Context-bound state of the media router.
final Context mContext;
- final CopyOnWriteArrayList<Callback> mCallbacks = new CopyOnWriteArrayList<Callback>();
- final ArrayList<Selector> mSelectors = new ArrayList<Selector>();
+ final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords =
+ new CopyOnWriteArrayList<CallbackRecord>();
+
+ /**
+ * Flag for {@link #addCallback}: Actively scan for routes while this callback
+ * is registered.
+ * <p>
+ * When this flag is specified, the media router will actively scan for new
+ * routes. Certain routes, such as wifi display routes, may not be discoverable
+ * except when actively scanning. This flag is typically used when the route picker
+ * dialog has been opened by the user to ensure that the route information is
+ * up to date.
+ * </p><p>
+ * Active scanning may consume a significant amount of power and may have intrusive
+ * effects on wireless connectivity. Therefore it is important that active scanning
+ * only be requested when it is actually needed to satisfy a user request to
+ * discover and select a new route.
+ * </p>
+ */
+ public static final int CALLBACK_FLAG_ACTIVE_SCAN = 1 << 0;
+
+ /**
+ * Flag for {@link #addCallback}: Do not filter route events.
+ * <p>
+ * When this flag is specified, the callback will be invoked for events that affect any
+ * route event if they do not match the callback's associated media route selector.
+ * </p>
+ */
+ public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
+
+ /**
+ * Flag for {@link #isRouteAvailable}: Ignore the default route.
+ * <p>
+ * This flag is used to determine whether a matching non-default route is available.
+ * This constraint may be used to decide whether to offer the route chooser dialog
+ * to the user. There is no point offering the chooser if there are no
+ * non-default choices.
+ * </p>
+ */
+ public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
+
+ /**
+ * Flag for {@link #isRouteAvailable}: Consider whether matching routes
+ * might be discovered if an active scan were performed.
+ * <p>
+ * If no existing routes match the route selector, then this flag is used to
+ * determine whether to consider whether any route providers that require active
+ * scans might discover matching routes if an active scan were actually performed.
+ * </p><p>
+ * This flag may be used to decide whether to offer the route chooser dialog to the user.
+ * When the dialog is opened, an active scan will be performed which may cause
+ * additional routes to be discovered by any providers that require active scans.
+ * </p>
+ */
+ public static final int AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN = 1 << 1;
MediaRouter(Context context) {
mContext = context;
@@ -181,18 +232,21 @@
* @return The previously selected route if it matched the selector, otherwise the
* newly selected default route which is guaranteed to never be null.
*
- * @see Selector
+ * @see MediaRouteSelector
* @see RouteInfo#matchesSelector
* @see RouteInfo#isDefault
*/
- public RouteInfo updateSelectedRoute(Selector selector) {
+ public RouteInfo updateSelectedRoute(MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
checkCallingThread();
+ if (DEBUG) {
+ Log.d(TAG, "updateSelectedRoute: " + selector);
+ }
RouteInfo route = sGlobal.getSelectedRoute();
- if (!route.isDefault() && route.matchesSelector(selector)) {
+ if (!route.isDefault() && !route.matchesSelector(selector)) {
route = sGlobal.getDefaultRoute();
sGlobal.selectRoute(route);
}
@@ -210,28 +264,166 @@
}
checkCallingThread();
+ if (DEBUG) {
+ Log.d(TAG, "selectRoute: " + route);
+ }
sGlobal.selectRoute(route);
}
/**
- * Adds a callback to listen to changes to media routes.
+ * Returns true if there is a route that matches the specified selector
+ * or, depending on the specified availability flags, if it is possible to discover one.
+ * <p>
+ * This method first considers whether there are any available
+ * routes that match the selector regardless of whether they are enabled or
+ * disabled. If not and the {@link #AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN} flag
+ * was specifies, then it considers whether any of the route providers
+ * could discover a matching route if an active scan were performed.
+ * </p>
*
+ * @param selector The selector to match.
+ * @param flags Flags to control the determination of whether a route may be available.
+ * May be zero or a combination of
+ * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} and
+ * {@link #AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN}.
+ * @return True if a matching route may be available.
+ */
+ public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ checkCallingThread();
+
+ return sGlobal.isRouteAvailable(selector, flags);
+ }
+
+ /**
+ * Registers a callback to discover routes that match the selector and to receive
+ * events when they change.
+ * <p>
+ * This is a convenience method that has the same effect as calling
+ * {@link #addCallback(MediaRouteSelector, Callback, int)} without flags.
+ * </p>
+ *
+ * @param selector A route selector that indicates the kinds of routes that the
+ * callback would like to discover.
* @param callback The callback to add.
* @see #removeCallback
*/
- public void addCallback(Callback callback) {
+ public void addCallback(MediaRouteSelector selector, Callback callback) {
+ addCallback(selector, callback, 0);
+ }
+
+ /**
+ * Registers a callback to discover routes that match the selector and to receive
+ * events when they change.
+ * <p>
+ * The selector describes the kinds of routes that the application wants to
+ * discover. For example, if the application wants to use
+ * live audio routes then it should include the
+ * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO live audio media control intent category}
+ * in its selector when it adds a callback to the media router.
+ * The selector may include any number of categories.
+ * </p><p>
+ * If the callback has already been registered, then the selector is added to
+ * the set of selectors being monitored by the callback.
+ * </p><p>
+ * By default, the callback will only be invoked for events that affect routes
+ * that match the specified selector. Event filtering may be disabled by specifying
+ * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag when the callback is registered.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * public class MyActivity extends Activity {
+ * private MediaRouter mRouter;
+ * private MediaRouter.Callback mCallback;
+ * private MediaRouteSelector mSelector;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * mRouter = Mediarouter.getInstance(this);
+ * mCallback = new MyCallback();
+ * mSelector = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ * }
+ *
+ * // Add the callback on resume to tell the media router what kinds of routes
+ * // the application is interested in so that it can try to discover suitable ones.
+ * public void onResume() {
+ * super.onResume();
+ *
+ * mediaRouter.addCallback(mSelector, mCallback);
+ *
+ * MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
+ * // do something with the route...
+ * }
+ *
+ * // Remove the selector on pause to tell the media router that it no longer
+ * // needs to invest effort trying to discover routes of these kinds for now.
+ * public void onPause() {
+ * super.onPause();
+ *
+ * mediaRouter.removeCallback(mCallback);
+ * }
+ *
+ * private final class MyCallback extends MediaRouter.Callback {
+ * // Implement callback methods as needed.
+ * }
+ * }
+ * </pre>
+ *
+ * @param selector A route selector that indicates the kinds of routes that the
+ * callback would like to discover.
+ * @param callback The callback to add.
+ * @param flags Flags to control the behavior of the callback.
+ * May be zero or a combination of {@link #CALLBACK_FLAG_ACTIVE_SCAN} and
+ * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
+ * @see #removeCallback
+ */
+ public void addCallback(MediaRouteSelector selector, Callback callback, int flags) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
checkCallingThread();
- if (!mCallbacks.contains(callback)) {
- mCallbacks.add(callback);
+ if (DEBUG) {
+ Log.d(TAG, "addCallback: selector=" + selector
+ + ", callback=" + callback + ", flags=" + Integer.toHexString(flags));
+ }
+
+ CallbackRecord record;
+ int index = findCallbackRecord(callback);
+ if (index < 0) {
+ record = new CallbackRecord(callback);
+ mCallbackRecords.add(record);
+ } else {
+ record = mCallbackRecords.get(index);
+ }
+ boolean updateNeeded = false;
+ if ((flags & ~record.mFlags) != 0) {
+ record.mFlags |= flags;
+ updateNeeded = true;
+ }
+ if (!record.mSelector.contains(selector)) {
+ record.mSelector = new MediaRouteSelector.Builder(record.mSelector)
+ .addSelector(selector)
+ .build();
+ updateNeeded = true;
+ }
+ if (updateNeeded) {
+ sGlobal.updateDiscoveryRequest();
}
}
/**
- * Removes the specified callback. It will no longer receive information about
+ * Removes the specified callback. It will no longer receive events about
* changes to media routes.
*
* @param callback The callback to remove.
@@ -243,82 +435,25 @@
}
checkCallingThread();
- mCallbacks.remove(callback);
- }
-
- /**
- * Adds a selector that provides hints about the kinds of routes that the application
- * is interested in. The selector may provide additional information that
- * route providers need in order to discover routes with the specified capabilities.
- * <p>
- * Adding a selector only increases the possible set of routes that an application
- * can discover using the media router; adding a selector does not restrict or
- * filter the available routes in any way.
- * </p><p>
- * The application should not modify the selector object after it has been added
- * to the media router. To make changes, first remove the selector, then modify
- * it, then add the selector back.
- * </p>
- *
- * <h3>Example</h3>
- * <pre>
- * private MediaRouter.Selector mSelector;
- *
- * // Add the selector on resume to tell the media router what kinds of routes
- * // the application is interested in so that it can try to discover suitable ones.
- * public void onResume() {
- * super.onResume();
- * MediaRouter mediaRouter = MediaRouter.getInstance(context);
- * if (mSelector == null) {
- * mSelector = new MediaRouter.Selector();
- * mSelector.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
- * mSelector.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
- * }
- * mediaRouter.addSelector(mSelector);
- * mediaRouter.updateSelectedRoute(mSelector);
- * }
- *
- * // Remove the selector on pause to tell the media router that it no longer
- * // needs to invest effort trying to discover routes of these kinds for now.
- * public void onPause() {
- * super.onPause();
- * MediaRouter mediaRouter = MediaRouter.getInstance(context);
- * if (mSelector != null) {
- * mediaRouter.removeSelector(mSelector);
- * }
- * }
- * </pre>
- *
- * @param selector The selector to add.
- * @see #removeSelector
- */
- public void addSelector(Selector selector) {
- if (selector == null) {
- throw new IllegalArgumentException("selector must not be null");
+ if (DEBUG) {
+ Log.d(TAG, "removeCallback: callback=" + callback);
}
- checkCallingThread();
- if (!mSelectors.contains(selector)) {
- mSelectors.add(selector);
- sGlobal.updateCompositeSelector();
+ int index = findCallbackRecord(callback);
+ if (index >= 0) {
+ mCallbackRecords.remove(index);
+ sGlobal.updateDiscoveryRequest();
}
}
- /**
- * Removes a route selector.
- *
- * @param selector The selector to remove.
- * @see #addSelector
- */
- public void removeSelector(Selector selector) {
- if (selector == null) {
- throw new IllegalArgumentException("selector must not be null");
+ private int findCallbackRecord(Callback callback) {
+ final int count = mCallbackRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mCallbackRecords.get(i).mCallback == callback) {
+ return i;
+ }
}
- checkCallingThread();
-
- if (mSelectors.remove(selector)) {
- sGlobal.updateCompositeSelector();
- }
+ return -1;
}
/**
@@ -335,6 +470,9 @@
}
checkCallingThread();
+ if (DEBUG) {
+ Log.d(TAG, "addProvider: " + providerInstance);
+ }
sGlobal.addProvider(providerInstance);
}
@@ -352,52 +490,10 @@
}
checkCallingThread();
- sGlobal.removeProvider(providerInstance);
- }
-
- /**
- * Starts actively scanning for route changes.
- * <p>
- * This method should typically be invoked when the route picker dialog has been
- * opened by the user to ensure that the set of known routes is fresh and up to date.
- * </p><p>
- * Active scanning may consume a significant amount of power and may have intrusive
- * effects on wireless connectivity. Therefore it is important that active scanning
- * only be requested when it is actually needed by the application.
- * </p><p>
- * Calls to this method nest and must be matched by an equal number of calls
- * to {@link #stopActiveScan}.
- * </p>
- *
- * @see #stopActiveScan
- */
- public void startActiveScan() {
- checkCallingThread();
- sGlobal.startActiveScan();
- }
-
- /**
- * Stops actively scanning for route changes.
- * <p>
- * This method should typically be invoked when the route picker dialog has been
- * closed by the user. Media route providers may continue passively scanning
- * for routes.
- * </p><p>
- * This method must be called once for each matching call to {@link #startActiveScan}.
- * </p>
- *
- * @see #startActiveScan
- */
- public void stopActiveScan() {
- checkCallingThread();
- sGlobal.stopActiveScan();
- }
-
- void combineSelectors(Selector result) {
- final int count = mSelectors.size();
- for (int i = 0; i < count; i++) {
- result.add(mSelectors.get(i));
+ if (DEBUG) {
+ Log.d(TAG, "removeProvider: " + providerInstance);
}
+ sGlobal.removeProvider(providerInstance);
}
/**
@@ -427,10 +523,9 @@
private final ProviderInfo mProvider;
private final String mDescriptorId;
private String mName;
- private String mStatus;
- private Drawable mIconDrawable;
- private int mIconResource;
+ private String mDescription;
private boolean mEnabled;
+ private boolean mConnecting;
private final ArrayList<IntentFilter> mControlFilters = new ArrayList<IntentFilter>();
private int mPlaybackType;
private int mPlaybackStream;
@@ -440,7 +535,7 @@
private Display mPresentationDisplay;
private int mPresentationDisplayId = -1;
private Bundle mExtras;
- private RouteDescriptor mDescriptor;
+ private MediaRouteDescriptor mDescriptor;
/**
* The default playback type, "local", indicating the presentation of the media
@@ -494,9 +589,13 @@
}
/**
- * Gets the name of this route.
+ * Gets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
*
- * @return The user-friendly name of a media route. This is the string presented
+ * @return The user-visible name of a media route. This is the string presented
* to users who may select this as the active route.
*/
public String getName() {
@@ -504,52 +603,41 @@
}
/**
- * Gets the status of this route.
+ * Gets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
*
- * @return The user-friendly status for a media route. This may include a description
- * of the currently playing media, if available.
+ * @return The description of the route, or null if none.
*/
- public String getStatus() {
- return mStatus;
- }
-
- /**
- * Get the icon representing this route.
- * This icon will be used in picker UIs if available.
- *
- * @return The icon representing this route or null if no icon is available.
- */
- public Drawable getIconDrawable() {
- checkCallingThread();
- if (mIconDrawable == null) {
- if (mIconResource != 0) {
- Resources resources = mProvider.getResources();
- if (resources != null) {
- try {
- mIconDrawable = resources.getDrawable(mIconResource);
- } catch (Resources.NotFoundException ex) {
- Log.w(TAG, "Unable to load media route icon drawable resource "
- + "from provider.", ex);
- }
- }
- }
- }
- return mIconDrawable;
+ public String getDescription() {
+ return mDescription;
}
/**
* Returns true if this route is enabled and may be selected.
*
- * @return true if this route is enabled and may be selected.
+ * @return True if this route is enabled.
*/
public boolean isEnabled() {
return mEnabled;
}
/**
+ * Returns true if the route is in the process of connecting and is not
+ * yet ready for use.
+ *
+ * @return True if this route is in the process of connecting.
+ */
+ public boolean isConnecting() {
+ return mConnecting;
+ }
+
+ /**
* Returns true if this route is currently selected.
*
- * @return true if this route is currently selected.
+ * @return True if this route is currently selected.
*
* @see MediaRouter#getSelectedRoute
*/
@@ -561,7 +649,7 @@
/**
* Returns true if this route is the default route.
*
- * @return true if this route is the default route.
+ * @return True if this route is the default route.
*
* @see MediaRouter#getDefaultRoute
*/
@@ -594,24 +682,12 @@
* @return True if the route supports at least one of the capabilities
* described in the media route selector.
*/
- public boolean matchesSelector(Selector selector) {
+ public boolean matchesSelector(MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
checkCallingThread();
-
- final int filterCount = mControlFilters.size();
- final List<String> categories = selector.getControlCategories();
- final int categoryCount = categories.size();
- for (int i = 0; i < filterCount; i++) {
- for (int j = 0; j < categoryCount; j++) {
- String category = categories.get(j);
- if (mControlFilters.get(i).hasCategory(category)) {
- return true;
- }
- }
- }
- return false;
+ return selector.matchesControlFilters(mControlFilters);
}
/**
@@ -838,8 +914,9 @@
@Override
public String toString() {
return "MediaRouter.RouteInfo{ name=" + mName
- + ", status=" + mStatus
+ + ", description=" + mDescription
+ ", enabled=" + mEnabled
+ + ", connecting=" + mConnecting
+ ", playbackType=" + mPlaybackType
+ ", playbackStream=" + mPlaybackStream
+ ", volumeHandling=" + mVolumeHandling
@@ -851,7 +928,7 @@
+ " }";
}
- int updateDescriptor(RouteDescriptor descriptor) {
+ int updateDescriptor(MediaRouteDescriptor descriptor) {
int changes = 0;
if (mDescriptor != descriptor) {
mDescriptor = descriptor;
@@ -860,30 +937,21 @@
mName = descriptor.getName();
changes |= CHANGE_GENERAL;
}
- if (!equal(mStatus, descriptor.getStatus())) {
- mStatus = descriptor.getStatus();
- changes |= CHANGE_GENERAL;
- }
- if (mIconResource != descriptor.getIconResource()) {
- mIconResource = descriptor.getIconResource();
- mIconDrawable = null;
- changes |= CHANGE_GENERAL;
- }
- if (mIconResource == 0
- && mIconDrawable != descriptor.getIconDrawable()) {
- mIconDrawable = descriptor.getIconDrawable();
+ if (!equal(mDescription, descriptor.getDescription())) {
+ mDescription = descriptor.getDescription();
changes |= CHANGE_GENERAL;
}
if (mEnabled != descriptor.isEnabled()) {
mEnabled = descriptor.isEnabled();
changes |= CHANGE_GENERAL;
}
- IntentFilter[] descriptorControlFilters = descriptor.getControlFilters();
- if (!hasSameControlFilters(descriptorControlFilters)) {
+ if (mConnecting != descriptor.isConnecting()) {
+ mConnecting = descriptor.isConnecting();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!mControlFilters.equals(descriptor.getControlFilters())) {
mControlFilters.clear();
- for (IntentFilter f : descriptorControlFilters) {
- mControlFilters.add(f);
- }
+ mControlFilters.addAll(descriptor.getControlFilters());
changes |= CHANGE_GENERAL;
}
if (mPlaybackType != descriptor.getPlaybackType()) {
@@ -920,19 +988,6 @@
return changes;
}
- boolean hasSameControlFilters(IntentFilter[] controlFilters) {
- final int count = mControlFilters.size();
- if (count != controlFilters.length) {
- return false;
- }
- for (int i = 0; i < count; i++) {
- if (!mControlFilters.get(i).equals(controlFilters[i])) {
- return false;
- }
- }
- return true;
- }
-
String getDescriptorId() {
return mDescriptorId;
}
@@ -952,9 +1007,11 @@
public static final class ProviderInfo {
private final MediaRouteProvider mProviderInstance;
private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
+ private final ArrayList<IntentFilter> mDiscoverableControlFilters =
+ new ArrayList<IntentFilter>();
private final ProviderMetadata mMetadata;
- private ProviderDescriptor mDescriptor;
+ private MediaRouteProviderDescriptor mDescriptor;
private Resources mResources;
private boolean mResourcesNotAvailable;
@@ -988,12 +1045,38 @@
/**
* Returns true if the provider requires active scans to discover routes.
+ * <p>
+ * To provide the best user experience, a media route provider should passively
+ * discover and publish changes to route descriptors in the background.
+ * However, for some providers, scanning for routes may use a significant
+ * amount of power or may interfere with wireless network connectivity.
+ * If this is the case, then the provider will indicate that it requires
+ * active scans to discover routes by setting this flag. Active scans
+ * will be performed when the user opens the route chooser dialog.
+ * </p>
*/
public boolean isActiveScanRequired() {
checkCallingThread();
return mDescriptor != null && mDescriptor.isActiveScanRequired();
}
+ /**
+ * Gets a list of {@link MediaControlIntent media route control filters} that
+ * describe the union of capabilities of all routes that this provider can
+ * possibly discover.
+ * <p>
+ * Because a route provider may not know what to look for until an
+ * application actually asks for it, the contents of the discoverable control
+ * filter list may change depending on the route selectors that applications have
+ * actually specified when {@link MediaRouter#addCallback registering callbacks}
+ * on the media router to discover routes.
+ * </p>
+ */
+ public List<IntentFilter> getDiscoverableControlFilters() {
+ checkCallingThread();
+ return mDiscoverableControlFilters;
+ }
+
Resources getResources() {
if (mResources == null && !mResourcesNotAvailable) {
String packageName = getPackageName();
@@ -1009,9 +1092,17 @@
return mResources;
}
- boolean updateDescriptor(ProviderDescriptor descriptor) {
+ boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) {
if (mDescriptor != descriptor) {
mDescriptor = descriptor;
+ if (descriptor != null) {
+ if (!mDiscoverableControlFilters.equals(
+ descriptor.getDiscoverableControlFilters())) {
+ mDiscoverableControlFilters.clear();
+ mDiscoverableControlFilters.addAll(
+ descriptor.getDiscoverableControlFilters());
+ }
+ }
return true;
}
return false;
@@ -1030,99 +1121,21 @@
@Override
public String toString() {
return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName()
+ + ", isActiveScanRequired=" + isActiveScanRequired()
+ " }";
}
}
/**
- * A media route selector describes the capabilities of routes that applications
- * would like to discover and use.
- * <p>
- * Selector objects are used in two ways. First, the application may add a selector
- * to the media router using the {@link MediaRouter#addSelector MediaRouter.addSelector}
- * method to indicate interest in routes that support the capabilities expressed by the
- * selector. Second, the application may use a selector to determine whether a given
- * route supports these capabilities.
- * </p>
- *
- * <h3>Example</h3>
- * <pre>
- * MediaRouter.Selector selector = new MediaRouter.Selector();
- * selector.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
- * selector.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
- *
- * MediaRouter router = MediaRouter.getInstance(context);
- * router.addSelector(selector);
- * </pre>
- */
- public static final class Selector {
- private final ArrayList<String> mControlCategories;
-
- /**
- * Creates an empty selector.
- */
- public Selector() {
- mControlCategories = new ArrayList<String>();
- }
-
- /**
- * Creates a copy of another selector.
- *
- * @param other The selector to copy.
- */
- public Selector(Selector other) {
- if (other == null) {
- throw new IllegalArgumentException("other must not be null");
- }
- mControlCategories = new ArrayList<String>(other.mControlCategories);
- }
-
- /**
- * Clears the contents of the selector.
- */
- public void clear() {
- mControlCategories.clear();
- }
-
- /**
- * Adds a {@link MediaControlIntent media control category} to the selector.
- *
- * @param category The category to add to the set of desired capabilities, such as
- * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
- */
- public void addControlCategory(String category) {
- if (category == null) {
- throw new IllegalArgumentException("category must not be null");
- }
- if (!mControlCategories.contains(category)) {
- mControlCategories.add(category);
- }
- }
-
- void add(Selector other) {
- final int count = other.mControlCategories.size();
- for (int i = 0; i < count; i++) {
- String category = other.mControlCategories.get(i);
- if (!mControlCategories.contains(category)) {
- mControlCategories.add(category);
- }
- }
- }
-
- List<String> getControlCategories() {
- return mControlCategories;
- }
- }
-
- /**
* Interface for receiving events about media routing changes.
* All methods of this interface will be called from the application's main thread.
* <p>
* A Callback will only receive events relevant to routes that the callback
- * was registered for.
+ * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
+ * flag was specified in {@link MediaRouter#addCallback(MediaRouteSelector, Callback, int)}.
* </p>
*
- * @see MediaRouter#addCallback(Callback)
+ * @see MediaRouter#addCallback(MediaRouteSelector, Callback, int)
* @see MediaRouter#removeCallback(Callback)
*/
public static abstract class Callback {
@@ -1212,6 +1225,15 @@
*/
public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
}
+
+ /**
+ * Called when a property of the indicated media route provider has changed.
+ *
+ * @param router The media router reporting the event.
+ * @param provider The provider that was changed.
+ */
+ public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
+ }
}
/**
@@ -1221,22 +1243,39 @@
*/
public static abstract class ControlRequestCallback {
/**
- * Result code: The media control action succeeded.
- */
- public static final int REQUEST_SUCCEEDED = 0;
-
- /**
- * Result code: The media control action failed.
- */
- public static final int REQUEST_FAILED = -1;
-
- /**
- * Called with the result of the media control request.
+ * Called when a media control request succeeds.
*
- * @param result The result code: {@link #REQUEST_SUCCEEDED}, or {@link #REQUEST_FAILED}.
- * @param data Additional result data. Contents depend on the media control action.
+ * @param data Result data, or null if none.
+ * Contents depend on the {@link MediaControlIntent media control action}.
*/
- public void onResult(int result, Bundle data) {
+ public void onResult(Bundle data) {
+ }
+
+ /**
+ * Called when a media control request fails.
+ *
+ * @param error A localized error message which may be shown to the user, or null
+ * if the cause of the error is unclear.
+ * @param data Error data, or null if none.
+ * Contents depend on the {@link MediaControlIntent media control action}.
+ */
+ public void onError(String error, Bundle data) {
+ }
+ }
+
+ private static final class CallbackRecord {
+ public final Callback mCallback;
+ public MediaRouteSelector mSelector;
+ public int mFlags;
+
+ public CallbackRecord(Callback callback) {
+ mCallback = callback;
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+
+ public boolean filterRouteEvent(RouteInfo route) {
+ return (mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
+ || route.matchesSelector(mSelector);
}
}
@@ -1259,13 +1298,12 @@
private final CallbackHandler mCallbackHandler = new CallbackHandler();
private final DisplayManagerCompat mDisplayManager;
private final SystemMediaRouteProvider mSystemProvider;
- private final Selector mCompositeSelector = new Selector();
private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
private RouteInfo mDefaultRoute;
private RouteInfo mSelectedRoute;
private MediaRouteProvider.RouteController mSelectedRouteController;
- private int mActiveScanRequestCount;
+ private MediaRouteDiscoveryRequest mDiscoveryRequest;
GlobalMediaRouter(Context applicationContext) {
mApplicationContext = applicationContext;
@@ -1324,7 +1362,7 @@
}
}
if (callback != null) {
- callback.onResult(ControlRequestCallback.REQUEST_FAILED, null);
+ callback.onError(null, null);
}
}
@@ -1383,10 +1421,77 @@
setSelectedRouteInternal(route);
}
- public void updateCompositeSelector() {
- mCompositeSelector.clear();
+ public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
+ // Check whether any existing routes match the selector.
+ final int routeCount = mRoutes.size();
+ for (int i = 0; i < routeCount; i++) {
+ RouteInfo route = mRoutes.get(i);
+ if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0
+ && route.isDefault()) {
+ continue;
+ }
+ if (route.matchesSelector(selector)) {
+ return true;
+ }
+ }
+
+ // Check whether any provider could possibly discover a matching route
+ // if a required active scan were performed.
+ if ((flags & AVAILABILITY_FLAG_CONSIDER_ACTIVE_SCAN) != 0) {
+ final int providerCount = mProviders.size();
+ for (int i = 0; i < providerCount; i++) {
+ ProviderInfo provider = mProviders.get(i);
+ if (provider.isActiveScanRequired() && selector.matchesControlFilters(
+ provider.getDiscoverableControlFilters())) {
+ return true;
+ }
+ }
+ }
+
+ // It doesn't look like we can find a matching route right now.
+ return false;
+ }
+
+ public void updateDiscoveryRequest() {
+ // Combine all of the callback selectors and active scan flags.
+ boolean activeScan = false;
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
for (MediaRouter router : mRouters.values()) {
- router.combineSelectors(mCompositeSelector);
+ final int count = router.mCallbackRecords.size();
+ for (int i = 0; i < count; i++) {
+ CallbackRecord callback = router.mCallbackRecords.get(i);
+ builder.addSelector(callback.mSelector);
+ if ((callback.mFlags & CALLBACK_FLAG_ACTIVE_SCAN) != 0) {
+ activeScan = true;
+ }
+ }
+ }
+ MediaRouteSelector selector = builder.build();
+
+ // Create a new discovery request.
+ if (mDiscoveryRequest != null
+ && mDiscoveryRequest.getSelector().equals(selector)
+ && mDiscoveryRequest.isActiveScan() == activeScan) {
+ return; // no change
+ }
+ if (selector.isEmpty() && !activeScan) {
+ // Discovery is not needed.
+ if (mDiscoveryRequest == null) {
+ return; // no change
+ }
+ mDiscoveryRequest = null;
+ } else {
+ // Discovery is needed.
+ mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest);
+ }
+
+ // Notify providers.
+ final int providerCount = mProviders.size();
+ for (int i = 0; i < providerCount; i++) {
+ mProviders.get(i).mProviderInstance.setDiscoveryRequest(mDiscoveryRequest);
}
}
@@ -1396,58 +1501,40 @@
// 1. Add the provider to the list.
ProviderInfo provider = new ProviderInfo(providerInstance);
mProviders.add(provider);
+ if (DEBUG) {
+ Log.d(TAG, "Provider added: " + provider);
+ }
mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider);
// 2. Create the provider's contents.
updateProviderContents(provider, providerInstance.getDescriptor());
// 3. Register the provider callback.
- providerInstance.addCallback(mProviderCallback);
- // 4. Start active scans if needed.
- if (mActiveScanRequestCount != 0) {
- providerInstance.onStartActiveScan();
- }
+ providerInstance.setCallback(mProviderCallback);
+ // 4. Set the discovery request.
+ providerInstance.setDiscoveryRequest(mDiscoveryRequest);
}
}
public void removeProvider(MediaRouteProvider providerInstance) {
int index = findProviderInfo(providerInstance);
if (index >= 0) {
- // 1. Stop active scans if needed.
- if (mActiveScanRequestCount != 0) {
- providerInstance.onStopActiveScan();
- }
- // 2. Unregister the provider callback.
- providerInstance.removeCallback(mProviderCallback);
+ // 1. Unregister the provider callback.
+ providerInstance.setCallback(null);
+ // 2. Clear the discovery request.
+ providerInstance.setDiscoveryRequest(null);
// 3. Delete the provider's contents.
ProviderInfo provider = mProviders.get(index);
updateProviderContents(provider, null);
// 4. Remove the provider from the list.
+ if (DEBUG) {
+ Log.d(TAG, "Provider removed: " + provider);
+ }
mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider);
mProviders.remove(index);
}
}
- public void startActiveScan() {
- mActiveScanRequestCount += 1;
- if (mActiveScanRequestCount == 1) {
- final int count = mProviders.size();
- for (int i = 0; i < count; i++) {
- mProviders.get(i).mProviderInstance.onStartActiveScan();
- }
- }
- }
-
- public void stopActiveScan() {
- mActiveScanRequestCount -= 1;
- if (mActiveScanRequestCount == 0) {
- final int count = mProviders.size();
- for (int i = 0; i < count; i++) {
- mProviders.get(i).mProviderInstance.onStopActiveScan();
- }
- }
- }
-
private void updateProviderDescriptor(MediaRouteProvider providerInstance,
- ProviderDescriptor descriptor) {
+ MediaRouteProviderDescriptor descriptor) {
int index = findProviderInfo(providerInstance);
if (index >= 0) {
// Update the provider's contents.
@@ -1467,16 +1554,18 @@
}
private void updateProviderContents(ProviderInfo provider,
- ProviderDescriptor providerDescriptor) {
+ MediaRouteProviderDescriptor providerDescriptor) {
if (provider.updateDescriptor(providerDescriptor)) {
// Update all existing routes and reorder them to match
// the order of their descriptors.
int targetIndex = 0;
if (providerDescriptor != null) {
if (providerDescriptor.isValid()) {
- final RouteDescriptor[] routeDescriptors = providerDescriptor.getRoutes();
- for (int i = 0; i < routeDescriptors.length; i++) {
- final RouteDescriptor routeDescriptor = routeDescriptors[i];
+ final List<MediaRouteDescriptor> routeDescriptors =
+ providerDescriptor.getRoutes();
+ final int routeCount = routeDescriptors.size();
+ for (int i = 0; i < routeCount; i++) {
+ final MediaRouteDescriptor routeDescriptor = routeDescriptors.get(i);
final String id = routeDescriptor.getId();
final int sourceIndex = provider.findRouteByDescriptorId(id);
if (sourceIndex < 0) {
@@ -1487,6 +1576,9 @@
// 2. Create the route's contents.
route.updateDescriptor(routeDescriptor);
// 3. Notify clients about addition.
+ if (DEBUG) {
+ Log.d(TAG, "Route added: " + route);
+ }
mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
} else if (sourceIndex < targetIndex) {
Log.w(TAG, "Ignoring route descriptor with duplicate id: "
@@ -1502,14 +1594,24 @@
unselectRouteIfNeeded(route);
// 4. Notify clients about changes.
if ((changes & RouteInfo.CHANGE_GENERAL) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route changed: " + route);
+ }
mCallbackHandler.post(
CallbackHandler.MSG_ROUTE_CHANGED, route);
}
if ((changes & RouteInfo.CHANGE_VOLUME) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route volume changed: " + route);
+ }
mCallbackHandler.post(
CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route);
}
if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route presentation display changed: "
+ + route);
+ }
mCallbackHandler.post(CallbackHandler.
MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route);
}
@@ -1526,14 +1628,23 @@
RouteInfo route = provider.mRoutes.get(i);
route.updateDescriptor(null);
// 2. Remove the route from the list.
- mRoutes.remove(provider);
+ mRoutes.remove(route);
provider.mRoutes.remove(i);
// 3. Unselect route if needed before notifying about removal.
unselectRouteIfNeeded(route);
// 4. Notify clients about removal.
+ if (DEBUG) {
+ Log.d(TAG, "Route removed: " + route);
+ }
mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route);
}
+ // Notify provider changed.
+ if (DEBUG) {
+ Log.d(TAG, "Provider changed: " + provider);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider);
+
// Choose a new selected route if needed.
selectRouteIfNeeded();
}
@@ -1581,6 +1692,9 @@
private void setSelectedRouteInternal(RouteInfo route) {
if (mSelectedRoute != route) {
if (mSelectedRoute != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Route unselected: " + mSelectedRoute);
+ }
mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute);
if (mSelectedRouteController != null) {
mSelectedRouteController.onUnselect();
@@ -1597,6 +1711,9 @@
if (mSelectedRouteController != null) {
mSelectedRouteController.onSelect();
}
+ if (DEBUG) {
+ Log.d(TAG, "Route selected: " + mSelectedRoute);
+ }
mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute);
}
}
@@ -1618,7 +1735,7 @@
private final class ProviderCallback extends MediaRouteProvider.Callback {
@Override
public void onDescriptorChanged(MediaRouteProvider provider,
- ProviderDescriptor descriptor) {
+ MediaRouteProviderDescriptor descriptor) {
updateProviderDescriptor(provider, descriptor);
}
}
@@ -1627,15 +1744,21 @@
private final ArrayList<MediaRouter> mTempMediaRouters =
new ArrayList<MediaRouter>();
- public static final int MSG_ROUTE_ADDED = 1;
- public static final int MSG_ROUTE_REMOVED = 2;
- public static final int MSG_ROUTE_CHANGED = 3;
- public static final int MSG_ROUTE_VOLUME_CHANGED = 4;
- public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = 5;
- public static final int MSG_ROUTE_SELECTED = 6;
- public static final int MSG_ROUTE_UNSELECTED = 7;
- public static final int MSG_PROVIDER_ADDED = 8;
- public static final int MSG_PROVIDER_REMOVED = 9;
+ private static final int MSG_TYPE_MASK = 0xff00;
+ private static final int MSG_TYPE_ROUTE = 0x0100;
+ private static final int MSG_TYPE_PROVIDER = 0x0200;
+
+ public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1;
+ public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2;
+ public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3;
+ public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4;
+ public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5;
+ public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6;
+ public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7;
+
+ public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1;
+ public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2;
+ public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3;
public void post(int msg, Object obj) {
obtainMessage(msg, obj).sendToTarget();
@@ -1655,9 +1778,9 @@
final int routerCount = mTempMediaRouters.size();
for (int i = 0; i < routerCount; i++) {
final MediaRouter router = mTempMediaRouters.get(i);
- if (!router.mCallbacks.isEmpty()) {
- for (MediaRouter.Callback callback : router.mCallbacks) {
- invokeCallback(router, callback, what, obj);
+ if (!router.mCallbackRecords.isEmpty()) {
+ for (CallbackRecord record : router.mCallbackRecords) {
+ invokeCallback(router, record, what, obj);
}
}
}
@@ -1683,36 +1806,54 @@
}
}
- private void invokeCallback(MediaRouter router, MediaRouter.Callback callback,
+ private void invokeCallback(MediaRouter router, CallbackRecord record,
int what, Object obj) {
- switch (what) {
- case MSG_ROUTE_ADDED:
- callback.onRouteAdded(router, (RouteInfo)obj);
+ final MediaRouter.Callback callback = record.mCallback;
+ switch (what & MSG_TYPE_MASK) {
+ case MSG_TYPE_ROUTE: {
+ final RouteInfo route = (RouteInfo)obj;
+ if (!record.filterRouteEvent(route)) {
+ break;
+ }
+ switch (what) {
+ case MSG_ROUTE_ADDED:
+ callback.onRouteAdded(router, route);
+ break;
+ case MSG_ROUTE_REMOVED:
+ callback.onRouteRemoved(router, route);
+ break;
+ case MSG_ROUTE_CHANGED:
+ callback.onRouteChanged(router, route);
+ break;
+ case MSG_ROUTE_VOLUME_CHANGED:
+ callback.onRouteVolumeChanged(router, route);
+ break;
+ case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
+ callback.onRoutePresentationDisplayChanged(router, route);
+ break;
+ case MSG_ROUTE_SELECTED:
+ callback.onRouteSelected(router, route);
+ break;
+ case MSG_ROUTE_UNSELECTED:
+ callback.onRouteUnselected(router, route);
+ break;
+ }
break;
- case MSG_ROUTE_REMOVED:
- callback.onRouteRemoved(router, (RouteInfo)obj);
- break;
- case MSG_ROUTE_CHANGED:
- callback.onRouteChanged(router, (RouteInfo)obj);
- break;
- case MSG_ROUTE_VOLUME_CHANGED:
- callback.onRouteVolumeChanged(router, (RouteInfo)obj);
- break;
- case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
- callback.onRoutePresentationDisplayChanged(router, (RouteInfo)obj);
- break;
- case MSG_ROUTE_SELECTED:
- callback.onRouteSelected(router, (RouteInfo)obj);
- break;
- case MSG_ROUTE_UNSELECTED:
- callback.onRouteUnselected(router, (RouteInfo)obj);
- break;
- case MSG_PROVIDER_ADDED:
- callback.onProviderAdded(router, (ProviderInfo)obj);
- break;
- case MSG_PROVIDER_REMOVED:
- callback.onProviderRemoved(router, (ProviderInfo)obj);
- break;
+ }
+ case MSG_TYPE_PROVIDER: {
+ final ProviderInfo provider = (ProviderInfo)obj;
+ switch (what) {
+ case MSG_PROVIDER_ADDED:
+ callback.onProviderAdded(router, provider);
+ break;
+ case MSG_PROVIDER_REMOVED:
+ callback.onProviderRemoved(router, provider);
+ break;
+ case MSG_PROVIDER_CHANGED:
+ callback.onProviderChanged(router, provider);
+ break;
+ }
+ }
}
}
}
diff --git a/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java
index 0c619b7..b8d2f0e 100644
--- a/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/RegisteredMediaRouteProvider.java
@@ -34,6 +34,7 @@
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.List;
/**
* Maintains a connection to a particular media route provider service.
@@ -41,7 +42,7 @@
final class RegisteredMediaRouteProvider extends MediaRouteProvider
implements ServiceConnection {
private static final String TAG = "RegisteredMediaRouteProvider";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private final ComponentName mComponentName;
private final PrivateHandler mPrivateHandler;
@@ -50,7 +51,6 @@
private boolean mBound;
private Connection mActiveConnection;
private boolean mConnectionReady;
- private boolean mActiveScanRequested;
public RegisteredMediaRouteProvider(Context context, ComponentName componentName) {
super(context, new ProviderMetadata(componentName.getPackageName()));
@@ -61,11 +61,13 @@
@Override
public RouteController onCreateRouteController(String routeId) {
- ProviderDescriptor descriptor = getDescriptor();
+ MediaRouteProviderDescriptor descriptor = getDescriptor();
if (descriptor != null) {
- RouteDescriptor[] routes = descriptor.getRoutes();
- for (int i = 0; i < routes.length; i++) {
- if (routes[i].getId().equals(routeId)) {
+ List<MediaRouteDescriptor> routes = descriptor.getRoutes();
+ final int count = routes.size();
+ for (int i = 0; i < count; i++) {
+ final MediaRouteDescriptor route = routes.get(i);
+ if (route.getId().equals(routeId)) {
Controller controller = new Controller(routeId);
mControllers.add(controller);
if (mConnectionReady) {
@@ -79,18 +81,9 @@
}
@Override
- public void onStartActiveScan() {
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
if (mConnectionReady) {
- mActiveScanRequested = true;
- mActiveConnection.startActiveScan();
- }
- }
-
- @Override
- public void onStopActiveScan() {
- if (mConnectionReady) {
- mActiveScanRequested = false;
- mActiveConnection.stopActiveScan();
+ mActiveConnection.setDiscoveryRequest(request);
}
}
@@ -174,8 +167,10 @@
if (mActiveConnection == connection) {
mConnectionReady = true;
attachControllersToConnection();
- if (mActiveScanRequested) {
- mActiveConnection.startActiveScan();
+
+ MediaRouteDiscoveryRequest request = getDiscoveryRequest();
+ if (request != null) {
+ mActiveConnection.setDiscoveryRequest(request);
}
}
}
@@ -199,7 +194,7 @@
}
private void onConnectionDescriptorChanged(Connection connection,
- ProviderDescriptor descriptor) {
+ MediaRouteProviderDescriptor descriptor) {
if (mActiveConnection == connection) {
if (DEBUG) {
Log.d(TAG, this + ": Descriptor changed, descriptor=" + descriptor);
@@ -381,7 +376,7 @@
private void failPendingCallbacks() {
int count = 0;
for (int i = 0; i < mPendingCallbacks.size(); i++) {
- mPendingCallbacks.get(i).onResult(ControlRequestCallback.REQUEST_FAILED, null);
+ mPendingCallbacks.get(i).onError(null, null);
}
mPendingCallbacks.clear();
}
@@ -394,7 +389,7 @@
ControlRequestCallback callback = mPendingCallbacks.get(requestId);
if (callback != null) {
mPendingCallbacks.remove(requestId);
- callback.onResult(ControlRequestCallback.REQUEST_FAILED, null);
+ callback.onError(null, null);
}
return true;
}
@@ -411,7 +406,7 @@
mPendingRegisterRequestId = 0;
mServiceVersion = serviceVersion;
onConnectionDescriptorChanged(this,
- ProviderDescriptor.fromBundle(descriptorBundle));
+ MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
onConnectionReady(this);
return true;
}
@@ -421,18 +416,27 @@
public boolean onDescriptorChanged(Bundle descriptorBundle) {
if (mServiceVersion != 0) {
onConnectionDescriptorChanged(this,
- ProviderDescriptor.fromBundle(descriptorBundle));
+ MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
return true;
}
return false;
}
- public boolean onControlRequestResult(int requestId, int resultCode,
- Bundle resultData) {
+ public boolean onControlRequestSucceeded(int requestId, Bundle data) {
ControlRequestCallback callback = mPendingCallbacks.get(requestId);
if (callback != null) {
mPendingCallbacks.remove(requestId);
- callback.onResult(resultCode, resultData);
+ callback.onResult(data);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onControlRequestFailed(int requestId, String error, Bundle data) {
+ ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+ if (callback != null) {
+ mPendingCallbacks.remove(requestId);
+ callback.onError(error, data);
return true;
}
return false;
@@ -499,14 +503,9 @@
return false;
}
- public void startActiveScan() {
- sendRequest(MediaRouteProviderService.CLIENT_MSG_START_ACTIVE_SCAN,
- mNextRequestId++, 0, null, null);
- }
-
- public void stopActiveScan() {
- sendRequest(MediaRouteProviderService.CLIENT_MSG_STOP_ACTIVE_SCAN,
- mNextRequestId++, 0, null, null);
+ public void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ sendRequest(MediaRouteProviderService.CLIENT_MSG_SET_DISCOVERY_REQUEST,
+ mNextRequestId++, 0, request != null ? request.asBundle() : null, null);
}
private boolean sendRequest(int what, int requestId, int arg, Object obj, Bundle data) {
@@ -562,14 +561,15 @@
final int requestId = msg.arg1;
final int arg = msg.arg2;
final Object obj = msg.obj;
- if (!processMessage(what, requestId, arg, obj)) {
+ final Bundle data = msg.peekData();
+ if (!processMessage(what, requestId, arg, obj, data)) {
if (DEBUG) {
Log.d(TAG, "Unhandled message from server: " + msg);
}
}
}
- private boolean processMessage(int what, int requestId, int arg, Object obj) {
+ private boolean processMessage(int what, int requestId, int arg, Object obj, Bundle data) {
Connection connection = mConnectionRef.get();
if (connection != null) {
switch (what) {
@@ -593,13 +593,22 @@
}
break;
- case MediaRouteProviderService.SERVICE_MSG_CONTROL_RESULT:
+ case MediaRouteProviderService.SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED:
if (obj == null || obj instanceof Bundle) {
- return connection.onControlRequestResult(
- requestId, arg, (Bundle)obj);
+ return connection.onControlRequestSucceeded(
+ requestId, (Bundle)obj);
}
break;
- }
+
+ case MediaRouteProviderService.SERVICE_MSG_CONTROL_REQUEST_FAILED:
+ if (obj == null || obj instanceof Bundle) {
+ String error =
+ data.getString(MediaRouteProviderService.SERVICE_DATA_ERROR);
+ return connection.onControlRequestFailed(
+ requestId, error, (Bundle)obj);
+ }
+ break;
+ }
}
return false;
}
diff --git a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
index 1246e96..bf84033 100644
--- a/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/SystemMediaRouteProvider.java
@@ -24,12 +24,10 @@
import android.media.AudioManager;
import android.os.Build;
import android.support.v7.mediarouter.R;
-import android.util.Log;
import android.view.Display;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.List;
/**
* Provides routes for built-in system destinations such as the local display
@@ -101,12 +99,14 @@
static class LegacyImpl extends SystemMediaRouteProvider {
private static final int PLAYBACK_STREAM = AudioManager.STREAM_MUSIC;
- private static final IntentFilter[] CONTROL_FILTERS;
+ private static final ArrayList<IntentFilter> CONTROL_FILTERS;
static {
- CONTROL_FILTERS = new IntentFilter[1];
- CONTROL_FILTERS[0] = new IntentFilter();
- CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
- CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+
+ CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ CONTROL_FILTERS.add(f);
}
private final AudioManager mAudioManager;
@@ -125,18 +125,23 @@
private void publishRoutes() {
Resources r = getContext().getResources();
- RouteDescriptor defaultRoute = new RouteDescriptor(
- DEFAULT_ROUTE_ID, r.getString(R.string.system_route_name));
- defaultRoute.setControlFilters(CONTROL_FILTERS);
- defaultRoute.setPlaybackStream(PLAYBACK_STREAM);
- defaultRoute.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL);
- defaultRoute.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
- defaultRoute.setVolumeMax(mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM));
+ int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
mLastReportedVolume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
- defaultRoute.setVolume(mLastReportedVolume);
+ MediaRouteDescriptor defaultRoute = new MediaRouteDescriptor.Builder(
+ DEFAULT_ROUTE_ID, r.getString(R.string.mr_system_route_name))
+ .addControlFilters(CONTROL_FILTERS)
+ .setPlaybackStream(PLAYBACK_STREAM)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+ .setVolumeMax(maxVolume)
+ .setVolume(mLastReportedVolume)
+ .build();
- ProviderDescriptor providerDescriptor = new ProviderDescriptor();
- providerDescriptor.setRoutes(new RouteDescriptor[] { defaultRoute });
+ MediaRouteProviderDescriptor providerDescriptor =
+ new MediaRouteProviderDescriptor.Builder()
+ .addDiscoverableControlFilters(CONTROL_FILTERS)
+ .addRoute(defaultRoute)
+ .build();
setDescriptor(providerDescriptor);
}
@@ -196,41 +201,33 @@
*/
static class JellybeanImpl extends SystemMediaRouteProvider
implements MediaRouterJellybean.Callback, MediaRouterJellybean.VolumeCallback {
- protected static final int ALL_ROUTE_TYPES =
- MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
- | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
- | MediaRouterJellybean.ROUTE_TYPE_USER;
-
- private static final IntentFilter[] LIVE_AUDIO_CONTROL_FILTERS;
+ private static final ArrayList<IntentFilter> LIVE_AUDIO_CONTROL_FILTERS;
static {
- LIVE_AUDIO_CONTROL_FILTERS = new IntentFilter[1];
- LIVE_AUDIO_CONTROL_FILTERS[0] = new IntentFilter();
- LIVE_AUDIO_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+
+ LIVE_AUDIO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ LIVE_AUDIO_CONTROL_FILTERS.add(f);
}
- private static final IntentFilter[] LIVE_VIDEO_CONTROL_FILTERS;
+ private static final ArrayList<IntentFilter> LIVE_VIDEO_CONTROL_FILTERS;
static {
- LIVE_VIDEO_CONTROL_FILTERS = new IntentFilter[1];
- LIVE_VIDEO_CONTROL_FILTERS[0] = new IntentFilter();
- LIVE_VIDEO_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
- }
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
- private static final IntentFilter[] ALL_CONTROL_FILTERS;
- static {
- ALL_CONTROL_FILTERS = new IntentFilter[1];
- ALL_CONTROL_FILTERS[0] = new IntentFilter();
- ALL_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
- ALL_CONTROL_FILTERS[0].addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ LIVE_VIDEO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ LIVE_VIDEO_CONTROL_FILTERS.add(f);
}
private final SyncCallback mSyncCallback;
- private Method mSelectRouteIntMethod;
- private Method mGetSystemAudioRouteMethod;
protected final Object mRouterObj;
protected final Object mCallbackObj;
protected final Object mVolumeCallbackObj;
protected final Object mUserRouteCategoryObj;
+ protected int mRouteTypes;
+ protected boolean mActiveScan;
+ protected boolean mCallbackRegistered;
// Maintains an association from framework routes to support library routes.
// Note that we cannot use the tag field for this because an application may
@@ -243,6 +240,9 @@
protected final ArrayList<UserRouteRecord> mUserRouteRecords =
new ArrayList<UserRouteRecord>();
+ private MediaRouterJellybean.SelectRouteWorkaround mSelectRouteWorkaround;
+ private MediaRouterJellybean.GetDefaultRouteWorkaround mGetDefaultRouteWorkaround;
+
public JellybeanImpl(Context context, SyncCallback syncCallback) {
super(context);
mSyncCallback = syncCallback;
@@ -252,37 +252,67 @@
Resources r = context.getResources();
mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
- mRouterObj, r.getString(R.string.user_route_category_name), false);
+ mRouterObj, r.getString(R.string.mr_user_route_category_name), false);
- addInitialSystemRoutes();
- MediaRouterJellybean.addCallback(mRouterObj, ALL_ROUTE_TYPES, mCallbackObj);
+ updateSystemRoutes();
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ int newRouteTypes = 0;
+ boolean newActiveScan = false;
+ if (request != null) {
+ final MediaRouteSelector selector = request.getSelector();
+ final List<String> categories = selector.getControlCategories();
+ final int count = categories.size();
+ for (int i = 0; i < count; i++) {
+ String category = categories.get(i);
+ if (category.equals(MediaControlIntent.CATEGORY_LIVE_AUDIO)) {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO;
+ } else if (category.equals(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO;
+ } else {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_USER;
+ }
+ }
+ newActiveScan = request.isActiveScan();
+ }
+
+ if (mRouteTypes != newRouteTypes || mActiveScan != newActiveScan) {
+ mRouteTypes = newRouteTypes;
+ mActiveScan = newActiveScan;
+ updateCallback();
+ updateSystemRoutes();
+ }
}
@Override
public void onRouteAdded(Object routeObj) {
- if (getUserRouteRecord(routeObj) == null) {
- int index = findSystemRouteRecord(routeObj);
- if (index < 0) {
- addSystemRouteNoPublish(routeObj);
- publishRoutes();
- }
+ if (addSystemRouteNoPublish(routeObj)) {
+ publishRoutes();
}
}
- private void addInitialSystemRoutes() {
+ private void updateSystemRoutes() {
+ boolean changed = false;
for (Object routeObj : MediaRouterJellybean.getRoutes(mRouterObj)) {
- addSystemRouteNoPublish(routeObj);
+ changed |= addSystemRouteNoPublish(routeObj);
}
- publishRoutes();
+ if (changed) {
+ publishRoutes();
+ }
}
- private void addSystemRouteNoPublish(Object routeObj) {
- if (getUserRouteRecord(routeObj) == null) {
+ private boolean addSystemRouteNoPublish(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null
+ && findSystemRouteRecord(routeObj) < 0) {
boolean isDefault = (getDefaultRoute() == routeObj);
SystemRouteRecord record = new SystemRouteRecord(routeObj, isDefault);
updateSystemRouteDescriptor(record);
mSystemRouteRecords.add(record);
+ return true;
}
+ return false;
}
@Override
@@ -316,8 +346,10 @@
SystemRouteRecord record = mSystemRouteRecords.get(index);
int newVolume = MediaRouterJellybean.RouteInfo.getVolume(routeObj);
if (newVolume != record.mRouteDescriptor.getVolume()) {
- record.mRouteDescriptor = new RouteDescriptor(record.mRouteDescriptor);
- record.mRouteDescriptor.setVolume(newVolume);
+ record.mRouteDescriptor =
+ new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+ .setVolume(newVolume)
+ .build();
publishRoutes();
}
}
@@ -326,7 +358,8 @@
@Override
public void onRouteSelected(int type, Object routeObj) {
- if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj, ALL_ROUTE_TYPES)) {
+ if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES)) {
// The currently selected route has already changed so this callback
// is stale. Drop it to prevent getting into sync loops.
return;
@@ -398,7 +431,7 @@
// route in the framework media router then ensure it is selected in
// the compat media router.
Object routeObj = MediaRouterJellybean.getSelectedRoute(
- mRouterObj, ALL_ROUTE_TYPES);
+ mRouterObj, MediaRouterJellybean.ALL_ROUTE_TYPES);
int index = findSystemRouteRecord(routeObj);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
@@ -457,15 +490,17 @@
}
protected void publishRoutes() {
+ MediaRouteProviderDescriptor.Builder builder =
+ new MediaRouteProviderDescriptor.Builder()
+ .addDiscoverableControlFilters(LIVE_AUDIO_CONTROL_FILTERS)
+ .addDiscoverableControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
+
int count = mSystemRouteRecords.size();
- RouteDescriptor[] routeDescriptors = new RouteDescriptor[count];
for (int i = 0; i < count; i++) {
- routeDescriptors[i] = mSystemRouteRecords.get(i).mRouteDescriptor;
+ builder.addRoute(mSystemRouteRecords.get(i).mRouteDescriptor);
}
- ProviderDescriptor providerDescriptor = new ProviderDescriptor();
- providerDescriptor.setRoutes(routeDescriptors);
- setDescriptor(providerDescriptor);
+ setDescriptor(builder.build());
}
protected int findSystemRouteRecord(Object routeObj) {
@@ -513,46 +548,38 @@
// (with a log message so we can track down the problem).
CharSequence name = MediaRouterJellybean.RouteInfo.getName(
record.mRouteObj, getContext());
- record.mRouteDescriptor = new RouteDescriptor(
+ MediaRouteDescriptor.Builder builder = new MediaRouteDescriptor.Builder(
record.mRouteDescriptorId, name != null ? name.toString() : "");
+ onBuildSystemRouteDescriptor(record, builder);
+ record.mRouteDescriptor = builder.build();
+ }
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
int supportedTypes = MediaRouterJellybean.RouteInfo.getSupportedTypes(
record.mRouteObj);
if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO) != 0) {
- if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
- record.mRouteDescriptor.setControlFilters(ALL_CONTROL_FILTERS);
- } else {
- record.mRouteDescriptor.setControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
- }
- } else if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
- record.mRouteDescriptor.setControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
+ builder.addControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
+ }
+ if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+ builder.addControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
}
- CharSequence status = MediaRouterJellybean.RouteInfo.getStatus(record.mRouteObj);
- if (status != null) {
- record.mRouteDescriptor.setStatus(status.toString());
- }
- record.mRouteDescriptor.setIconDrawable(
- MediaRouterJellybean.RouteInfo.getIconDrawable(record.mRouteObj));
- record.mRouteDescriptor.setPlaybackType(
+ builder.setPlaybackType(
MediaRouterJellybean.RouteInfo.getPlaybackType(record.mRouteObj));
- record.mRouteDescriptor.setPlaybackStream(
+ builder.setPlaybackStream(
MediaRouterJellybean.RouteInfo.getPlaybackStream(record.mRouteObj));
- record.mRouteDescriptor.setVolume(
+ builder.setVolume(
MediaRouterJellybean.RouteInfo.getVolume(record.mRouteObj));
- record.mRouteDescriptor.setVolumeMax(
+ builder.setVolumeMax(
MediaRouterJellybean.RouteInfo.getVolumeMax(record.mRouteObj));
- record.mRouteDescriptor.setVolumeHandling(
+ builder.setVolumeHandling(
MediaRouterJellybean.RouteInfo.getVolumeHandling(record.mRouteObj));
}
protected void updateUserRouteProperties(UserRouteRecord record) {
MediaRouterJellybean.UserRouteInfo.setName(
record.mRouteObj, record.mRoute.getName());
- MediaRouterJellybean.UserRouteInfo.setStatus(
- record.mRouteObj, normalizeStatus(record.mRoute.getStatus()));
- MediaRouterJellybean.UserRouteInfo.setIconDrawable(
- record.mRouteObj, record.mRoute.getIconDrawable());
MediaRouterJellybean.UserRouteInfo.setPlaybackType(
record.mRouteObj, record.mRoute.getPlaybackType());
MediaRouterJellybean.UserRouteInfo.setPlaybackStream(
@@ -565,11 +592,16 @@
record.mRouteObj, record.mRoute.getVolumeHandling());
}
- // The framework MediaRouter crashes if we set a null status even though
- // RouteInfo.getStatus() may return null. So we need to use a different
- // value instead.
- private static String normalizeStatus(String status) {
- return status != null ? status : "";
+ protected void updateCallback() {
+ if (mCallbackRegistered) {
+ mCallbackRegistered = false;
+ MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+ }
+
+ if (mRouteTypes != 0) {
+ mCallbackRegistered = true;
+ MediaRouterJellybean.addCallback(mRouterObj, mRouteTypes, mCallbackObj);
+ }
}
protected Object createCallbackObj() {
@@ -581,63 +613,18 @@
}
protected void selectRoute(Object routeObj) {
- int types = MediaRouterJellybean.RouteInfo.getSupportedTypes(routeObj);
- if ((types & MediaRouterJellybean.ROUTE_TYPE_USER) == 0) {
- // Handle non-user routes.
- // On JB and JB MR1, the selectRoute() API only supports programmatically
- // selecting user routes. So instead we rely on the hidden selectRouteInt()
- // method on these versions of the platform. This limitation was removed
- // in JB MR2. See also the JellybeanMr2Impl implementation of this method.
- if (mSelectRouteIntMethod == null) {
- try {
- mSelectRouteIntMethod = mRouterObj.getClass().getMethod(
- "selectRouteInt", int.class, MediaRouterJellybean.RouteInfo.clazz);
- } catch (NoSuchMethodException ex) {
- Log.w(TAG, "Cannot programmatically select non-user route "
- + "because the platform is missing the selectRouteInt() "
- + "method. Media routing may not work.", ex);
- return;
- }
- }
- try {
- mSelectRouteIntMethod.invoke(mRouterObj, ALL_ROUTE_TYPES, routeObj);
- } catch (IllegalAccessException ex) {
- Log.w(TAG, "Cannot programmatically select non-user route. "
- + "Media routing may not work.", ex);
- } catch (InvocationTargetException ex) {
- Log.w(TAG, "Cannot programmatically select non-user route. "
- + "Media routing may not work.", ex);
- }
- } else {
- // Handle user routes.
- MediaRouterJellybean.selectRoute(mRouterObj, ALL_ROUTE_TYPES, routeObj);
+ if (mSelectRouteWorkaround == null) {
+ mSelectRouteWorkaround = new MediaRouterJellybean.SelectRouteWorkaround();
}
+ mSelectRouteWorkaround.selectRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
}
protected Object getDefaultRoute() {
- // On JB and JB MR1, the getDefaultRoute() API does not exist.
- // Instead there is a hidden getSystemAudioRoute() that does the same thing.
- // See also the JellybeanMr2Impl implementation of this method.
- if (mGetSystemAudioRouteMethod == null) {
- try {
- mGetSystemAudioRouteMethod = mRouterObj.getClass().getMethod(
- "getSystemAudioRoute");
- } catch (NoSuchMethodException ex) {
- // Fall through.
- }
+ if (mGetDefaultRouteWorkaround == null) {
+ mGetDefaultRouteWorkaround = new MediaRouterJellybean.GetDefaultRouteWorkaround();
}
- if (mGetSystemAudioRouteMethod != null) {
- try {
- return mGetSystemAudioRouteMethod.invoke(mRouterObj);
- } catch (IllegalAccessException ex) {
- // Fall through.
- } catch (InvocationTargetException ex) {
- // Fall through.
- }
- }
- // Could not find the method or it does not work.
- // Return the first route and hope for the best.
- return MediaRouterJellybean.getRoutes(mRouterObj).get(0);
+ return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj);
}
/**
@@ -649,7 +636,7 @@
public final Object mRouteObj;
public final String mRouteDescriptorId;
- public RouteDescriptor mRouteDescriptor; // assigned immediately after creation
+ public MediaRouteDescriptor mRouteDescriptor; // assigned immediately after creation
public SystemRouteRecord(Object routeObj, boolean isDefault) {
mRouteObj = routeObj;
@@ -677,6 +664,9 @@
*/
private static class JellybeanMr1Impl extends JellybeanImpl
implements MediaRouterJellybeanMr1.Callback {
+ private MediaRouterJellybeanMr1.ActiveScanWorkaround mActiveScanWorkaround;
+ private MediaRouterJellybeanMr1.IsConnectingWorkaround mIsConnectingWorkaround;
+
public JellybeanMr1Impl(Context context, SyncCallback syncCallback) {
super(context, syncCallback);
}
@@ -692,32 +682,57 @@
? newPresentationDisplay.getDisplayId() : -1);
if (newPresentationDisplayId
!= record.mRouteDescriptor.getPresentationDisplayId()) {
- record.mRouteDescriptor = new RouteDescriptor(record.mRouteDescriptor);
- record.mRouteDescriptor.setPresentationDisplayId(newPresentationDisplayId);
+ record.mRouteDescriptor =
+ new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+ .setPresentationDisplayId(newPresentationDisplayId)
+ .build();
publishRoutes();
}
}
}
@Override
- protected void updateSystemRouteDescriptor(SystemRouteRecord record) {
- super.updateSystemRouteDescriptor(record);
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
if (!MediaRouterJellybeanMr1.RouteInfo.isEnabled(record.mRouteObj)) {
- record.mRouteDescriptor.setEnabled(false);
+ builder.setEnabled(false);
}
+
+ if (isConnecting(record)) {
+ builder.setConnecting(true);
+ }
+
Display presentationDisplay =
MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(record.mRouteObj);
if (presentationDisplay != null) {
- record.mRouteDescriptor.setPresentationDisplayId(
- presentationDisplay.getDisplayId());
+ builder.setPresentationDisplayId(presentationDisplay.getDisplayId());
}
}
@Override
+ protected void updateCallback() {
+ super.updateCallback();
+
+ if (mActiveScanWorkaround == null) {
+ mActiveScanWorkaround = new MediaRouterJellybeanMr1.ActiveScanWorkaround(
+ getContext(), getHandler());
+ }
+ mActiveScanWorkaround.setActiveScanRouteTypes(mActiveScan ? mRouteTypes : 0);
+ }
+
+ @Override
protected Object createCallbackObj() {
return MediaRouterJellybeanMr1.createCallback(this);
}
+
+ protected boolean isConnecting(SystemRouteRecord record) {
+ if (mIsConnectingWorkaround == null) {
+ mIsConnectingWorkaround = new MediaRouterJellybeanMr1.IsConnectingWorkaround();
+ }
+ return mIsConnectingWorkaround.isConnecting(record.mRouteObj);
+ }
}
/**
@@ -729,13 +744,51 @@
}
@Override
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
+
+ CharSequence description =
+ MediaRouterJellybeanMr2.RouteInfo.getDescription(record.mRouteObj);
+ if (description != null) {
+ builder.setDescription(description.toString());
+ }
+ }
+
+ @Override
protected void selectRoute(Object routeObj) {
- MediaRouterJellybean.selectRoute(mRouterObj, ALL_ROUTE_TYPES, routeObj);
+ MediaRouterJellybean.selectRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
}
@Override
protected Object getDefaultRoute() {
return MediaRouterJellybeanMr2.getDefaultRoute(mRouterObj);
}
+
+ @Override
+ protected void updateUserRouteProperties(UserRouteRecord record) {
+ super.updateUserRouteProperties(record);
+
+ MediaRouterJellybeanMr2.UserRouteInfo.setDescription(
+ record.mRouteObj, record.mRoute.getDescription());
+ }
+
+ @Override
+ protected void updateCallback() {
+ if (mCallbackRegistered) {
+ MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+ }
+
+ mCallbackRegistered = true;
+ MediaRouterJellybeanMr2.addCallback(mRouterObj, mRouteTypes, mCallbackObj,
+ MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS
+ | (mActiveScan ? MediaRouter.CALLBACK_FLAG_ACTIVE_SCAN : 0));
+ }
+
+ @Override
+ protected boolean isConnecting(SystemRouteRecord record) {
+ return MediaRouterJellybeanMr2.RouteInfo.isConnecting(record.mRouteObj);
+ }
}
}