MediaRouter.java revision 70e11e50eecfc8f1dfb76316d099e4331ebd28f7
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.bluetooth.BluetoothA2dp;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.content.res.Resources;
25import android.graphics.drawable.Drawable;
26import android.os.Handler;
27import android.util.Log;
28
29import java.util.ArrayList;
30import java.util.HashMap;
31import java.util.List;
32
33/**
34 * MediaRouter allows applications to control the routing of media channels
35 * and streams from the current device to external speakers and destination devices.
36 *
37 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
38 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
39 * Context.MEDIA_ROUTER_SERVICE}.
40 *
41 * <p>The media router API is not thread-safe; all interactions with it must be
42 * done from the main thread of the process.</p>
43 */
44public class MediaRouter {
45    private static final String TAG = "MediaRouter";
46
47    static class Static {
48        final Resources mResources;
49        final AudioManager mAudioManager;
50        final Handler mHandler;
51        final ArrayList<CallbackInfo> mCallbacks = new ArrayList<CallbackInfo>();
52
53        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
54        final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
55
56        final RouteCategory mSystemCategory;
57        final HeadphoneChangedBroadcastReceiver mHeadphoneChangedReceiver;
58
59        RouteInfo mDefaultAudio;
60        RouteInfo mBluetoothA2dpRoute;
61
62        RouteInfo mSelectedRoute;
63
64        Static(Context appContext) {
65            mResources = Resources.getSystem();
66            mHandler = new Handler(appContext.getMainLooper());
67
68            mAudioManager = (AudioManager)appContext.getSystemService(Context.AUDIO_SERVICE);
69
70            // XXX this doesn't deal with locale changes!
71            mSystemCategory = new RouteCategory(mResources.getText(
72                    com.android.internal.R.string.default_audio_route_category_name),
73                    ROUTE_TYPE_LIVE_AUDIO, false);
74
75            final IntentFilter speakerFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
76            speakerFilter.addAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG);
77            speakerFilter.addAction(Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG);
78            speakerFilter.addAction(Intent.ACTION_HDMI_AUDIO_PLUG);
79            mHeadphoneChangedReceiver = new HeadphoneChangedBroadcastReceiver();
80            appContext.registerReceiver(mHeadphoneChangedReceiver, speakerFilter);
81        }
82
83        // Called after sStatic is initialized
84        void initDefaultRoutes() {
85            mDefaultAudio = new RouteInfo(mSystemCategory);
86            mDefaultAudio.mName = mResources.getText(
87                    com.android.internal.R.string.default_audio_route_name);
88            mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
89            addRoute(mDefaultAudio);
90        }
91    }
92
93    static Static sStatic;
94
95    /**
96     * Route type flag for live audio.
97     *
98     * <p>A device that supports live audio routing will allow the media audio stream
99     * to be routed to supported destinations. This can include internal speakers or
100     * audio jacks on the device itself, A2DP devices, and more.</p>
101     *
102     * <p>Once initiated this routing is transparent to the application. All audio
103     * played on the media stream will be routed to the selected destination.</p>
104     */
105    public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
106
107    /**
108     * Route type flag for application-specific usage.
109     *
110     * <p>Unlike other media route types, user routes are managed by the application.
111     * The MediaRouter will manage and dispatch events for user routes, but the application
112     * is expected to interpret the meaning of these events and perform the requested
113     * routing tasks.</p>
114     */
115    public static final int ROUTE_TYPE_USER = 0x00800000;
116
117    // Maps application contexts
118    static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
119
120    static String typesToString(int types) {
121        final StringBuilder result = new StringBuilder();
122        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
123            result.append("ROUTE_TYPE_LIVE_AUDIO ");
124        }
125        if ((types & ROUTE_TYPE_USER) != 0) {
126            result.append("ROUTE_TYPE_USER ");
127        }
128        return result.toString();
129    }
130
131    /** @hide */
132    public MediaRouter(Context context) {
133        synchronized (Static.class) {
134            if (sStatic == null) {
135                sStatic = new Static(context.getApplicationContext());
136                sStatic.initDefaultRoutes();
137            }
138        }
139    }
140
141    /**
142     * @hide for use by framework routing UI
143     */
144    public RouteInfo getSystemAudioRoute() {
145        return sStatic.mDefaultAudio;
146    }
147
148    /**
149     * Return the currently selected route for the given types
150     *
151     * @param type route types
152     * @return the selected route
153     */
154    public RouteInfo getSelectedRoute(int type) {
155        return sStatic.mSelectedRoute;
156    }
157
158    static void onHeadphonesPlugged(boolean headphonesPresent, String headphonesName) {
159        sStatic.mDefaultAudio.mName = headphonesPresent
160                ? headphonesName
161                : sStatic.mResources.getText(
162                        com.android.internal.R.string.default_audio_route_name);
163        dispatchRouteChanged(sStatic.mDefaultAudio);
164    }
165
166    /**
167     * Add a callback to listen to events about specific kinds of media routes.
168     * If the specified callback is already registered, its registration will be updated for any
169     * additional route types specified.
170     *
171     * @param types Types of routes this callback is interested in
172     * @param cb Callback to add
173     */
174    public void addCallback(int types, Callback cb) {
175        final int count = sStatic.mCallbacks.size();
176        for (int i = 0; i < count; i++) {
177            final CallbackInfo info = sStatic.mCallbacks.get(i);
178            if (info.cb == cb) {
179                info.type &= types;
180                return;
181            }
182        }
183        sStatic.mCallbacks.add(new CallbackInfo(cb, types, this));
184    }
185
186    /**
187     * Remove the specified callback. It will no longer receive events about media routing.
188     *
189     * @param cb Callback to remove
190     */
191    public void removeCallback(Callback cb) {
192        final int count = sStatic.mCallbacks.size();
193        for (int i = 0; i < count; i++) {
194            if (sStatic.mCallbacks.get(i).cb == cb) {
195                sStatic.mCallbacks.remove(i);
196                return;
197            }
198        }
199        Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
200    }
201
202    /**
203     * Select the specified route to use for output of the given media types.
204     *
205     * @param types type flags indicating which types this route should be used for.
206     *              The route must support at least a subset.
207     * @param route Route to select
208     */
209    public void selectRoute(int types, RouteInfo route) {
210        selectRouteStatic(types, route);
211    }
212
213    static void selectRouteStatic(int types, RouteInfo route) {
214        if (sStatic.mSelectedRoute == route) return;
215
216        if (sStatic.mSelectedRoute != null) {
217            // TODO filter types properly
218            dispatchRouteUnselected(types & sStatic.mSelectedRoute.getSupportedTypes(),
219                    sStatic.mSelectedRoute);
220        }
221        sStatic.mSelectedRoute = route;
222        if (route != null) {
223            // TODO filter types properly
224            dispatchRouteSelected(types & route.getSupportedTypes(), route);
225        }
226    }
227
228    /**
229     * Add an app-specified route for media to the MediaRouter.
230     * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
231     *
232     * @param info Definition of the route to add
233     * @see #createUserRoute()
234     * @see #removeUserRoute(UserRouteInfo)
235     */
236    public void addUserRoute(UserRouteInfo info) {
237        addRoute(info);
238    }
239
240    static void addRoute(RouteInfo info) {
241        final RouteCategory cat = info.getCategory();
242        if (!sStatic.mCategories.contains(cat)) {
243            sStatic.mCategories.add(cat);
244        }
245        final boolean onlyRoute = sStatic.mRoutes.isEmpty();
246        if (cat.isGroupable() && !(info instanceof RouteGroup)) {
247            // Enforce that any added route in a groupable category must be in a group.
248            final RouteGroup group = new RouteGroup(info.getCategory());
249            sStatic.mRoutes.add(group);
250            dispatchRouteAdded(group);
251
252            final int at = group.getRouteCount();
253            group.addRoute(info);
254            dispatchRouteGrouped(info, group, at);
255
256            info = group;
257        } else {
258            sStatic.mRoutes.add(info);
259            dispatchRouteAdded(info);
260        }
261
262        if (onlyRoute) {
263            selectRouteStatic(info.getSupportedTypes(), info);
264        }
265    }
266
267    /**
268     * Remove an app-specified route for media from the MediaRouter.
269     *
270     * @param info Definition of the route to remove
271     * @see #addUserRoute(UserRouteInfo)
272     */
273    public void removeUserRoute(UserRouteInfo info) {
274        removeRoute(info);
275    }
276
277    /**
278     * Remove all app-specified routes from the MediaRouter.
279     *
280     * @see #removeUserRoute(UserRouteInfo)
281     */
282    public void clearUserRoutes() {
283        for (int i = 0; i < sStatic.mRoutes.size(); i++) {
284            final RouteInfo info = sStatic.mRoutes.get(i);
285            if (info instanceof UserRouteInfo) {
286                removeRouteAt(i);
287                i--;
288            }
289        }
290    }
291
292    static void removeRoute(RouteInfo info) {
293        if (sStatic.mRoutes.remove(info)) {
294            final RouteCategory removingCat = info.getCategory();
295            final int count = sStatic.mRoutes.size();
296            boolean found = false;
297            for (int i = 0; i < count; i++) {
298                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
299                if (removingCat == cat) {
300                    found = true;
301                    break;
302                }
303            }
304            if (!found) {
305                sStatic.mCategories.remove(removingCat);
306            }
307            dispatchRouteRemoved(info);
308        }
309    }
310
311    void removeRouteAt(int routeIndex) {
312        if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) {
313            final RouteInfo info = sStatic.mRoutes.remove(routeIndex);
314            final RouteCategory removingCat = info.getCategory();
315            final int count = sStatic.mRoutes.size();
316            boolean found = false;
317            for (int i = 0; i < count; i++) {
318                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
319                if (removingCat == cat) {
320                    found = true;
321                    break;
322                }
323            }
324            if (!found) {
325                sStatic.mCategories.remove(removingCat);
326            }
327            dispatchRouteRemoved(info);
328        }
329    }
330
331    /**
332     * Return the number of {@link MediaRouter.RouteCategory categories} currently
333     * represented by routes known to this MediaRouter.
334     *
335     * @return the number of unique categories represented by this MediaRouter's known routes
336     */
337    public int getCategoryCount() {
338        return sStatic.mCategories.size();
339    }
340
341    /**
342     * Return the {@link MediaRouter.RouteCategory category} at the given index.
343     * Valid indices are in the range [0-getCategoryCount).
344     *
345     * @param index which category to return
346     * @return the category at index
347     */
348    public RouteCategory getCategoryAt(int index) {
349        return sStatic.mCategories.get(index);
350    }
351
352    /**
353     * Return the number of {@link MediaRouter.RouteInfo routes} currently known
354     * to this MediaRouter.
355     *
356     * @return the number of routes tracked by this router
357     */
358    public int getRouteCount() {
359        return sStatic.mRoutes.size();
360    }
361
362    /**
363     * Return the route at the specified index.
364     *
365     * @param index index of the route to return
366     * @return the route at index
367     */
368    public RouteInfo getRouteAt(int index) {
369        return sStatic.mRoutes.get(index);
370    }
371
372    static int getRouteCountStatic() {
373        return sStatic.mRoutes.size();
374    }
375
376    static RouteInfo getRouteAtStatic(int index) {
377        return sStatic.mRoutes.get(index);
378    }
379
380    /**
381     * Create a new user route that may be modified and registered for use by the application.
382     *
383     * @param category The category the new route will belong to
384     * @return A new UserRouteInfo for use by the application
385     *
386     * @see #addUserRoute(UserRouteInfo)
387     * @see #removeUserRoute(UserRouteInfo)
388     * @see #createRouteCategory(CharSequence)
389     */
390    public UserRouteInfo createUserRoute(RouteCategory category) {
391        return new UserRouteInfo(category);
392    }
393
394    /**
395     * Create a new route category. Each route must belong to a category.
396     *
397     * @param name Name of the new category
398     * @param isGroupable true if routes in this category may be grouped with one another
399     * @return the new RouteCategory
400     */
401    public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
402        return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
403    }
404
405    static void updateRoute(final RouteInfo info) {
406        dispatchRouteChanged(info);
407    }
408
409    static void dispatchRouteSelected(int type, RouteInfo info) {
410        final int count = sStatic.mCallbacks.size();
411        for (int i = 0; i < count; i++) {
412            final CallbackInfo cbi = sStatic.mCallbacks.get(i);
413            if ((cbi.type & type) != 0) {
414                cbi.cb.onRouteSelected(cbi.router, type, info);
415            }
416        }
417    }
418
419    static void dispatchRouteUnselected(int type, RouteInfo info) {
420        final int count = sStatic.mCallbacks.size();
421        for (int i = 0; i < count; i++) {
422            final CallbackInfo cbi = sStatic.mCallbacks.get(i);
423            if ((cbi.type & type) != 0) {
424                cbi.cb.onRouteUnselected(cbi.router, type, info);
425            }
426        }
427    }
428
429    static void dispatchRouteChanged(RouteInfo info) {
430        final int count = sStatic.mCallbacks.size();
431        for (int i = 0; i < count; i++) {
432            final CallbackInfo cbi = sStatic.mCallbacks.get(i);
433            if ((cbi.type & info.mSupportedTypes) != 0) {
434                cbi.cb.onRouteChanged(cbi.router, info);
435            }
436        }
437    }
438
439    static void dispatchRouteAdded(RouteInfo info) {
440        final int count = sStatic.mCallbacks.size();
441        for (int i = 0; i < count; i++) {
442            final CallbackInfo cbi = sStatic.mCallbacks.get(i);
443            if ((cbi.type & info.mSupportedTypes) != 0) {
444                cbi.cb.onRouteAdded(cbi.router, info);
445            }
446        }
447    }
448
449    static void dispatchRouteRemoved(RouteInfo info) {
450        final int count = sStatic.mCallbacks.size();
451        for (int i = 0; i < count; i++) {
452            final CallbackInfo cbi = sStatic.mCallbacks.get(i);
453            if ((cbi.type & info.mSupportedTypes) != 0) {
454                cbi.cb.onRouteRemoved(cbi.router, info);
455            }
456        }
457    }
458
459    static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
460        final int count = sStatic.mCallbacks.size();
461        for (int i = 0; i < count; i++) {
462            final CallbackInfo cbi = sStatic.mCallbacks.get(i);
463            if ((cbi.type & group.mSupportedTypes) != 0) {
464                cbi.cb.onRouteGrouped(cbi.router, info, group, index);
465            }
466        }
467    }
468
469    static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
470        final int count = sStatic.mCallbacks.size();
471        for (int i = 0; i < count; i++) {
472            final CallbackInfo cbi = sStatic.mCallbacks.get(i);
473            if ((cbi.type & group.mSupportedTypes) != 0) {
474                cbi.cb.onRouteUngrouped(cbi.router, info, group);
475            }
476        }
477    }
478
479    static void onA2dpDeviceConnected() {
480        final RouteInfo info = new RouteInfo(sStatic.mSystemCategory);
481        info.mName = "Bluetooth"; // TODO Fetch the real name of the device
482        sStatic.mBluetoothA2dpRoute = info;
483        addRoute(sStatic.mBluetoothA2dpRoute);
484    }
485
486    static void onA2dpDeviceDisconnected() {
487        removeRoute(sStatic.mBluetoothA2dpRoute);
488        sStatic.mBluetoothA2dpRoute = null;
489    }
490
491    /**
492     * Information about a media route.
493     */
494    public static class RouteInfo {
495        CharSequence mName;
496        private CharSequence mStatus;
497        int mSupportedTypes;
498        RouteGroup mGroup;
499        final RouteCategory mCategory;
500        Drawable mIcon;
501
502        RouteInfo(RouteCategory category) {
503            mCategory = category;
504        }
505
506        /**
507         * @return The user-friendly name of a media route. This is the string presented
508         * to users who may select this as the active route.
509         */
510        public CharSequence getName() {
511            return mName;
512        }
513
514        /**
515         * @return The user-friendly status for a media route. This may include a description
516         * of the currently playing media, if available.
517         */
518        public CharSequence getStatus() {
519            return mStatus;
520        }
521
522        /**
523         * @return A media type flag set describing which types this route supports.
524         */
525        public int getSupportedTypes() {
526            return mSupportedTypes;
527        }
528
529        /**
530         * @return The group that this route belongs to.
531         */
532        public RouteGroup getGroup() {
533            return mGroup;
534        }
535
536        /**
537         * @return the category this route belongs to.
538         */
539        public RouteCategory getCategory() {
540            return mCategory;
541        }
542
543        /**
544         * Get the icon representing this route.
545         * This icon will be used in picker UIs if available.
546         *
547         * @return the icon representing this route or null if no icon is available
548         */
549        public Drawable getIconDrawable() {
550            return mIcon;
551        }
552
553        void setStatusInt(CharSequence status) {
554            if (!status.equals(mStatus)) {
555                mStatus = status;
556                routeUpdated();
557                if (mGroup != null) {
558                    mGroup.memberStatusChanged(this, status);
559                }
560                routeUpdated();
561            }
562        }
563
564        void routeUpdated() {
565            updateRoute(this);
566        }
567
568        @Override
569        public String toString() {
570            String supportedTypes = typesToString(mSupportedTypes);
571            return "RouteInfo{ name=" + mName + ", status=" + mStatus +
572                    " category=" + mCategory +
573                    " supportedTypes=" + supportedTypes + "}";
574        }
575    }
576
577    /**
578     * Information about a route that the application may define and modify.
579     *
580     * @see MediaRouter.RouteInfo
581     */
582    public static class UserRouteInfo extends RouteInfo {
583        RemoteControlClient mRcc;
584        private Object mTag;
585
586        UserRouteInfo(RouteCategory category) {
587            super(category);
588            mSupportedTypes = ROUTE_TYPE_USER;
589        }
590
591        /**
592         * Set the user-visible name of this route.
593         * @param name Name to display to the user to describe this route
594         */
595        public void setName(CharSequence name) {
596            mName = name;
597            routeUpdated();
598        }
599
600        /**
601         * Set the current user-visible status for this route.
602         * @param status Status to display to the user to describe what the endpoint
603         * of this route is currently doing
604         */
605        public void setStatus(CharSequence status) {
606            setStatusInt(status);
607        }
608
609        /**
610         * Set the RemoteControlClient responsible for reporting playback info for this
611         * user route.
612         *
613         * <p>If this route manages remote playback, the data exposed by this
614         * RemoteControlClient will be used to reflect and update information
615         * such as route volume info in related UIs.</p>
616         *
617         * @param rcc RemoteControlClient associated with this route
618         */
619        public void setRemoteControlClient(RemoteControlClient rcc) {
620            mRcc = rcc;
621        }
622
623        /**
624         * Set an icon that will be used to represent this route.
625         * The system may use this icon in picker UIs or similar.
626         *
627         * @param icon icon drawable to use to represent this route
628         */
629        public void setIconDrawable(Drawable icon) {
630            mIcon = icon;
631        }
632
633        /**
634         * Set an icon that will be used to represent this route.
635         * The system may use this icon in picker UIs or similar.
636         *
637         * @param resId Resource ID of an icon drawable to use to represent this route
638         */
639        public void setIconResource(int resId) {
640            setIconDrawable(sStatic.mResources.getDrawable(resId));
641        }
642
643        /**
644         * Set an application-specific tag object for this route.
645         * The application may use this to store arbitrary data associated with the
646         * route for internal tracking.
647         *
648         * <p>Note that the lifespan of a route may be well past the lifespan of
649         * an Activity or other Context; take care that objects you store here
650         * will not keep more data in memory alive than you intend.</p>
651         *
652         * @param tag Arbitrary, app-specific data for this route to hold for later use
653         */
654        public void setTag(Object tag) {
655            mTag = tag;
656        }
657
658        /**
659         * @return The tag object previously set by the application
660         * @see #setTag(Object)
661         */
662        public Object getTag() {
663            return mTag;
664        }
665    }
666
667    /**
668     * Information about a route that consists of multiple other routes in a group.
669     */
670    public static class RouteGroup extends RouteInfo {
671        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
672        private boolean mUpdateName;
673
674        RouteGroup(RouteCategory category) {
675            super(category);
676            mGroup = this;
677        }
678
679        public CharSequence getName() {
680            if (mUpdateName) updateName();
681            return super.getName();
682        }
683
684        /**
685         * Add a route to this group. The route must not currently belong to another group.
686         *
687         * @param route route to add to this group
688         */
689        public void addRoute(RouteInfo route) {
690            if (route.getGroup() != null) {
691                throw new IllegalStateException("Route " + route + " is already part of a group.");
692            }
693            if (route.getCategory() != mCategory) {
694                throw new IllegalArgumentException(
695                        "Route cannot be added to a group with a different category. " +
696                            "(Route category=" + route.getCategory() +
697                            " group category=" + mCategory + ")");
698            }
699            final int at = mRoutes.size();
700            mRoutes.add(route);
701            mUpdateName = true;
702            dispatchRouteGrouped(route, this, at);
703            routeUpdated();
704        }
705
706        /**
707         * Add a route to this group before the specified index.
708         *
709         * @param route route to add
710         * @param insertAt insert the new route before this index
711         */
712        public void addRoute(RouteInfo route, int insertAt) {
713            if (route.getGroup() != null) {
714                throw new IllegalStateException("Route " + route + " is already part of a group.");
715            }
716            if (route.getCategory() != mCategory) {
717                throw new IllegalArgumentException(
718                        "Route cannot be added to a group with a different category. " +
719                            "(Route category=" + route.getCategory() +
720                            " group category=" + mCategory + ")");
721            }
722            mRoutes.add(insertAt, route);
723            mUpdateName = true;
724            dispatchRouteGrouped(route, this, insertAt);
725            routeUpdated();
726        }
727
728        /**
729         * Remove a route from this group.
730         *
731         * @param route route to remove
732         */
733        public void removeRoute(RouteInfo route) {
734            if (route.getGroup() != this) {
735                throw new IllegalArgumentException("Route " + route +
736                        " is not a member of this group.");
737            }
738            mRoutes.remove(route);
739            mUpdateName = true;
740            dispatchRouteUngrouped(route, this);
741            routeUpdated();
742        }
743
744        /**
745         * Remove the route at the specified index from this group.
746         *
747         * @param index index of the route to remove
748         */
749        public void removeRoute(int index) {
750            RouteInfo route = mRoutes.remove(index);
751            mUpdateName = true;
752            dispatchRouteUngrouped(route, this);
753            routeUpdated();
754        }
755
756        /**
757         * @return The number of routes in this group
758         */
759        public int getRouteCount() {
760            return mRoutes.size();
761        }
762
763        /**
764         * Return the route in this group at the specified index
765         *
766         * @param index Index to fetch
767         * @return The route at index
768         */
769        public RouteInfo getRouteAt(int index) {
770            return mRoutes.get(index);
771        }
772
773        /**
774         * Set an icon that will be used to represent this group.
775         * The system may use this icon in picker UIs or similar.
776         *
777         * @param icon icon drawable to use to represent this group
778         */
779        public void setIconDrawable(Drawable icon) {
780            mIcon = icon;
781        }
782
783        /**
784         * Set an icon that will be used to represent this group.
785         * The system may use this icon in picker UIs or similar.
786         *
787         * @param resId Resource ID of an icon drawable to use to represent this group
788         */
789        public void setIconResource(int resId) {
790            setIconDrawable(sStatic.mResources.getDrawable(resId));
791        }
792
793        void memberNameChanged(RouteInfo info, CharSequence name) {
794            mUpdateName = true;
795            routeUpdated();
796        }
797
798        void memberStatusChanged(RouteInfo info, CharSequence status) {
799            setStatusInt(status);
800        }
801
802        void updateName() {
803            final StringBuilder sb = new StringBuilder();
804            final int count = mRoutes.size();
805            for (int i = 0; i < count; i++) {
806                final RouteInfo info = mRoutes.get(i);
807                if (i > 0) sb.append(", ");
808                sb.append(info.mName);
809            }
810            mName = sb.toString();
811            mUpdateName = false;
812        }
813    }
814
815    /**
816     * Definition of a category of routes. All routes belong to a category.
817     */
818    public static class RouteCategory {
819        CharSequence mName;
820        int mTypes;
821        final boolean mGroupable;
822
823        RouteCategory(CharSequence name, int types, boolean groupable) {
824            mName = name;
825            mTypes = types;
826            mGroupable = groupable;
827        }
828
829        /**
830         * @return the name of this route category
831         */
832        public CharSequence getName() {
833            return mName;
834        }
835
836        /**
837         * Return the current list of routes in this category that have been added
838         * to the MediaRouter.
839         *
840         * <p>This list will not include routes that are nested within RouteGroups.
841         * A RouteGroup is treated as a single route within its category.</p>
842         *
843         * @param out a List to fill with the routes in this category. If this parameter is
844         *            non-null, it will be cleared, filled with the current routes with this
845         *            category, and returned. If this parameter is null, a new List will be
846         *            allocated to report the category's current routes.
847         * @return A list with the routes in this category that have been added to the MediaRouter.
848         */
849        public List<RouteInfo> getRoutes(List<RouteInfo> out) {
850            if (out == null) {
851                out = new ArrayList<RouteInfo>();
852            } else {
853                out.clear();
854            }
855
856            final int count = getRouteCountStatic();
857            for (int i = 0; i < count; i++) {
858                final RouteInfo route = getRouteAtStatic(i);
859                if (route.mCategory == this) {
860                    out.add(route);
861                }
862            }
863            return out;
864        }
865
866        /**
867         * @return Flag set describing the route types supported by this category
868         */
869        public int getSupportedTypes() {
870            return mTypes;
871        }
872
873        /**
874         * Return whether or not this category supports grouping.
875         *
876         * <p>If this method returns true, all routes obtained from this category
877         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
878         *
879         * @return true if this category supports
880         */
881        public boolean isGroupable() {
882            return mGroupable;
883        }
884
885        public String toString() {
886            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
887                    " groupable=" + mGroupable + " routes=" + sStatic.mRoutes.size() + " }";
888        }
889    }
890
891    static class CallbackInfo {
892        public int type;
893        public final Callback cb;
894        public final MediaRouter router;
895
896        public CallbackInfo(Callback cb, int type, MediaRouter router) {
897            this.cb = cb;
898            this.type = type;
899            this.router = router;
900        }
901    }
902
903    /**
904     * Interface for receiving events about media routing changes.
905     * All methods of this interface will be called from the application's main thread.
906     *
907     * <p>A Callback will only receive events relevant to routes that the callback
908     * was registered for.</p>
909     *
910     * @see MediaRouter#addCallback(int, Callback)
911     * @see MediaRouter#removeCallback(Callback)
912     */
913    public interface Callback {
914        /**
915         * Called when the supplied route becomes selected as the active route
916         * for the given route type.
917         *
918         * @param router the MediaRouter reporting the event
919         * @param type Type flag set indicating the routes that have been selected
920         * @param info Route that has been selected for the given route types
921         */
922        public void onRouteSelected(MediaRouter router, int type, RouteInfo info);
923
924        /**
925         * Called when the supplied route becomes unselected as the active route
926         * for the given route type.
927         *
928         * @param router the MediaRouter reporting the event
929         * @param type Type flag set indicating the routes that have been unselected
930         * @param info Route that has been unselected for the given route types
931         */
932        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
933
934        /**
935         * Called when a route for the specified type was added.
936         *
937         * @param router the MediaRouter reporting the event
938         * @param info Route that has become available for use
939         */
940        public void onRouteAdded(MediaRouter router, RouteInfo info);
941
942        /**
943         * Called when a route for the specified type was removed.
944         *
945         * @param router the MediaRouter reporting the event
946         * @param info Route that has been removed from availability
947         */
948        public void onRouteRemoved(MediaRouter router, RouteInfo info);
949
950        /**
951         * Called when an aspect of the indicated route has changed.
952         *
953         * <p>This will not indicate that the types supported by this route have
954         * changed, only that cosmetic info such as name or status have been updated.</p>
955         *
956         * @param router the MediaRouter reporting the event
957         * @param info The route that was changed
958         */
959        public void onRouteChanged(MediaRouter router, RouteInfo info);
960
961        /**
962         * Called when a route is added to a group.
963         *
964         * @param router the MediaRouter reporting the event
965         * @param info The route that was added
966         * @param group The group the route was added to
967         * @param index The route index within group that info was added at
968         */
969        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index);
970
971        /**
972         * Called when a route is removed from a group.
973         *
974         * @param router the MediaRouter reporting the event
975         * @param info The route that was removed
976         * @param group The group the route was removed from
977         */
978        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
979    }
980
981    /**
982     * Stub implementation of the {@link MediaRouter.Callback} interface.
983     * Each interface method is defined as a no-op. Override just the ones
984     * you need.
985     */
986    public static class SimpleCallback implements Callback {
987
988        @Override
989        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
990        }
991
992        @Override
993        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
994        }
995
996        @Override
997        public void onRouteAdded(MediaRouter router, RouteInfo info) {
998        }
999
1000        @Override
1001        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
1002        }
1003
1004        @Override
1005        public void onRouteChanged(MediaRouter router, RouteInfo info) {
1006        }
1007
1008        @Override
1009        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1010                int index) {
1011        }
1012
1013        @Override
1014        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
1015        }
1016
1017    }
1018
1019    class BtChangedBroadcastReceiver extends BroadcastReceiver {
1020        @Override
1021        public void onReceive(Context context, Intent intent) {
1022            final String action = intent.getAction();
1023            if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
1024                final int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1);
1025                if (state == BluetoothA2dp.STATE_CONNECTED) {
1026                    onA2dpDeviceConnected();
1027                } else if (state == BluetoothA2dp.STATE_DISCONNECTING ||
1028                        state == BluetoothA2dp.STATE_DISCONNECTED) {
1029                    onA2dpDeviceDisconnected();
1030                }
1031            }
1032        }
1033    }
1034
1035    static class HeadphoneChangedBroadcastReceiver extends BroadcastReceiver {
1036        @Override
1037        public void onReceive(Context context, Intent intent) {
1038            final String action = intent.getAction();
1039            if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
1040                final boolean plugged = intent.getIntExtra("state", 0) != 0;
1041                final String name = sStatic.mResources.getString(
1042                        com.android.internal.R.string.default_audio_route_name_headphones);
1043                onHeadphonesPlugged(plugged, name);
1044            } else if (Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG.equals(action) ||
1045                    Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG.equals(action)) {
1046                final boolean plugged = intent.getIntExtra("state", 0) != 0;
1047                final String name = sStatic.mResources.getString(
1048                        com.android.internal.R.string.default_audio_route_name_dock_speakers);
1049                onHeadphonesPlugged(plugged, name);
1050            } else if (Intent.ACTION_HDMI_AUDIO_PLUG.equals(action)) {
1051                final boolean plugged = intent.getIntExtra("state", 0) != 0;
1052                final String name = sStatic.mResources.getString(
1053                        com.android.internal.R.string.default_audio_route_name_hdmi);
1054                onHeadphonesPlugged(plugged, name);
1055            }
1056        }
1057    }
1058}
1059