• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Static(Context appContext)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
startMonitoringRoutes(Context appContext)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 
updateRoutes(AudioRoutesInfo newRoutes)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 
typesToString(int types)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 */
MediaRouter(Context context)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      */
getSystemAudioRoute()209     public RouteInfo getSystemAudioRoute() {
210         return sStatic.mDefaultAudio;
211     }
212 
213     /**
214      * @hide for use by framework routing UI
215      */
getSystemAudioCategory()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      */
getSelectedRoute(int type)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      */
addCallback(int types, Callback cb)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      */
removeCallback(Callback cb)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      */
selectRoute(int types, RouteInfo route)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      */
selectRouteInt(int types, RouteInfo route)282     public void selectRouteInt(int types, RouteInfo route) {
283         selectRouteStatic(types, route);
284     }
285 
selectRouteStatic(int types, RouteInfo route)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      */
addUserRoute(UserRouteInfo info)325     public void addUserRoute(UserRouteInfo info) {
326         addRoute(info);
327     }
328 
329     /**
330      * @hide Framework use only
331      */
addRouteInt(RouteInfo info)332     public void addRouteInt(RouteInfo info) {
333         addRoute(info);
334     }
335 
addRoute(RouteInfo info)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      */
removeUserRoute(UserRouteInfo info)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      */
clearUserRoutes()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      */
removeRouteInt(RouteInfo info)391     public void removeRouteInt(RouteInfo info) {
392         removeRoute(info);
393     }
394 
removeRoute(RouteInfo info)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 
removeRouteAt(int routeIndex)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      */
getCategoryCount()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      */
getCategoryAt(int index)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      */
getRouteCount()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      */
getRouteAt(int index)481     public RouteInfo getRouteAt(int index) {
482         return sStatic.mRoutes.get(index);
483     }
484 
getRouteCountStatic()485     static int getRouteCountStatic() {
486         return sStatic.mRoutes.size();
487     }
488 
getRouteAtStatic(int index)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      */
createUserRoute(RouteCategory category)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      */
createRouteCategory(CharSequence name, boolean isGroupable)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      */
createRouteCategory(int nameResId, boolean isGroupable)525     public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
526         return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
527     }
528 
updateRoute(final RouteInfo info)529     static void updateRoute(final RouteInfo info) {
530         dispatchRouteChanged(info);
531     }
532 
dispatchRouteSelected(int type, RouteInfo info)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 
dispatchRouteUnselected(int type, RouteInfo info)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 
dispatchRouteChanged(RouteInfo info)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 
dispatchRouteAdded(RouteInfo info)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 
dispatchRouteRemoved(RouteInfo info)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 
dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index)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 
dispatchRouteUngrouped(RouteInfo info, RouteGroup group)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 
dispatchRouteVolumeChanged(RouteInfo info)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 
systemVolumeChanged(int newValue)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 
RouteInfo(RouteCategory category)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          */
getName()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          */
getName(Context context)682         public CharSequence getName(Context context) {
683             return getName(context.getResources());
684         }
685 
getName(Resources res)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          */
getStatus()697         public CharSequence getStatus() {
698             return mStatus;
699         }
700 
701         /**
702          * @return A media type flag set describing which types this route supports.
703          */
getSupportedTypes()704         public int getSupportedTypes() {
705             return mSupportedTypes;
706         }
707 
708         /**
709          * @return The group that this route belongs to.
710          */
getGroup()711         public RouteGroup getGroup() {
712             return mGroup;
713         }
714 
715         /**
716          * @return the category this route belongs to.
717          */
getCategory()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          */
getIconDrawable()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          */
setTag(Object tag)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          */
getTag()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          */
getPlaybackType()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          */
getPlaybackStream()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          */
getVolume()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          */
requestSetVolume(int volume)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          */
requestUpdateVolume(int direction)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          */
getVolumeMax()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          */
getVolumeHandling()853         public int getVolumeHandling() {
854             return mVolumeHandling;
855         }
856 
setStatusInt(CharSequence status)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 
routeUpdated()885         void routeUpdated() {
886             updateRoute(this);
887         }
888 
889         @Override
toString()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 
UserRouteInfo(RouteCategory category)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          */
setName(CharSequence name)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          */
setName(int resId)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          */
setStatus(CharSequence status)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          */
setRemoteControlClient(RemoteControlClient rcc)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          */
getRemoteControlClient()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          */
setIconDrawable(Drawable icon)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          */
setIconResource(int resId)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          */
setVolumeCallback(VolumeCallback vcb)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          */
setPlaybackType(int type)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          */
setVolumeHandling(int volumeHandling)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          */
setVolume(int volume)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
requestSetVolume(int volume)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
requestUpdateVolume(int direction)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          */
setVolumeMax(int volumeMax)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          */
setPlaybackStream(int stream)1082         public void setPlaybackStream(int stream) {
1083             if (mPlaybackStream != stream) {
1084                 mPlaybackStream = stream;
1085                 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream);
1086             }
1087         }
1088 
updatePlaybackInfoOnRcc()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 
setPlaybackInfoOnRcc(int what, int value)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 
RouteGroup(RouteCategory category)1125         RouteGroup(RouteCategory category) {
1126             super(category);
1127             mGroup = this;
1128             mVolumeHandling = PLAYBACK_VOLUME_FIXED;
1129         }
1130 
getName(Resources res)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          */
addRoute(RouteInfo route)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          */
addRoute(RouteInfo route, int insertAt)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          */
removeRoute(RouteInfo route)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          */
removeRoute(int index)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          */
getRouteCount()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          */
getRouteAt(int index)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          */
setIconDrawable(Drawable icon)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          */
setIconResource(int resId)1249         public void setIconResource(int resId) {
1250             setIconDrawable(sStatic.mResources.getDrawable(resId));
1251         }
1252 
1253         @Override
requestSetVolume(int volume)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
requestUpdateVolume(int direction)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 
memberNameChanged(RouteInfo info, CharSequence name)1296         void memberNameChanged(RouteInfo info, CharSequence name) {
1297             mUpdateName = true;
1298             routeUpdated();
1299         }
1300 
memberStatusChanged(RouteInfo info, CharSequence status)1301         void memberStatusChanged(RouteInfo info, CharSequence status) {
1302             setStatusInt(status);
1303         }
1304 
memberVolumeChanged(RouteInfo info)1305         void memberVolumeChanged(RouteInfo info) {
1306             updateVolume();
1307         }
1308 
updateVolume()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
routeUpdated()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 
updateName()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
toString()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 
RouteCategory(CharSequence name, int types, boolean groupable)1392         RouteCategory(CharSequence name, int types, boolean groupable) {
1393             mName = name;
1394             mTypes = types;
1395             mGroupable = groupable;
1396         }
1397 
RouteCategory(int nameResId, int types, boolean groupable)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          */
getName()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          */
getName(Context context)1417         public CharSequence getName(Context context) {
1418             return getName(context.getResources());
1419         }
1420 
getName(Resources res)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          */
getRoutes(List<RouteInfo> out)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          */
getSupportedTypes()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          */
isGroupable()1473         public boolean isGroupable() {
1474             return mGroupable;
1475         }
1476 
toString()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 
CallbackInfo(Callback cb, int type, MediaRouter router)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          */
onRouteSelected(MediaRouter router, int type, RouteInfo info)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          */
onRouteUnselected(MediaRouter router, int type, RouteInfo info)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          */
onRouteAdded(MediaRouter router, RouteInfo info)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          */
onRouteRemoved(MediaRouter router, RouteInfo info)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          */
onRouteChanged(MediaRouter router, RouteInfo info)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          */
onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)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          */
onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)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          */
onRouteVolumeChanged(MediaRouter router, RouteInfo info)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
onRouteSelected(MediaRouter router, int type, RouteInfo info)1590         public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
1591         }
1592 
1593         @Override
onRouteUnselected(MediaRouter router, int type, RouteInfo info)1594         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
1595         }
1596 
1597         @Override
onRouteAdded(MediaRouter router, RouteInfo info)1598         public void onRouteAdded(MediaRouter router, RouteInfo info) {
1599         }
1600 
1601         @Override
onRouteRemoved(MediaRouter router, RouteInfo info)1602         public void onRouteRemoved(MediaRouter router, RouteInfo info) {
1603         }
1604 
1605         @Override
onRouteChanged(MediaRouter router, RouteInfo info)1606         public void onRouteChanged(MediaRouter router, RouteInfo info) {
1607         }
1608 
1609         @Override
onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)1610         public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1611                 int index) {
1612         }
1613 
1614         @Override
onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)1615         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
1616         }
1617 
1618         @Override
onRouteVolumeChanged(MediaRouter router, RouteInfo info)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 
VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route)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          */
onVolumeUpdateRequest(RouteInfo info, int direction)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          */
onVolumeSetRequest(RouteInfo info, int volume)1658         public abstract void onVolumeSetRequest(RouteInfo info, int volume);
1659     }
1660 
1661     static class VolumeChangeReceiver extends BroadcastReceiver {
1662 
1663         @Override
onReceive(Context context, Intent intent)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