MediaRouter.java revision b072a9686c29bfbc05b732076a4e89bcca8db08a
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.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.res.Resources;
24import android.graphics.drawable.Drawable;
25import android.hardware.display.DisplayManager;
26import android.hardware.display.WifiDisplay;
27import android.hardware.display.WifiDisplayStatus;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.text.TextUtils;
33import android.util.Log;
34import android.view.Display;
35import android.view.DisplayInfo;
36
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.List;
40import java.util.concurrent.CopyOnWriteArrayList;
41
42/**
43 * MediaRouter allows applications to control the routing of media channels
44 * and streams from the current device to external speakers and destination devices.
45 *
46 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
47 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
48 * Context.MEDIA_ROUTER_SERVICE}.
49 *
50 * <p>The media router API is not thread-safe; all interactions with it must be
51 * done from the main thread of the process.</p>
52 */
53public class MediaRouter {
54    private static final String TAG = "MediaRouter";
55
56    static class Static {
57        final Resources mResources;
58        final IAudioService mAudioService;
59        final DisplayManager mDisplayService;
60        final Handler mHandler;
61        final CopyOnWriteArrayList<CallbackInfo> mCallbacks =
62                new CopyOnWriteArrayList<CallbackInfo>();
63
64        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
65        final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
66
67        final RouteCategory mSystemCategory;
68
69        final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
70
71        RouteInfo mDefaultAudioVideo;
72        RouteInfo mBluetoothA2dpRoute;
73
74        RouteInfo mSelectedRoute;
75
76        WifiDisplayStatus mLastKnownWifiDisplayStatus;
77
78        final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
79            public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
80                mHandler.post(new Runnable() {
81                    @Override public void run() {
82                        updateAudioRoutes(newRoutes);
83                    }
84                });
85            }
86        };
87
88        Static(Context appContext) {
89            mResources = Resources.getSystem();
90            mHandler = new Handler(appContext.getMainLooper());
91
92            IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
93            mAudioService = IAudioService.Stub.asInterface(b);
94
95            mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE);
96
97            mSystemCategory = new RouteCategory(
98                    com.android.internal.R.string.default_audio_route_category_name,
99                    ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false);
100            mSystemCategory.mIsSystem = true;
101        }
102
103        // Called after sStatic is initialized
104        void startMonitoringRoutes(Context appContext) {
105            mDefaultAudioVideo = new RouteInfo(mSystemCategory);
106            mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name;
107            mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
108            addRouteStatic(mDefaultAudioVideo);
109
110            // This will select the active wifi display route if there is one.
111            updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus());
112
113            appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(),
114                    new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED));
115            appContext.registerReceiver(new VolumeChangeReceiver(),
116                    new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
117
118            AudioRoutesInfo newAudioRoutes = null;
119            try {
120                newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
121            } catch (RemoteException e) {
122            }
123            if (newAudioRoutes != null) {
124                // This will select the active BT route if there is one and the current
125                // selected route is the default system route, or if there is no selected
126                // route yet.
127                updateAudioRoutes(newAudioRoutes);
128            }
129
130            // Select the default route if the above didn't sync us up
131            // appropriately with relevant system state.
132            if (mSelectedRoute == null) {
133                selectRouteStatic(mDefaultAudioVideo.getSupportedTypes(), mDefaultAudioVideo);
134            }
135        }
136
137        void updateAudioRoutes(AudioRoutesInfo newRoutes) {
138            if (newRoutes.mMainType != mCurAudioRoutesInfo.mMainType) {
139                mCurAudioRoutesInfo.mMainType = newRoutes.mMainType;
140                int name;
141                if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0
142                        || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) {
143                    name = com.android.internal.R.string.default_audio_route_name_headphones;
144                } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
145                    name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
146                } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) {
147                    name = com.android.internal.R.string.default_media_route_name_hdmi;
148                } else {
149                    name = com.android.internal.R.string.default_audio_route_name;
150                }
151                sStatic.mDefaultAudioVideo.mNameResId = name;
152                dispatchRouteChanged(sStatic.mDefaultAudioVideo);
153            }
154
155            boolean a2dpEnabled;
156            try {
157                a2dpEnabled = mAudioService.isBluetoothA2dpOn();
158            } catch (RemoteException e) {
159                Log.e(TAG, "Error querying Bluetooth A2DP state", e);
160                a2dpEnabled = false;
161            }
162
163            if (!TextUtils.equals(newRoutes.mBluetoothName, mCurAudioRoutesInfo.mBluetoothName)) {
164                mCurAudioRoutesInfo.mBluetoothName = newRoutes.mBluetoothName;
165                if (mCurAudioRoutesInfo.mBluetoothName != null) {
166                    if (sStatic.mBluetoothA2dpRoute == null) {
167                        final RouteInfo info = new RouteInfo(sStatic.mSystemCategory);
168                        info.mName = mCurAudioRoutesInfo.mBluetoothName;
169                        info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
170                        sStatic.mBluetoothA2dpRoute = info;
171                        addRouteStatic(sStatic.mBluetoothA2dpRoute);
172                    } else {
173                        sStatic.mBluetoothA2dpRoute.mName = mCurAudioRoutesInfo.mBluetoothName;
174                        dispatchRouteChanged(sStatic.mBluetoothA2dpRoute);
175                    }
176                } else if (sStatic.mBluetoothA2dpRoute != null) {
177                    removeRoute(sStatic.mBluetoothA2dpRoute);
178                    sStatic.mBluetoothA2dpRoute = null;
179                }
180            }
181
182            if (mBluetoothA2dpRoute != null) {
183                if (mCurAudioRoutesInfo.mMainType != AudioRoutesInfo.MAIN_SPEAKER &&
184                        mSelectedRoute == mBluetoothA2dpRoute) {
185                    selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo);
186                } else if (mCurAudioRoutesInfo.mMainType == AudioRoutesInfo.MAIN_SPEAKER &&
187                        (mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) &&
188                        a2dpEnabled) {
189                    selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute);
190                }
191            }
192        }
193    }
194
195    static Static sStatic;
196
197    /**
198     * Route type flag for live audio.
199     *
200     * <p>A device that supports live audio routing will allow the media audio stream
201     * to be routed to supported destinations. This can include internal speakers or
202     * audio jacks on the device itself, A2DP devices, and more.</p>
203     *
204     * <p>Once initiated this routing is transparent to the application. All audio
205     * played on the media stream will be routed to the selected destination.</p>
206     */
207    public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
208
209    /**
210     * Route type flag for live video.
211     *
212     * <p>A device that supports live video routing will allow a mirrored version
213     * of the device's primary display or a customized
214     * {@link android.app.Presentation Presentation} to be routed to supported destinations.</p>
215     *
216     * <p>Once initiated, display mirroring is transparent to the application.
217     * While remote routing is active the application may use a
218     * {@link android.app.Presentation Presentation} to replace the mirrored view
219     * on the external display with different content.</p>
220     */
221    public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
222
223    /**
224     * Route type flag for application-specific usage.
225     *
226     * <p>Unlike other media route types, user routes are managed by the application.
227     * The MediaRouter will manage and dispatch events for user routes, but the application
228     * is expected to interpret the meaning of these events and perform the requested
229     * routing tasks.</p>
230     */
231    public static final int ROUTE_TYPE_USER = 0x00800000;
232
233    // Maps application contexts
234    static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
235
236    static String typesToString(int types) {
237        final StringBuilder result = new StringBuilder();
238        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
239            result.append("ROUTE_TYPE_LIVE_AUDIO ");
240        }
241        if ((types & ROUTE_TYPE_USER) != 0) {
242            result.append("ROUTE_TYPE_USER ");
243        }
244        return result.toString();
245    }
246
247    /** @hide */
248    public MediaRouter(Context context) {
249        synchronized (Static.class) {
250            if (sStatic == null) {
251                final Context appContext = context.getApplicationContext();
252                sStatic = new Static(appContext);
253                sStatic.startMonitoringRoutes(appContext);
254            }
255        }
256    }
257
258    /**
259     * @hide for use by framework routing UI
260     */
261    public RouteInfo getSystemAudioRoute() {
262        return sStatic.mDefaultAudioVideo;
263    }
264
265    /**
266     * @hide for use by framework routing UI
267     */
268    public RouteCategory getSystemAudioCategory() {
269        return sStatic.mSystemCategory;
270    }
271
272    /**
273     * Return the currently selected route for the given types
274     *
275     * @param type route types
276     * @return the selected route
277     */
278    public RouteInfo getSelectedRoute(int type) {
279        return sStatic.mSelectedRoute;
280    }
281
282    /**
283     * Add a callback to listen to events about specific kinds of media routes.
284     * If the specified callback is already registered, its registration will be updated for any
285     * additional route types specified.
286     *
287     * @param types Types of routes this callback is interested in
288     * @param cb Callback to add
289     */
290    public void addCallback(int types, Callback cb) {
291        final int count = sStatic.mCallbacks.size();
292        for (int i = 0; i < count; i++) {
293            final CallbackInfo info = sStatic.mCallbacks.get(i);
294            if (info.cb == cb) {
295                info.type |= types;
296                return;
297            }
298        }
299        sStatic.mCallbacks.add(new CallbackInfo(cb, types, this));
300    }
301
302    /**
303     * Remove the specified callback. It will no longer receive events about media routing.
304     *
305     * @param cb Callback to remove
306     */
307    public void removeCallback(Callback cb) {
308        final int count = sStatic.mCallbacks.size();
309        for (int i = 0; i < count; i++) {
310            if (sStatic.mCallbacks.get(i).cb == cb) {
311                sStatic.mCallbacks.remove(i);
312                return;
313            }
314        }
315        Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
316    }
317
318    /**
319     * Select the specified route to use for output of the given media types.
320     *
321     * @param types type flags indicating which types this route should be used for.
322     *              The route must support at least a subset.
323     * @param route Route to select
324     */
325    public void selectRoute(int types, RouteInfo route) {
326        // Applications shouldn't programmatically change anything but user routes.
327        types &= ROUTE_TYPE_USER;
328        selectRouteStatic(types, route);
329    }
330
331    /**
332     * @hide internal use
333     */
334    public void selectRouteInt(int types, RouteInfo route) {
335        selectRouteStatic(types, route);
336    }
337
338    static void selectRouteStatic(int types, RouteInfo route) {
339        final RouteInfo oldRoute = sStatic.mSelectedRoute;
340        if (oldRoute == route) return;
341        if ((route.getSupportedTypes() & types) == 0) {
342            Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
343                    typesToString(route.getSupportedTypes()) + " into route types " +
344                    typesToString(types));
345            return;
346        }
347
348        final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute;
349        if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 &&
350                (route == btRoute || route == sStatic.mDefaultAudioVideo)) {
351            try {
352                sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute);
353            } catch (RemoteException e) {
354                Log.e(TAG, "Error changing Bluetooth A2DP state", e);
355            }
356        }
357
358        final WifiDisplay activeDisplay =
359                sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay();
360        final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null;
361        final boolean newRouteHasAddress = route != null && route.mDeviceAddress != null;
362        if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) {
363            if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) {
364                sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress);
365            } else if (activeDisplay != null && !newRouteHasAddress) {
366                sStatic.mDisplayService.disconnectWifiDisplay();
367            }
368        }
369
370        if (oldRoute != null) {
371            // TODO filter types properly
372            dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute);
373        }
374        sStatic.mSelectedRoute = route;
375        if (route != null) {
376            // TODO filter types properly
377            dispatchRouteSelected(types & route.getSupportedTypes(), route);
378        }
379    }
380
381    /**
382     * Compare the device address of a display and a route.
383     * Nulls/no device address will match another null/no address.
384     */
385    static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) {
386        final boolean routeHasAddress = info != null && info.mDeviceAddress != null;
387        if (display == null && !routeHasAddress) {
388            return true;
389        }
390
391        if (display != null && routeHasAddress) {
392            return display.getDeviceAddress().equals(info.mDeviceAddress);
393        }
394        return false;
395    }
396
397    /**
398     * Add an app-specified route for media to the MediaRouter.
399     * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
400     *
401     * @param info Definition of the route to add
402     * @see #createUserRoute()
403     * @see #removeUserRoute(UserRouteInfo)
404     */
405    public void addUserRoute(UserRouteInfo info) {
406        addRouteStatic(info);
407    }
408
409    /**
410     * @hide Framework use only
411     */
412    public void addRouteInt(RouteInfo info) {
413        addRouteStatic(info);
414    }
415
416    static void addRouteStatic(RouteInfo info) {
417        final RouteCategory cat = info.getCategory();
418        if (!sStatic.mCategories.contains(cat)) {
419            sStatic.mCategories.add(cat);
420        }
421        if (cat.isGroupable() && !(info instanceof RouteGroup)) {
422            // Enforce that any added route in a groupable category must be in a group.
423            final RouteGroup group = new RouteGroup(info.getCategory());
424            group.mSupportedTypes = info.mSupportedTypes;
425            sStatic.mRoutes.add(group);
426            dispatchRouteAdded(group);
427            group.addRoute(info);
428
429            info = group;
430        } else {
431            sStatic.mRoutes.add(info);
432            dispatchRouteAdded(info);
433        }
434    }
435
436    /**
437     * Remove an app-specified route for media from the MediaRouter.
438     *
439     * @param info Definition of the route to remove
440     * @see #addUserRoute(UserRouteInfo)
441     */
442    public void removeUserRoute(UserRouteInfo info) {
443        removeRoute(info);
444    }
445
446    /**
447     * Remove all app-specified routes from the MediaRouter.
448     *
449     * @see #removeUserRoute(UserRouteInfo)
450     */
451    public void clearUserRoutes() {
452        for (int i = 0; i < sStatic.mRoutes.size(); i++) {
453            final RouteInfo info = sStatic.mRoutes.get(i);
454            // TODO Right now, RouteGroups only ever contain user routes.
455            // The code below will need to change if this assumption does.
456            if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
457                removeRouteAt(i);
458                i--;
459            }
460        }
461    }
462
463    /**
464     * @hide internal use only
465     */
466    public void removeRouteInt(RouteInfo info) {
467        removeRoute(info);
468    }
469
470    static void removeRoute(RouteInfo info) {
471        if (sStatic.mRoutes.remove(info)) {
472            final RouteCategory removingCat = info.getCategory();
473            final int count = sStatic.mRoutes.size();
474            boolean found = false;
475            for (int i = 0; i < count; i++) {
476                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
477                if (removingCat == cat) {
478                    found = true;
479                    break;
480                }
481            }
482            if (info == sStatic.mSelectedRoute) {
483                // Removing the currently selected route? Select the default before we remove it.
484                // TODO: Be smarter about the route types here; this selects for all valid.
485                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudioVideo);
486            }
487            if (!found) {
488                sStatic.mCategories.remove(removingCat);
489            }
490            dispatchRouteRemoved(info);
491        }
492    }
493
494    void removeRouteAt(int routeIndex) {
495        if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) {
496            final RouteInfo info = sStatic.mRoutes.remove(routeIndex);
497            final RouteCategory removingCat = info.getCategory();
498            final int count = sStatic.mRoutes.size();
499            boolean found = false;
500            for (int i = 0; i < count; i++) {
501                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
502                if (removingCat == cat) {
503                    found = true;
504                    break;
505                }
506            }
507            if (info == sStatic.mSelectedRoute) {
508                // Removing the currently selected route? Select the default before we remove it.
509                // TODO: Be smarter about the route types here; this selects for all valid.
510                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_USER,
511                        sStatic.mDefaultAudioVideo);
512            }
513            if (!found) {
514                sStatic.mCategories.remove(removingCat);
515            }
516            dispatchRouteRemoved(info);
517        }
518    }
519
520    /**
521     * Return the number of {@link MediaRouter.RouteCategory categories} currently
522     * represented by routes known to this MediaRouter.
523     *
524     * @return the number of unique categories represented by this MediaRouter's known routes
525     */
526    public int getCategoryCount() {
527        return sStatic.mCategories.size();
528    }
529
530    /**
531     * Return the {@link MediaRouter.RouteCategory category} at the given index.
532     * Valid indices are in the range [0-getCategoryCount).
533     *
534     * @param index which category to return
535     * @return the category at index
536     */
537    public RouteCategory getCategoryAt(int index) {
538        return sStatic.mCategories.get(index);
539    }
540
541    /**
542     * Return the number of {@link MediaRouter.RouteInfo routes} currently known
543     * to this MediaRouter.
544     *
545     * @return the number of routes tracked by this router
546     */
547    public int getRouteCount() {
548        return sStatic.mRoutes.size();
549    }
550
551    /**
552     * Return the route at the specified index.
553     *
554     * @param index index of the route to return
555     * @return the route at index
556     */
557    public RouteInfo getRouteAt(int index) {
558        return sStatic.mRoutes.get(index);
559    }
560
561    static int getRouteCountStatic() {
562        return sStatic.mRoutes.size();
563    }
564
565    static RouteInfo getRouteAtStatic(int index) {
566        return sStatic.mRoutes.get(index);
567    }
568
569    /**
570     * Create a new user route that may be modified and registered for use by the application.
571     *
572     * @param category The category the new route will belong to
573     * @return A new UserRouteInfo for use by the application
574     *
575     * @see #addUserRoute(UserRouteInfo)
576     * @see #removeUserRoute(UserRouteInfo)
577     * @see #createRouteCategory(CharSequence)
578     */
579    public UserRouteInfo createUserRoute(RouteCategory category) {
580        return new UserRouteInfo(category);
581    }
582
583    /**
584     * Create a new route category. Each route must belong to a category.
585     *
586     * @param name Name of the new category
587     * @param isGroupable true if routes in this category may be grouped with one another
588     * @return the new RouteCategory
589     */
590    public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
591        return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
592    }
593
594    /**
595     * Create a new route category. Each route must belong to a category.
596     *
597     * @param nameResId Resource ID of the name of the new category
598     * @param isGroupable true if routes in this category may be grouped with one another
599     * @return the new RouteCategory
600     */
601    public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
602        return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
603    }
604
605    static void updateRoute(final RouteInfo info) {
606        dispatchRouteChanged(info);
607    }
608
609    static void dispatchRouteSelected(int type, RouteInfo info) {
610        for (CallbackInfo cbi : sStatic.mCallbacks) {
611            if ((cbi.type & type) != 0) {
612                cbi.cb.onRouteSelected(cbi.router, type, info);
613            }
614        }
615    }
616
617    static void dispatchRouteUnselected(int type, RouteInfo info) {
618        for (CallbackInfo cbi : sStatic.mCallbacks) {
619            if ((cbi.type & type) != 0) {
620                cbi.cb.onRouteUnselected(cbi.router, type, info);
621            }
622        }
623    }
624
625    static void dispatchRouteChanged(RouteInfo info) {
626        for (CallbackInfo cbi : sStatic.mCallbacks) {
627            if ((cbi.type & info.mSupportedTypes) != 0) {
628                cbi.cb.onRouteChanged(cbi.router, info);
629            }
630        }
631    }
632
633    static void dispatchRouteAdded(RouteInfo info) {
634        for (CallbackInfo cbi : sStatic.mCallbacks) {
635            if ((cbi.type & info.mSupportedTypes) != 0) {
636                cbi.cb.onRouteAdded(cbi.router, info);
637            }
638        }
639    }
640
641    static void dispatchRouteRemoved(RouteInfo info) {
642        for (CallbackInfo cbi : sStatic.mCallbacks) {
643            if ((cbi.type & info.mSupportedTypes) != 0) {
644                cbi.cb.onRouteRemoved(cbi.router, info);
645            }
646        }
647    }
648
649    static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
650        for (CallbackInfo cbi : sStatic.mCallbacks) {
651            if ((cbi.type & group.mSupportedTypes) != 0) {
652                cbi.cb.onRouteGrouped(cbi.router, info, group, index);
653            }
654        }
655    }
656
657    static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
658        for (CallbackInfo cbi : sStatic.mCallbacks) {
659            if ((cbi.type & group.mSupportedTypes) != 0) {
660                cbi.cb.onRouteUngrouped(cbi.router, info, group);
661            }
662        }
663    }
664
665    static void dispatchRouteVolumeChanged(RouteInfo info) {
666        for (CallbackInfo cbi : sStatic.mCallbacks) {
667            if ((cbi.type & info.mSupportedTypes) != 0) {
668                cbi.cb.onRouteVolumeChanged(cbi.router, info);
669            }
670        }
671    }
672
673    static void systemVolumeChanged(int newValue) {
674        final RouteInfo selectedRoute = sStatic.mSelectedRoute;
675        if (selectedRoute == null) return;
676
677        if (selectedRoute == sStatic.mBluetoothA2dpRoute ||
678                selectedRoute == sStatic.mDefaultAudioVideo) {
679            dispatchRouteVolumeChanged(selectedRoute);
680        } else if (sStatic.mBluetoothA2dpRoute != null) {
681            try {
682                dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ?
683                        sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo);
684            } catch (RemoteException e) {
685                Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e);
686            }
687        } else {
688            dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo);
689        }
690    }
691
692    static void updateWifiDisplayStatus(WifiDisplayStatus newStatus) {
693        final WifiDisplayStatus oldStatus = sStatic.mLastKnownWifiDisplayStatus;
694
695        // TODO Naive implementation. Make this smarter later.
696        boolean wantScan = false;
697        boolean blockScan = false;
698        WifiDisplay[] oldDisplays = oldStatus != null ?
699                oldStatus.getRememberedDisplays() : new WifiDisplay[0];
700        WifiDisplay[] newDisplays = newStatus.getRememberedDisplays();
701        WifiDisplay[] availableDisplays = newStatus.getAvailableDisplays();
702        WifiDisplay activeDisplay = newStatus.getActiveDisplay();
703
704        for (int i = 0; i < newDisplays.length; i++) {
705            final WifiDisplay d = newDisplays[i];
706            final WifiDisplay oldRemembered = findMatchingDisplay(d, oldDisplays);
707            if (oldRemembered == null) {
708                addRouteStatic(makeWifiDisplayRoute(d,
709                        findMatchingDisplay(d, availableDisplays) != null));
710                wantScan = true;
711            } else {
712                final boolean available = findMatchingDisplay(d, availableDisplays) != null;
713                final RouteInfo route = findWifiDisplayRoute(d);
714                updateWifiDisplayRoute(route, d, available, newStatus);
715            }
716            if (d.equals(activeDisplay)) {
717                final RouteInfo activeRoute = findWifiDisplayRoute(d);
718                if (activeRoute != null) {
719                    selectRouteStatic(activeRoute.getSupportedTypes(), activeRoute);
720
721                    // Don't scan if we're already connected to a wifi display,
722                    // the scanning process can cause a hiccup with some configurations.
723                    blockScan = true;
724                }
725            }
726        }
727        for (int i = 0; i < oldDisplays.length; i++) {
728            final WifiDisplay d = oldDisplays[i];
729            final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays);
730            if (newDisplay == null) {
731                removeRoute(findWifiDisplayRoute(d));
732            }
733        }
734
735        if (wantScan && !blockScan) {
736            sStatic.mDisplayService.scanWifiDisplays();
737        }
738
739        sStatic.mLastKnownWifiDisplayStatus = newStatus;
740    }
741
742    static RouteInfo makeWifiDisplayRoute(WifiDisplay display, boolean available) {
743        final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory);
744        newRoute.mDeviceAddress = display.getDeviceAddress();
745        newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
746        newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
747        newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE;
748
749        newRoute.setStatusCode(available ?
750                RouteInfo.STATUS_AVAILABLE : RouteInfo.STATUS_CONNECTING);
751        newRoute.mEnabled = available;
752
753        newRoute.mName = makeWifiDisplayName(display);
754        return newRoute;
755    }
756
757    static String makeWifiDisplayName(WifiDisplay display) {
758        String name = display.getDeviceAlias();
759        if (TextUtils.isEmpty(name)) {
760            name = display.getDeviceName();
761        }
762        return name;
763    }
764
765    private static void updateWifiDisplayRoute(RouteInfo route, WifiDisplay display,
766            boolean available, WifiDisplayStatus wifiDisplayStatus) {
767        final boolean isScanning =
768                wifiDisplayStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING;
769
770        boolean changed = false;
771        int newStatus = RouteInfo.STATUS_NONE;
772
773        if (available) {
774            newStatus = isScanning ? RouteInfo.STATUS_SCANNING : RouteInfo.STATUS_AVAILABLE;
775        } else {
776            newStatus = RouteInfo.STATUS_NOT_AVAILABLE;
777        }
778
779        if (display.equals(wifiDisplayStatus.getActiveDisplay())) {
780            final int activeState = wifiDisplayStatus.getActiveDisplayState();
781            switch (activeState) {
782                case WifiDisplayStatus.DISPLAY_STATE_CONNECTED:
783                    newStatus = RouteInfo.STATUS_NONE;
784                    break;
785                case WifiDisplayStatus.DISPLAY_STATE_CONNECTING:
786                    newStatus = RouteInfo.STATUS_CONNECTING;
787                    break;
788                case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED:
789                    Log.e(TAG, "Active display is not connected!");
790                    break;
791            }
792        }
793
794        final String newName = makeWifiDisplayName(display);
795        if (route.getName().equals(newName)) {
796            route.mName = newName;
797            changed = true;
798        }
799
800        changed |= route.mEnabled != available;
801        route.mEnabled = available;
802
803        changed |= route.setStatusCode(newStatus);
804
805        if (changed) {
806            dispatchRouteChanged(route);
807        }
808
809        if (!available && route == sStatic.mSelectedRoute) {
810            // Oops, no longer available. Reselect the default.
811            final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo;
812            selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute);
813        }
814    }
815
816    private static WifiDisplay findMatchingDisplay(WifiDisplay address, WifiDisplay[] displays) {
817        for (int i = 0; i < displays.length; i++) {
818            final WifiDisplay d = displays[i];
819            if (d.equals(address)) {
820                return d;
821            }
822        }
823        return null;
824    }
825
826    private static RouteInfo findWifiDisplayRoute(WifiDisplay d) {
827        final int count = sStatic.mRoutes.size();
828        for (int i = 0; i < count; i++) {
829            final RouteInfo info = sStatic.mRoutes.get(i);
830            if (d.getDeviceAddress().equals(info.mDeviceAddress)) {
831                return info;
832            }
833        }
834        return null;
835    }
836
837    /**
838     * Information about a media route.
839     */
840    public static class RouteInfo {
841        CharSequence mName;
842        int mNameResId;
843        private CharSequence mStatus;
844        int mSupportedTypes;
845        RouteGroup mGroup;
846        final RouteCategory mCategory;
847        Drawable mIcon;
848        // playback information
849        int mPlaybackType = PLAYBACK_TYPE_LOCAL;
850        int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
851        int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
852        int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
853        int mPlaybackStream = AudioManager.STREAM_MUSIC;
854        VolumeCallbackInfo mVcb;
855
856        String mDeviceAddress;
857        boolean mEnabled = true;
858
859        // A predetermined connection status that can override mStatus
860        private int mStatusCode;
861
862        /** @hide */ public static final int STATUS_NONE = 0;
863        /** @hide */ public static final int STATUS_SCANNING = 1;
864        /** @hide */ public static final int STATUS_CONNECTING = 2;
865        /** @hide */ public static final int STATUS_AVAILABLE = 3;
866        /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4;
867
868        private Object mTag;
869
870        /**
871         * The default playback type, "local", indicating the presentation of the media is happening
872         * on the same device (e.g. a phone, a tablet) as where it is controlled from.
873         * @see #setPlaybackType(int)
874         */
875        public final static int PLAYBACK_TYPE_LOCAL = 0;
876        /**
877         * A playback type indicating the presentation of the media is happening on
878         * a different device (i.e. the remote device) than where it is controlled from.
879         * @see #setPlaybackType(int)
880         */
881        public final static int PLAYBACK_TYPE_REMOTE = 1;
882        /**
883         * Playback information indicating the playback volume is fixed, i.e. it cannot be
884         * controlled from this object. An example of fixed playback volume is a remote player,
885         * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
886         * than attenuate at the source.
887         * @see #setVolumeHandling(int)
888         */
889        public final static int PLAYBACK_VOLUME_FIXED = 0;
890        /**
891         * Playback information indicating the playback volume is variable and can be controlled
892         * from this object.
893         */
894        public final static int PLAYBACK_VOLUME_VARIABLE = 1;
895
896        RouteInfo(RouteCategory category) {
897            mCategory = category;
898        }
899
900        /**
901         * @return The user-friendly name of a media route. This is the string presented
902         * to users who may select this as the active route.
903         */
904        public CharSequence getName() {
905            return getName(sStatic.mResources);
906        }
907
908        /**
909         * Return the properly localized/resource selected name of this route.
910         *
911         * @param context Context used to resolve the correct configuration to load
912         * @return The user-friendly name of the media route. This is the string presented
913         * to users who may select this as the active route.
914         */
915        public CharSequence getName(Context context) {
916            return getName(context.getResources());
917        }
918
919        CharSequence getName(Resources res) {
920            if (mNameResId != 0) {
921                return mName = res.getText(mNameResId);
922            }
923            return mName;
924        }
925
926        /**
927         * @return The user-friendly status for a media route. This may include a description
928         * of the currently playing media, if available.
929         */
930        public CharSequence getStatus() {
931            return mStatus;
932        }
933
934        /**
935         * Set this route's status by predetermined status code. If the caller
936         * should dispatch a route changed event this call will return true;
937         */
938        boolean setStatusCode(int statusCode) {
939            if (statusCode != mStatusCode) {
940                mStatusCode = statusCode;
941                int resId = 0;
942                switch (statusCode) {
943                    case STATUS_SCANNING:
944                        resId = com.android.internal.R.string.media_route_status_scanning;
945                        break;
946                    case STATUS_CONNECTING:
947                        resId = com.android.internal.R.string.media_route_status_connecting;
948                        break;
949                    case STATUS_AVAILABLE:
950                        resId = com.android.internal.R.string.media_route_status_available;
951                        break;
952                    case STATUS_NOT_AVAILABLE:
953                        resId = com.android.internal.R.string.media_route_status_not_available;
954                        break;
955                }
956                mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null;
957                return true;
958            }
959            return false;
960        }
961
962        /**
963         * @hide
964         */
965        public int getStatusCode() {
966            return mStatusCode;
967        }
968
969        /**
970         * @return A media type flag set describing which types this route supports.
971         */
972        public int getSupportedTypes() {
973            return mSupportedTypes;
974        }
975
976        /**
977         * @return The group that this route belongs to.
978         */
979        public RouteGroup getGroup() {
980            return mGroup;
981        }
982
983        /**
984         * @return the category this route belongs to.
985         */
986        public RouteCategory getCategory() {
987            return mCategory;
988        }
989
990        /**
991         * Get the icon representing this route.
992         * This icon will be used in picker UIs if available.
993         *
994         * @return the icon representing this route or null if no icon is available
995         */
996        public Drawable getIconDrawable() {
997            return mIcon;
998        }
999
1000        /**
1001         * Set an application-specific tag object for this route.
1002         * The application may use this to store arbitrary data associated with the
1003         * route for internal tracking.
1004         *
1005         * <p>Note that the lifespan of a route may be well past the lifespan of
1006         * an Activity or other Context; take care that objects you store here
1007         * will not keep more data in memory alive than you intend.</p>
1008         *
1009         * @param tag Arbitrary, app-specific data for this route to hold for later use
1010         */
1011        public void setTag(Object tag) {
1012            mTag = tag;
1013            routeUpdated();
1014        }
1015
1016        /**
1017         * @return The tag object previously set by the application
1018         * @see #setTag(Object)
1019         */
1020        public Object getTag() {
1021            return mTag;
1022        }
1023
1024        /**
1025         * @return the type of playback associated with this route
1026         * @see UserRouteInfo#setPlaybackType(int)
1027         */
1028        public int getPlaybackType() {
1029            return mPlaybackType;
1030        }
1031
1032        /**
1033         * @return the stream over which the playback associated with this route is performed
1034         * @see UserRouteInfo#setPlaybackStream(int)
1035         */
1036        public int getPlaybackStream() {
1037            return mPlaybackStream;
1038        }
1039
1040        /**
1041         * Return the current volume for this route. Depending on the route, this may only
1042         * be valid if the route is currently selected.
1043         *
1044         * @return the volume at which the playback associated with this route is performed
1045         * @see UserRouteInfo#setVolume(int)
1046         */
1047        public int getVolume() {
1048            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1049                int vol = 0;
1050                try {
1051                    vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
1052                } catch (RemoteException e) {
1053                    Log.e(TAG, "Error getting local stream volume", e);
1054                }
1055                return vol;
1056            } else {
1057                return mVolume;
1058            }
1059        }
1060
1061        /**
1062         * Request a volume change for this route.
1063         * @param volume value between 0 and getVolumeMax
1064         */
1065        public void requestSetVolume(int volume) {
1066            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1067                try {
1068                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
1069                } catch (RemoteException e) {
1070                    Log.e(TAG, "Error setting local stream volume", e);
1071                }
1072            } else {
1073                Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " +
1074                        "Non-local volume playback on system route? " +
1075                        "Could not request volume change.");
1076            }
1077        }
1078
1079        /**
1080         * Request an incremental volume update for this route.
1081         * @param direction Delta to apply to the current volume
1082         */
1083        public void requestUpdateVolume(int direction) {
1084            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1085                try {
1086                    final int volume =
1087                            Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
1088                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
1089                } catch (RemoteException e) {
1090                    Log.e(TAG, "Error setting local stream volume", e);
1091                }
1092            } else {
1093                Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " +
1094                        "Non-local volume playback on system route? " +
1095                        "Could not request volume change.");
1096            }
1097        }
1098
1099        /**
1100         * @return the maximum volume at which the playback associated with this route is performed
1101         * @see UserRouteInfo#setVolumeMax(int)
1102         */
1103        public int getVolumeMax() {
1104            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1105                int volMax = 0;
1106                try {
1107                    volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
1108                } catch (RemoteException e) {
1109                    Log.e(TAG, "Error getting local stream volume", e);
1110                }
1111                return volMax;
1112            } else {
1113                return mVolumeMax;
1114            }
1115        }
1116
1117        /**
1118         * @return how volume is handling on the route
1119         * @see UserRouteInfo#setVolumeHandling(int)
1120         */
1121        public int getVolumeHandling() {
1122            return mVolumeHandling;
1123        }
1124
1125        /**
1126         * @return true if this route is enabled and may be selected
1127         */
1128        public boolean isEnabled() {
1129            return mEnabled;
1130        }
1131
1132        void setStatusInt(CharSequence status) {
1133            if (!status.equals(mStatus)) {
1134                mStatus = status;
1135                if (mGroup != null) {
1136                    mGroup.memberStatusChanged(this, status);
1137                }
1138                routeUpdated();
1139            }
1140        }
1141
1142        final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
1143            public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
1144                sStatic.mHandler.post(new Runnable() {
1145                    @Override
1146                    public void run() {
1147                        if (mVcb != null) {
1148                            if (direction != 0) {
1149                                mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
1150                            } else {
1151                                mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
1152                            }
1153                        }
1154                    }
1155                });
1156            }
1157        };
1158
1159        void routeUpdated() {
1160            updateRoute(this);
1161        }
1162
1163        @Override
1164        public String toString() {
1165            String supportedTypes = typesToString(getSupportedTypes());
1166            return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() +
1167                    " category=" + getCategory() +
1168                    " supportedTypes=" + supportedTypes + "}";
1169        }
1170    }
1171
1172    /**
1173     * Information about a route that the application may define and modify.
1174     * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
1175     * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
1176     *
1177     * @see MediaRouter.RouteInfo
1178     */
1179    public static class UserRouteInfo extends RouteInfo {
1180        RemoteControlClient mRcc;
1181
1182        UserRouteInfo(RouteCategory category) {
1183            super(category);
1184            mSupportedTypes = ROUTE_TYPE_USER;
1185            mPlaybackType = PLAYBACK_TYPE_REMOTE;
1186            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
1187        }
1188
1189        /**
1190         * Set the user-visible name of this route.
1191         * @param name Name to display to the user to describe this route
1192         */
1193        public void setName(CharSequence name) {
1194            mName = name;
1195            routeUpdated();
1196        }
1197
1198        /**
1199         * Set the user-visible name of this route.
1200         * @param resId Resource ID of the name to display to the user to describe this route
1201         */
1202        public void setName(int resId) {
1203            mNameResId = resId;
1204            mName = null;
1205            routeUpdated();
1206        }
1207
1208        /**
1209         * Set the current user-visible status for this route.
1210         * @param status Status to display to the user to describe what the endpoint
1211         * of this route is currently doing
1212         */
1213        public void setStatus(CharSequence status) {
1214            setStatusInt(status);
1215        }
1216
1217        /**
1218         * Set the RemoteControlClient responsible for reporting playback info for this
1219         * user route.
1220         *
1221         * <p>If this route manages remote playback, the data exposed by this
1222         * RemoteControlClient will be used to reflect and update information
1223         * such as route volume info in related UIs.</p>
1224         *
1225         * <p>The RemoteControlClient must have been previously registered with
1226         * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
1227         *
1228         * @param rcc RemoteControlClient associated with this route
1229         */
1230        public void setRemoteControlClient(RemoteControlClient rcc) {
1231            mRcc = rcc;
1232            updatePlaybackInfoOnRcc();
1233        }
1234
1235        /**
1236         * Retrieve the RemoteControlClient associated with this route, if one has been set.
1237         *
1238         * @return the RemoteControlClient associated with this route
1239         * @see #setRemoteControlClient(RemoteControlClient)
1240         */
1241        public RemoteControlClient getRemoteControlClient() {
1242            return mRcc;
1243        }
1244
1245        /**
1246         * Set an icon that will be used to represent this route.
1247         * The system may use this icon in picker UIs or similar.
1248         *
1249         * @param icon icon drawable to use to represent this route
1250         */
1251        public void setIconDrawable(Drawable icon) {
1252            mIcon = icon;
1253        }
1254
1255        /**
1256         * Set an icon that will be used to represent this route.
1257         * The system may use this icon in picker UIs or similar.
1258         *
1259         * @param resId Resource ID of an icon drawable to use to represent this route
1260         */
1261        public void setIconResource(int resId) {
1262            setIconDrawable(sStatic.mResources.getDrawable(resId));
1263        }
1264
1265        /**
1266         * Set a callback to be notified of volume update requests
1267         * @param vcb
1268         */
1269        public void setVolumeCallback(VolumeCallback vcb) {
1270            mVcb = new VolumeCallbackInfo(vcb, this);
1271        }
1272
1273        /**
1274         * Defines whether playback associated with this route is "local"
1275         *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
1276         *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
1277         * @param type
1278         */
1279        public void setPlaybackType(int type) {
1280            if (mPlaybackType != type) {
1281                mPlaybackType = type;
1282                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type);
1283            }
1284        }
1285
1286        /**
1287         * Defines whether volume for the playback associated with this route is fixed
1288         * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
1289         * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
1290         * @param volumeHandling
1291         */
1292        public void setVolumeHandling(int volumeHandling) {
1293            if (mVolumeHandling != volumeHandling) {
1294                mVolumeHandling = volumeHandling;
1295                setPlaybackInfoOnRcc(
1296                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling);
1297            }
1298        }
1299
1300        /**
1301         * Defines at what volume the playback associated with this route is performed (for user
1302         * feedback purposes). This information is only used when the playback is not local.
1303         * @param volume
1304         */
1305        public void setVolume(int volume) {
1306            volume = Math.max(0, Math.min(volume, getVolumeMax()));
1307            if (mVolume != volume) {
1308                mVolume = volume;
1309                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume);
1310                dispatchRouteVolumeChanged(this);
1311                if (mGroup != null) {
1312                    mGroup.memberVolumeChanged(this);
1313                }
1314            }
1315        }
1316
1317        @Override
1318        public void requestSetVolume(int volume) {
1319            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
1320                if (mVcb == null) {
1321                    Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
1322                    return;
1323                }
1324                mVcb.vcb.onVolumeSetRequest(this, volume);
1325            }
1326        }
1327
1328        @Override
1329        public void requestUpdateVolume(int direction) {
1330            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
1331                if (mVcb == null) {
1332                    Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
1333                    return;
1334                }
1335                mVcb.vcb.onVolumeUpdateRequest(this, direction);
1336            }
1337        }
1338
1339        /**
1340         * Defines the maximum volume at which the playback associated with this route is performed
1341         * (for user feedback purposes). This information is only used when the playback is not
1342         * local.
1343         * @param volumeMax
1344         */
1345        public void setVolumeMax(int volumeMax) {
1346            if (mVolumeMax != volumeMax) {
1347                mVolumeMax = volumeMax;
1348                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax);
1349            }
1350        }
1351
1352        /**
1353         * Defines over what stream type the media is presented.
1354         * @param stream
1355         */
1356        public void setPlaybackStream(int stream) {
1357            if (mPlaybackStream != stream) {
1358                mPlaybackStream = stream;
1359                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream);
1360            }
1361        }
1362
1363        private void updatePlaybackInfoOnRcc() {
1364            if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) {
1365                mRcc.setPlaybackInformation(
1366                        RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax);
1367                mRcc.setPlaybackInformation(
1368                        RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume);
1369                mRcc.setPlaybackInformation(
1370                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling);
1371                mRcc.setPlaybackInformation(
1372                        RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream);
1373                mRcc.setPlaybackInformation(
1374                        RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType);
1375                // let AudioService know whom to call when remote volume needs to be updated
1376                try {
1377                    sStatic.mAudioService.registerRemoteVolumeObserverForRcc(
1378                            mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */);
1379                } catch (RemoteException e) {
1380                    Log.e(TAG, "Error registering remote volume observer", e);
1381                }
1382            }
1383        }
1384
1385        private void setPlaybackInfoOnRcc(int what, int value) {
1386            if (mRcc != null) {
1387                mRcc.setPlaybackInformation(what, value);
1388            }
1389        }
1390    }
1391
1392    /**
1393     * Information about a route that consists of multiple other routes in a group.
1394     */
1395    public static class RouteGroup extends RouteInfo {
1396        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
1397        private boolean mUpdateName;
1398
1399        RouteGroup(RouteCategory category) {
1400            super(category);
1401            mGroup = this;
1402            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
1403        }
1404
1405        CharSequence getName(Resources res) {
1406            if (mUpdateName) updateName();
1407            return super.getName(res);
1408        }
1409
1410        /**
1411         * Add a route to this group. The route must not currently belong to another group.
1412         *
1413         * @param route route to add to this group
1414         */
1415        public void addRoute(RouteInfo route) {
1416            if (route.getGroup() != null) {
1417                throw new IllegalStateException("Route " + route + " is already part of a group.");
1418            }
1419            if (route.getCategory() != mCategory) {
1420                throw new IllegalArgumentException(
1421                        "Route cannot be added to a group with a different category. " +
1422                            "(Route category=" + route.getCategory() +
1423                            " group category=" + mCategory + ")");
1424            }
1425            final int at = mRoutes.size();
1426            mRoutes.add(route);
1427            route.mGroup = this;
1428            mUpdateName = true;
1429            updateVolume();
1430            routeUpdated();
1431            dispatchRouteGrouped(route, this, at);
1432        }
1433
1434        /**
1435         * Add a route to this group before the specified index.
1436         *
1437         * @param route route to add
1438         * @param insertAt insert the new route before this index
1439         */
1440        public void addRoute(RouteInfo route, int insertAt) {
1441            if (route.getGroup() != null) {
1442                throw new IllegalStateException("Route " + route + " is already part of a group.");
1443            }
1444            if (route.getCategory() != mCategory) {
1445                throw new IllegalArgumentException(
1446                        "Route cannot be added to a group with a different category. " +
1447                            "(Route category=" + route.getCategory() +
1448                            " group category=" + mCategory + ")");
1449            }
1450            mRoutes.add(insertAt, route);
1451            route.mGroup = this;
1452            mUpdateName = true;
1453            updateVolume();
1454            routeUpdated();
1455            dispatchRouteGrouped(route, this, insertAt);
1456        }
1457
1458        /**
1459         * Remove a route from this group.
1460         *
1461         * @param route route to remove
1462         */
1463        public void removeRoute(RouteInfo route) {
1464            if (route.getGroup() != this) {
1465                throw new IllegalArgumentException("Route " + route +
1466                        " is not a member of this group.");
1467            }
1468            mRoutes.remove(route);
1469            route.mGroup = null;
1470            mUpdateName = true;
1471            updateVolume();
1472            dispatchRouteUngrouped(route, this);
1473            routeUpdated();
1474        }
1475
1476        /**
1477         * Remove the route at the specified index from this group.
1478         *
1479         * @param index index of the route to remove
1480         */
1481        public void removeRoute(int index) {
1482            RouteInfo route = mRoutes.remove(index);
1483            route.mGroup = null;
1484            mUpdateName = true;
1485            updateVolume();
1486            dispatchRouteUngrouped(route, this);
1487            routeUpdated();
1488        }
1489
1490        /**
1491         * @return The number of routes in this group
1492         */
1493        public int getRouteCount() {
1494            return mRoutes.size();
1495        }
1496
1497        /**
1498         * Return the route in this group at the specified index
1499         *
1500         * @param index Index to fetch
1501         * @return The route at index
1502         */
1503        public RouteInfo getRouteAt(int index) {
1504            return mRoutes.get(index);
1505        }
1506
1507        /**
1508         * Set an icon that will be used to represent this group.
1509         * The system may use this icon in picker UIs or similar.
1510         *
1511         * @param icon icon drawable to use to represent this group
1512         */
1513        public void setIconDrawable(Drawable icon) {
1514            mIcon = icon;
1515        }
1516
1517        /**
1518         * Set an icon that will be used to represent this group.
1519         * The system may use this icon in picker UIs or similar.
1520         *
1521         * @param resId Resource ID of an icon drawable to use to represent this group
1522         */
1523        public void setIconResource(int resId) {
1524            setIconDrawable(sStatic.mResources.getDrawable(resId));
1525        }
1526
1527        @Override
1528        public void requestSetVolume(int volume) {
1529            final int maxVol = getVolumeMax();
1530            if (maxVol == 0) {
1531                return;
1532            }
1533
1534            final float scaledVolume = (float) volume / maxVol;
1535            final int routeCount = getRouteCount();
1536            for (int i = 0; i < routeCount; i++) {
1537                final RouteInfo route = getRouteAt(i);
1538                final int routeVol = (int) (scaledVolume * route.getVolumeMax());
1539                route.requestSetVolume(routeVol);
1540            }
1541            if (volume != mVolume) {
1542                mVolume = volume;
1543                dispatchRouteVolumeChanged(this);
1544            }
1545        }
1546
1547        @Override
1548        public void requestUpdateVolume(int direction) {
1549            final int maxVol = getVolumeMax();
1550            if (maxVol == 0) {
1551                return;
1552            }
1553
1554            final int routeCount = getRouteCount();
1555            int volume = 0;
1556            for (int i = 0; i < routeCount; i++) {
1557                final RouteInfo route = getRouteAt(i);
1558                route.requestUpdateVolume(direction);
1559                final int routeVol = route.getVolume();
1560                if (routeVol > volume) {
1561                    volume = routeVol;
1562                }
1563            }
1564            if (volume != mVolume) {
1565                mVolume = volume;
1566                dispatchRouteVolumeChanged(this);
1567            }
1568        }
1569
1570        void memberNameChanged(RouteInfo info, CharSequence name) {
1571            mUpdateName = true;
1572            routeUpdated();
1573        }
1574
1575        void memberStatusChanged(RouteInfo info, CharSequence status) {
1576            setStatusInt(status);
1577        }
1578
1579        void memberVolumeChanged(RouteInfo info) {
1580            updateVolume();
1581        }
1582
1583        void updateVolume() {
1584            // A group always represents the highest component volume value.
1585            final int routeCount = getRouteCount();
1586            int volume = 0;
1587            for (int i = 0; i < routeCount; i++) {
1588                final int routeVol = getRouteAt(i).getVolume();
1589                if (routeVol > volume) {
1590                    volume = routeVol;
1591                }
1592            }
1593            if (volume != mVolume) {
1594                mVolume = volume;
1595                dispatchRouteVolumeChanged(this);
1596            }
1597        }
1598
1599        @Override
1600        void routeUpdated() {
1601            int types = 0;
1602            final int count = mRoutes.size();
1603            if (count == 0) {
1604                // Don't keep empty groups in the router.
1605                MediaRouter.removeRoute(this);
1606                return;
1607            }
1608
1609            int maxVolume = 0;
1610            boolean isLocal = true;
1611            boolean isFixedVolume = true;
1612            for (int i = 0; i < count; i++) {
1613                final RouteInfo route = mRoutes.get(i);
1614                types |= route.mSupportedTypes;
1615                final int routeMaxVolume = route.getVolumeMax();
1616                if (routeMaxVolume > maxVolume) {
1617                    maxVolume = routeMaxVolume;
1618                }
1619                isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
1620                isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
1621            }
1622            mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
1623            mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
1624            mSupportedTypes = types;
1625            mVolumeMax = maxVolume;
1626            mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
1627            super.routeUpdated();
1628        }
1629
1630        void updateName() {
1631            final StringBuilder sb = new StringBuilder();
1632            final int count = mRoutes.size();
1633            for (int i = 0; i < count; i++) {
1634                final RouteInfo info = mRoutes.get(i);
1635                // TODO: There's probably a much more correct way to localize this.
1636                if (i > 0) sb.append(", ");
1637                sb.append(info.mName);
1638            }
1639            mName = sb.toString();
1640            mUpdateName = false;
1641        }
1642
1643        @Override
1644        public String toString() {
1645            StringBuilder sb = new StringBuilder(super.toString());
1646            sb.append('[');
1647            final int count = mRoutes.size();
1648            for (int i = 0; i < count; i++) {
1649                if (i > 0) sb.append(", ");
1650                sb.append(mRoutes.get(i));
1651            }
1652            sb.append(']');
1653            return sb.toString();
1654        }
1655    }
1656
1657    /**
1658     * Definition of a category of routes. All routes belong to a category.
1659     */
1660    public static class RouteCategory {
1661        CharSequence mName;
1662        int mNameResId;
1663        int mTypes;
1664        final boolean mGroupable;
1665        boolean mIsSystem;
1666
1667        RouteCategory(CharSequence name, int types, boolean groupable) {
1668            mName = name;
1669            mTypes = types;
1670            mGroupable = groupable;
1671        }
1672
1673        RouteCategory(int nameResId, int types, boolean groupable) {
1674            mNameResId = nameResId;
1675            mTypes = types;
1676            mGroupable = groupable;
1677        }
1678
1679        /**
1680         * @return the name of this route category
1681         */
1682        public CharSequence getName() {
1683            return getName(sStatic.mResources);
1684        }
1685
1686        /**
1687         * Return the properly localized/configuration dependent name of this RouteCategory.
1688         *
1689         * @param context Context to resolve name resources
1690         * @return the name of this route category
1691         */
1692        public CharSequence getName(Context context) {
1693            return getName(context.getResources());
1694        }
1695
1696        CharSequence getName(Resources res) {
1697            if (mNameResId != 0) {
1698                return res.getText(mNameResId);
1699            }
1700            return mName;
1701        }
1702
1703        /**
1704         * Return the current list of routes in this category that have been added
1705         * to the MediaRouter.
1706         *
1707         * <p>This list will not include routes that are nested within RouteGroups.
1708         * A RouteGroup is treated as a single route within its category.</p>
1709         *
1710         * @param out a List to fill with the routes in this category. If this parameter is
1711         *            non-null, it will be cleared, filled with the current routes with this
1712         *            category, and returned. If this parameter is null, a new List will be
1713         *            allocated to report the category's current routes.
1714         * @return A list with the routes in this category that have been added to the MediaRouter.
1715         */
1716        public List<RouteInfo> getRoutes(List<RouteInfo> out) {
1717            if (out == null) {
1718                out = new ArrayList<RouteInfo>();
1719            } else {
1720                out.clear();
1721            }
1722
1723            final int count = getRouteCountStatic();
1724            for (int i = 0; i < count; i++) {
1725                final RouteInfo route = getRouteAtStatic(i);
1726                if (route.mCategory == this) {
1727                    out.add(route);
1728                }
1729            }
1730            return out;
1731        }
1732
1733        /**
1734         * @return Flag set describing the route types supported by this category
1735         */
1736        public int getSupportedTypes() {
1737            return mTypes;
1738        }
1739
1740        /**
1741         * Return whether or not this category supports grouping.
1742         *
1743         * <p>If this method returns true, all routes obtained from this category
1744         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
1745         *
1746         * @return true if this category supports
1747         */
1748        public boolean isGroupable() {
1749            return mGroupable;
1750        }
1751
1752        /**
1753         * @return true if this is the category reserved for system routes.
1754         * @hide
1755         */
1756        public boolean isSystem() {
1757            return mIsSystem;
1758        }
1759
1760        public String toString() {
1761            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
1762                    " groupable=" + mGroupable + " }";
1763        }
1764    }
1765
1766    static class CallbackInfo {
1767        public int type;
1768        public final Callback cb;
1769        public final MediaRouter router;
1770
1771        public CallbackInfo(Callback cb, int type, MediaRouter router) {
1772            this.cb = cb;
1773            this.type = type;
1774            this.router = router;
1775        }
1776    }
1777
1778    /**
1779     * Interface for receiving events about media routing changes.
1780     * All methods of this interface will be called from the application's main thread.
1781     *
1782     * <p>A Callback will only receive events relevant to routes that the callback
1783     * was registered for.</p>
1784     *
1785     * @see MediaRouter#addCallback(int, Callback)
1786     * @see MediaRouter#removeCallback(Callback)
1787     */
1788    public static abstract class Callback {
1789        /**
1790         * Called when the supplied route becomes selected as the active route
1791         * for the given route type.
1792         *
1793         * @param router the MediaRouter reporting the event
1794         * @param type Type flag set indicating the routes that have been selected
1795         * @param info Route that has been selected for the given route types
1796         */
1797        public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
1798
1799        /**
1800         * Called when the supplied route becomes unselected as the active route
1801         * for the given route type.
1802         *
1803         * @param router the MediaRouter reporting the event
1804         * @param type Type flag set indicating the routes that have been unselected
1805         * @param info Route that has been unselected for the given route types
1806         */
1807        public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
1808
1809        /**
1810         * Called when a route for the specified type was added.
1811         *
1812         * @param router the MediaRouter reporting the event
1813         * @param info Route that has become available for use
1814         */
1815        public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
1816
1817        /**
1818         * Called when a route for the specified type was removed.
1819         *
1820         * @param router the MediaRouter reporting the event
1821         * @param info Route that has been removed from availability
1822         */
1823        public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
1824
1825        /**
1826         * Called when an aspect of the indicated route has changed.
1827         *
1828         * <p>This will not indicate that the types supported by this route have
1829         * changed, only that cosmetic info such as name or status have been updated.</p>
1830         *
1831         * @param router the MediaRouter reporting the event
1832         * @param info The route that was changed
1833         */
1834        public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
1835
1836        /**
1837         * Called when a route is added to a group.
1838         *
1839         * @param router the MediaRouter reporting the event
1840         * @param info The route that was added
1841         * @param group The group the route was added to
1842         * @param index The route index within group that info was added at
1843         */
1844        public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1845                int index);
1846
1847        /**
1848         * Called when a route is removed from a group.
1849         *
1850         * @param router the MediaRouter reporting the event
1851         * @param info The route that was removed
1852         * @param group The group the route was removed from
1853         */
1854        public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
1855
1856        /**
1857         * Called when a route's volume changes.
1858         *
1859         * @param router the MediaRouter reporting the event
1860         * @param info The route with altered volume
1861         */
1862        public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
1863    }
1864
1865    /**
1866     * Stub implementation of {@link MediaRouter.Callback}.
1867     * Each abstract method is defined as a no-op. Override just the ones
1868     * you need.
1869     */
1870    public static class SimpleCallback extends Callback {
1871
1872        @Override
1873        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
1874        }
1875
1876        @Override
1877        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
1878        }
1879
1880        @Override
1881        public void onRouteAdded(MediaRouter router, RouteInfo info) {
1882        }
1883
1884        @Override
1885        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
1886        }
1887
1888        @Override
1889        public void onRouteChanged(MediaRouter router, RouteInfo info) {
1890        }
1891
1892        @Override
1893        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1894                int index) {
1895        }
1896
1897        @Override
1898        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
1899        }
1900
1901        @Override
1902        public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
1903        }
1904    }
1905
1906    static class VolumeCallbackInfo {
1907        public final VolumeCallback vcb;
1908        public final RouteInfo route;
1909
1910        public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
1911            this.vcb = vcb;
1912            this.route = route;
1913        }
1914    }
1915
1916    /**
1917     * Interface for receiving events about volume changes.
1918     * All methods of this interface will be called from the application's main thread.
1919     *
1920     * <p>A VolumeCallback will only receive events relevant to routes that the callback
1921     * was registered for.</p>
1922     *
1923     * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
1924     */
1925    public static abstract class VolumeCallback {
1926        /**
1927         * Called when the volume for the route should be increased or decreased.
1928         * @param info the route affected by this event
1929         * @param direction an integer indicating whether the volume is to be increased
1930         *     (positive value) or decreased (negative value).
1931         *     For bundled changes, the absolute value indicates the number of changes
1932         *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
1933         */
1934        public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
1935        /**
1936         * Called when the volume for the route should be set to the given value
1937         * @param info the route affected by this event
1938         * @param volume an integer indicating the new volume value that should be used, always
1939         *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
1940         */
1941        public abstract void onVolumeSetRequest(RouteInfo info, int volume);
1942    }
1943
1944    static class VolumeChangeReceiver extends BroadcastReceiver {
1945        @Override
1946        public void onReceive(Context context, Intent intent) {
1947            if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
1948                final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
1949                        -1);
1950                if (streamType != AudioManager.STREAM_MUSIC) {
1951                    return;
1952                }
1953
1954                final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
1955                final int oldVolume = intent.getIntExtra(
1956                        AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
1957                if (newVolume != oldVolume) {
1958                    systemVolumeChanged(newVolume);
1959                }
1960            }
1961        }
1962    }
1963
1964    static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver {
1965        @Override
1966        public void onReceive(Context context, Intent intent) {
1967            if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
1968                updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
1969                        DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
1970            }
1971        }
1972    }
1973}
1974