Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.IntentFilter;
     23 import android.content.res.Resources;
     24 import android.graphics.drawable.Drawable;
     25 import android.os.Handler;
     26 import android.os.IBinder;
     27 import android.os.RemoteException;
     28 import android.os.ServiceManager;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 
     32 import java.util.ArrayList;
     33 import java.util.HashMap;
     34 import java.util.List;
     35 import java.util.concurrent.CopyOnWriteArrayList;
     36 
     37 /**
     38  * MediaRouter allows applications to control the routing of media channels
     39  * and streams from the current device to external speakers and destination devices.
     40  *
     41  * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
     42  * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
     43  * Context.MEDIA_ROUTER_SERVICE}.
     44  *
     45  * <p>The media router API is not thread-safe; all interactions with it must be
     46  * done from the main thread of the process.</p>
     47  */
     48 public class MediaRouter {
     49     private static final String TAG = "MediaRouter";
     50 
     51     static class Static {
     52         final Resources mResources;
     53         final IAudioService mAudioService;
     54         final Handler mHandler;
     55         final CopyOnWriteArrayList<CallbackInfo> mCallbacks =
     56                 new CopyOnWriteArrayList<CallbackInfo>();
     57 
     58         final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
     59         final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
     60 
     61         final RouteCategory mSystemCategory;
     62 
     63         final AudioRoutesInfo mCurRoutesInfo = new AudioRoutesInfo();
     64 
     65         RouteInfo mDefaultAudio;
     66         RouteInfo mBluetoothA2dpRoute;
     67 
     68         RouteInfo mSelectedRoute;
     69 
     70         final IAudioRoutesObserver.Stub mRoutesObserver = new IAudioRoutesObserver.Stub() {
     71             public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
     72                 mHandler.post(new Runnable() {
     73                     @Override public void run() {
     74                         updateRoutes(newRoutes);
     75                     }
     76                 });
     77             }
     78         };
     79 
     80         Static(Context appContext) {
     81             mResources = Resources.getSystem();
     82             mHandler = new Handler(appContext.getMainLooper());
     83 
     84             IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
     85             mAudioService = IAudioService.Stub.asInterface(b);
     86 
     87             mSystemCategory = new RouteCategory(
     88                     com.android.internal.R.string.default_audio_route_category_name,
     89                     ROUTE_TYPE_LIVE_AUDIO, false);
     90         }
     91 
     92         // Called after sStatic is initialized
     93         void startMonitoringRoutes(Context appContext) {
     94             mDefaultAudio = new RouteInfo(mSystemCategory);
     95             mDefaultAudio.mNameResId = com.android.internal.R.string.default_audio_route_name;
     96             mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
     97             addRoute(mDefaultAudio);
     98 
     99             appContext.registerReceiver(new VolumeChangeReceiver(),
    100                     new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
    101 
    102             AudioRoutesInfo newRoutes = null;
    103             try {
    104                 newRoutes = mAudioService.startWatchingRoutes(mRoutesObserver);
    105             } catch (RemoteException e) {
    106             }
    107             if (newRoutes != null) {
    108                 updateRoutes(newRoutes);
    109             }
    110         }
    111 
    112         void updateRoutes(AudioRoutesInfo newRoutes) {
    113             if (newRoutes.mMainType != mCurRoutesInfo.mMainType) {
    114                 mCurRoutesInfo.mMainType = newRoutes.mMainType;
    115                 int name;
    116                 if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0
    117                         || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) {
    118                     name = com.android.internal.R.string.default_audio_route_name_headphones;
    119                 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
    120                     name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
    121                 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) {
    122                     name = com.android.internal.R.string.default_audio_route_name_hdmi;
    123                 } else {
    124                     name = com.android.internal.R.string.default_audio_route_name;
    125                 }
    126                 sStatic.mDefaultAudio.mNameResId = name;
    127                 dispatchRouteChanged(sStatic.mDefaultAudio);
    128             }
    129             if (!TextUtils.equals(newRoutes.mBluetoothName, mCurRoutesInfo.mBluetoothName)) {
    130                 mCurRoutesInfo.mBluetoothName = newRoutes.mBluetoothName;
    131                 if (mCurRoutesInfo.mBluetoothName != null) {
    132                     if (sStatic.mBluetoothA2dpRoute == null) {
    133                         final RouteInfo info = new RouteInfo(sStatic.mSystemCategory);
    134                         info.mName = mCurRoutesInfo.mBluetoothName;
    135                         info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
    136                         sStatic.mBluetoothA2dpRoute = info;
    137                         addRoute(sStatic.mBluetoothA2dpRoute);
    138                         try {
    139                             if (mAudioService.isBluetoothA2dpOn()) {
    140                                 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute);
    141                             }
    142                         } catch (RemoteException e) {
    143                             Log.e(TAG, "Error selecting Bluetooth A2DP route", e);
    144                         }
    145                     } else {
    146                         sStatic.mBluetoothA2dpRoute.mName = mCurRoutesInfo.mBluetoothName;
    147                         dispatchRouteChanged(sStatic.mBluetoothA2dpRoute);
    148                     }
    149                 } else if (sStatic.mBluetoothA2dpRoute != null) {
    150                     removeRoute(sStatic.mBluetoothA2dpRoute);
    151                     sStatic.mBluetoothA2dpRoute = null;
    152                 }
    153             }
    154         }
    155     }
    156 
    157     static Static sStatic;
    158 
    159     /**
    160      * Route type flag for live audio.
    161      *
    162      * <p>A device that supports live audio routing will allow the media audio stream
    163      * to be routed to supported destinations. This can include internal speakers or
    164      * audio jacks on the device itself, A2DP devices, and more.</p>
    165      *
    166      * <p>Once initiated this routing is transparent to the application. All audio
    167      * played on the media stream will be routed to the selected destination.</p>
    168      */
    169     public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
    170 
    171     /**
    172      * Route type flag for application-specific usage.
    173      *
    174      * <p>Unlike other media route types, user routes are managed by the application.
    175      * The MediaRouter will manage and dispatch events for user routes, but the application
    176      * is expected to interpret the meaning of these events and perform the requested
    177      * routing tasks.</p>
    178      */
    179     public static final int ROUTE_TYPE_USER = 0x00800000;
    180 
    181     // Maps application contexts
    182     static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
    183 
    184     static String typesToString(int types) {
    185         final StringBuilder result = new StringBuilder();
    186         if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
    187             result.append("ROUTE_TYPE_LIVE_AUDIO ");
    188         }
    189         if ((types & ROUTE_TYPE_USER) != 0) {
    190             result.append("ROUTE_TYPE_USER ");
    191         }
    192         return result.toString();
    193     }
    194 
    195     /** @hide */
    196     public MediaRouter(Context context) {
    197         synchronized (Static.class) {
    198             if (sStatic == null) {
    199                 final Context appContext = context.getApplicationContext();
    200                 sStatic = new Static(appContext);
    201                 sStatic.startMonitoringRoutes(appContext);
    202             }
    203         }
    204     }
    205 
    206     /**
    207      * @hide for use by framework routing UI
    208      */
    209     public RouteInfo getSystemAudioRoute() {
    210         return sStatic.mDefaultAudio;
    211     }
    212 
    213     /**
    214      * @hide for use by framework routing UI
    215      */
    216     public RouteCategory getSystemAudioCategory() {
    217         return sStatic.mSystemCategory;
    218     }
    219 
    220     /**
    221      * Return the currently selected route for the given types
    222      *
    223      * @param type route types
    224      * @return the selected route
    225      */
    226     public RouteInfo getSelectedRoute(int type) {
    227         return sStatic.mSelectedRoute;
    228     }
    229 
    230     /**
    231      * Add a callback to listen to events about specific kinds of media routes.
    232      * If the specified callback is already registered, its registration will be updated for any
    233      * additional route types specified.
    234      *
    235      * @param types Types of routes this callback is interested in
    236      * @param cb Callback to add
    237      */
    238     public void addCallback(int types, Callback cb) {
    239         final int count = sStatic.mCallbacks.size();
    240         for (int i = 0; i < count; i++) {
    241             final CallbackInfo info = sStatic.mCallbacks.get(i);
    242             if (info.cb == cb) {
    243                 info.type |= types;
    244                 return;
    245             }
    246         }
    247         sStatic.mCallbacks.add(new CallbackInfo(cb, types, this));
    248     }
    249 
    250     /**
    251      * Remove the specified callback. It will no longer receive events about media routing.
    252      *
    253      * @param cb Callback to remove
    254      */
    255     public void removeCallback(Callback cb) {
    256         final int count = sStatic.mCallbacks.size();
    257         for (int i = 0; i < count; i++) {
    258             if (sStatic.mCallbacks.get(i).cb == cb) {
    259                 sStatic.mCallbacks.remove(i);
    260                 return;
    261             }
    262         }
    263         Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
    264     }
    265 
    266     /**
    267      * Select the specified route to use for output of the given media types.
    268      *
    269      * @param types type flags indicating which types this route should be used for.
    270      *              The route must support at least a subset.
    271      * @param route Route to select
    272      */
    273     public void selectRoute(int types, RouteInfo route) {
    274         // Applications shouldn't programmatically change anything but user routes.
    275         types &= ROUTE_TYPE_USER;
    276         selectRouteStatic(types, route);
    277     }
    278 
    279     /**
    280      * @hide internal use
    281      */
    282     public void selectRouteInt(int types, RouteInfo route) {
    283         selectRouteStatic(types, route);
    284     }
    285 
    286     static void selectRouteStatic(int types, RouteInfo route) {
    287         if (sStatic.mSelectedRoute == route) return;
    288         if ((route.getSupportedTypes() & types) == 0) {
    289             Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
    290                     typesToString(route.getSupportedTypes()) + " into route types " +
    291                     typesToString(types));
    292             return;
    293         }
    294 
    295         final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute;
    296         if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 &&
    297                 (route == btRoute || route == sStatic.mDefaultAudio)) {
    298             try {
    299                 sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute);
    300             } catch (RemoteException e) {
    301                 Log.e(TAG, "Error changing Bluetooth A2DP state", e);
    302             }
    303         }
    304 
    305         if (sStatic.mSelectedRoute != null) {
    306             // TODO filter types properly
    307             dispatchRouteUnselected(types & sStatic.mSelectedRoute.getSupportedTypes(),
    308                     sStatic.mSelectedRoute);
    309         }
    310         sStatic.mSelectedRoute = route;
    311         if (route != null) {
    312             // TODO filter types properly
    313             dispatchRouteSelected(types & route.getSupportedTypes(), route);
    314         }
    315     }
    316 
    317     /**
    318      * Add an app-specified route for media to the MediaRouter.
    319      * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
    320      *
    321      * @param info Definition of the route to add
    322      * @see #createUserRoute()
    323      * @see #removeUserRoute(UserRouteInfo)
    324      */
    325     public void addUserRoute(UserRouteInfo info) {
    326         addRoute(info);
    327     }
    328 
    329     /**
    330      * @hide Framework use only
    331      */
    332     public void addRouteInt(RouteInfo info) {
    333         addRoute(info);
    334     }
    335 
    336     static void addRoute(RouteInfo info) {
    337         final RouteCategory cat = info.getCategory();
    338         if (!sStatic.mCategories.contains(cat)) {
    339             sStatic.mCategories.add(cat);
    340         }
    341         final boolean onlyRoute = sStatic.mRoutes.isEmpty();
    342         if (cat.isGroupable() && !(info instanceof RouteGroup)) {
    343             // Enforce that any added route in a groupable category must be in a group.
    344             final RouteGroup group = new RouteGroup(info.getCategory());
    345             group.mSupportedTypes = info.mSupportedTypes;
    346             sStatic.mRoutes.add(group);
    347             dispatchRouteAdded(group);
    348             group.addRoute(info);
    349 
    350             info = group;
    351         } else {
    352             sStatic.mRoutes.add(info);
    353             dispatchRouteAdded(info);
    354         }
    355 
    356         if (onlyRoute) {
    357             selectRouteStatic(info.getSupportedTypes(), info);
    358         }
    359     }
    360 
    361     /**
    362      * Remove an app-specified route for media from the MediaRouter.
    363      *
    364      * @param info Definition of the route to remove
    365      * @see #addUserRoute(UserRouteInfo)
    366      */
    367     public void removeUserRoute(UserRouteInfo info) {
    368         removeRoute(info);
    369     }
    370 
    371     /**
    372      * Remove all app-specified routes from the MediaRouter.
    373      *
    374      * @see #removeUserRoute(UserRouteInfo)
    375      */
    376     public void clearUserRoutes() {
    377         for (int i = 0; i < sStatic.mRoutes.size(); i++) {
    378             final RouteInfo info = sStatic.mRoutes.get(i);
    379             // TODO Right now, RouteGroups only ever contain user routes.
    380             // The code below will need to change if this assumption does.
    381             if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
    382                 removeRouteAt(i);
    383                 i--;
    384             }
    385         }
    386     }
    387 
    388     /**
    389      * @hide internal use only
    390      */
    391     public void removeRouteInt(RouteInfo info) {
    392         removeRoute(info);
    393     }
    394 
    395     static void removeRoute(RouteInfo info) {
    396         if (sStatic.mRoutes.remove(info)) {
    397             final RouteCategory removingCat = info.getCategory();
    398             final int count = sStatic.mRoutes.size();
    399             boolean found = false;
    400             for (int i = 0; i < count; i++) {
    401                 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
    402                 if (removingCat == cat) {
    403                     found = true;
    404                     break;
    405                 }
    406             }
    407             if (info == sStatic.mSelectedRoute) {
    408                 // Removing the currently selected route? Select the default before we remove it.
    409                 // TODO: Be smarter about the route types here; this selects for all valid.
    410                 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio);
    411             }
    412             if (!found) {
    413                 sStatic.mCategories.remove(removingCat);
    414             }
    415             dispatchRouteRemoved(info);
    416         }
    417     }
    418 
    419     void removeRouteAt(int routeIndex) {
    420         if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) {
    421             final RouteInfo info = sStatic.mRoutes.remove(routeIndex);
    422             final RouteCategory removingCat = info.getCategory();
    423             final int count = sStatic.mRoutes.size();
    424             boolean found = false;
    425             for (int i = 0; i < count; i++) {
    426                 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
    427                 if (removingCat == cat) {
    428                     found = true;
    429                     break;
    430                 }
    431             }
    432             if (info == sStatic.mSelectedRoute) {
    433                 // Removing the currently selected route? Select the default before we remove it.
    434                 // TODO: Be smarter about the route types here; this selects for all valid.
    435                 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio);
    436             }
    437             if (!found) {
    438                 sStatic.mCategories.remove(removingCat);
    439             }
    440             dispatchRouteRemoved(info);
    441         }
    442     }
    443 
    444     /**
    445      * Return the number of {@link MediaRouter.RouteCategory categories} currently
    446      * represented by routes known to this MediaRouter.
    447      *
    448      * @return the number of unique categories represented by this MediaRouter's known routes
    449      */
    450     public int getCategoryCount() {
    451         return sStatic.mCategories.size();
    452     }
    453 
    454     /**
    455      * Return the {@link MediaRouter.RouteCategory category} at the given index.
    456      * Valid indices are in the range [0-getCategoryCount).
    457      *
    458      * @param index which category to return
    459      * @return the category at index
    460      */
    461     public RouteCategory getCategoryAt(int index) {
    462         return sStatic.mCategories.get(index);
    463     }
    464 
    465     /**
    466      * Return the number of {@link MediaRouter.RouteInfo routes} currently known
    467      * to this MediaRouter.
    468      *
    469      * @return the number of routes tracked by this router
    470      */
    471     public int getRouteCount() {
    472         return sStatic.mRoutes.size();
    473     }
    474 
    475     /**
    476      * Return the route at the specified index.
    477      *
    478      * @param index index of the route to return
    479      * @return the route at index
    480      */
    481     public RouteInfo getRouteAt(int index) {
    482         return sStatic.mRoutes.get(index);
    483     }
    484 
    485     static int getRouteCountStatic() {
    486         return sStatic.mRoutes.size();
    487     }
    488 
    489     static RouteInfo getRouteAtStatic(int index) {
    490         return sStatic.mRoutes.get(index);
    491     }
    492 
    493     /**
    494      * Create a new user route that may be modified and registered for use by the application.
    495      *
    496      * @param category The category the new route will belong to
    497      * @return A new UserRouteInfo for use by the application
    498      *
    499      * @see #addUserRoute(UserRouteInfo)
    500      * @see #removeUserRoute(UserRouteInfo)
    501      * @see #createRouteCategory(CharSequence)
    502      */
    503     public UserRouteInfo createUserRoute(RouteCategory category) {
    504         return new UserRouteInfo(category);
    505     }
    506 
    507     /**
    508      * Create a new route category. Each route must belong to a category.
    509      *
    510      * @param name Name of the new category
    511      * @param isGroupable true if routes in this category may be grouped with one another
    512      * @return the new RouteCategory
    513      */
    514     public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
    515         return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
    516     }
    517 
    518     /**
    519      * Create a new route category. Each route must belong to a category.
    520      *
    521      * @param nameResId Resource ID of the name of the new category
    522      * @param isGroupable true if routes in this category may be grouped with one another
    523      * @return the new RouteCategory
    524      */
    525     public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
    526         return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
    527     }
    528 
    529     static void updateRoute(final RouteInfo info) {
    530         dispatchRouteChanged(info);
    531     }
    532 
    533     static void dispatchRouteSelected(int type, RouteInfo info) {
    534         for (CallbackInfo cbi : sStatic.mCallbacks) {
    535             if ((cbi.type & type) != 0) {
    536                 cbi.cb.onRouteSelected(cbi.router, type, info);
    537             }
    538         }
    539     }
    540 
    541     static void dispatchRouteUnselected(int type, RouteInfo info) {
    542         for (CallbackInfo cbi : sStatic.mCallbacks) {
    543             if ((cbi.type & type) != 0) {
    544                 cbi.cb.onRouteUnselected(cbi.router, type, info);
    545             }
    546         }
    547     }
    548 
    549     static void dispatchRouteChanged(RouteInfo info) {
    550         for (CallbackInfo cbi : sStatic.mCallbacks) {
    551             if ((cbi.type & info.mSupportedTypes) != 0) {
    552                 cbi.cb.onRouteChanged(cbi.router, info);
    553             }
    554         }
    555     }
    556 
    557     static void dispatchRouteAdded(RouteInfo info) {
    558         for (CallbackInfo cbi : sStatic.mCallbacks) {
    559             if ((cbi.type & info.mSupportedTypes) != 0) {
    560                 cbi.cb.onRouteAdded(cbi.router, info);
    561             }
    562         }
    563     }
    564 
    565     static void dispatchRouteRemoved(RouteInfo info) {
    566         for (CallbackInfo cbi : sStatic.mCallbacks) {
    567             if ((cbi.type & info.mSupportedTypes) != 0) {
    568                 cbi.cb.onRouteRemoved(cbi.router, info);
    569             }
    570         }
    571     }
    572 
    573     static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
    574         for (CallbackInfo cbi : sStatic.mCallbacks) {
    575             if ((cbi.type & group.mSupportedTypes) != 0) {
    576                 cbi.cb.onRouteGrouped(cbi.router, info, group, index);
    577             }
    578         }
    579     }
    580 
    581     static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
    582         for (CallbackInfo cbi : sStatic.mCallbacks) {
    583             if ((cbi.type & group.mSupportedTypes) != 0) {
    584                 cbi.cb.onRouteUngrouped(cbi.router, info, group);
    585             }
    586         }
    587     }
    588 
    589     static void dispatchRouteVolumeChanged(RouteInfo info) {
    590         for (CallbackInfo cbi : sStatic.mCallbacks) {
    591             if ((cbi.type & info.mSupportedTypes) != 0) {
    592                 cbi.cb.onRouteVolumeChanged(cbi.router, info);
    593             }
    594         }
    595     }
    596 
    597     static void systemVolumeChanged(int newValue) {
    598         final RouteInfo selectedRoute = sStatic.mSelectedRoute;
    599         if (selectedRoute == null) return;
    600 
    601         if (selectedRoute == sStatic.mBluetoothA2dpRoute ||
    602                 selectedRoute == sStatic.mDefaultAudio) {
    603             dispatchRouteVolumeChanged(selectedRoute);
    604         } else if (sStatic.mBluetoothA2dpRoute != null) {
    605             try {
    606                 dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ?
    607                         sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudio);
    608             } catch (RemoteException e) {
    609                 Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e);
    610             }
    611         } else {
    612             dispatchRouteVolumeChanged(sStatic.mDefaultAudio);
    613         }
    614     }
    615 
    616     /**
    617      * Information about a media route.
    618      */
    619     public static class RouteInfo {
    620         CharSequence mName;
    621         int mNameResId;
    622         private CharSequence mStatus;
    623         int mSupportedTypes;
    624         RouteGroup mGroup;
    625         final RouteCategory mCategory;
    626         Drawable mIcon;
    627         // playback information
    628         int mPlaybackType = PLAYBACK_TYPE_LOCAL;
    629         int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
    630         int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
    631         int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
    632         int mPlaybackStream = AudioManager.STREAM_MUSIC;
    633         VolumeCallbackInfo mVcb;
    634 
    635         private Object mTag;
    636 
    637         /**
    638          * The default playback type, "local", indicating the presentation of the media is happening
    639          * on the same device (e.g. a phone, a tablet) as where it is controlled from.
    640          * @see #setPlaybackType(int)
    641          */
    642         public final static int PLAYBACK_TYPE_LOCAL = 0;
    643         /**
    644          * A playback type indicating the presentation of the media is happening on
    645          * a different device (i.e. the remote device) than where it is controlled from.
    646          * @see #setPlaybackType(int)
    647          */
    648         public final static int PLAYBACK_TYPE_REMOTE = 1;
    649         /**
    650          * Playback information indicating the playback volume is fixed, i.e. it cannot be
    651          * controlled from this object. An example of fixed playback volume is a remote player,
    652          * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
    653          * than attenuate at the source.
    654          * @see #setVolumeHandling(int)
    655          */
    656         public final static int PLAYBACK_VOLUME_FIXED = 0;
    657         /**
    658          * Playback information indicating the playback volume is variable and can be controlled
    659          * from this object.
    660          */
    661         public final static int PLAYBACK_VOLUME_VARIABLE = 1;
    662 
    663         RouteInfo(RouteCategory category) {
    664             mCategory = category;
    665         }
    666 
    667         /**
    668          * @return The user-friendly name of a media route. This is the string presented
    669          * to users who may select this as the active route.
    670          */
    671         public CharSequence getName() {
    672             return getName(sStatic.mResources);
    673         }
    674 
    675         /**
    676          * Return the properly localized/resource selected name of this route.
    677          *
    678          * @param context Context used to resolve the correct configuration to load
    679          * @return The user-friendly name of the media route. This is the string presented
    680          * to users who may select this as the active route.
    681          */
    682         public CharSequence getName(Context context) {
    683             return getName(context.getResources());
    684         }
    685 
    686         CharSequence getName(Resources res) {
    687             if (mNameResId != 0) {
    688                 return mName = res.getText(mNameResId);
    689             }
    690             return mName;
    691         }
    692 
    693         /**
    694          * @return The user-friendly status for a media route. This may include a description
    695          * of the currently playing media, if available.
    696          */
    697         public CharSequence getStatus() {
    698             return mStatus;
    699         }
    700 
    701         /**
    702          * @return A media type flag set describing which types this route supports.
    703          */
    704         public int getSupportedTypes() {
    705             return mSupportedTypes;
    706         }
    707 
    708         /**
    709          * @return The group that this route belongs to.
    710          */
    711         public RouteGroup getGroup() {
    712             return mGroup;
    713         }
    714 
    715         /**
    716          * @return the category this route belongs to.
    717          */
    718         public RouteCategory getCategory() {
    719             return mCategory;
    720         }
    721 
    722         /**
    723          * Get the icon representing this route.
    724          * This icon will be used in picker UIs if available.
    725          *
    726          * @return the icon representing this route or null if no icon is available
    727          */
    728         public Drawable getIconDrawable() {
    729             return mIcon;
    730         }
    731 
    732         /**
    733          * Set an application-specific tag object for this route.
    734          * The application may use this to store arbitrary data associated with the
    735          * route for internal tracking.
    736          *
    737          * <p>Note that the lifespan of a route may be well past the lifespan of
    738          * an Activity or other Context; take care that objects you store here
    739          * will not keep more data in memory alive than you intend.</p>
    740          *
    741          * @param tag Arbitrary, app-specific data for this route to hold for later use
    742          */
    743         public void setTag(Object tag) {
    744             mTag = tag;
    745             routeUpdated();
    746         }
    747 
    748         /**
    749          * @return The tag object previously set by the application
    750          * @see #setTag(Object)
    751          */
    752         public Object getTag() {
    753             return mTag;
    754         }
    755 
    756         /**
    757          * @return the type of playback associated with this route
    758          * @see UserRouteInfo#setPlaybackType(int)
    759          */
    760         public int getPlaybackType() {
    761             return mPlaybackType;
    762         }
    763 
    764         /**
    765          * @return the stream over which the playback associated with this route is performed
    766          * @see UserRouteInfo#setPlaybackStream(int)
    767          */
    768         public int getPlaybackStream() {
    769             return mPlaybackStream;
    770         }
    771 
    772         /**
    773          * Return the current volume for this route. Depending on the route, this may only
    774          * be valid if the route is currently selected.
    775          *
    776          * @return the volume at which the playback associated with this route is performed
    777          * @see UserRouteInfo#setVolume(int)
    778          */
    779         public int getVolume() {
    780             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
    781                 int vol = 0;
    782                 try {
    783                     vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
    784                 } catch (RemoteException e) {
    785                     Log.e(TAG, "Error getting local stream volume", e);
    786                 }
    787                 return vol;
    788             } else {
    789                 return mVolume;
    790             }
    791         }
    792 
    793         /**
    794          * Request a volume change for this route.
    795          * @param volume value between 0 and getVolumeMax
    796          */
    797         public void requestSetVolume(int volume) {
    798             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
    799                 try {
    800                     sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
    801                 } catch (RemoteException e) {
    802                     Log.e(TAG, "Error setting local stream volume", e);
    803                 }
    804             } else {
    805                 Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " +
    806                         "Non-local volume playback on system route? " +
    807                         "Could not request volume change.");
    808             }
    809         }
    810 
    811         /**
    812          * Request an incremental volume update for this route.
    813          * @param direction Delta to apply to the current volume
    814          */
    815         public void requestUpdateVolume(int direction) {
    816             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
    817                 try {
    818                     final int volume =
    819                             Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
    820                     sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
    821                 } catch (RemoteException e) {
    822                     Log.e(TAG, "Error setting local stream volume", e);
    823                 }
    824             } else {
    825                 Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " +
    826                         "Non-local volume playback on system route? " +
    827                         "Could not request volume change.");
    828             }
    829         }
    830 
    831         /**
    832          * @return the maximum volume at which the playback associated with this route is performed
    833          * @see UserRouteInfo#setVolumeMax(int)
    834          */
    835         public int getVolumeMax() {
    836             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
    837                 int volMax = 0;
    838                 try {
    839                     volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
    840                 } catch (RemoteException e) {
    841                     Log.e(TAG, "Error getting local stream volume", e);
    842                 }
    843                 return volMax;
    844             } else {
    845                 return mVolumeMax;
    846             }
    847         }
    848 
    849         /**
    850          * @return how volume is handling on the route
    851          * @see UserRouteInfo#setVolumeHandling(int)
    852          */
    853         public int getVolumeHandling() {
    854             return mVolumeHandling;
    855         }
    856 
    857         void setStatusInt(CharSequence status) {
    858             if (!status.equals(mStatus)) {
    859                 mStatus = status;
    860                 if (mGroup != null) {
    861                     mGroup.memberStatusChanged(this, status);
    862                 }
    863                 routeUpdated();
    864             }
    865         }
    866 
    867         final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
    868             public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
    869                 sStatic.mHandler.post(new Runnable() {
    870                     @Override
    871                     public void run() {
    872                       //Log.d(TAG, "dispatchRemoteVolumeUpdate dir=" + direction + " val=" + value);
    873                         if (mVcb != null) {
    874                             if (direction != 0) {
    875                                 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
    876                             } else {
    877                                 mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
    878                             }
    879                         }
    880                     }
    881                 });
    882             }
    883         };
    884 
    885         void routeUpdated() {
    886             updateRoute(this);
    887         }
    888 
    889         @Override
    890         public String toString() {
    891             String supportedTypes = typesToString(getSupportedTypes());
    892             return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() +
    893                     " category=" + getCategory() +
    894                     " supportedTypes=" + supportedTypes + "}";
    895         }
    896     }
    897 
    898     /**
    899      * Information about a route that the application may define and modify.
    900      * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
    901      * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
    902      *
    903      * @see MediaRouter.RouteInfo
    904      */
    905     public static class UserRouteInfo extends RouteInfo {
    906         RemoteControlClient mRcc;
    907 
    908         UserRouteInfo(RouteCategory category) {
    909             super(category);
    910             mSupportedTypes = ROUTE_TYPE_USER;
    911             mPlaybackType = PLAYBACK_TYPE_REMOTE;
    912             mVolumeHandling = PLAYBACK_VOLUME_FIXED;
    913         }
    914 
    915         /**
    916          * Set the user-visible name of this route.
    917          * @param name Name to display to the user to describe this route
    918          */
    919         public void setName(CharSequence name) {
    920             mName = name;
    921             routeUpdated();
    922         }
    923 
    924         /**
    925          * Set the user-visible name of this route.
    926          * @param resId Resource ID of the name to display to the user to describe this route
    927          */
    928         public void setName(int resId) {
    929             mNameResId = resId;
    930             mName = null;
    931             routeUpdated();
    932         }
    933 
    934         /**
    935          * Set the current user-visible status for this route.
    936          * @param status Status to display to the user to describe what the endpoint
    937          * of this route is currently doing
    938          */
    939         public void setStatus(CharSequence status) {
    940             setStatusInt(status);
    941         }
    942 
    943         /**
    944          * Set the RemoteControlClient responsible for reporting playback info for this
    945          * user route.
    946          *
    947          * <p>If this route manages remote playback, the data exposed by this
    948          * RemoteControlClient will be used to reflect and update information
    949          * such as route volume info in related UIs.</p>
    950          *
    951          * <p>The RemoteControlClient must have been previously registered with
    952          * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
    953          *
    954          * @param rcc RemoteControlClient associated with this route
    955          */
    956         public void setRemoteControlClient(RemoteControlClient rcc) {
    957             mRcc = rcc;
    958             updatePlaybackInfoOnRcc();
    959         }
    960 
    961         /**
    962          * Retrieve the RemoteControlClient associated with this route, if one has been set.
    963          *
    964          * @return the RemoteControlClient associated with this route
    965          * @see #setRemoteControlClient(RemoteControlClient)
    966          */
    967         public RemoteControlClient getRemoteControlClient() {
    968             return mRcc;
    969         }
    970 
    971         /**
    972          * Set an icon that will be used to represent this route.
    973          * The system may use this icon in picker UIs or similar.
    974          *
    975          * @param icon icon drawable to use to represent this route
    976          */
    977         public void setIconDrawable(Drawable icon) {
    978             mIcon = icon;
    979         }
    980 
    981         /**
    982          * Set an icon that will be used to represent this route.
    983          * The system may use this icon in picker UIs or similar.
    984          *
    985          * @param resId Resource ID of an icon drawable to use to represent this route
    986          */
    987         public void setIconResource(int resId) {
    988             setIconDrawable(sStatic.mResources.getDrawable(resId));
    989         }
    990 
    991         /**
    992          * Set a callback to be notified of volume update requests
    993          * @param vcb
    994          */
    995         public void setVolumeCallback(VolumeCallback vcb) {
    996             mVcb = new VolumeCallbackInfo(vcb, this);
    997         }
    998 
    999         /**
   1000          * Defines whether playback associated with this route is "local"
   1001          *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
   1002          *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
   1003          * @param type
   1004          */
   1005         public void setPlaybackType(int type) {
   1006             if (mPlaybackType != type) {
   1007                 mPlaybackType = type;
   1008                 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type);
   1009             }
   1010         }
   1011 
   1012         /**
   1013          * Defines whether volume for the playback associated with this route is fixed
   1014          * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
   1015          * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
   1016          * @param volumeHandling
   1017          */
   1018         public void setVolumeHandling(int volumeHandling) {
   1019             if (mVolumeHandling != volumeHandling) {
   1020                 mVolumeHandling = volumeHandling;
   1021                 setPlaybackInfoOnRcc(
   1022                         RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling);
   1023             }
   1024         }
   1025 
   1026         /**
   1027          * Defines at what volume the playback associated with this route is performed (for user
   1028          * feedback purposes). This information is only used when the playback is not local.
   1029          * @param volume
   1030          */
   1031         public void setVolume(int volume) {
   1032             volume = Math.max(0, Math.min(volume, getVolumeMax()));
   1033             if (mVolume != volume) {
   1034                 mVolume = volume;
   1035                 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume);
   1036                 dispatchRouteVolumeChanged(this);
   1037                 if (mGroup != null) {
   1038                     mGroup.memberVolumeChanged(this);
   1039                 }
   1040             }
   1041         }
   1042 
   1043         @Override
   1044         public void requestSetVolume(int volume) {
   1045             if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
   1046                 if (mVcb == null) {
   1047                     Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
   1048                     return;
   1049                 }
   1050                 mVcb.vcb.onVolumeSetRequest(this, volume);
   1051             }
   1052         }
   1053 
   1054         @Override
   1055         public void requestUpdateVolume(int direction) {
   1056             if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
   1057                 if (mVcb == null) {
   1058                     Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
   1059                     return;
   1060                 }
   1061                 mVcb.vcb.onVolumeUpdateRequest(this, direction);
   1062             }
   1063         }
   1064 
   1065         /**
   1066          * Defines the maximum volume at which the playback associated with this route is performed
   1067          * (for user feedback purposes). This information is only used when the playback is not
   1068          * local.
   1069          * @param volumeMax
   1070          */
   1071         public void setVolumeMax(int volumeMax) {
   1072             if (mVolumeMax != volumeMax) {
   1073                 mVolumeMax = volumeMax;
   1074                 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax);
   1075             }
   1076         }
   1077 
   1078         /**
   1079          * Defines over what stream type the media is presented.
   1080          * @param stream
   1081          */
   1082         public void setPlaybackStream(int stream) {
   1083             if (mPlaybackStream != stream) {
   1084                 mPlaybackStream = stream;
   1085                 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream);
   1086             }
   1087         }
   1088 
   1089         private void updatePlaybackInfoOnRcc() {
   1090             if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) {
   1091                 mRcc.setPlaybackInformation(
   1092                         RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax);
   1093                 mRcc.setPlaybackInformation(
   1094                         RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume);
   1095                 mRcc.setPlaybackInformation(
   1096                         RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling);
   1097                 mRcc.setPlaybackInformation(
   1098                         RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream);
   1099                 mRcc.setPlaybackInformation(
   1100                         RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType);
   1101                 // let AudioService know whom to call when remote volume needs to be updated
   1102                 try {
   1103                     sStatic.mAudioService.registerRemoteVolumeObserverForRcc(
   1104                             mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */);
   1105                 } catch (RemoteException e) {
   1106                     Log.e(TAG, "Error registering remote volume observer", e);
   1107                 }
   1108             }
   1109         }
   1110 
   1111         private void setPlaybackInfoOnRcc(int what, int value) {
   1112             if (mRcc != null) {
   1113                 mRcc.setPlaybackInformation(what, value);
   1114             }
   1115         }
   1116     }
   1117 
   1118     /**
   1119      * Information about a route that consists of multiple other routes in a group.
   1120      */
   1121     public static class RouteGroup extends RouteInfo {
   1122         final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
   1123         private boolean mUpdateName;
   1124 
   1125         RouteGroup(RouteCategory category) {
   1126             super(category);
   1127             mGroup = this;
   1128             mVolumeHandling = PLAYBACK_VOLUME_FIXED;
   1129         }
   1130 
   1131         CharSequence getName(Resources res) {
   1132             if (mUpdateName) updateName();
   1133             return super.getName(res);
   1134         }
   1135 
   1136         /**
   1137          * Add a route to this group. The route must not currently belong to another group.
   1138          *
   1139          * @param route route to add to this group
   1140          */
   1141         public void addRoute(RouteInfo route) {
   1142             if (route.getGroup() != null) {
   1143                 throw new IllegalStateException("Route " + route + " is already part of a group.");
   1144             }
   1145             if (route.getCategory() != mCategory) {
   1146                 throw new IllegalArgumentException(
   1147                         "Route cannot be added to a group with a different category. " +
   1148                             "(Route category=" + route.getCategory() +
   1149                             " group category=" + mCategory + ")");
   1150             }
   1151             final int at = mRoutes.size();
   1152             mRoutes.add(route);
   1153             route.mGroup = this;
   1154             mUpdateName = true;
   1155             updateVolume();
   1156             routeUpdated();
   1157             dispatchRouteGrouped(route, this, at);
   1158         }
   1159 
   1160         /**
   1161          * Add a route to this group before the specified index.
   1162          *
   1163          * @param route route to add
   1164          * @param insertAt insert the new route before this index
   1165          */
   1166         public void addRoute(RouteInfo route, int insertAt) {
   1167             if (route.getGroup() != null) {
   1168                 throw new IllegalStateException("Route " + route + " is already part of a group.");
   1169             }
   1170             if (route.getCategory() != mCategory) {
   1171                 throw new IllegalArgumentException(
   1172                         "Route cannot be added to a group with a different category. " +
   1173                             "(Route category=" + route.getCategory() +
   1174                             " group category=" + mCategory + ")");
   1175             }
   1176             mRoutes.add(insertAt, route);
   1177             route.mGroup = this;
   1178             mUpdateName = true;
   1179             updateVolume();
   1180             routeUpdated();
   1181             dispatchRouteGrouped(route, this, insertAt);
   1182         }
   1183 
   1184         /**
   1185          * Remove a route from this group.
   1186          *
   1187          * @param route route to remove
   1188          */
   1189         public void removeRoute(RouteInfo route) {
   1190             if (route.getGroup() != this) {
   1191                 throw new IllegalArgumentException("Route " + route +
   1192                         " is not a member of this group.");
   1193             }
   1194             mRoutes.remove(route);
   1195             route.mGroup = null;
   1196             mUpdateName = true;
   1197             updateVolume();
   1198             dispatchRouteUngrouped(route, this);
   1199             routeUpdated();
   1200         }
   1201 
   1202         /**
   1203          * Remove the route at the specified index from this group.
   1204          *
   1205          * @param index index of the route to remove
   1206          */
   1207         public void removeRoute(int index) {
   1208             RouteInfo route = mRoutes.remove(index);
   1209             route.mGroup = null;
   1210             mUpdateName = true;
   1211             updateVolume();
   1212             dispatchRouteUngrouped(route, this);
   1213             routeUpdated();
   1214         }
   1215 
   1216         /**
   1217          * @return The number of routes in this group
   1218          */
   1219         public int getRouteCount() {
   1220             return mRoutes.size();
   1221         }
   1222 
   1223         /**
   1224          * Return the route in this group at the specified index
   1225          *
   1226          * @param index Index to fetch
   1227          * @return The route at index
   1228          */
   1229         public RouteInfo getRouteAt(int index) {
   1230             return mRoutes.get(index);
   1231         }
   1232 
   1233         /**
   1234          * Set an icon that will be used to represent this group.
   1235          * The system may use this icon in picker UIs or similar.
   1236          *
   1237          * @param icon icon drawable to use to represent this group
   1238          */
   1239         public void setIconDrawable(Drawable icon) {
   1240             mIcon = icon;
   1241         }
   1242 
   1243         /**
   1244          * Set an icon that will be used to represent this group.
   1245          * The system may use this icon in picker UIs or similar.
   1246          *
   1247          * @param resId Resource ID of an icon drawable to use to represent this group
   1248          */
   1249         public void setIconResource(int resId) {
   1250             setIconDrawable(sStatic.mResources.getDrawable(resId));
   1251         }
   1252 
   1253         @Override
   1254         public void requestSetVolume(int volume) {
   1255             final int maxVol = getVolumeMax();
   1256             if (maxVol == 0) {
   1257                 return;
   1258             }
   1259 
   1260             final float scaledVolume = (float) volume / maxVol;
   1261             final int routeCount = getRouteCount();
   1262             for (int i = 0; i < routeCount; i++) {
   1263                 final RouteInfo route = getRouteAt(i);
   1264                 final int routeVol = (int) (scaledVolume * route.getVolumeMax());
   1265                 route.requestSetVolume(routeVol);
   1266             }
   1267             if (volume != mVolume) {
   1268                 mVolume = volume;
   1269                 dispatchRouteVolumeChanged(this);
   1270             }
   1271         }
   1272 
   1273         @Override
   1274         public void requestUpdateVolume(int direction) {
   1275             final int maxVol = getVolumeMax();
   1276             if (maxVol == 0) {
   1277                 return;
   1278             }
   1279 
   1280             final int routeCount = getRouteCount();
   1281             int volume = 0;
   1282             for (int i = 0; i < routeCount; i++) {
   1283                 final RouteInfo route = getRouteAt(i);
   1284                 route.requestUpdateVolume(direction);
   1285                 final int routeVol = route.getVolume();
   1286                 if (routeVol > volume) {
   1287                     volume = routeVol;
   1288                 }
   1289             }
   1290             if (volume != mVolume) {
   1291                 mVolume = volume;
   1292                 dispatchRouteVolumeChanged(this);
   1293             }
   1294         }
   1295 
   1296         void memberNameChanged(RouteInfo info, CharSequence name) {
   1297             mUpdateName = true;
   1298             routeUpdated();
   1299         }
   1300 
   1301         void memberStatusChanged(RouteInfo info, CharSequence status) {
   1302             setStatusInt(status);
   1303         }
   1304 
   1305         void memberVolumeChanged(RouteInfo info) {
   1306             updateVolume();
   1307         }
   1308 
   1309         void updateVolume() {
   1310             // A group always represents the highest component volume value.
   1311             final int routeCount = getRouteCount();
   1312             int volume = 0;
   1313             for (int i = 0; i < routeCount; i++) {
   1314                 final int routeVol = getRouteAt(i).getVolume();
   1315                 if (routeVol > volume) {
   1316                     volume = routeVol;
   1317                 }
   1318             }
   1319             if (volume != mVolume) {
   1320                 mVolume = volume;
   1321                 dispatchRouteVolumeChanged(this);
   1322             }
   1323         }
   1324 
   1325         @Override
   1326         void routeUpdated() {
   1327             int types = 0;
   1328             final int count = mRoutes.size();
   1329             if (count == 0) {
   1330                 // Don't keep empty groups in the router.
   1331                 MediaRouter.removeRoute(this);
   1332                 return;
   1333             }
   1334 
   1335             int maxVolume = 0;
   1336             boolean isLocal = true;
   1337             boolean isFixedVolume = true;
   1338             for (int i = 0; i < count; i++) {
   1339                 final RouteInfo route = mRoutes.get(i);
   1340                 types |= route.mSupportedTypes;
   1341                 final int routeMaxVolume = route.getVolumeMax();
   1342                 if (routeMaxVolume > maxVolume) {
   1343                     maxVolume = routeMaxVolume;
   1344                 }
   1345                 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
   1346                 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
   1347             }
   1348             mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
   1349             mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
   1350             mSupportedTypes = types;
   1351             mVolumeMax = maxVolume;
   1352             mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
   1353             super.routeUpdated();
   1354         }
   1355 
   1356         void updateName() {
   1357             final StringBuilder sb = new StringBuilder();
   1358             final int count = mRoutes.size();
   1359             for (int i = 0; i < count; i++) {
   1360                 final RouteInfo info = mRoutes.get(i);
   1361                 // TODO: There's probably a much more correct way to localize this.
   1362                 if (i > 0) sb.append(", ");
   1363                 sb.append(info.mName);
   1364             }
   1365             mName = sb.toString();
   1366             mUpdateName = false;
   1367         }
   1368 
   1369         @Override
   1370         public String toString() {
   1371             StringBuilder sb = new StringBuilder(super.toString());
   1372             sb.append('[');
   1373             final int count = mRoutes.size();
   1374             for (int i = 0; i < count; i++) {
   1375                 if (i > 0) sb.append(", ");
   1376                 sb.append(mRoutes.get(i));
   1377             }
   1378             sb.append(']');
   1379             return sb.toString();
   1380         }
   1381     }
   1382 
   1383     /**
   1384      * Definition of a category of routes. All routes belong to a category.
   1385      */
   1386     public static class RouteCategory {
   1387         CharSequence mName;
   1388         int mNameResId;
   1389         int mTypes;
   1390         final boolean mGroupable;
   1391 
   1392         RouteCategory(CharSequence name, int types, boolean groupable) {
   1393             mName = name;
   1394             mTypes = types;
   1395             mGroupable = groupable;
   1396         }
   1397 
   1398         RouteCategory(int nameResId, int types, boolean groupable) {
   1399             mNameResId = nameResId;
   1400             mTypes = types;
   1401             mGroupable = groupable;
   1402         }
   1403 
   1404         /**
   1405          * @return the name of this route category
   1406          */
   1407         public CharSequence getName() {
   1408             return getName(sStatic.mResources);
   1409         }
   1410 
   1411         /**
   1412          * Return the properly localized/configuration dependent name of this RouteCategory.
   1413          *
   1414          * @param context Context to resolve name resources
   1415          * @return the name of this route category
   1416          */
   1417         public CharSequence getName(Context context) {
   1418             return getName(context.getResources());
   1419         }
   1420 
   1421         CharSequence getName(Resources res) {
   1422             if (mNameResId != 0) {
   1423                 return res.getText(mNameResId);
   1424             }
   1425             return mName;
   1426         }
   1427 
   1428         /**
   1429          * Return the current list of routes in this category that have been added
   1430          * to the MediaRouter.
   1431          *
   1432          * <p>This list will not include routes that are nested within RouteGroups.
   1433          * A RouteGroup is treated as a single route within its category.</p>
   1434          *
   1435          * @param out a List to fill with the routes in this category. If this parameter is
   1436          *            non-null, it will be cleared, filled with the current routes with this
   1437          *            category, and returned. If this parameter is null, a new List will be
   1438          *            allocated to report the category's current routes.
   1439          * @return A list with the routes in this category that have been added to the MediaRouter.
   1440          */
   1441         public List<RouteInfo> getRoutes(List<RouteInfo> out) {
   1442             if (out == null) {
   1443                 out = new ArrayList<RouteInfo>();
   1444             } else {
   1445                 out.clear();
   1446             }
   1447 
   1448             final int count = getRouteCountStatic();
   1449             for (int i = 0; i < count; i++) {
   1450                 final RouteInfo route = getRouteAtStatic(i);
   1451                 if (route.mCategory == this) {
   1452                     out.add(route);
   1453                 }
   1454             }
   1455             return out;
   1456         }
   1457 
   1458         /**
   1459          * @return Flag set describing the route types supported by this category
   1460          */
   1461         public int getSupportedTypes() {
   1462             return mTypes;
   1463         }
   1464 
   1465         /**
   1466          * Return whether or not this category supports grouping.
   1467          *
   1468          * <p>If this method returns true, all routes obtained from this category
   1469          * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
   1470          *
   1471          * @return true if this category supports
   1472          */
   1473         public boolean isGroupable() {
   1474             return mGroupable;
   1475         }
   1476 
   1477         public String toString() {
   1478             return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
   1479                     " groupable=" + mGroupable + " }";
   1480         }
   1481     }
   1482 
   1483     static class CallbackInfo {
   1484         public int type;
   1485         public final Callback cb;
   1486         public final MediaRouter router;
   1487 
   1488         public CallbackInfo(Callback cb, int type, MediaRouter router) {
   1489             this.cb = cb;
   1490             this.type = type;
   1491             this.router = router;
   1492         }
   1493     }
   1494 
   1495     /**
   1496      * Interface for receiving events about media routing changes.
   1497      * All methods of this interface will be called from the application's main thread.
   1498      *
   1499      * <p>A Callback will only receive events relevant to routes that the callback
   1500      * was registered for.</p>
   1501      *
   1502      * @see MediaRouter#addCallback(int, Callback)
   1503      * @see MediaRouter#removeCallback(Callback)
   1504      */
   1505     public static abstract class Callback {
   1506         /**
   1507          * Called when the supplied route becomes selected as the active route
   1508          * for the given route type.
   1509          *
   1510          * @param router the MediaRouter reporting the event
   1511          * @param type Type flag set indicating the routes that have been selected
   1512          * @param info Route that has been selected for the given route types
   1513          */
   1514         public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
   1515 
   1516         /**
   1517          * Called when the supplied route becomes unselected as the active route
   1518          * for the given route type.
   1519          *
   1520          * @param router the MediaRouter reporting the event
   1521          * @param type Type flag set indicating the routes that have been unselected
   1522          * @param info Route that has been unselected for the given route types
   1523          */
   1524         public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
   1525 
   1526         /**
   1527          * Called when a route for the specified type was added.
   1528          *
   1529          * @param router the MediaRouter reporting the event
   1530          * @param info Route that has become available for use
   1531          */
   1532         public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
   1533 
   1534         /**
   1535          * Called when a route for the specified type was removed.
   1536          *
   1537          * @param router the MediaRouter reporting the event
   1538          * @param info Route that has been removed from availability
   1539          */
   1540         public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
   1541 
   1542         /**
   1543          * Called when an aspect of the indicated route has changed.
   1544          *
   1545          * <p>This will not indicate that the types supported by this route have
   1546          * changed, only that cosmetic info such as name or status have been updated.</p>
   1547          *
   1548          * @param router the MediaRouter reporting the event
   1549          * @param info The route that was changed
   1550          */
   1551         public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
   1552 
   1553         /**
   1554          * Called when a route is added to a group.
   1555          *
   1556          * @param router the MediaRouter reporting the event
   1557          * @param info The route that was added
   1558          * @param group The group the route was added to
   1559          * @param index The route index within group that info was added at
   1560          */
   1561         public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
   1562                 int index);
   1563 
   1564         /**
   1565          * Called when a route is removed from a group.
   1566          *
   1567          * @param router the MediaRouter reporting the event
   1568          * @param info The route that was removed
   1569          * @param group The group the route was removed from
   1570          */
   1571         public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
   1572 
   1573         /**
   1574          * Called when a route's volume changes.
   1575          *
   1576          * @param router the MediaRouter reporting the event
   1577          * @param info The route with altered volume
   1578          */
   1579         public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
   1580     }
   1581 
   1582     /**
   1583      * Stub implementation of {@link MediaRouter.Callback}.
   1584      * Each abstract method is defined as a no-op. Override just the ones
   1585      * you need.
   1586      */
   1587     public static class SimpleCallback extends Callback {
   1588 
   1589         @Override
   1590         public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
   1591         }
   1592 
   1593         @Override
   1594         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
   1595         }
   1596 
   1597         @Override
   1598         public void onRouteAdded(MediaRouter router, RouteInfo info) {
   1599         }
   1600 
   1601         @Override
   1602         public void onRouteRemoved(MediaRouter router, RouteInfo info) {
   1603         }
   1604 
   1605         @Override
   1606         public void onRouteChanged(MediaRouter router, RouteInfo info) {
   1607         }
   1608 
   1609         @Override
   1610         public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
   1611                 int index) {
   1612         }
   1613 
   1614         @Override
   1615         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
   1616         }
   1617 
   1618         @Override
   1619         public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
   1620         }
   1621     }
   1622 
   1623     static class VolumeCallbackInfo {
   1624         public final VolumeCallback vcb;
   1625         public final RouteInfo route;
   1626 
   1627         public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
   1628             this.vcb = vcb;
   1629             this.route = route;
   1630         }
   1631     }
   1632 
   1633     /**
   1634      * Interface for receiving events about volume changes.
   1635      * All methods of this interface will be called from the application's main thread.
   1636      *
   1637      * <p>A VolumeCallback will only receive events relevant to routes that the callback
   1638      * was registered for.</p>
   1639      *
   1640      * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
   1641      */
   1642     public static abstract class VolumeCallback {
   1643         /**
   1644          * Called when the volume for the route should be increased or decreased.
   1645          * @param info the route affected by this event
   1646          * @param direction an integer indicating whether the volume is to be increased
   1647          *     (positive value) or decreased (negative value).
   1648          *     For bundled changes, the absolute value indicates the number of changes
   1649          *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
   1650          */
   1651         public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
   1652         /**
   1653          * Called when the volume for the route should be set to the given value
   1654          * @param info the route affected by this event
   1655          * @param volume an integer indicating the new volume value that should be used, always
   1656          *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
   1657          */
   1658         public abstract void onVolumeSetRequest(RouteInfo info, int volume);
   1659     }
   1660 
   1661     static class VolumeChangeReceiver extends BroadcastReceiver {
   1662 
   1663         @Override
   1664         public void onReceive(Context context, Intent intent) {
   1665             if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
   1666                 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
   1667                         -1);
   1668                 if (streamType != AudioManager.STREAM_MUSIC) {
   1669                     return;
   1670                 }
   1671 
   1672                 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
   1673                 final int oldVolume = intent.getIntExtra(
   1674                         AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
   1675                 if (newVolume != oldVolume) {
   1676                     systemVolumeChanged(newVolume);
   1677                 }
   1678             }
   1679         }
   1680 
   1681     }
   1682 }
   1683