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