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