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