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