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