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