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