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