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