Add MediaRouter API.

This is just the initial state tracking. Still to go is
actually triggering Bluetooth A2DP correctly and tracking
process state in the system server.

Change-Id: I33031d52799d6e2d7208910da833831085cc3677
diff --git a/api/current.txt b/api/current.txt
index ec35e21..43128ee 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -97,6 +97,7 @@
     field public static final java.lang.String RECORD_AUDIO = "android.permission.RECORD_AUDIO";
     field public static final java.lang.String REORDER_TASKS = "android.permission.REORDER_TASKS";
     field public static final deprecated java.lang.String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
+    field public static final java.lang.String ROUTE_MEDIA_OUTPUT = "android.permission.ROUTE_MEDIA_OUTPUT";
     field public static final java.lang.String SEND_SMS = "android.permission.SEND_SMS";
     field public static final java.lang.String SET_ACTIVITY_WATCHER = "android.permission.SET_ACTIVITY_WATCHER";
     field public static final java.lang.String SET_ALARM = "com.android.alarm.permission.SET_ALARM";
@@ -11486,6 +11487,71 @@
     field public static final int DEFAULT = 0; // 0x0
   }
 
+  public class MediaRouter {
+    method public void addCallback(int, android.media.MediaRouter.Callback);
+    method public void addUserRoute(android.media.MediaRouter.UserRouteInfo);
+    method public android.media.MediaRouter.RouteCategory createRouteCategory(java.lang.CharSequence, boolean);
+    method public android.media.MediaRouter.UserRouteInfo createUserRoute(android.media.MediaRouter.RouteCategory);
+    method public static android.media.MediaRouter forApplication(android.content.Context);
+    method public android.media.MediaRouter.RouteCategory getCategoryAt(int);
+    method public int getCategoryCount();
+    method public android.media.MediaRouter.RouteInfo getRouteAt(int);
+    method public int getRouteCount();
+    method public void removeCallback(android.media.MediaRouter.Callback);
+    method public void removeUserRoute(android.media.MediaRouter.UserRouteInfo);
+    method public void selectRoute(int, android.media.MediaRouter.RouteInfo);
+    method public void setRouteVolume(int, float);
+    field public static final int ROUTE_TYPE_LIVE_AUDIO = 1; // 0x1
+    field public static final int ROUTE_TYPE_USER = 8388608; // 0x800000
+  }
+
+  public static abstract interface MediaRouter.Callback {
+    method public abstract void onRouteAdded(int, android.media.MediaRouter.RouteInfo);
+    method public abstract void onRouteChanged(android.media.MediaRouter.RouteInfo);
+    method public abstract void onRouteRemoved(int, android.media.MediaRouter.RouteInfo);
+    method public abstract void onRouteSelected(int, android.media.MediaRouter.RouteInfo);
+    method public abstract void onRouteUnselected(int, android.media.MediaRouter.RouteInfo);
+    method public abstract void onVolumeChanged(int, float);
+  }
+
+  public class MediaRouter.RouteCategory {
+    method public java.lang.CharSequence getName();
+    method public android.media.MediaRouter.RouteInfo getRouteAt(int);
+    method public int getRouteCount();
+    method public int getSupportedTypes();
+    method public boolean isGroupable();
+  }
+
+  public class MediaRouter.RouteGroup extends android.media.MediaRouter.RouteInfo {
+    method public void addRoute(android.media.MediaRouter.RouteInfo);
+    method public void addRoute(android.media.MediaRouter.RouteInfo, int);
+    method public void removeRoute(android.media.MediaRouter.RouteInfo);
+    method public void removeRoute(int);
+  }
+
+  public class MediaRouter.RouteInfo {
+    method public android.media.MediaRouter.RouteCategory getCategory();
+    method public android.media.MediaRouter.RouteGroup getGroup();
+    method public java.lang.CharSequence getName();
+    method public java.lang.CharSequence getStatus();
+    method public int getSupportedTypes();
+  }
+
+  public static class MediaRouter.SimpleCallback implements android.media.MediaRouter.Callback {
+    ctor public MediaRouter.SimpleCallback();
+    method public void onRouteAdded(int, android.media.MediaRouter.RouteInfo);
+    method public void onRouteChanged(android.media.MediaRouter.RouteInfo);
+    method public void onRouteRemoved(int, android.media.MediaRouter.RouteInfo);
+    method public void onRouteSelected(int, android.media.MediaRouter.RouteInfo);
+    method public void onRouteUnselected(int, android.media.MediaRouter.RouteInfo);
+    method public void onVolumeChanged(int, float);
+  }
+
+  public class MediaRouter.UserRouteInfo extends android.media.MediaRouter.RouteInfo {
+    method public void setName(java.lang.CharSequence);
+    method public void setStatus(java.lang.CharSequence);
+  }
+
   public class MediaScannerConnection implements android.content.ServiceConnection {
     ctor public MediaScannerConnection(android.content.Context, android.media.MediaScannerConnection.MediaScannerConnectionClient);
     method public void connect();
diff --git a/core/java/android/view/ActionProvider.java b/core/java/android/view/ActionProvider.java
index ed976ab..9150d19 100644
--- a/core/java/android/view/ActionProvider.java
+++ b/core/java/android/view/ActionProvider.java
@@ -19,28 +19,25 @@
 import android.content.Context;
 
 /**
- * This class is a mediator for accomplishing a given task, for example sharing a file.
- * It is responsible for creating a view that performs an action that accomplishes the task.
- * This class also implements other functions such a performing a default action.
- * <p>
- * An ActionProvider can be optionally specified for a {@link MenuItem} and in such a
- * case it will be responsible for creating the action view that appears in the
- * {@link android.app.ActionBar} as a substitute for the menu item when the item is
- * displayed as an action item. Also the provider is responsible for performing a
- * default action if a menu item placed on the overflow menu of the ActionBar is
- * selected and none of the menu item callbacks has handled the selection. For this
- * case the provider can also optionally provide a sub-menu for accomplishing the
- * task at hand.
- * </p>
- * <p>
- * There are two ways for using an action provider for creating and handling of action views:
+ * An ActionProvider defines rich menu interaction in a single component.
+ * ActionProvider can generate action views for use in the action bar,
+ * dynamically populate submenus of a MenuItem, and handle default menu
+ * item invocations.
+ *
+ * <p>An ActionProvider can be optionally specified for a {@link MenuItem} and will be
+ * responsible for creating the action view that appears in the {@link android.app.ActionBar}
+ * in place of a simple button in the bar. When the menu item is presented in a way that
+ * does not allow custom action views, (e.g. in an overflow menu,) the ActionProvider
+ * can perform a default action.</p>
+ *
+ * <p>There are two ways to use an action provider:
  * <ul>
  * <li>
- * Setting the action provider on a {@link MenuItem} directly by calling
+ * Set the action provider on a {@link MenuItem} directly by calling
  * {@link MenuItem#setActionProvider(ActionProvider)}.
  * </li>
  * <li>
- * Declaring the action provider in the menu XML resource. For example:
+ * Declare the action provider in an XML menu resource. For example:
  * <pre>
  * <code>
  *   &lt;item android:id="@+id/my_menu_item"
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index abd7e92..f1448a8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -636,6 +636,12 @@
         android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
         android:protectionLevel="signature" />
 
+    <!-- Allows an application to route media output to other devices. -->
+    <permission android:name="android.permission.ROUTE_MEDIA_OUTPUT"
+                android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
+                android:label="@string/permlab_route_media_output"
+                android:description="@string/permdesc_route_media_output" />
+
     <!-- =========================================== -->
     <!-- Permissions associated with telephony state -->
     <!-- =========================================== -->
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 51be22d..801fdf6 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -881,6 +881,11 @@
   <java-symbol type="string" name="granularity_label_word" />
   <java-symbol type="string" name="granularity_label_link" />
   <java-symbol type="string" name="granularity_label_line" />
+  <java-symbol type="string" name="default_audio_route_name" />
+  <java-symbol type="string" name="default_audio_route_name_headphones" />
+  <java-symbol type="string" name="default_audio_route_name_dock_speakers" />
+  <java-symbol type="string" name="default_audio_route_name_hdmi" />
+  <java-symbol type="string" name="default_audio_route_category_name" />
 
   <java-symbol type="plurals" name="abbrev_in_num_days" />
   <java-symbol type="plurals" name="abbrev_in_num_hours" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 21efe4e..c143a4e 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3102,6 +3102,11 @@
     <!-- Description of an application permission,  used to invoke default container service to copy content. -->
     <string name="permdesc_copyProtectedData">Allows the app to invoke default container service to copy content. Not for use by normal apps.</string>
 
+    <!-- Title of an application permission that lets an application route media output. -->
+    <string name="permlab_route_media_output">Route media output</string>
+    <!-- Description of an application permission that lets an application route media output. -->
+    <string name="permdesc_route_media_output">Allows an application to route media output to other external devices.</string>
+
     <!-- Shown in the tutorial for tap twice for zoom control. -->
     <string name="tutorial_double_tap_to_zoom_message_short">Touch twice for zoom control</string>
 
@@ -3516,4 +3521,25 @@
          from the activity resolver to use just this once. [CHAR LIMIT=25] -->
     <string name="activity_resolver_use_once">Just once</string>
 
+    <!-- Name of the default audio route when nothing is connected to
+         a headphone or other wired audio output jack. [CHAR LIMIT=25] -->
+    <string name="default_audio_route_name">Phone speaker</string>
+
+    <!-- Name of the default audio route for tablets when nothing
+         is connected to a headphone or other wired audio output jack. [CHAR LIMIT=25] -->
+    <string name="default_audio_route_name" product="tablet">Tablet speakers</string>
+
+    <!-- Name of the default audio route when wired headphones are
+         connected. [CHAR LIMIT=25] -->
+    <string name="default_audio_route_name_headphones">Headphones</string>
+
+    <!-- Name of the default audio route when an audio dock is connected. [CHAR LIMIT=25] -->
+    <string name="default_audio_route_name_dock_speakers">Dock speakers</string>
+
+    <!-- Name of the default audio route when HDMI is connected. [CHAR LIMIT=25] -->
+    <string name="default_audio_route_name_hdmi">HDMI audio</string>
+
+    <!-- Name of the default audio route category. [CHAR LIMIT=25] -->
+    <string name="default_audio_route_category_name">System</string>
+
 </resources>
diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java
new file mode 100644
index 0000000..b23443d
--- /dev/null
+++ b/media/java/android/media/MediaRouter.java
@@ -0,0 +1,875 @@
+/*
+ * Copyright (C) 2012 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.media;
+
+import android.bluetooth.BluetoothA2dp;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * MediaRouter allows applications to control the routing of media channels
+ * and streams from the current device to external speakers and destination devices.
+ *
+ * <p>Media routes should only be modified on your application's main thread.</p>
+ */
+public class MediaRouter {
+    private static final String TAG = "MediaRouter";
+
+    private Context mAppContext;
+    private AudioManager mAudioManager;
+    private Handler mHandler;
+    private final ArrayList<CallbackInfo> mCallbacks = new ArrayList<CallbackInfo>();
+
+    private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
+    private final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
+
+    private final RouteCategory mSystemCategory;
+    private RouteInfo mDefaultAudio;
+    private RouteInfo mBluetoothA2dpRoute;
+
+    private RouteInfo mSelectedRoute;
+
+    // These get removed when an activity dies
+    final ArrayList<BroadcastReceiver> mRegisteredReceivers = new ArrayList<BroadcastReceiver>();
+
+    /**
+     * Route type flag for live audio.
+     *
+     * <p>A device that supports live audio routing will allow the media audio stream
+     * to be routed to supported destinations. This can include internal speakers or
+     * audio jacks on the device itself, A2DP devices, and more.</p>
+     *
+     * <p>Once initiated this routing is transparent to the application. All audio
+     * played on the media stream will be routed to the selected destination.</p>
+     */
+    public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
+
+    /**
+     * Route type flag for application-specific usage.
+     *
+     * <p>Unlike other media route types, user routes are managed by the application.
+     * The MediaRouter will manage and dispatch events for user routes, but the application
+     * is expected to interpret the meaning of these events and perform the requested
+     * routing tasks.</p>
+     */
+    public static final int ROUTE_TYPE_USER = 0x00800000;
+
+    // Maps application contexts
+    static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
+
+    /**
+     * Return a MediaRouter for the application that the specified Context belongs to.
+     * The behavior or availability of media routing may depend on
+     * various parameters of the context.
+     *
+     * @param context Context for the desired router
+     * @return Router for the supplied Context
+     */
+    public static MediaRouter forApplication(Context context) {
+        final Context appContext = context.getApplicationContext();
+        if (!sRouters.containsKey(appContext)) {
+            final MediaRouter r = new MediaRouter(appContext);
+            sRouters.put(appContext, r);
+            return r;
+        } else {
+            return sRouters.get(appContext);
+        }
+    }
+
+    static String typesToString(int types) {
+        final StringBuilder result = new StringBuilder();
+        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
+            result.append("ROUTE_TYPE_LIVE_AUDIO ");
+        }
+        if ((types & ROUTE_TYPE_USER) != 0) {
+            result.append("ROUTE_TYPE_USER ");
+        }
+        return result.toString();
+    }
+
+    private MediaRouter(Context context) {
+        mAppContext = context;
+        mHandler = new Handler(mAppContext.getMainLooper());
+
+        mAudioManager = (AudioManager) mAppContext.getSystemService(Context.AUDIO_SERVICE);
+        mSystemCategory = new RouteCategory(mAppContext.getText(
+                com.android.internal.R.string.default_audio_route_category_name),
+                ROUTE_TYPE_LIVE_AUDIO, false);
+
+        registerReceivers();
+
+        createDefaultRoutes();
+    }
+
+    private void registerReceivers() {
+        final BroadcastReceiver volumeReceiver = new VolumeChangedBroadcastReceiver();
+        mAppContext.registerReceiver(volumeReceiver,
+                new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
+        mRegisteredReceivers.add(volumeReceiver);
+
+        final IntentFilter speakerFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+        speakerFilter.addAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG);
+        speakerFilter.addAction(Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG);
+        speakerFilter.addAction(Intent.ACTION_HDMI_AUDIO_PLUG);
+        final BroadcastReceiver plugReceiver = new HeadphoneChangedBroadcastReceiver();
+        mAppContext.registerReceiver(plugReceiver, speakerFilter);
+        mRegisteredReceivers.add(plugReceiver);
+    }
+
+    void unregisterReceivers() {
+        final int count = mRegisteredReceivers.size();
+        for (int i = 0; i < count; i++) {
+            final BroadcastReceiver r = mRegisteredReceivers.get(i);
+            mAppContext.unregisterReceiver(r);
+        }
+        mRegisteredReceivers.clear();
+    }
+
+    private void createDefaultRoutes() {
+        mDefaultAudio = new RouteInfo(mSystemCategory);
+        mDefaultAudio.mName = mAppContext.getText(
+                com.android.internal.R.string.default_audio_route_name);
+        mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
+        addRoute(mDefaultAudio);
+    }
+
+    void onHeadphonesPlugged(boolean headphonesPresent, String headphonesName) {
+        mDefaultAudio.mName = headphonesPresent ? headphonesName : mAppContext.getText(
+                com.android.internal.R.string.default_audio_route_name);
+        dispatchRouteChanged(mDefaultAudio);
+    }
+
+    /**
+     * Set volume for the specified route types.
+     *
+     * @param types Volume will be set for these route types
+     * @param volume Volume to set in the range 0.f (inaudible) to 1.f (full volume).
+     */
+    public void setRouteVolume(int types, float volume) {
+        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
+            final int index = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, index, 0);
+        }
+        if ((types & ROUTE_TYPE_USER) != 0) {
+            dispatchVolumeChanged(ROUTE_TYPE_USER, volume);
+        }
+    }
+
+    /**
+     * Add a callback to listen to events about specific kinds of media routes.
+     * If the specified callback is already registered, its registration will be updated for any
+     * additional route types specified.
+     *
+     * @param types Types of routes this callback is interested in
+     * @param cb Callback to add
+     */
+    public void addCallback(int types, Callback cb) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            final CallbackInfo info = mCallbacks.get(i);
+            if (info.cb == cb) {
+                info.type &= types;
+                return;
+            }
+        }
+        mCallbacks.add(new CallbackInfo(cb, types));
+    }
+
+    /**
+     * Remove the specified callback. It will no longer receive events about media routing.
+     *
+     * @param cb Callback to remove
+     */
+    public void removeCallback(Callback cb) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            if (mCallbacks.get(i).cb == cb) {
+                mCallbacks.remove(i);
+                return;
+            }
+        }
+        Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
+    }
+
+    public void selectRoute(int types, RouteInfo route) {
+        if (mSelectedRoute == route) return;
+
+        if (mSelectedRoute != null) {
+            // TODO filter types properly
+            dispatchRouteUnselected(types & mSelectedRoute.getSupportedTypes(), mSelectedRoute);
+        }
+        mSelectedRoute = route;
+        if (route != null) {
+            // TODO filter types properly
+            dispatchRouteSelected(types & route.getSupportedTypes(), route);
+        }
+    }
+
+    /**
+     * Add an app-specified route for media to the MediaRouter.
+     * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
+     *
+     * @param info Definition of the route to add
+     * @see #createUserRoute()
+     * @see #removeUserRoute(UserRouteInfo)
+     */
+    public void addUserRoute(UserRouteInfo info) {
+        addRoute(info);
+    }
+
+    void addRoute(RouteInfo info) {
+        final RouteCategory cat = info.getCategory();
+        if (!mCategories.contains(cat)) {
+            mCategories.add(cat);
+        }
+        if (info.getCategory().isGroupable() && !(info instanceof RouteGroup)) {
+            // Enforce that any added route in a groupable category must be in a group.
+            final RouteGroup group = new RouteGroup(info.getCategory());
+            group.addRoute(info);
+            info = group;
+        }
+        final boolean onlyRoute = mRoutes.isEmpty();
+        mRoutes.add(info);
+        dispatchRouteAdded(info);
+        if (onlyRoute) {
+            selectRoute(info.getSupportedTypes(), info);
+        }
+    }
+
+    /**
+     * Remove an app-specified route for media from the MediaRouter.
+     *
+     * @param info Definition of the route to remove
+     * @see #addUserRoute(UserRouteInfo)
+     */
+    public void removeUserRoute(UserRouteInfo info) {
+        removeRoute(info);
+    }
+
+    void removeRoute(RouteInfo info) {
+        if (mRoutes.remove(info)) {
+            final RouteCategory removingCat = info.getCategory();
+            final int count = mRoutes.size();
+            boolean found = false;
+            for (int i = 0; i < count; i++) {
+                final RouteCategory cat = mRoutes.get(i).getCategory();
+                if (removingCat == cat) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                mCategories.remove(removingCat);
+            }
+            dispatchRouteRemoved(info);
+        }
+    }
+
+    /**
+     * Return the number of {@link MediaRouter.RouteCategory categories} currently
+     * represented by routes known to this MediaRouter.
+     *
+     * @return the number of unique categories represented by this MediaRouter's known routes
+     */
+    public int getCategoryCount() {
+        return mCategories.size();
+    }
+
+    /**
+     * Return the {@link MediaRouter.RouteCategory category} at the given index.
+     * Valid indices are in the range [0-getCategoryCount).
+     *
+     * @param index which category to return
+     * @return the category at index
+     */
+    public RouteCategory getCategoryAt(int index) {
+        return mCategories.get(index);
+    }
+
+    /**
+     * Return the number of {@link MediaRouter.RouteInfo routes} currently known
+     * to this MediaRouter.
+     *
+     * @return the number of routes tracked by this router
+     */
+    public int getRouteCount() {
+        return mRoutes.size();
+    }
+
+    /**
+     * Return the route at the specified index.
+     *
+     * @param index index of the route to return
+     * @return the route at index
+     */
+    public RouteInfo getRouteAt(int index) {
+        return mRoutes.get(index);
+    }
+
+    /**
+     * Create a new user route that may be modified and registered for use by the application.
+     *
+     * @param category The category the new route will belong to
+     * @return A new UserRouteInfo for use by the application
+     *
+     * @see #addUserRoute(UserRouteInfo)
+     * @see #removeUserRoute(UserRouteInfo)
+     * @see #createRouteCategory(CharSequence)
+     */
+    public UserRouteInfo createUserRoute(RouteCategory category) {
+        return new UserRouteInfo(category);
+    }
+
+    /**
+     * Create a new route category. Each route must belong to a category.
+     *
+     * @param name Name of the new category
+     * @param isGroupable true if routes in this category may be grouped with one another
+     * @return the new RouteCategory
+     */
+    public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
+        return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
+    }
+
+    void updateRoute(final RouteInfo info) {
+        dispatchRouteChanged(info);
+    }
+
+    void dispatchRouteSelected(int type, RouteInfo info) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            final CallbackInfo cbi = mCallbacks.get(i);
+            if ((cbi.type & type) != 0) {
+                cbi.cb.onRouteSelected(type, info);
+            }
+        }
+    }
+
+    void dispatchRouteUnselected(int type, RouteInfo info) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            final CallbackInfo cbi = mCallbacks.get(i);
+            if ((cbi.type & type) != 0) {
+                cbi.cb.onRouteUnselected(type, info);
+            }
+        }
+    }
+
+    void dispatchRouteChanged(RouteInfo info) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            final CallbackInfo cbi = mCallbacks.get(i);
+            if ((cbi.type & info.mSupportedTypes) != 0) {
+                cbi.cb.onRouteChanged(info);
+            }
+        }
+    }
+
+    void dispatchRouteAdded(RouteInfo info) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            final CallbackInfo cbi = mCallbacks.get(i);
+            if ((cbi.type & info.mSupportedTypes) != 0) {
+                cbi.cb.onRouteAdded(info.mSupportedTypes, info);
+            }
+        }
+    }
+
+    void dispatchRouteRemoved(RouteInfo info) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            final CallbackInfo cbi = mCallbacks.get(i);
+            if ((cbi.type & info.mSupportedTypes) != 0) {
+                cbi.cb.onRouteRemoved(info.mSupportedTypes, info);
+            }
+        }
+    }
+
+    void dispatchVolumeChanged(int type, float volume) {
+        final int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            final CallbackInfo cbi = mCallbacks.get(i);
+            if ((cbi.type & type) != 0) {
+                cbi.cb.onVolumeChanged(type, volume);
+            }
+        }
+    }
+
+    void onA2dpDeviceConnected() {
+        final RouteInfo info = new RouteInfo(mSystemCategory);
+        info.mName = "Bluetooth"; // TODO Fetch the real name of the device
+        mBluetoothA2dpRoute = info;
+        addRoute(mBluetoothA2dpRoute);
+    }
+
+    void onA2dpDeviceDisconnected() {
+        removeRoute(mBluetoothA2dpRoute);
+        mBluetoothA2dpRoute = null;
+    }
+
+    /**
+     * Information about a media route.
+     */
+    public class RouteInfo {
+        CharSequence mName;
+        private CharSequence mStatus;
+        int mSupportedTypes;
+        RouteGroup mGroup;
+        final RouteCategory mCategory;
+
+        RouteInfo(RouteCategory category) {
+            mCategory = category;
+            category.mRoutes.add(this);
+        }
+
+        /**
+         * @return The user-friendly name of a media route. This is the string presented
+         * to users who may select this as the active route.
+         */
+        public CharSequence getName() {
+            return mName;
+        }
+
+        /**
+         * @return The user-friendly status for a media route. This may include a description
+         * of the currently playing media, if available.
+         */
+        public CharSequence getStatus() {
+            return mStatus;
+        }
+
+        /**
+         * @return A media type flag set describing which types this route supports.
+         */
+        public int getSupportedTypes() {
+            return mSupportedTypes;
+        }
+
+        /**
+         * @return The group that this route belongs to.
+         */
+        public RouteGroup getGroup() {
+            return mGroup;
+        }
+
+        /**
+         * @return the category this route belongs to.
+         */
+        public RouteCategory getCategory() {
+            return mCategory;
+        }
+
+        void setStatusInt(CharSequence status) {
+            if (!status.equals(mStatus)) {
+                mStatus = status;
+                routeUpdated();
+                if (mGroup != null) {
+                    mGroup.memberStatusChanged(this, status);
+                }
+                routeUpdated();
+            }
+        }
+
+        void routeUpdated() {
+            updateRoute(this);
+        }
+
+        @Override
+        public String toString() {
+            String supportedTypes = typesToString(mSupportedTypes);
+            return "RouteInfo{ name=" + mName + ", status=" + mStatus +
+                    " category=" + mCategory +
+                    " supportedTypes=" + supportedTypes + "}";
+        }
+    }
+
+    /**
+     * Information about a route that the application may define and modify.
+     *
+     * @see MediaRouter.RouteInfo
+     */
+    public class UserRouteInfo extends RouteInfo {
+
+        UserRouteInfo(RouteCategory category) {
+            super(category);
+            mSupportedTypes = ROUTE_TYPE_USER;
+        }
+
+        /**
+         * Set the user-visible name of this route.
+         * @param name Name to display to the user to describe this route
+         */
+        public void setName(CharSequence name) {
+            mName = name;
+            routeUpdated();
+        }
+
+        /**
+         * Set the current user-visible status for this route.
+         * @param status Status to display to the user to describe what the endpoint
+         * of this route is currently doing
+         */
+        public void setStatus(CharSequence status) {
+            setStatusInt(status);
+        }
+    }
+
+    /**
+     * Information about a route that consists of multiple other routes in a group.
+     */
+    public class RouteGroup extends RouteInfo {
+        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
+        private boolean mUpdateName;
+
+        RouteGroup(RouteCategory category) {
+            super(category);
+            mGroup = this;
+        }
+
+        public CharSequence getName() {
+            if (mUpdateName) updateName();
+            return super.getName();
+        }
+
+        /**
+         * Add a route to this group. The route must not currently belong to another group.
+         *
+         * @param route route to add to this group
+         */
+        public void addRoute(RouteInfo route) {
+            if (route.getGroup() != null) {
+                throw new IllegalStateException("Route " + route + " is already part of a group.");
+            }
+            if (route.getCategory() != mCategory) {
+                throw new IllegalArgumentException(
+                        "Route cannot be added to a group with a different category. " +
+                            "(Route category=" + route.getCategory() +
+                            " group category=" + mCategory + ")");
+            }
+            mRoutes.add(route);
+            mUpdateName = true;
+            routeUpdated();
+        }
+
+        /**
+         * Add a route to this group before the specified index.
+         *
+         * @param route route to add
+         * @param insertAt insert the new route before this index
+         */
+        public void addRoute(RouteInfo route, int insertAt) {
+            if (route.getGroup() != null) {
+                throw new IllegalStateException("Route " + route + " is already part of a group.");
+            }
+            if (route.getCategory() != mCategory) {
+                throw new IllegalArgumentException(
+                        "Route cannot be added to a group with a different category. " +
+                            "(Route category=" + route.getCategory() +
+                            " group category=" + mCategory + ")");
+            }
+            mRoutes.add(insertAt, route);
+            mUpdateName = true;
+            routeUpdated();
+        }
+
+        /**
+         * Remove a route from this group.
+         *
+         * @param route route to remove
+         */
+        public void removeRoute(RouteInfo route) {
+            if (route.getGroup() != this) {
+                throw new IllegalArgumentException("Route " + route +
+                        " is not a member of this group.");
+            }
+            mRoutes.remove(route);
+            mUpdateName = true;
+            routeUpdated();
+        }
+
+        /**
+         * Remove the route at the specified index from this group.
+         *
+         * @param index index of the route to remove
+         */
+        public void removeRoute(int index) {
+            mRoutes.remove(index);
+            mUpdateName = true;
+            routeUpdated();
+        }
+
+        void memberNameChanged(RouteInfo info, CharSequence name) {
+            mUpdateName = true;
+            routeUpdated();
+        }
+
+        void memberStatusChanged(RouteInfo info, CharSequence status) {
+            setStatusInt(status);
+        }
+
+        void updateName() {
+            final StringBuilder sb = new StringBuilder();
+            final int count = mRoutes.size();
+            for (int i = 0; i < count; i++) {
+                final RouteInfo info = mRoutes.get(i);
+                if (i > 0) sb.append(", ");
+                sb.append(info.mName);
+            }
+            mName = sb.toString();
+            mUpdateName = false;
+        }
+    }
+
+    /**
+     * Definition of a category of routes. All routes belong to a category.
+     */
+    public class RouteCategory {
+        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
+        CharSequence mName;
+        int mTypes;
+        final boolean mGroupable;
+
+        RouteCategory(CharSequence name, int types, boolean groupable) {
+            mName = name;
+            mTypes = types;
+            mGroupable = groupable;
+        }
+
+        /**
+         * @return the name of this route category
+         */
+        public CharSequence getName() {
+            return mName;
+        }
+
+        /**
+         * @return the number of routes in this category
+         */
+        public int getRouteCount() {
+            return mRoutes.size();
+        }
+
+        /**
+         * Return a route from this category
+         *
+         * @param index Index from [0-getRouteCount)
+         * @return the route at the given index
+         */
+        public RouteInfo getRouteAt(int index) {
+            return mRoutes.get(index);
+        }
+
+        /**
+         * @return Flag set describing the route types supported by this category
+         */
+        public int getSupportedTypes() {
+            return mTypes;
+        }
+
+        /**
+         * Return whether or not this category supports grouping.
+         *
+         * <p>If this method returns true, all routes obtained from this category
+         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.
+         *
+         * @return true if this category supports
+         */
+        public boolean isGroupable() {
+            return mGroupable;
+        }
+
+        public String toString() {
+            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
+                    " groupable=" + mGroupable + " routes=" + mRoutes.size() + " }";
+        }
+    }
+
+    static class CallbackInfo {
+        public int type;
+        public Callback cb;
+
+        public CallbackInfo(Callback cb, int type) {
+            this.cb = cb;
+            this.type = type;
+        }
+    }
+
+    /**
+     * 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.</p>
+     *
+     * @see MediaRouter#addCallback(int, Callback)
+     * @see MediaRouter#removeCallback(Callback)
+     */
+    public interface Callback {
+        /**
+         * Called when the supplied route becomes selected as the active route
+         * for the given route type.
+         *
+         * @param type Type flag set indicating the routes that have been selected
+         * @param info Route that has been selected for the given route types
+         */
+        public void onRouteSelected(int type, RouteInfo info);
+
+        /**
+         * Called when the supplied route becomes unselected as the active route
+         * for the given route type.
+         *
+         * @param type Type flag set indicating the routes that have been unselected
+         * @param info Route that has been unselected for the given route types
+         */
+        public void onRouteUnselected(int type, RouteInfo info);
+
+        /**
+         * Called when the volume is changed for the specified route types.
+         *
+         * @param type Type flags indicating which volume type was changed
+         * @param volume New volume value in the range 0 (inaudible) to 1 (full)
+         */
+        public void onVolumeChanged(int type, float volume);
+
+        /**
+         * Called when a route for the specified type was added.
+         *
+         * @param type Type flags indicating which types the added route supports
+         * @param info Route that has become available for use
+         */
+        public void onRouteAdded(int type, RouteInfo info);
+
+        /**
+         * Called when a route for the specified type was removed.
+         *
+         * @param type Type flags indicating which types the removed route supported
+         * @param info Route that has been removed from availability
+         */
+        public void onRouteRemoved(int type, RouteInfo info);
+
+        /**
+         * Called when an aspect of the indicated route has changed.
+         *
+         * <p>This will not indicate that the types supported by this route have
+         * changed, only that cosmetic info such as name or status have been updated.</p>
+         *
+         * @param info The route that was changed
+         */
+        public void onRouteChanged(RouteInfo info);
+    }
+
+    /**
+     * Stub implementation of the {@link MediaRouter.Callback} interface.
+     * Each interface method is defined as a no-op. Override just the ones
+     * you need.
+     */
+    public static class SimpleCallback implements Callback {
+
+        @Override
+        public void onRouteSelected(int type, RouteInfo info) {
+
+        }
+
+        @Override
+        public void onRouteUnselected(int type, RouteInfo info) {
+
+        }
+
+        @Override
+        public void onVolumeChanged(int type, float volume) {
+
+        }
+
+        @Override
+        public void onRouteAdded(int type, RouteInfo info) {
+
+        }
+
+        @Override
+        public void onRouteRemoved(int type, RouteInfo info) {
+
+        }
+
+        @Override
+        public void onRouteChanged(RouteInfo info) {
+
+        }
+
+    }
+
+    class VolumeChangedBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (AudioManager.VOLUME_CHANGED_ACTION.equals(action) &&
+                    AudioManager.STREAM_MUSIC == intent.getIntExtra(
+                            AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1)) {
+                final int maxVol = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+                final int volExtra = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
+                final float volume = (float) volExtra / maxVol;
+                dispatchVolumeChanged(ROUTE_TYPE_LIVE_AUDIO, volume);
+            }
+        }
+    }
+
+    class BtChangedBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+                final int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1);
+                if (state == BluetoothA2dp.STATE_CONNECTED) {
+                    onA2dpDeviceConnected();
+                } else if (state == BluetoothA2dp.STATE_DISCONNECTING ||
+                        state == BluetoothA2dp.STATE_DISCONNECTED) {
+                    onA2dpDeviceDisconnected();
+                }
+            }
+        }
+    }
+
+    class HeadphoneChangedBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
+                final boolean plugged = intent.getIntExtra("state", 0) != 0;
+                final String name = mAppContext.getString(
+                        com.android.internal.R.string.default_audio_route_name_headphones);
+                onHeadphonesPlugged(plugged, name);
+            } else if (Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG.equals(action) ||
+                    Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG.equals(action)) {
+                final boolean plugged = intent.getIntExtra("state", 0) != 0;
+                final String name = mAppContext.getString(
+                        com.android.internal.R.string.default_audio_route_name_dock_speakers);
+                onHeadphonesPlugged(plugged, name);
+            } else if (Intent.ACTION_HDMI_AUDIO_PLUG.equals(action)) {
+                final boolean plugged = intent.getIntExtra("state", 0) != 0;
+                final String name = mAppContext.getString(
+                        com.android.internal.R.string.default_audio_route_name_hdmi);
+                onHeadphonesPlugged(plugged, name);
+            }
+        }
+    }
+}