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 
17 package android.media;
18 
19 import android.Manifest;
20 import android.annotation.DrawableRes;
21 import android.annotation.NonNull;
22 import android.app.ActivityThread;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.pm.PackageManager;
28 import android.content.res.Resources;
29 import android.graphics.drawable.Drawable;
30 import android.hardware.display.DisplayManager;
31 import android.hardware.display.WifiDisplay;
32 import android.hardware.display.WifiDisplayStatus;
33 import android.media.session.MediaSession;
34 import android.os.Handler;
35 import android.os.IBinder;
36 import android.os.Process;
37 import android.os.RemoteException;
38 import android.os.ServiceManager;
39 import android.os.UserHandle;
40 import android.text.TextUtils;
41 import android.util.Log;
42 import android.view.Display;
43 
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Objects;
48 import java.util.concurrent.CopyOnWriteArrayList;
49 
50 /**
51  * MediaRouter allows applications to control the routing of media channels
52  * and streams from the current device to external speakers and destination devices.
53  *
54  * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
55  * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
56  * Context.MEDIA_ROUTER_SERVICE}.
57  *
58  * <p>The media router API is not thread-safe; all interactions with it must be
59  * done from the main thread of the process.</p>
60  */
61 public class MediaRouter {
62     private static final String TAG = "MediaRouter";
63     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
64 
65     static class Static implements DisplayManager.DisplayListener {
66         final Context mAppContext;
67         final Resources mResources;
68         final IAudioService mAudioService;
69         final DisplayManager mDisplayService;
70         final IMediaRouterService mMediaRouterService;
71         final Handler mHandler;
72         final CopyOnWriteArrayList<CallbackInfo> mCallbacks =
73                 new CopyOnWriteArrayList<CallbackInfo>();
74 
75         final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
76         final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
77 
78         final RouteCategory mSystemCategory;
79 
80         final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
81 
82         RouteInfo mDefaultAudioVideo;
83         RouteInfo mBluetoothA2dpRoute;
84 
85         RouteInfo mSelectedRoute;
86 
87         final boolean mCanConfigureWifiDisplays;
88         boolean mActivelyScanningWifiDisplays;
89         String mPreviousActiveWifiDisplayAddress;
90 
91         int mDiscoveryRequestRouteTypes;
92         boolean mDiscoverRequestActiveScan;
93 
94         int mCurrentUserId = -1;
95         IMediaRouterClient mClient;
96         MediaRouterClientState mClientState;
97 
98         final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
99             @Override
100             public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
101                 mHandler.post(new Runnable() {
102                     @Override public void run() {
103                         updateAudioRoutes(newRoutes);
104                     }
105                 });
106             }
107         };
108 
Static(Context appContext)109         Static(Context appContext) {
110             mAppContext = appContext;
111             mResources = Resources.getSystem();
112             mHandler = new Handler(appContext.getMainLooper());
113 
114             IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
115             mAudioService = IAudioService.Stub.asInterface(b);
116 
117             mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE);
118 
119             mMediaRouterService = IMediaRouterService.Stub.asInterface(
120                     ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
121 
122             mSystemCategory = new RouteCategory(
123                     com.android.internal.R.string.default_audio_route_category_name,
124                     ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false);
125             mSystemCategory.mIsSystem = true;
126 
127             // Only the system can configure wifi displays.  The display manager
128             // enforces this with a permission check.  Set a flag here so that we
129             // know whether this process is actually allowed to scan and connect.
130             mCanConfigureWifiDisplays = appContext.checkPermission(
131                     Manifest.permission.CONFIGURE_WIFI_DISPLAY,
132                     Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
133         }
134 
135         // Called after sStatic is initialized
startMonitoringRoutes(Context appContext)136         void startMonitoringRoutes(Context appContext) {
137             mDefaultAudioVideo = new RouteInfo(mSystemCategory);
138             mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name;
139             mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
140             mDefaultAudioVideo.updatePresentationDisplay();
141             addRouteStatic(mDefaultAudioVideo);
142 
143             // This will select the active wifi display route if there is one.
144             updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus());
145 
146             appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(),
147                     new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED));
148             appContext.registerReceiver(new VolumeChangeReceiver(),
149                     new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
150 
151             mDisplayService.registerDisplayListener(this, mHandler);
152 
153             AudioRoutesInfo newAudioRoutes = null;
154             try {
155                 newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
156             } catch (RemoteException e) {
157             }
158             if (newAudioRoutes != null) {
159                 // This will select the active BT route if there is one and the current
160                 // selected route is the default system route, or if there is no selected
161                 // route yet.
162                 updateAudioRoutes(newAudioRoutes);
163             }
164 
165             // Bind to the media router service.
166             rebindAsUser(UserHandle.myUserId());
167 
168             // Select the default route if the above didn't sync us up
169             // appropriately with relevant system state.
170             if (mSelectedRoute == null) {
171                 selectDefaultRouteStatic();
172             }
173         }
174 
updateAudioRoutes(AudioRoutesInfo newRoutes)175         void updateAudioRoutes(AudioRoutesInfo newRoutes) {
176             if (newRoutes.mainType != mCurAudioRoutesInfo.mainType) {
177                 mCurAudioRoutesInfo.mainType = newRoutes.mainType;
178                 int name;
179                 if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0
180                         || (newRoutes.mainType&AudioRoutesInfo.MAIN_HEADSET) != 0) {
181                     name = com.android.internal.R.string.default_audio_route_name_headphones;
182                 } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
183                     name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
184                 } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HDMI) != 0) {
185                     name = com.android.internal.R.string.default_media_route_name_hdmi;
186                 } else {
187                     name = com.android.internal.R.string.default_audio_route_name;
188                 }
189                 sStatic.mDefaultAudioVideo.mNameResId = name;
190                 dispatchRouteChanged(sStatic.mDefaultAudioVideo);
191             }
192 
193             final int mainType = mCurAudioRoutesInfo.mainType;
194 
195             if (!TextUtils.equals(newRoutes.bluetoothName, mCurAudioRoutesInfo.bluetoothName)) {
196                 mCurAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName;
197                 if (mCurAudioRoutesInfo.bluetoothName != null) {
198                     if (sStatic.mBluetoothA2dpRoute == null) {
199                         final RouteInfo info = new RouteInfo(sStatic.mSystemCategory);
200                         info.mName = mCurAudioRoutesInfo.bluetoothName;
201                         info.mDescription = sStatic.mResources.getText(
202                                 com.android.internal.R.string.bluetooth_a2dp_audio_route_name);
203                         info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
204                         sStatic.mBluetoothA2dpRoute = info;
205                         addRouteStatic(sStatic.mBluetoothA2dpRoute);
206                     } else {
207                         sStatic.mBluetoothA2dpRoute.mName = mCurAudioRoutesInfo.bluetoothName;
208                         dispatchRouteChanged(sStatic.mBluetoothA2dpRoute);
209                     }
210                 } else if (sStatic.mBluetoothA2dpRoute != null) {
211                     removeRouteStatic(sStatic.mBluetoothA2dpRoute);
212                     sStatic.mBluetoothA2dpRoute = null;
213                 }
214             }
215 
216             if (mBluetoothA2dpRoute != null) {
217                 final boolean a2dpEnabled = isBluetoothA2dpOn();
218                 if (mainType != AudioRoutesInfo.MAIN_SPEAKER &&
219                         mSelectedRoute == mBluetoothA2dpRoute && !a2dpEnabled) {
220                     selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false);
221                 } else if ((mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) &&
222                         a2dpEnabled) {
223                     selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false);
224                 }
225             }
226         }
227 
isBluetoothA2dpOn()228         boolean isBluetoothA2dpOn() {
229             try {
230                 return mAudioService.isBluetoothA2dpOn();
231             } catch (RemoteException e) {
232                 Log.e(TAG, "Error querying Bluetooth A2DP state", e);
233                 return false;
234             }
235         }
236 
updateDiscoveryRequest()237         void updateDiscoveryRequest() {
238             // What are we looking for today?
239             int routeTypes = 0;
240             int passiveRouteTypes = 0;
241             boolean activeScan = false;
242             boolean activeScanWifiDisplay = false;
243             final int count = mCallbacks.size();
244             for (int i = 0; i < count; i++) {
245                 CallbackInfo cbi = mCallbacks.get(i);
246                 if ((cbi.flags & (CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
247                         | CALLBACK_FLAG_REQUEST_DISCOVERY)) != 0) {
248                     // Discovery explicitly requested.
249                     routeTypes |= cbi.type;
250                 } else if ((cbi.flags & CALLBACK_FLAG_PASSIVE_DISCOVERY) != 0) {
251                     // Discovery only passively requested.
252                     passiveRouteTypes |= cbi.type;
253                 } else {
254                     // Legacy case since applications don't specify the discovery flag.
255                     // Unfortunately we just have to assume they always need discovery
256                     // whenever they have a callback registered.
257                     routeTypes |= cbi.type;
258                 }
259                 if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
260                     activeScan = true;
261                     if ((cbi.type & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
262                         activeScanWifiDisplay = true;
263                     }
264                 }
265             }
266             if (routeTypes != 0 || activeScan) {
267                 // If someone else requests discovery then enable the passive listeners.
268                 // This is used by the MediaRouteButton and MediaRouteActionProvider since
269                 // they don't receive lifecycle callbacks from the Activity.
270                 routeTypes |= passiveRouteTypes;
271             }
272 
273             // Update wifi display scanning.
274             // TODO: All of this should be managed by the media router service.
275             if (mCanConfigureWifiDisplays) {
276                 if (mSelectedRoute != null
277                         && mSelectedRoute.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) {
278                     // Don't scan while already connected to a remote display since
279                     // it may interfere with the ongoing transmission.
280                     activeScanWifiDisplay = false;
281                 }
282                 if (activeScanWifiDisplay) {
283                     if (!mActivelyScanningWifiDisplays) {
284                         mActivelyScanningWifiDisplays = true;
285                         mDisplayService.startWifiDisplayScan();
286                     }
287                 } else {
288                     if (mActivelyScanningWifiDisplays) {
289                         mActivelyScanningWifiDisplays = false;
290                         mDisplayService.stopWifiDisplayScan();
291                     }
292                 }
293             }
294 
295             // Tell the media router service all about it.
296             if (routeTypes != mDiscoveryRequestRouteTypes
297                     || activeScan != mDiscoverRequestActiveScan) {
298                 mDiscoveryRequestRouteTypes = routeTypes;
299                 mDiscoverRequestActiveScan = activeScan;
300                 publishClientDiscoveryRequest();
301             }
302         }
303 
304         @Override
onDisplayAdded(int displayId)305         public void onDisplayAdded(int displayId) {
306             updatePresentationDisplays(displayId);
307         }
308 
309         @Override
onDisplayChanged(int displayId)310         public void onDisplayChanged(int displayId) {
311             updatePresentationDisplays(displayId);
312         }
313 
314         @Override
onDisplayRemoved(int displayId)315         public void onDisplayRemoved(int displayId) {
316             updatePresentationDisplays(displayId);
317         }
318 
getAllPresentationDisplays()319         public Display[] getAllPresentationDisplays() {
320             return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
321         }
322 
updatePresentationDisplays(int changedDisplayId)323         private void updatePresentationDisplays(int changedDisplayId) {
324             final int count = mRoutes.size();
325             for (int i = 0; i < count; i++) {
326                 final RouteInfo route = mRoutes.get(i);
327                 if (route.updatePresentationDisplay() || (route.mPresentationDisplay != null
328                         && route.mPresentationDisplay.getDisplayId() == changedDisplayId)) {
329                     dispatchRoutePresentationDisplayChanged(route);
330                 }
331             }
332         }
333 
setSelectedRoute(RouteInfo info, boolean explicit)334         void setSelectedRoute(RouteInfo info, boolean explicit) {
335             // Must be non-reentrant.
336             mSelectedRoute = info;
337             publishClientSelectedRoute(explicit);
338         }
339 
rebindAsUser(int userId)340         void rebindAsUser(int userId) {
341             if (mCurrentUserId != userId || userId < 0 || mClient == null) {
342                 if (mClient != null) {
343                     try {
344                         mMediaRouterService.unregisterClient(mClient);
345                     } catch (RemoteException ex) {
346                         Log.e(TAG, "Unable to unregister media router client.", ex);
347                     }
348                     mClient = null;
349                 }
350 
351                 mCurrentUserId = userId;
352 
353                 try {
354                     Client client = new Client();
355                     mMediaRouterService.registerClientAsUser(client,
356                             mAppContext.getPackageName(), userId);
357                     mClient = client;
358                 } catch (RemoteException ex) {
359                     Log.e(TAG, "Unable to register media router client.", ex);
360                 }
361 
362                 publishClientDiscoveryRequest();
363                 publishClientSelectedRoute(false);
364                 updateClientState();
365             }
366         }
367 
publishClientDiscoveryRequest()368         void publishClientDiscoveryRequest() {
369             if (mClient != null) {
370                 try {
371                     mMediaRouterService.setDiscoveryRequest(mClient,
372                             mDiscoveryRequestRouteTypes, mDiscoverRequestActiveScan);
373                 } catch (RemoteException ex) {
374                     Log.e(TAG, "Unable to publish media router client discovery request.", ex);
375                 }
376             }
377         }
378 
publishClientSelectedRoute(boolean explicit)379         void publishClientSelectedRoute(boolean explicit) {
380             if (mClient != null) {
381                 try {
382                     mMediaRouterService.setSelectedRoute(mClient,
383                             mSelectedRoute != null ? mSelectedRoute.mGlobalRouteId : null,
384                             explicit);
385                 } catch (RemoteException ex) {
386                     Log.e(TAG, "Unable to publish media router client selected route.", ex);
387                 }
388             }
389         }
390 
updateClientState()391         void updateClientState() {
392             // Update the client state.
393             mClientState = null;
394             if (mClient != null) {
395                 try {
396                     mClientState = mMediaRouterService.getState(mClient);
397                 } catch (RemoteException ex) {
398                     Log.e(TAG, "Unable to retrieve media router client state.", ex);
399                 }
400             }
401             final ArrayList<MediaRouterClientState.RouteInfo> globalRoutes =
402                     mClientState != null ? mClientState.routes : null;
403             final String globallySelectedRouteId = mClientState != null ?
404                     mClientState.globallySelectedRouteId : null;
405 
406             // Add or update routes.
407             final int globalRouteCount = globalRoutes != null ? globalRoutes.size() : 0;
408             for (int i = 0; i < globalRouteCount; i++) {
409                 final MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(i);
410                 RouteInfo route = findGlobalRoute(globalRoute.id);
411                 if (route == null) {
412                     route = makeGlobalRoute(globalRoute);
413                     addRouteStatic(route);
414                 } else {
415                     updateGlobalRoute(route, globalRoute);
416                 }
417             }
418 
419             // Synchronize state with the globally selected route.
420             if (globallySelectedRouteId != null) {
421                 final RouteInfo route = findGlobalRoute(globallySelectedRouteId);
422                 if (route == null) {
423                     Log.w(TAG, "Could not find new globally selected route: "
424                             + globallySelectedRouteId);
425                 } else if (route != mSelectedRoute) {
426                     if (DEBUG) {
427                         Log.d(TAG, "Selecting new globally selected route: " + route);
428                     }
429                     selectRouteStatic(route.mSupportedTypes, route, false);
430                 }
431             } else if (mSelectedRoute != null && mSelectedRoute.mGlobalRouteId != null) {
432                 if (DEBUG) {
433                     Log.d(TAG, "Unselecting previous globally selected route: " + mSelectedRoute);
434                 }
435                 selectDefaultRouteStatic();
436             }
437 
438             // Remove defunct routes.
439             outer: for (int i = mRoutes.size(); i-- > 0; ) {
440                 final RouteInfo route = mRoutes.get(i);
441                 final String globalRouteId = route.mGlobalRouteId;
442                 if (globalRouteId != null) {
443                     for (int j = 0; j < globalRouteCount; j++) {
444                         MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(j);
445                         if (globalRouteId.equals(globalRoute.id)) {
446                             continue outer; // found
447                         }
448                     }
449                     // not found
450                     removeRouteStatic(route);
451                 }
452             }
453         }
454 
requestSetVolume(RouteInfo route, int volume)455         void requestSetVolume(RouteInfo route, int volume) {
456             if (route.mGlobalRouteId != null && mClient != null) {
457                 try {
458                     mMediaRouterService.requestSetVolume(mClient,
459                             route.mGlobalRouteId, volume);
460                 } catch (RemoteException ex) {
461                     Log.w(TAG, "Unable to request volume change.", ex);
462                 }
463             }
464         }
465 
requestUpdateVolume(RouteInfo route, int direction)466         void requestUpdateVolume(RouteInfo route, int direction) {
467             if (route.mGlobalRouteId != null && mClient != null) {
468                 try {
469                     mMediaRouterService.requestUpdateVolume(mClient,
470                             route.mGlobalRouteId, direction);
471                 } catch (RemoteException ex) {
472                     Log.w(TAG, "Unable to request volume change.", ex);
473                 }
474             }
475         }
476 
makeGlobalRoute(MediaRouterClientState.RouteInfo globalRoute)477         RouteInfo makeGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) {
478             RouteInfo route = new RouteInfo(sStatic.mSystemCategory);
479             route.mGlobalRouteId = globalRoute.id;
480             route.mName = globalRoute.name;
481             route.mDescription = globalRoute.description;
482             route.mSupportedTypes = globalRoute.supportedTypes;
483             route.mEnabled = globalRoute.enabled;
484             route.setRealStatusCode(globalRoute.statusCode);
485             route.mPlaybackType = globalRoute.playbackType;
486             route.mPlaybackStream = globalRoute.playbackStream;
487             route.mVolume = globalRoute.volume;
488             route.mVolumeMax = globalRoute.volumeMax;
489             route.mVolumeHandling = globalRoute.volumeHandling;
490             route.mPresentationDisplayId = globalRoute.presentationDisplayId;
491             route.updatePresentationDisplay();
492             return route;
493         }
494 
updateGlobalRoute(RouteInfo route, MediaRouterClientState.RouteInfo globalRoute)495         void updateGlobalRoute(RouteInfo route, MediaRouterClientState.RouteInfo globalRoute) {
496             boolean changed = false;
497             boolean volumeChanged = false;
498             boolean presentationDisplayChanged = false;
499 
500             if (!Objects.equals(route.mName, globalRoute.name)) {
501                 route.mName = globalRoute.name;
502                 changed = true;
503             }
504             if (!Objects.equals(route.mDescription, globalRoute.description)) {
505                 route.mDescription = globalRoute.description;
506                 changed = true;
507             }
508             final int oldSupportedTypes = route.mSupportedTypes;
509             if (oldSupportedTypes != globalRoute.supportedTypes) {
510                 route.mSupportedTypes = globalRoute.supportedTypes;
511                 changed = true;
512             }
513             if (route.mEnabled != globalRoute.enabled) {
514                 route.mEnabled = globalRoute.enabled;
515                 changed = true;
516             }
517             if (route.mRealStatusCode != globalRoute.statusCode) {
518                 route.setRealStatusCode(globalRoute.statusCode);
519                 changed = true;
520             }
521             if (route.mPlaybackType != globalRoute.playbackType) {
522                 route.mPlaybackType = globalRoute.playbackType;
523                 changed = true;
524             }
525             if (route.mPlaybackStream != globalRoute.playbackStream) {
526                 route.mPlaybackStream = globalRoute.playbackStream;
527                 changed = true;
528             }
529             if (route.mVolume != globalRoute.volume) {
530                 route.mVolume = globalRoute.volume;
531                 changed = true;
532                 volumeChanged = true;
533             }
534             if (route.mVolumeMax != globalRoute.volumeMax) {
535                 route.mVolumeMax = globalRoute.volumeMax;
536                 changed = true;
537                 volumeChanged = true;
538             }
539             if (route.mVolumeHandling != globalRoute.volumeHandling) {
540                 route.mVolumeHandling = globalRoute.volumeHandling;
541                 changed = true;
542                 volumeChanged = true;
543             }
544             if (route.mPresentationDisplayId != globalRoute.presentationDisplayId) {
545                 route.mPresentationDisplayId = globalRoute.presentationDisplayId;
546                 route.updatePresentationDisplay();
547                 changed = true;
548                 presentationDisplayChanged = true;
549             }
550 
551             if (changed) {
552                 dispatchRouteChanged(route, oldSupportedTypes);
553             }
554             if (volumeChanged) {
555                 dispatchRouteVolumeChanged(route);
556             }
557             if (presentationDisplayChanged) {
558                 dispatchRoutePresentationDisplayChanged(route);
559             }
560         }
561 
findGlobalRoute(String globalRouteId)562         RouteInfo findGlobalRoute(String globalRouteId) {
563             final int count = mRoutes.size();
564             for (int i = 0; i < count; i++) {
565                 final RouteInfo route = mRoutes.get(i);
566                 if (globalRouteId.equals(route.mGlobalRouteId)) {
567                     return route;
568                 }
569             }
570             return null;
571         }
572 
573         final class Client extends IMediaRouterClient.Stub {
574             @Override
onStateChanged()575             public void onStateChanged() {
576                 mHandler.post(new Runnable() {
577                     @Override
578                     public void run() {
579                         if (Client.this == mClient) {
580                             updateClientState();
581                         }
582                     }
583                 });
584             }
585         }
586     }
587 
588     static Static sStatic;
589 
590     /**
591      * Route type flag for live audio.
592      *
593      * <p>A device that supports live audio routing will allow the media audio stream
594      * to be routed to supported destinations. This can include internal speakers or
595      * audio jacks on the device itself, A2DP devices, and more.</p>
596      *
597      * <p>Once initiated this routing is transparent to the application. All audio
598      * played on the media stream will be routed to the selected destination.</p>
599      */
600     public static final int ROUTE_TYPE_LIVE_AUDIO = 1 << 0;
601 
602     /**
603      * Route type flag for live video.
604      *
605      * <p>A device that supports live video routing will allow a mirrored version
606      * of the device's primary display or a customized
607      * {@link android.app.Presentation Presentation} to be routed to supported destinations.</p>
608      *
609      * <p>Once initiated, display mirroring is transparent to the application.
610      * While remote routing is active the application may use a
611      * {@link android.app.Presentation Presentation} to replace the mirrored view
612      * on the external display with different content.</p>
613      *
614      * @see RouteInfo#getPresentationDisplay()
615      * @see android.app.Presentation
616      */
617     public static final int ROUTE_TYPE_LIVE_VIDEO = 1 << 1;
618 
619     /**
620      * Temporary interop constant to identify remote displays.
621      * @hide To be removed when media router API is updated.
622      */
623     public static final int ROUTE_TYPE_REMOTE_DISPLAY = 1 << 2;
624 
625     /**
626      * Route type flag for application-specific usage.
627      *
628      * <p>Unlike other media route types, user routes are managed by the application.
629      * The MediaRouter will manage and dispatch events for user routes, but the application
630      * is expected to interpret the meaning of these events and perform the requested
631      * routing tasks.</p>
632      */
633     public static final int ROUTE_TYPE_USER = 1 << 23;
634 
635     static final int ROUTE_TYPE_ANY = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO
636             | ROUTE_TYPE_REMOTE_DISPLAY | ROUTE_TYPE_USER;
637 
638     /**
639      * Flag for {@link #addCallback}: Actively scan for routes while this callback
640      * is registered.
641      * <p>
642      * When this flag is specified, the media router will actively scan for new
643      * routes.  Certain routes, such as wifi display routes, may not be discoverable
644      * except when actively scanning.  This flag is typically used when the route picker
645      * dialog has been opened by the user to ensure that the route information is
646      * up to date.
647      * </p><p>
648      * Active scanning may consume a significant amount of power and may have intrusive
649      * effects on wireless connectivity.  Therefore it is important that active scanning
650      * only be requested when it is actually needed to satisfy a user request to
651      * discover and select a new route.
652      * </p>
653      */
654     public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0;
655 
656     /**
657      * Flag for {@link #addCallback}: Do not filter route events.
658      * <p>
659      * When this flag is specified, the callback will be invoked for event that affect any
660      * route even if they do not match the callback's filter.
661      * </p>
662      */
663     public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
664 
665     /**
666      * Explicitly requests discovery.
667      *
668      * @hide Future API ported from support library.  Revisit this later.
669      */
670     public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2;
671 
672     /**
673      * Requests that discovery be performed but only if there is some other active
674      * callback already registered.
675      *
676      * @hide Compatibility workaround for the fact that applications do not currently
677      * request discovery explicitly (except when using the support library API).
678      */
679     public static final int CALLBACK_FLAG_PASSIVE_DISCOVERY = 1 << 3;
680 
681     /**
682      * Flag for {@link #isRouteAvailable}: Ignore the default route.
683      * <p>
684      * This flag is used to determine whether a matching non-default route is available.
685      * This constraint may be used to decide whether to offer the route chooser dialog
686      * to the user.  There is no point offering the chooser if there are no
687      * non-default choices.
688      * </p>
689      *
690      * @hide Future API ported from support library.  Revisit this later.
691      */
692     public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
693 
694     // Maps application contexts
695     static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
696 
typesToString(int types)697     static String typesToString(int types) {
698         final StringBuilder result = new StringBuilder();
699         if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
700             result.append("ROUTE_TYPE_LIVE_AUDIO ");
701         }
702         if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) {
703             result.append("ROUTE_TYPE_LIVE_VIDEO ");
704         }
705         if ((types & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
706             result.append("ROUTE_TYPE_REMOTE_DISPLAY ");
707         }
708         if ((types & ROUTE_TYPE_USER) != 0) {
709             result.append("ROUTE_TYPE_USER ");
710         }
711         return result.toString();
712     }
713 
714     /** @hide */
MediaRouter(Context context)715     public MediaRouter(Context context) {
716         synchronized (Static.class) {
717             if (sStatic == null) {
718                 final Context appContext = context.getApplicationContext();
719                 sStatic = new Static(appContext);
720                 sStatic.startMonitoringRoutes(appContext);
721             }
722         }
723     }
724 
725     /**
726      * Gets the default route for playing media content on the system.
727      * <p>
728      * The system always provides a default route.
729      * </p>
730      *
731      * @return The default route, which is guaranteed to never be null.
732      */
getDefaultRoute()733     public RouteInfo getDefaultRoute() {
734         return sStatic.mDefaultAudioVideo;
735     }
736 
737     /**
738      * @hide for use by framework routing UI
739      */
getSystemCategory()740     public RouteCategory getSystemCategory() {
741         return sStatic.mSystemCategory;
742     }
743 
744     /** @hide */
getSelectedRoute()745     public RouteInfo getSelectedRoute() {
746         return getSelectedRoute(ROUTE_TYPE_ANY);
747     }
748 
749     /**
750      * Return the currently selected route for any of the given types
751      *
752      * @param type route types
753      * @return the selected route
754      */
getSelectedRoute(int type)755     public RouteInfo getSelectedRoute(int type) {
756         if (sStatic.mSelectedRoute != null &&
757                 (sStatic.mSelectedRoute.mSupportedTypes & type) != 0) {
758             // If the selected route supports any of the types supplied, it's still considered
759             // 'selected' for that type.
760             return sStatic.mSelectedRoute;
761         } else if (type == ROUTE_TYPE_USER) {
762             // The caller specifically asked for a user route and the currently selected route
763             // doesn't qualify.
764             return null;
765         }
766         // If the above didn't match and we're not specifically asking for a user route,
767         // consider the default selected.
768         return sStatic.mDefaultAudioVideo;
769     }
770 
771     /**
772      * Returns true if there is a route that matches the specified types.
773      * <p>
774      * This method returns true if there are any available routes that match the types
775      * regardless of whether they are enabled or disabled.  If the
776      * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then
777      * the method will only consider non-default routes.
778      * </p>
779      *
780      * @param types The types to match.
781      * @param flags Flags to control the determination of whether a route may be available.
782      * May be zero or {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE}.
783      * @return True if a matching route may be available.
784      *
785      * @hide Future API ported from support library.  Revisit this later.
786      */
isRouteAvailable(int types, int flags)787     public boolean isRouteAvailable(int types, int flags) {
788         final int count = sStatic.mRoutes.size();
789         for (int i = 0; i < count; i++) {
790             RouteInfo route = sStatic.mRoutes.get(i);
791             if (route.matchesTypes(types)) {
792                 if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) == 0
793                         || route != sStatic.mDefaultAudioVideo) {
794                     return true;
795                 }
796             }
797         }
798 
799         // It doesn't look like we can find a matching route right now.
800         return false;
801     }
802 
803     /**
804      * Add a callback to listen to events about specific kinds of media routes.
805      * If the specified callback is already registered, its registration will be updated for any
806      * additional route types specified.
807      * <p>
808      * This is a convenience method that has the same effect as calling
809      * {@link #addCallback(int, Callback, int)} without flags.
810      * </p>
811      *
812      * @param types Types of routes this callback is interested in
813      * @param cb Callback to add
814      */
addCallback(int types, Callback cb)815     public void addCallback(int types, Callback cb) {
816         addCallback(types, cb, 0);
817     }
818 
819     /**
820      * Add a callback to listen to events about specific kinds of media routes.
821      * If the specified callback is already registered, its registration will be updated for any
822      * additional route types specified.
823      * <p>
824      * By default, the callback will only be invoked for events that affect routes
825      * that match the specified selector.  The filtering may be disabled by specifying
826      * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag.
827      * </p>
828      *
829      * @param types Types of routes this callback is interested in
830      * @param cb Callback to add
831      * @param flags Flags to control the behavior of the callback.
832      * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and
833      * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
834      */
addCallback(int types, Callback cb, int flags)835     public void addCallback(int types, Callback cb, int flags) {
836         CallbackInfo info;
837         int index = findCallbackInfo(cb);
838         if (index >= 0) {
839             info = sStatic.mCallbacks.get(index);
840             info.type |= types;
841             info.flags |= flags;
842         } else {
843             info = new CallbackInfo(cb, types, flags, this);
844             sStatic.mCallbacks.add(info);
845         }
846         sStatic.updateDiscoveryRequest();
847     }
848 
849     /**
850      * Remove the specified callback. It will no longer receive events about media routing.
851      *
852      * @param cb Callback to remove
853      */
removeCallback(Callback cb)854     public void removeCallback(Callback cb) {
855         int index = findCallbackInfo(cb);
856         if (index >= 0) {
857             sStatic.mCallbacks.remove(index);
858             sStatic.updateDiscoveryRequest();
859         } else {
860             Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
861         }
862     }
863 
findCallbackInfo(Callback cb)864     private int findCallbackInfo(Callback cb) {
865         final int count = sStatic.mCallbacks.size();
866         for (int i = 0; i < count; i++) {
867             final CallbackInfo info = sStatic.mCallbacks.get(i);
868             if (info.cb == cb) {
869                 return i;
870             }
871         }
872         return -1;
873     }
874 
875     /**
876      * Select the specified route to use for output of the given media types.
877      * <p class="note">
878      * As API version 18, this function may be used to select any route.
879      * In prior versions, this function could only be used to select user
880      * routes and would ignore any attempt to select a system route.
881      * </p>
882      *
883      * @param types type flags indicating which types this route should be used for.
884      *              The route must support at least a subset.
885      * @param route Route to select
886      * @throws IllegalArgumentException if the given route is {@code null}
887      */
selectRoute(int types, @NonNull RouteInfo route)888     public void selectRoute(int types, @NonNull RouteInfo route) {
889         if (route == null) {
890             throw new IllegalArgumentException("Route cannot be null.");
891         }
892         selectRouteStatic(types, route, true);
893     }
894 
895     /**
896      * @hide internal use
897      */
selectRouteInt(int types, RouteInfo route, boolean explicit)898     public void selectRouteInt(int types, RouteInfo route, boolean explicit) {
899         selectRouteStatic(types, route, explicit);
900     }
901 
selectRouteStatic(int types, @NonNull RouteInfo route, boolean explicit)902     static void selectRouteStatic(int types, @NonNull RouteInfo route, boolean explicit) {
903         assert(route != null);
904         final RouteInfo oldRoute = sStatic.mSelectedRoute;
905         if (oldRoute == route) return;
906         if (!route.matchesTypes(types)) {
907             Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
908                     typesToString(route.getSupportedTypes()) + " into route types " +
909                     typesToString(types));
910             return;
911         }
912 
913         final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute;
914         if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 &&
915                 (route == btRoute || route == sStatic.mDefaultAudioVideo)) {
916             try {
917                 sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute);
918             } catch (RemoteException e) {
919                 Log.e(TAG, "Error changing Bluetooth A2DP state", e);
920             }
921         }
922 
923         final WifiDisplay activeDisplay =
924                 sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay();
925         final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null;
926         final boolean newRouteHasAddress = route.mDeviceAddress != null;
927         if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) {
928             if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) {
929                 if (sStatic.mCanConfigureWifiDisplays) {
930                     sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress);
931                 } else {
932                     Log.e(TAG, "Cannot connect to wifi displays because this process "
933                             + "is not allowed to do so.");
934                 }
935             } else if (activeDisplay != null && !newRouteHasAddress) {
936                 sStatic.mDisplayService.disconnectWifiDisplay();
937             }
938         }
939 
940         sStatic.setSelectedRoute(route, explicit);
941 
942         if (oldRoute != null) {
943             dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute);
944             if (oldRoute.resolveStatusCode()) {
945                 dispatchRouteChanged(oldRoute);
946             }
947         }
948         if (route != null) {
949             if (route.resolveStatusCode()) {
950                 dispatchRouteChanged(route);
951             }
952             dispatchRouteSelected(types & route.getSupportedTypes(), route);
953         }
954 
955         // The behavior of active scans may depend on the currently selected route.
956         sStatic.updateDiscoveryRequest();
957     }
958 
selectDefaultRouteStatic()959     static void selectDefaultRouteStatic() {
960         // TODO: Be smarter about the route types here; this selects for all valid.
961         if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute
962                 && sStatic.mBluetoothA2dpRoute != null && sStatic.isBluetoothA2dpOn()) {
963             selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false);
964         } else {
965             selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false);
966         }
967     }
968 
969     /**
970      * Compare the device address of a display and a route.
971      * Nulls/no device address will match another null/no address.
972      */
matchesDeviceAddress(WifiDisplay display, RouteInfo info)973     static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) {
974         final boolean routeHasAddress = info != null && info.mDeviceAddress != null;
975         if (display == null && !routeHasAddress) {
976             return true;
977         }
978 
979         if (display != null && routeHasAddress) {
980             return display.getDeviceAddress().equals(info.mDeviceAddress);
981         }
982         return false;
983     }
984 
985     /**
986      * Add an app-specified route for media to the MediaRouter.
987      * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
988      *
989      * @param info Definition of the route to add
990      * @see #createUserRoute(RouteCategory)
991      * @see #removeUserRoute(UserRouteInfo)
992      */
addUserRoute(UserRouteInfo info)993     public void addUserRoute(UserRouteInfo info) {
994         addRouteStatic(info);
995     }
996 
997     /**
998      * @hide Framework use only
999      */
addRouteInt(RouteInfo info)1000     public void addRouteInt(RouteInfo info) {
1001         addRouteStatic(info);
1002     }
1003 
addRouteStatic(RouteInfo info)1004     static void addRouteStatic(RouteInfo info) {
1005         final RouteCategory cat = info.getCategory();
1006         if (!sStatic.mCategories.contains(cat)) {
1007             sStatic.mCategories.add(cat);
1008         }
1009         if (cat.isGroupable() && !(info instanceof RouteGroup)) {
1010             // Enforce that any added route in a groupable category must be in a group.
1011             final RouteGroup group = new RouteGroup(info.getCategory());
1012             group.mSupportedTypes = info.mSupportedTypes;
1013             sStatic.mRoutes.add(group);
1014             dispatchRouteAdded(group);
1015             group.addRoute(info);
1016 
1017             info = group;
1018         } else {
1019             sStatic.mRoutes.add(info);
1020             dispatchRouteAdded(info);
1021         }
1022     }
1023 
1024     /**
1025      * Remove an app-specified route for media from the MediaRouter.
1026      *
1027      * @param info Definition of the route to remove
1028      * @see #addUserRoute(UserRouteInfo)
1029      */
removeUserRoute(UserRouteInfo info)1030     public void removeUserRoute(UserRouteInfo info) {
1031         removeRouteStatic(info);
1032     }
1033 
1034     /**
1035      * Remove all app-specified routes from the MediaRouter.
1036      *
1037      * @see #removeUserRoute(UserRouteInfo)
1038      */
clearUserRoutes()1039     public void clearUserRoutes() {
1040         for (int i = 0; i < sStatic.mRoutes.size(); i++) {
1041             final RouteInfo info = sStatic.mRoutes.get(i);
1042             // TODO Right now, RouteGroups only ever contain user routes.
1043             // The code below will need to change if this assumption does.
1044             if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
1045                 removeRouteStatic(info);
1046                 i--;
1047             }
1048         }
1049     }
1050 
1051     /**
1052      * @hide internal use only
1053      */
removeRouteInt(RouteInfo info)1054     public void removeRouteInt(RouteInfo info) {
1055         removeRouteStatic(info);
1056     }
1057 
removeRouteStatic(RouteInfo info)1058     static void removeRouteStatic(RouteInfo info) {
1059         if (sStatic.mRoutes.remove(info)) {
1060             final RouteCategory removingCat = info.getCategory();
1061             final int count = sStatic.mRoutes.size();
1062             boolean found = false;
1063             for (int i = 0; i < count; i++) {
1064                 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
1065                 if (removingCat == cat) {
1066                     found = true;
1067                     break;
1068                 }
1069             }
1070             if (info.isSelected()) {
1071                 // Removing the currently selected route? Select the default before we remove it.
1072                 selectDefaultRouteStatic();
1073             }
1074             if (!found) {
1075                 sStatic.mCategories.remove(removingCat);
1076             }
1077             dispatchRouteRemoved(info);
1078         }
1079     }
1080 
1081     /**
1082      * Return the number of {@link MediaRouter.RouteCategory categories} currently
1083      * represented by routes known to this MediaRouter.
1084      *
1085      * @return the number of unique categories represented by this MediaRouter's known routes
1086      */
getCategoryCount()1087     public int getCategoryCount() {
1088         return sStatic.mCategories.size();
1089     }
1090 
1091     /**
1092      * Return the {@link MediaRouter.RouteCategory category} at the given index.
1093      * Valid indices are in the range [0-getCategoryCount).
1094      *
1095      * @param index which category to return
1096      * @return the category at index
1097      */
getCategoryAt(int index)1098     public RouteCategory getCategoryAt(int index) {
1099         return sStatic.mCategories.get(index);
1100     }
1101 
1102     /**
1103      * Return the number of {@link MediaRouter.RouteInfo routes} currently known
1104      * to this MediaRouter.
1105      *
1106      * @return the number of routes tracked by this router
1107      */
getRouteCount()1108     public int getRouteCount() {
1109         return sStatic.mRoutes.size();
1110     }
1111 
1112     /**
1113      * Return the route at the specified index.
1114      *
1115      * @param index index of the route to return
1116      * @return the route at index
1117      */
getRouteAt(int index)1118     public RouteInfo getRouteAt(int index) {
1119         return sStatic.mRoutes.get(index);
1120     }
1121 
getRouteCountStatic()1122     static int getRouteCountStatic() {
1123         return sStatic.mRoutes.size();
1124     }
1125 
getRouteAtStatic(int index)1126     static RouteInfo getRouteAtStatic(int index) {
1127         return sStatic.mRoutes.get(index);
1128     }
1129 
1130     /**
1131      * Create a new user route that may be modified and registered for use by the application.
1132      *
1133      * @param category The category the new route will belong to
1134      * @return A new UserRouteInfo for use by the application
1135      *
1136      * @see #addUserRoute(UserRouteInfo)
1137      * @see #removeUserRoute(UserRouteInfo)
1138      * @see #createRouteCategory(CharSequence, boolean)
1139      */
createUserRoute(RouteCategory category)1140     public UserRouteInfo createUserRoute(RouteCategory category) {
1141         return new UserRouteInfo(category);
1142     }
1143 
1144     /**
1145      * Create a new route category. Each route must belong to a category.
1146      *
1147      * @param name Name of the new category
1148      * @param isGroupable true if routes in this category may be grouped with one another
1149      * @return the new RouteCategory
1150      */
createRouteCategory(CharSequence name, boolean isGroupable)1151     public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
1152         return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
1153     }
1154 
1155     /**
1156      * Create a new route category. Each route must belong to a category.
1157      *
1158      * @param nameResId Resource ID of the name of the new category
1159      * @param isGroupable true if routes in this category may be grouped with one another
1160      * @return the new RouteCategory
1161      */
createRouteCategory(int nameResId, boolean isGroupable)1162     public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
1163         return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
1164     }
1165 
1166     /**
1167      * Rebinds the media router to handle routes that belong to the specified user.
1168      * Requires the interact across users permission to access the routes of another user.
1169      * <p>
1170      * This method is a complete hack to work around the singleton nature of the
1171      * media router when running inside of singleton processes like QuickSettings.
1172      * This mechanism should be burned to the ground when MediaRouter is redesigned.
1173      * Ideally the current user would be pulled from the Context but we need to break
1174      * down MediaRouter.Static before we can get there.
1175      * </p>
1176      *
1177      * @hide
1178      */
rebindAsUser(int userId)1179     public void rebindAsUser(int userId) {
1180         sStatic.rebindAsUser(userId);
1181     }
1182 
updateRoute(final RouteInfo info)1183     static void updateRoute(final RouteInfo info) {
1184         dispatchRouteChanged(info);
1185     }
1186 
dispatchRouteSelected(int type, RouteInfo info)1187     static void dispatchRouteSelected(int type, RouteInfo info) {
1188         for (CallbackInfo cbi : sStatic.mCallbacks) {
1189             if (cbi.filterRouteEvent(info)) {
1190                 cbi.cb.onRouteSelected(cbi.router, type, info);
1191             }
1192         }
1193     }
1194 
dispatchRouteUnselected(int type, RouteInfo info)1195     static void dispatchRouteUnselected(int type, RouteInfo info) {
1196         for (CallbackInfo cbi : sStatic.mCallbacks) {
1197             if (cbi.filterRouteEvent(info)) {
1198                 cbi.cb.onRouteUnselected(cbi.router, type, info);
1199             }
1200         }
1201     }
1202 
dispatchRouteChanged(RouteInfo info)1203     static void dispatchRouteChanged(RouteInfo info) {
1204         dispatchRouteChanged(info, info.mSupportedTypes);
1205     }
1206 
dispatchRouteChanged(RouteInfo info, int oldSupportedTypes)1207     static void dispatchRouteChanged(RouteInfo info, int oldSupportedTypes) {
1208         final int newSupportedTypes = info.mSupportedTypes;
1209         for (CallbackInfo cbi : sStatic.mCallbacks) {
1210             // Reconstruct some of the history for callbacks that may not have observed
1211             // all of the events needed to correctly interpret the current state.
1212             // FIXME: This is a strong signal that we should deprecate route type filtering
1213             // completely in the future because it can lead to inconsistencies in
1214             // applications.
1215             final boolean oldVisibility = cbi.filterRouteEvent(oldSupportedTypes);
1216             final boolean newVisibility = cbi.filterRouteEvent(newSupportedTypes);
1217             if (!oldVisibility && newVisibility) {
1218                 cbi.cb.onRouteAdded(cbi.router, info);
1219                 if (info.isSelected()) {
1220                     cbi.cb.onRouteSelected(cbi.router, newSupportedTypes, info);
1221                 }
1222             }
1223             if (oldVisibility || newVisibility) {
1224                 cbi.cb.onRouteChanged(cbi.router, info);
1225             }
1226             if (oldVisibility && !newVisibility) {
1227                 if (info.isSelected()) {
1228                     cbi.cb.onRouteUnselected(cbi.router, oldSupportedTypes, info);
1229                 }
1230                 cbi.cb.onRouteRemoved(cbi.router, info);
1231             }
1232         }
1233     }
1234 
dispatchRouteAdded(RouteInfo info)1235     static void dispatchRouteAdded(RouteInfo info) {
1236         for (CallbackInfo cbi : sStatic.mCallbacks) {
1237             if (cbi.filterRouteEvent(info)) {
1238                 cbi.cb.onRouteAdded(cbi.router, info);
1239             }
1240         }
1241     }
1242 
dispatchRouteRemoved(RouteInfo info)1243     static void dispatchRouteRemoved(RouteInfo info) {
1244         for (CallbackInfo cbi : sStatic.mCallbacks) {
1245             if (cbi.filterRouteEvent(info)) {
1246                 cbi.cb.onRouteRemoved(cbi.router, info);
1247             }
1248         }
1249     }
1250 
dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index)1251     static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
1252         for (CallbackInfo cbi : sStatic.mCallbacks) {
1253             if (cbi.filterRouteEvent(group)) {
1254                 cbi.cb.onRouteGrouped(cbi.router, info, group, index);
1255             }
1256         }
1257     }
1258 
dispatchRouteUngrouped(RouteInfo info, RouteGroup group)1259     static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
1260         for (CallbackInfo cbi : sStatic.mCallbacks) {
1261             if (cbi.filterRouteEvent(group)) {
1262                 cbi.cb.onRouteUngrouped(cbi.router, info, group);
1263             }
1264         }
1265     }
1266 
dispatchRouteVolumeChanged(RouteInfo info)1267     static void dispatchRouteVolumeChanged(RouteInfo info) {
1268         for (CallbackInfo cbi : sStatic.mCallbacks) {
1269             if (cbi.filterRouteEvent(info)) {
1270                 cbi.cb.onRouteVolumeChanged(cbi.router, info);
1271             }
1272         }
1273     }
1274 
dispatchRoutePresentationDisplayChanged(RouteInfo info)1275     static void dispatchRoutePresentationDisplayChanged(RouteInfo info) {
1276         for (CallbackInfo cbi : sStatic.mCallbacks) {
1277             if (cbi.filterRouteEvent(info)) {
1278                 cbi.cb.onRoutePresentationDisplayChanged(cbi.router, info);
1279             }
1280         }
1281     }
1282 
systemVolumeChanged(int newValue)1283     static void systemVolumeChanged(int newValue) {
1284         final RouteInfo selectedRoute = sStatic.mSelectedRoute;
1285         if (selectedRoute == null) return;
1286 
1287         if (selectedRoute == sStatic.mBluetoothA2dpRoute ||
1288                 selectedRoute == sStatic.mDefaultAudioVideo) {
1289             dispatchRouteVolumeChanged(selectedRoute);
1290         } else if (sStatic.mBluetoothA2dpRoute != null) {
1291             try {
1292                 dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ?
1293                         sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo);
1294             } catch (RemoteException e) {
1295                 Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e);
1296             }
1297         } else {
1298             dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo);
1299         }
1300     }
1301 
updateWifiDisplayStatus(WifiDisplayStatus status)1302     static void updateWifiDisplayStatus(WifiDisplayStatus status) {
1303         WifiDisplay[] displays;
1304         WifiDisplay activeDisplay;
1305         if (status.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) {
1306             displays = status.getDisplays();
1307             activeDisplay = status.getActiveDisplay();
1308 
1309             // Only the system is able to connect to wifi display routes.
1310             // The display manager will enforce this with a permission check but it
1311             // still publishes information about all available displays.
1312             // Filter the list down to just the active display.
1313             if (!sStatic.mCanConfigureWifiDisplays) {
1314                 if (activeDisplay != null) {
1315                     displays = new WifiDisplay[] { activeDisplay };
1316                 } else {
1317                     displays = WifiDisplay.EMPTY_ARRAY;
1318                 }
1319             }
1320         } else {
1321             displays = WifiDisplay.EMPTY_ARRAY;
1322             activeDisplay = null;
1323         }
1324         String activeDisplayAddress = activeDisplay != null ?
1325                 activeDisplay.getDeviceAddress() : null;
1326 
1327         // Add or update routes.
1328         for (int i = 0; i < displays.length; i++) {
1329             final WifiDisplay d = displays[i];
1330             if (shouldShowWifiDisplay(d, activeDisplay)) {
1331                 RouteInfo route = findWifiDisplayRoute(d);
1332                 if (route == null) {
1333                     route = makeWifiDisplayRoute(d, status);
1334                     addRouteStatic(route);
1335                 } else {
1336                     String address = d.getDeviceAddress();
1337                     boolean disconnected = !address.equals(activeDisplayAddress)
1338                             && address.equals(sStatic.mPreviousActiveWifiDisplayAddress);
1339                     updateWifiDisplayRoute(route, d, status, disconnected);
1340                 }
1341                 if (d.equals(activeDisplay)) {
1342                     selectRouteStatic(route.getSupportedTypes(), route, false);
1343                 }
1344             }
1345         }
1346 
1347         // Remove stale routes.
1348         for (int i = sStatic.mRoutes.size(); i-- > 0; ) {
1349             RouteInfo route = sStatic.mRoutes.get(i);
1350             if (route.mDeviceAddress != null) {
1351                 WifiDisplay d = findWifiDisplay(displays, route.mDeviceAddress);
1352                 if (d == null || !shouldShowWifiDisplay(d, activeDisplay)) {
1353                     removeRouteStatic(route);
1354                 }
1355             }
1356         }
1357 
1358         // Remember the current active wifi display address so that we can infer disconnections.
1359         // TODO: This hack will go away once all of this is moved into the media router service.
1360         sStatic.mPreviousActiveWifiDisplayAddress = activeDisplayAddress;
1361     }
1362 
shouldShowWifiDisplay(WifiDisplay d, WifiDisplay activeDisplay)1363     private static boolean shouldShowWifiDisplay(WifiDisplay d, WifiDisplay activeDisplay) {
1364         return d.isRemembered() || d.equals(activeDisplay);
1365     }
1366 
getWifiDisplayStatusCode(WifiDisplay d, WifiDisplayStatus wfdStatus)1367     static int getWifiDisplayStatusCode(WifiDisplay d, WifiDisplayStatus wfdStatus) {
1368         int newStatus;
1369         if (wfdStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING) {
1370             newStatus = RouteInfo.STATUS_SCANNING;
1371         } else if (d.isAvailable()) {
1372             newStatus = d.canConnect() ?
1373                     RouteInfo.STATUS_AVAILABLE: RouteInfo.STATUS_IN_USE;
1374         } else {
1375             newStatus = RouteInfo.STATUS_NOT_AVAILABLE;
1376         }
1377 
1378         if (d.equals(wfdStatus.getActiveDisplay())) {
1379             final int activeState = wfdStatus.getActiveDisplayState();
1380             switch (activeState) {
1381                 case WifiDisplayStatus.DISPLAY_STATE_CONNECTED:
1382                     newStatus = RouteInfo.STATUS_CONNECTED;
1383                     break;
1384                 case WifiDisplayStatus.DISPLAY_STATE_CONNECTING:
1385                     newStatus = RouteInfo.STATUS_CONNECTING;
1386                     break;
1387                 case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED:
1388                     Log.e(TAG, "Active display is not connected!");
1389                     break;
1390             }
1391         }
1392 
1393         return newStatus;
1394     }
1395 
isWifiDisplayEnabled(WifiDisplay d, WifiDisplayStatus wfdStatus)1396     static boolean isWifiDisplayEnabled(WifiDisplay d, WifiDisplayStatus wfdStatus) {
1397         return d.isAvailable() && (d.canConnect() || d.equals(wfdStatus.getActiveDisplay()));
1398     }
1399 
makeWifiDisplayRoute(WifiDisplay display, WifiDisplayStatus wfdStatus)1400     static RouteInfo makeWifiDisplayRoute(WifiDisplay display, WifiDisplayStatus wfdStatus) {
1401         final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory);
1402         newRoute.mDeviceAddress = display.getDeviceAddress();
1403         newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO
1404                 | ROUTE_TYPE_REMOTE_DISPLAY;
1405         newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
1406         newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE;
1407 
1408         newRoute.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus));
1409         newRoute.mEnabled = isWifiDisplayEnabled(display, wfdStatus);
1410         newRoute.mName = display.getFriendlyDisplayName();
1411         newRoute.mDescription = sStatic.mResources.getText(
1412                 com.android.internal.R.string.wireless_display_route_description);
1413         newRoute.updatePresentationDisplay();
1414         return newRoute;
1415     }
1416 
updateWifiDisplayRoute( RouteInfo route, WifiDisplay display, WifiDisplayStatus wfdStatus, boolean disconnected)1417     private static void updateWifiDisplayRoute(
1418             RouteInfo route, WifiDisplay display, WifiDisplayStatus wfdStatus,
1419             boolean disconnected) {
1420         boolean changed = false;
1421         final String newName = display.getFriendlyDisplayName();
1422         if (!route.getName().equals(newName)) {
1423             route.mName = newName;
1424             changed = true;
1425         }
1426 
1427         boolean enabled = isWifiDisplayEnabled(display, wfdStatus);
1428         changed |= route.mEnabled != enabled;
1429         route.mEnabled = enabled;
1430 
1431         changed |= route.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus));
1432 
1433         if (changed) {
1434             dispatchRouteChanged(route);
1435         }
1436 
1437         if ((!enabled || disconnected) && route.isSelected()) {
1438             // Oops, no longer available. Reselect the default.
1439             selectDefaultRouteStatic();
1440         }
1441     }
1442 
findWifiDisplay(WifiDisplay[] displays, String deviceAddress)1443     private static WifiDisplay findWifiDisplay(WifiDisplay[] displays, String deviceAddress) {
1444         for (int i = 0; i < displays.length; i++) {
1445             final WifiDisplay d = displays[i];
1446             if (d.getDeviceAddress().equals(deviceAddress)) {
1447                 return d;
1448             }
1449         }
1450         return null;
1451     }
1452 
findWifiDisplayRoute(WifiDisplay d)1453     private static RouteInfo findWifiDisplayRoute(WifiDisplay d) {
1454         final int count = sStatic.mRoutes.size();
1455         for (int i = 0; i < count; i++) {
1456             final RouteInfo info = sStatic.mRoutes.get(i);
1457             if (d.getDeviceAddress().equals(info.mDeviceAddress)) {
1458                 return info;
1459             }
1460         }
1461         return null;
1462     }
1463 
1464     /**
1465      * Information about a media route.
1466      */
1467     public static class RouteInfo {
1468         CharSequence mName;
1469         int mNameResId;
1470         CharSequence mDescription;
1471         private CharSequence mStatus;
1472         int mSupportedTypes;
1473         RouteGroup mGroup;
1474         final RouteCategory mCategory;
1475         Drawable mIcon;
1476         // playback information
1477         int mPlaybackType = PLAYBACK_TYPE_LOCAL;
1478         int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
1479         int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
1480         int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
1481         int mPlaybackStream = AudioManager.STREAM_MUSIC;
1482         VolumeCallbackInfo mVcb;
1483         Display mPresentationDisplay;
1484         int mPresentationDisplayId = -1;
1485 
1486         String mDeviceAddress;
1487         boolean mEnabled = true;
1488 
1489         // An id by which the route is known to the media router service.
1490         // Null if this route only exists as an artifact within this process.
1491         String mGlobalRouteId;
1492 
1493         // A predetermined connection status that can override mStatus
1494         private int mRealStatusCode;
1495         private int mResolvedStatusCode;
1496 
1497         /** @hide */ public static final int STATUS_NONE = 0;
1498         /** @hide */ public static final int STATUS_SCANNING = 1;
1499         /** @hide */ public static final int STATUS_CONNECTING = 2;
1500         /** @hide */ public static final int STATUS_AVAILABLE = 3;
1501         /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4;
1502         /** @hide */ public static final int STATUS_IN_USE = 5;
1503         /** @hide */ public static final int STATUS_CONNECTED = 6;
1504 
1505         private Object mTag;
1506 
1507         /**
1508          * The default playback type, "local", indicating the presentation of the media is happening
1509          * on the same device (e&#46;g&#46; a phone, a tablet) as where it is controlled from.
1510          * @see #getPlaybackType()
1511          */
1512         public final static int PLAYBACK_TYPE_LOCAL = 0;
1513         /**
1514          * A playback type indicating the presentation of the media is happening on
1515          * a different device (i&#46;e&#46; the remote device) than where it is controlled from.
1516          * @see #getPlaybackType()
1517          */
1518         public final static int PLAYBACK_TYPE_REMOTE = 1;
1519         /**
1520          * Playback information indicating the playback volume is fixed, i&#46;e&#46; it cannot be
1521          * controlled from this object. An example of fixed playback volume is a remote player,
1522          * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
1523          * than attenuate at the source.
1524          * @see #getVolumeHandling()
1525          */
1526         public final static int PLAYBACK_VOLUME_FIXED = 0;
1527         /**
1528          * Playback information indicating the playback volume is variable and can be controlled
1529          * from this object.
1530          * @see #getVolumeHandling()
1531          */
1532         public final static int PLAYBACK_VOLUME_VARIABLE = 1;
1533 
RouteInfo(RouteCategory category)1534         RouteInfo(RouteCategory category) {
1535             mCategory = category;
1536         }
1537 
1538         /**
1539          * Gets the user-visible name of the route.
1540          * <p>
1541          * The route name identifies the destination represented by the route.
1542          * It may be a user-supplied name, an alias, or device serial number.
1543          * </p>
1544          *
1545          * @return The user-visible name of a media route.  This is the string presented
1546          * to users who may select this as the active route.
1547          */
getName()1548         public CharSequence getName() {
1549             return getName(sStatic.mResources);
1550         }
1551 
1552         /**
1553          * Return the properly localized/resource user-visible name of this route.
1554          * <p>
1555          * The route name identifies the destination represented by the route.
1556          * It may be a user-supplied name, an alias, or device serial number.
1557          * </p>
1558          *
1559          * @param context Context used to resolve the correct configuration to load
1560          * @return The user-visible name of a media route.  This is the string presented
1561          * to users who may select this as the active route.
1562          */
getName(Context context)1563         public CharSequence getName(Context context) {
1564             return getName(context.getResources());
1565         }
1566 
getName(Resources res)1567         CharSequence getName(Resources res) {
1568             if (mNameResId != 0) {
1569                 return mName = res.getText(mNameResId);
1570             }
1571             return mName;
1572         }
1573 
1574         /**
1575          * Gets the user-visible description of the route.
1576          * <p>
1577          * The route description describes the kind of destination represented by the route.
1578          * It may be a user-supplied string, a model number or brand of device.
1579          * </p>
1580          *
1581          * @return The description of the route, or null if none.
1582          */
getDescription()1583         public CharSequence getDescription() {
1584             return mDescription;
1585         }
1586 
1587         /**
1588          * @return The user-visible status for a media route. This may include a description
1589          * of the currently playing media, if available.
1590          */
getStatus()1591         public CharSequence getStatus() {
1592             return mStatus;
1593         }
1594 
1595         /**
1596          * Set this route's status by predetermined status code. If the caller
1597          * should dispatch a route changed event this call will return true;
1598          */
setRealStatusCode(int statusCode)1599         boolean setRealStatusCode(int statusCode) {
1600             if (mRealStatusCode != statusCode) {
1601                 mRealStatusCode = statusCode;
1602                 return resolveStatusCode();
1603             }
1604             return false;
1605         }
1606 
1607         /**
1608          * Resolves the status code whenever the real status code or selection state
1609          * changes.
1610          */
resolveStatusCode()1611         boolean resolveStatusCode() {
1612             int statusCode = mRealStatusCode;
1613             if (isSelected()) {
1614                 switch (statusCode) {
1615                     // If the route is selected and its status appears to be between states
1616                     // then report it as connecting even though it has not yet had a chance
1617                     // to officially move into the CONNECTING state.  Note that routes in
1618                     // the NONE state are assumed to not require an explicit connection
1619                     // lifecycle whereas those that are AVAILABLE are assumed to have
1620                     // to eventually proceed to CONNECTED.
1621                     case STATUS_AVAILABLE:
1622                     case STATUS_SCANNING:
1623                         statusCode = STATUS_CONNECTING;
1624                         break;
1625                 }
1626             }
1627             if (mResolvedStatusCode == statusCode) {
1628                 return false;
1629             }
1630 
1631             mResolvedStatusCode = statusCode;
1632             int resId;
1633             switch (statusCode) {
1634                 case STATUS_SCANNING:
1635                     resId = com.android.internal.R.string.media_route_status_scanning;
1636                     break;
1637                 case STATUS_CONNECTING:
1638                     resId = com.android.internal.R.string.media_route_status_connecting;
1639                     break;
1640                 case STATUS_AVAILABLE:
1641                     resId = com.android.internal.R.string.media_route_status_available;
1642                     break;
1643                 case STATUS_NOT_AVAILABLE:
1644                     resId = com.android.internal.R.string.media_route_status_not_available;
1645                     break;
1646                 case STATUS_IN_USE:
1647                     resId = com.android.internal.R.string.media_route_status_in_use;
1648                     break;
1649                 case STATUS_CONNECTED:
1650                 case STATUS_NONE:
1651                 default:
1652                     resId = 0;
1653                     break;
1654             }
1655             mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null;
1656             return true;
1657         }
1658 
1659         /**
1660          * @hide
1661          */
getStatusCode()1662         public int getStatusCode() {
1663             return mResolvedStatusCode;
1664         }
1665 
1666         /**
1667          * @return A media type flag set describing which types this route supports.
1668          */
getSupportedTypes()1669         public int getSupportedTypes() {
1670             return mSupportedTypes;
1671         }
1672 
1673         /** @hide */
matchesTypes(int types)1674         public boolean matchesTypes(int types) {
1675             return (mSupportedTypes & types) != 0;
1676         }
1677 
1678         /**
1679          * @return The group that this route belongs to.
1680          */
getGroup()1681         public RouteGroup getGroup() {
1682             return mGroup;
1683         }
1684 
1685         /**
1686          * @return the category this route belongs to.
1687          */
getCategory()1688         public RouteCategory getCategory() {
1689             return mCategory;
1690         }
1691 
1692         /**
1693          * Get the icon representing this route.
1694          * This icon will be used in picker UIs if available.
1695          *
1696          * @return the icon representing this route or null if no icon is available
1697          */
getIconDrawable()1698         public Drawable getIconDrawable() {
1699             return mIcon;
1700         }
1701 
1702         /**
1703          * Set an application-specific tag object for this route.
1704          * The application may use this to store arbitrary data associated with the
1705          * route for internal tracking.
1706          *
1707          * <p>Note that the lifespan of a route may be well past the lifespan of
1708          * an Activity or other Context; take care that objects you store here
1709          * will not keep more data in memory alive than you intend.</p>
1710          *
1711          * @param tag Arbitrary, app-specific data for this route to hold for later use
1712          */
setTag(Object tag)1713         public void setTag(Object tag) {
1714             mTag = tag;
1715             routeUpdated();
1716         }
1717 
1718         /**
1719          * @return The tag object previously set by the application
1720          * @see #setTag(Object)
1721          */
getTag()1722         public Object getTag() {
1723             return mTag;
1724         }
1725 
1726         /**
1727          * @return the type of playback associated with this route
1728          * @see UserRouteInfo#setPlaybackType(int)
1729          */
getPlaybackType()1730         public int getPlaybackType() {
1731             return mPlaybackType;
1732         }
1733 
1734         /**
1735          * @return the stream over which the playback associated with this route is performed
1736          * @see UserRouteInfo#setPlaybackStream(int)
1737          */
getPlaybackStream()1738         public int getPlaybackStream() {
1739             return mPlaybackStream;
1740         }
1741 
1742         /**
1743          * Return the current volume for this route. Depending on the route, this may only
1744          * be valid if the route is currently selected.
1745          *
1746          * @return the volume at which the playback associated with this route is performed
1747          * @see UserRouteInfo#setVolume(int)
1748          */
getVolume()1749         public int getVolume() {
1750             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1751                 int vol = 0;
1752                 try {
1753                     vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
1754                 } catch (RemoteException e) {
1755                     Log.e(TAG, "Error getting local stream volume", e);
1756                 }
1757                 return vol;
1758             } else {
1759                 return mVolume;
1760             }
1761         }
1762 
1763         /**
1764          * Request a volume change for this route.
1765          * @param volume value between 0 and getVolumeMax
1766          */
requestSetVolume(int volume)1767         public void requestSetVolume(int volume) {
1768             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1769                 try {
1770                     sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0,
1771                             ActivityThread.currentPackageName());
1772                 } catch (RemoteException e) {
1773                     Log.e(TAG, "Error setting local stream volume", e);
1774                 }
1775             } else {
1776                 sStatic.requestSetVolume(this, volume);
1777             }
1778         }
1779 
1780         /**
1781          * Request an incremental volume update for this route.
1782          * @param direction Delta to apply to the current volume
1783          */
requestUpdateVolume(int direction)1784         public void requestUpdateVolume(int direction) {
1785             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1786                 try {
1787                     final int volume =
1788                             Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
1789                     sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0,
1790                             ActivityThread.currentPackageName());
1791                 } catch (RemoteException e) {
1792                     Log.e(TAG, "Error setting local stream volume", e);
1793                 }
1794             } else {
1795                 sStatic.requestUpdateVolume(this, direction);
1796             }
1797         }
1798 
1799         /**
1800          * @return the maximum volume at which the playback associated with this route is performed
1801          * @see UserRouteInfo#setVolumeMax(int)
1802          */
getVolumeMax()1803         public int getVolumeMax() {
1804             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1805                 int volMax = 0;
1806                 try {
1807                     volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
1808                 } catch (RemoteException e) {
1809                     Log.e(TAG, "Error getting local stream volume", e);
1810                 }
1811                 return volMax;
1812             } else {
1813                 return mVolumeMax;
1814             }
1815         }
1816 
1817         /**
1818          * @return how volume is handling on the route
1819          * @see UserRouteInfo#setVolumeHandling(int)
1820          */
getVolumeHandling()1821         public int getVolumeHandling() {
1822             return mVolumeHandling;
1823         }
1824 
1825         /**
1826          * Gets the {@link Display} that should be used by the application to show
1827          * a {@link android.app.Presentation} on an external display when this route is selected.
1828          * Depending on the route, this may only be valid if the route is currently
1829          * selected.
1830          * <p>
1831          * The preferred presentation display may change independently of the route
1832          * being selected or unselected.  For example, the presentation display
1833          * of the default system route may change when an external HDMI display is connected
1834          * or disconnected even though the route itself has not changed.
1835          * </p><p>
1836          * This method may return null if there is no external display associated with
1837          * the route or if the display is not ready to show UI yet.
1838          * </p><p>
1839          * The application should listen for changes to the presentation display
1840          * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
1841          * show or dismiss its {@link android.app.Presentation} accordingly when the display
1842          * becomes available or is removed.
1843          * </p><p>
1844          * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes.
1845          * </p>
1846          *
1847          * @return The preferred presentation display to use when this route is
1848          * selected or null if none.
1849          *
1850          * @see #ROUTE_TYPE_LIVE_VIDEO
1851          * @see android.app.Presentation
1852          */
getPresentationDisplay()1853         public Display getPresentationDisplay() {
1854             return mPresentationDisplay;
1855         }
1856 
updatePresentationDisplay()1857         boolean updatePresentationDisplay() {
1858             Display display = choosePresentationDisplay();
1859             if (mPresentationDisplay != display) {
1860                 mPresentationDisplay = display;
1861                 return true;
1862             }
1863             return false;
1864         }
1865 
choosePresentationDisplay()1866         private Display choosePresentationDisplay() {
1867             if ((mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) {
1868                 Display[] displays = sStatic.getAllPresentationDisplays();
1869 
1870                 // Ensure that the specified display is valid for presentations.
1871                 // This check will normally disallow the default display unless it was
1872                 // configured as a presentation display for some reason.
1873                 if (mPresentationDisplayId >= 0) {
1874                     for (Display display : displays) {
1875                         if (display.getDisplayId() == mPresentationDisplayId) {
1876                             return display;
1877                         }
1878                     }
1879                     return null;
1880                 }
1881 
1882                 // Find the indicated Wifi display by its address.
1883                 if (mDeviceAddress != null) {
1884                     for (Display display : displays) {
1885                         if (display.getType() == Display.TYPE_WIFI
1886                                 && mDeviceAddress.equals(display.getAddress())) {
1887                             return display;
1888                         }
1889                     }
1890                     return null;
1891                 }
1892 
1893                 // For the default route, choose the first presentation display from the list.
1894                 if (this == sStatic.mDefaultAudioVideo && displays.length > 0) {
1895                     return displays[0];
1896                 }
1897             }
1898             return null;
1899         }
1900 
1901         /** @hide */
getDeviceAddress()1902         public String getDeviceAddress() {
1903             return mDeviceAddress;
1904         }
1905 
1906         /**
1907          * Returns true if this route is enabled and may be selected.
1908          *
1909          * @return True if this route is enabled.
1910          */
isEnabled()1911         public boolean isEnabled() {
1912             return mEnabled;
1913         }
1914 
1915         /**
1916          * Returns true if the route is in the process of connecting and is not
1917          * yet ready for use.
1918          *
1919          * @return True if this route is in the process of connecting.
1920          */
isConnecting()1921         public boolean isConnecting() {
1922             return mResolvedStatusCode == STATUS_CONNECTING;
1923         }
1924 
1925         /** @hide */
isSelected()1926         public boolean isSelected() {
1927             return this == sStatic.mSelectedRoute;
1928         }
1929 
1930         /** @hide */
isDefault()1931         public boolean isDefault() {
1932             return this == sStatic.mDefaultAudioVideo;
1933         }
1934 
1935         /** @hide */
select()1936         public void select() {
1937             selectRouteStatic(mSupportedTypes, this, true);
1938         }
1939 
setStatusInt(CharSequence status)1940         void setStatusInt(CharSequence status) {
1941             if (!status.equals(mStatus)) {
1942                 mStatus = status;
1943                 if (mGroup != null) {
1944                     mGroup.memberStatusChanged(this, status);
1945                 }
1946                 routeUpdated();
1947             }
1948         }
1949 
1950         final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
1951             @Override
1952             public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
1953                 sStatic.mHandler.post(new Runnable() {
1954                     @Override
1955                     public void run() {
1956                         if (mVcb != null) {
1957                             if (direction != 0) {
1958                                 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
1959                             } else {
1960                                 mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
1961                             }
1962                         }
1963                     }
1964                 });
1965             }
1966         };
1967 
routeUpdated()1968         void routeUpdated() {
1969             updateRoute(this);
1970         }
1971 
1972         @Override
toString()1973         public String toString() {
1974             String supportedTypes = typesToString(getSupportedTypes());
1975             return getClass().getSimpleName() + "{ name=" + getName() +
1976                     ", description=" + getDescription() +
1977                     ", status=" + getStatus() +
1978                     ", category=" + getCategory() +
1979                     ", supportedTypes=" + supportedTypes +
1980                     ", presentationDisplay=" + mPresentationDisplay + " }";
1981         }
1982     }
1983 
1984     /**
1985      * Information about a route that the application may define and modify.
1986      * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
1987      * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
1988      *
1989      * @see MediaRouter.RouteInfo
1990      */
1991     public static class UserRouteInfo extends RouteInfo {
1992         RemoteControlClient mRcc;
1993         SessionVolumeProvider mSvp;
1994 
UserRouteInfo(RouteCategory category)1995         UserRouteInfo(RouteCategory category) {
1996             super(category);
1997             mSupportedTypes = ROUTE_TYPE_USER;
1998             mPlaybackType = PLAYBACK_TYPE_REMOTE;
1999             mVolumeHandling = PLAYBACK_VOLUME_FIXED;
2000         }
2001 
2002         /**
2003          * Set the user-visible name of this route.
2004          * @param name Name to display to the user to describe this route
2005          */
setName(CharSequence name)2006         public void setName(CharSequence name) {
2007             mName = name;
2008             routeUpdated();
2009         }
2010 
2011         /**
2012          * Set the user-visible name of this route.
2013          * <p>
2014          * The route name identifies the destination represented by the route.
2015          * It may be a user-supplied name, an alias, or device serial number.
2016          * </p>
2017          *
2018          * @param resId Resource ID of the name to display to the user to describe this route
2019          */
setName(int resId)2020         public void setName(int resId) {
2021             mNameResId = resId;
2022             mName = null;
2023             routeUpdated();
2024         }
2025 
2026         /**
2027          * Set the user-visible description of this route.
2028          * <p>
2029          * The route description describes the kind of destination represented by the route.
2030          * It may be a user-supplied string, a model number or brand of device.
2031          * </p>
2032          *
2033          * @param description The description of the route, or null if none.
2034          */
setDescription(CharSequence description)2035         public void setDescription(CharSequence description) {
2036             mDescription = description;
2037             routeUpdated();
2038         }
2039 
2040         /**
2041          * Set the current user-visible status for this route.
2042          * @param status Status to display to the user to describe what the endpoint
2043          * of this route is currently doing
2044          */
setStatus(CharSequence status)2045         public void setStatus(CharSequence status) {
2046             setStatusInt(status);
2047         }
2048 
2049         /**
2050          * Set the RemoteControlClient responsible for reporting playback info for this
2051          * user route.
2052          *
2053          * <p>If this route manages remote playback, the data exposed by this
2054          * RemoteControlClient will be used to reflect and update information
2055          * such as route volume info in related UIs.</p>
2056          *
2057          * <p>The RemoteControlClient must have been previously registered with
2058          * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
2059          *
2060          * @param rcc RemoteControlClient associated with this route
2061          */
setRemoteControlClient(RemoteControlClient rcc)2062         public void setRemoteControlClient(RemoteControlClient rcc) {
2063             mRcc = rcc;
2064             updatePlaybackInfoOnRcc();
2065         }
2066 
2067         /**
2068          * Retrieve the RemoteControlClient associated with this route, if one has been set.
2069          *
2070          * @return the RemoteControlClient associated with this route
2071          * @see #setRemoteControlClient(RemoteControlClient)
2072          */
getRemoteControlClient()2073         public RemoteControlClient getRemoteControlClient() {
2074             return mRcc;
2075         }
2076 
2077         /**
2078          * Set an icon that will be used to represent this route.
2079          * The system may use this icon in picker UIs or similar.
2080          *
2081          * @param icon icon drawable to use to represent this route
2082          */
setIconDrawable(Drawable icon)2083         public void setIconDrawable(Drawable icon) {
2084             mIcon = icon;
2085         }
2086 
2087         /**
2088          * Set an icon that will be used to represent this route.
2089          * The system may use this icon in picker UIs or similar.
2090          *
2091          * @param resId Resource ID of an icon drawable to use to represent this route
2092          */
setIconResource(@rawableRes int resId)2093         public void setIconResource(@DrawableRes int resId) {
2094             setIconDrawable(sStatic.mResources.getDrawable(resId));
2095         }
2096 
2097         /**
2098          * Set a callback to be notified of volume update requests
2099          * @param vcb
2100          */
setVolumeCallback(VolumeCallback vcb)2101         public void setVolumeCallback(VolumeCallback vcb) {
2102             mVcb = new VolumeCallbackInfo(vcb, this);
2103         }
2104 
2105         /**
2106          * Defines whether playback associated with this route is "local"
2107          *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
2108          *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
2109          * @param type
2110          */
setPlaybackType(int type)2111         public void setPlaybackType(int type) {
2112             if (mPlaybackType != type) {
2113                 mPlaybackType = type;
2114                 configureSessionVolume();
2115             }
2116         }
2117 
2118         /**
2119          * Defines whether volume for the playback associated with this route is fixed
2120          * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
2121          * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
2122          * @param volumeHandling
2123          */
setVolumeHandling(int volumeHandling)2124         public void setVolumeHandling(int volumeHandling) {
2125             if (mVolumeHandling != volumeHandling) {
2126                 mVolumeHandling = volumeHandling;
2127                 configureSessionVolume();
2128             }
2129         }
2130 
2131         /**
2132          * Defines at what volume the playback associated with this route is performed (for user
2133          * feedback purposes). This information is only used when the playback is not local.
2134          * @param volume
2135          */
setVolume(int volume)2136         public void setVolume(int volume) {
2137             volume = Math.max(0, Math.min(volume, getVolumeMax()));
2138             if (mVolume != volume) {
2139                 mVolume = volume;
2140                 if (mSvp != null) {
2141                     mSvp.setCurrentVolume(mVolume);
2142                 }
2143                 dispatchRouteVolumeChanged(this);
2144                 if (mGroup != null) {
2145                     mGroup.memberVolumeChanged(this);
2146                 }
2147             }
2148         }
2149 
2150         @Override
requestSetVolume(int volume)2151         public void requestSetVolume(int volume) {
2152             if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
2153                 if (mVcb == null) {
2154                     Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
2155                     return;
2156                 }
2157                 mVcb.vcb.onVolumeSetRequest(this, volume);
2158             }
2159         }
2160 
2161         @Override
requestUpdateVolume(int direction)2162         public void requestUpdateVolume(int direction) {
2163             if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
2164                 if (mVcb == null) {
2165                     Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
2166                     return;
2167                 }
2168                 mVcb.vcb.onVolumeUpdateRequest(this, direction);
2169             }
2170         }
2171 
2172         /**
2173          * Defines the maximum volume at which the playback associated with this route is performed
2174          * (for user feedback purposes). This information is only used when the playback is not
2175          * local.
2176          * @param volumeMax
2177          */
setVolumeMax(int volumeMax)2178         public void setVolumeMax(int volumeMax) {
2179             if (mVolumeMax != volumeMax) {
2180                 mVolumeMax = volumeMax;
2181                 configureSessionVolume();
2182             }
2183         }
2184 
2185         /**
2186          * Defines over what stream type the media is presented.
2187          * @param stream
2188          */
setPlaybackStream(int stream)2189         public void setPlaybackStream(int stream) {
2190             if (mPlaybackStream != stream) {
2191                 mPlaybackStream = stream;
2192                 configureSessionVolume();
2193             }
2194         }
2195 
updatePlaybackInfoOnRcc()2196         private void updatePlaybackInfoOnRcc() {
2197             configureSessionVolume();
2198         }
2199 
configureSessionVolume()2200         private void configureSessionVolume() {
2201             if (mRcc == null) {
2202                 if (DEBUG) {
2203                     Log.d(TAG, "No Rcc to configure volume for route " + mName);
2204                 }
2205                 return;
2206             }
2207             MediaSession session = mRcc.getMediaSession();
2208             if (session == null) {
2209                 if (DEBUG) {
2210                     Log.d(TAG, "Rcc has no session to configure volume");
2211                 }
2212                 return;
2213             }
2214             if (mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) {
2215                 int volumeControl = VolumeProvider.VOLUME_CONTROL_FIXED;
2216                 switch (mVolumeHandling) {
2217                     case RemoteControlClient.PLAYBACK_VOLUME_VARIABLE:
2218                         volumeControl = VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
2219                         break;
2220                     case RemoteControlClient.PLAYBACK_VOLUME_FIXED:
2221                     default:
2222                         break;
2223                 }
2224                 // Only register a new listener if necessary
2225                 if (mSvp == null || mSvp.getVolumeControl() != volumeControl
2226                         || mSvp.getMaxVolume() != mVolumeMax) {
2227                     mSvp = new SessionVolumeProvider(volumeControl, mVolumeMax, mVolume);
2228                     session.setPlaybackToRemote(mSvp);
2229                 }
2230             } else {
2231                 // We only know how to handle local and remote, fall back to local if not remote.
2232                 AudioAttributes.Builder bob = new AudioAttributes.Builder();
2233                 bob.setLegacyStreamType(mPlaybackStream);
2234                 session.setPlaybackToLocal(bob.build());
2235                 mSvp = null;
2236             }
2237         }
2238 
2239         class SessionVolumeProvider extends VolumeProvider {
2240 
SessionVolumeProvider(int volumeControl, int maxVolume, int currentVolume)2241             public SessionVolumeProvider(int volumeControl, int maxVolume, int currentVolume) {
2242                 super(volumeControl, maxVolume, currentVolume);
2243             }
2244 
2245             @Override
onSetVolumeTo(final int volume)2246             public void onSetVolumeTo(final int volume) {
2247                 sStatic.mHandler.post(new Runnable() {
2248                     @Override
2249                     public void run() {
2250                         if (mVcb != null) {
2251                             mVcb.vcb.onVolumeSetRequest(mVcb.route, volume);
2252                         }
2253                     }
2254                 });
2255             }
2256 
2257             @Override
onAdjustVolume(final int direction)2258             public void onAdjustVolume(final int direction) {
2259                 sStatic.mHandler.post(new Runnable() {
2260                     @Override
2261                     public void run() {
2262                         if (mVcb != null) {
2263                             mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
2264                         }
2265                     }
2266                 });
2267             }
2268         }
2269     }
2270 
2271     /**
2272      * Information about a route that consists of multiple other routes in a group.
2273      */
2274     public static class RouteGroup extends RouteInfo {
2275         final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
2276         private boolean mUpdateName;
2277 
RouteGroup(RouteCategory category)2278         RouteGroup(RouteCategory category) {
2279             super(category);
2280             mGroup = this;
2281             mVolumeHandling = PLAYBACK_VOLUME_FIXED;
2282         }
2283 
2284         @Override
getName(Resources res)2285         CharSequence getName(Resources res) {
2286             if (mUpdateName) updateName();
2287             return super.getName(res);
2288         }
2289 
2290         /**
2291          * Add a route to this group. The route must not currently belong to another group.
2292          *
2293          * @param route route to add to this group
2294          */
addRoute(RouteInfo route)2295         public void addRoute(RouteInfo route) {
2296             if (route.getGroup() != null) {
2297                 throw new IllegalStateException("Route " + route + " is already part of a group.");
2298             }
2299             if (route.getCategory() != mCategory) {
2300                 throw new IllegalArgumentException(
2301                         "Route cannot be added to a group with a different category. " +
2302                             "(Route category=" + route.getCategory() +
2303                             " group category=" + mCategory + ")");
2304             }
2305             final int at = mRoutes.size();
2306             mRoutes.add(route);
2307             route.mGroup = this;
2308             mUpdateName = true;
2309             updateVolume();
2310             routeUpdated();
2311             dispatchRouteGrouped(route, this, at);
2312         }
2313 
2314         /**
2315          * Add a route to this group before the specified index.
2316          *
2317          * @param route route to add
2318          * @param insertAt insert the new route before this index
2319          */
addRoute(RouteInfo route, int insertAt)2320         public void addRoute(RouteInfo route, int insertAt) {
2321             if (route.getGroup() != null) {
2322                 throw new IllegalStateException("Route " + route + " is already part of a group.");
2323             }
2324             if (route.getCategory() != mCategory) {
2325                 throw new IllegalArgumentException(
2326                         "Route cannot be added to a group with a different category. " +
2327                             "(Route category=" + route.getCategory() +
2328                             " group category=" + mCategory + ")");
2329             }
2330             mRoutes.add(insertAt, route);
2331             route.mGroup = this;
2332             mUpdateName = true;
2333             updateVolume();
2334             routeUpdated();
2335             dispatchRouteGrouped(route, this, insertAt);
2336         }
2337 
2338         /**
2339          * Remove a route from this group.
2340          *
2341          * @param route route to remove
2342          */
removeRoute(RouteInfo route)2343         public void removeRoute(RouteInfo route) {
2344             if (route.getGroup() != this) {
2345                 throw new IllegalArgumentException("Route " + route +
2346                         " is not a member of this group.");
2347             }
2348             mRoutes.remove(route);
2349             route.mGroup = null;
2350             mUpdateName = true;
2351             updateVolume();
2352             dispatchRouteUngrouped(route, this);
2353             routeUpdated();
2354         }
2355 
2356         /**
2357          * Remove the route at the specified index from this group.
2358          *
2359          * @param index index of the route to remove
2360          */
removeRoute(int index)2361         public void removeRoute(int index) {
2362             RouteInfo route = mRoutes.remove(index);
2363             route.mGroup = null;
2364             mUpdateName = true;
2365             updateVolume();
2366             dispatchRouteUngrouped(route, this);
2367             routeUpdated();
2368         }
2369 
2370         /**
2371          * @return The number of routes in this group
2372          */
getRouteCount()2373         public int getRouteCount() {
2374             return mRoutes.size();
2375         }
2376 
2377         /**
2378          * Return the route in this group at the specified index
2379          *
2380          * @param index Index to fetch
2381          * @return The route at index
2382          */
getRouteAt(int index)2383         public RouteInfo getRouteAt(int index) {
2384             return mRoutes.get(index);
2385         }
2386 
2387         /**
2388          * Set an icon that will be used to represent this group.
2389          * The system may use this icon in picker UIs or similar.
2390          *
2391          * @param icon icon drawable to use to represent this group
2392          */
setIconDrawable(Drawable icon)2393         public void setIconDrawable(Drawable icon) {
2394             mIcon = icon;
2395         }
2396 
2397         /**
2398          * Set an icon that will be used to represent this group.
2399          * The system may use this icon in picker UIs or similar.
2400          *
2401          * @param resId Resource ID of an icon drawable to use to represent this group
2402          */
setIconResource(@rawableRes int resId)2403         public void setIconResource(@DrawableRes int resId) {
2404             setIconDrawable(sStatic.mResources.getDrawable(resId));
2405         }
2406 
2407         @Override
requestSetVolume(int volume)2408         public void requestSetVolume(int volume) {
2409             final int maxVol = getVolumeMax();
2410             if (maxVol == 0) {
2411                 return;
2412             }
2413 
2414             final float scaledVolume = (float) volume / maxVol;
2415             final int routeCount = getRouteCount();
2416             for (int i = 0; i < routeCount; i++) {
2417                 final RouteInfo route = getRouteAt(i);
2418                 final int routeVol = (int) (scaledVolume * route.getVolumeMax());
2419                 route.requestSetVolume(routeVol);
2420             }
2421             if (volume != mVolume) {
2422                 mVolume = volume;
2423                 dispatchRouteVolumeChanged(this);
2424             }
2425         }
2426 
2427         @Override
requestUpdateVolume(int direction)2428         public void requestUpdateVolume(int direction) {
2429             final int maxVol = getVolumeMax();
2430             if (maxVol == 0) {
2431                 return;
2432             }
2433 
2434             final int routeCount = getRouteCount();
2435             int volume = 0;
2436             for (int i = 0; i < routeCount; i++) {
2437                 final RouteInfo route = getRouteAt(i);
2438                 route.requestUpdateVolume(direction);
2439                 final int routeVol = route.getVolume();
2440                 if (routeVol > volume) {
2441                     volume = routeVol;
2442                 }
2443             }
2444             if (volume != mVolume) {
2445                 mVolume = volume;
2446                 dispatchRouteVolumeChanged(this);
2447             }
2448         }
2449 
memberNameChanged(RouteInfo info, CharSequence name)2450         void memberNameChanged(RouteInfo info, CharSequence name) {
2451             mUpdateName = true;
2452             routeUpdated();
2453         }
2454 
memberStatusChanged(RouteInfo info, CharSequence status)2455         void memberStatusChanged(RouteInfo info, CharSequence status) {
2456             setStatusInt(status);
2457         }
2458 
memberVolumeChanged(RouteInfo info)2459         void memberVolumeChanged(RouteInfo info) {
2460             updateVolume();
2461         }
2462 
updateVolume()2463         void updateVolume() {
2464             // A group always represents the highest component volume value.
2465             final int routeCount = getRouteCount();
2466             int volume = 0;
2467             for (int i = 0; i < routeCount; i++) {
2468                 final int routeVol = getRouteAt(i).getVolume();
2469                 if (routeVol > volume) {
2470                     volume = routeVol;
2471                 }
2472             }
2473             if (volume != mVolume) {
2474                 mVolume = volume;
2475                 dispatchRouteVolumeChanged(this);
2476             }
2477         }
2478 
2479         @Override
routeUpdated()2480         void routeUpdated() {
2481             int types = 0;
2482             final int count = mRoutes.size();
2483             if (count == 0) {
2484                 // Don't keep empty groups in the router.
2485                 MediaRouter.removeRouteStatic(this);
2486                 return;
2487             }
2488 
2489             int maxVolume = 0;
2490             boolean isLocal = true;
2491             boolean isFixedVolume = true;
2492             for (int i = 0; i < count; i++) {
2493                 final RouteInfo route = mRoutes.get(i);
2494                 types |= route.mSupportedTypes;
2495                 final int routeMaxVolume = route.getVolumeMax();
2496                 if (routeMaxVolume > maxVolume) {
2497                     maxVolume = routeMaxVolume;
2498                 }
2499                 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
2500                 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
2501             }
2502             mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
2503             mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
2504             mSupportedTypes = types;
2505             mVolumeMax = maxVolume;
2506             mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
2507             super.routeUpdated();
2508         }
2509 
updateName()2510         void updateName() {
2511             final StringBuilder sb = new StringBuilder();
2512             final int count = mRoutes.size();
2513             for (int i = 0; i < count; i++) {
2514                 final RouteInfo info = mRoutes.get(i);
2515                 // TODO: There's probably a much more correct way to localize this.
2516                 if (i > 0) sb.append(", ");
2517                 sb.append(info.mName);
2518             }
2519             mName = sb.toString();
2520             mUpdateName = false;
2521         }
2522 
2523         @Override
toString()2524         public String toString() {
2525             StringBuilder sb = new StringBuilder(super.toString());
2526             sb.append('[');
2527             final int count = mRoutes.size();
2528             for (int i = 0; i < count; i++) {
2529                 if (i > 0) sb.append(", ");
2530                 sb.append(mRoutes.get(i));
2531             }
2532             sb.append(']');
2533             return sb.toString();
2534         }
2535     }
2536 
2537     /**
2538      * Definition of a category of routes. All routes belong to a category.
2539      */
2540     public static class RouteCategory {
2541         CharSequence mName;
2542         int mNameResId;
2543         int mTypes;
2544         final boolean mGroupable;
2545         boolean mIsSystem;
2546 
RouteCategory(CharSequence name, int types, boolean groupable)2547         RouteCategory(CharSequence name, int types, boolean groupable) {
2548             mName = name;
2549             mTypes = types;
2550             mGroupable = groupable;
2551         }
2552 
RouteCategory(int nameResId, int types, boolean groupable)2553         RouteCategory(int nameResId, int types, boolean groupable) {
2554             mNameResId = nameResId;
2555             mTypes = types;
2556             mGroupable = groupable;
2557         }
2558 
2559         /**
2560          * @return the name of this route category
2561          */
getName()2562         public CharSequence getName() {
2563             return getName(sStatic.mResources);
2564         }
2565 
2566         /**
2567          * Return the properly localized/configuration dependent name of this RouteCategory.
2568          *
2569          * @param context Context to resolve name resources
2570          * @return the name of this route category
2571          */
getName(Context context)2572         public CharSequence getName(Context context) {
2573             return getName(context.getResources());
2574         }
2575 
getName(Resources res)2576         CharSequence getName(Resources res) {
2577             if (mNameResId != 0) {
2578                 return res.getText(mNameResId);
2579             }
2580             return mName;
2581         }
2582 
2583         /**
2584          * Return the current list of routes in this category that have been added
2585          * to the MediaRouter.
2586          *
2587          * <p>This list will not include routes that are nested within RouteGroups.
2588          * A RouteGroup is treated as a single route within its category.</p>
2589          *
2590          * @param out a List to fill with the routes in this category. If this parameter is
2591          *            non-null, it will be cleared, filled with the current routes with this
2592          *            category, and returned. If this parameter is null, a new List will be
2593          *            allocated to report the category's current routes.
2594          * @return A list with the routes in this category that have been added to the MediaRouter.
2595          */
getRoutes(List<RouteInfo> out)2596         public List<RouteInfo> getRoutes(List<RouteInfo> out) {
2597             if (out == null) {
2598                 out = new ArrayList<RouteInfo>();
2599             } else {
2600                 out.clear();
2601             }
2602 
2603             final int count = getRouteCountStatic();
2604             for (int i = 0; i < count; i++) {
2605                 final RouteInfo route = getRouteAtStatic(i);
2606                 if (route.mCategory == this) {
2607                     out.add(route);
2608                 }
2609             }
2610             return out;
2611         }
2612 
2613         /**
2614          * @return Flag set describing the route types supported by this category
2615          */
getSupportedTypes()2616         public int getSupportedTypes() {
2617             return mTypes;
2618         }
2619 
2620         /**
2621          * Return whether or not this category supports grouping.
2622          *
2623          * <p>If this method returns true, all routes obtained from this category
2624          * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
2625          *
2626          * @return true if this category supports
2627          */
isGroupable()2628         public boolean isGroupable() {
2629             return mGroupable;
2630         }
2631 
2632         /**
2633          * @return true if this is the category reserved for system routes.
2634          * @hide
2635          */
isSystem()2636         public boolean isSystem() {
2637             return mIsSystem;
2638         }
2639 
2640         @Override
toString()2641         public String toString() {
2642             return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
2643                     " groupable=" + mGroupable + " }";
2644         }
2645     }
2646 
2647     static class CallbackInfo {
2648         public int type;
2649         public int flags;
2650         public final Callback cb;
2651         public final MediaRouter router;
2652 
CallbackInfo(Callback cb, int type, int flags, MediaRouter router)2653         public CallbackInfo(Callback cb, int type, int flags, MediaRouter router) {
2654             this.cb = cb;
2655             this.type = type;
2656             this.flags = flags;
2657             this.router = router;
2658         }
2659 
filterRouteEvent(RouteInfo route)2660         public boolean filterRouteEvent(RouteInfo route) {
2661             return filterRouteEvent(route.mSupportedTypes);
2662         }
2663 
filterRouteEvent(int supportedTypes)2664         public boolean filterRouteEvent(int supportedTypes) {
2665             return (flags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
2666                     || (type & supportedTypes) != 0;
2667         }
2668     }
2669 
2670     /**
2671      * Interface for receiving events about media routing changes.
2672      * All methods of this interface will be called from the application's main thread.
2673      * <p>
2674      * A Callback will only receive events relevant to routes that the callback
2675      * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
2676      * flag was specified in {@link MediaRouter#addCallback(int, Callback, int)}.
2677      * </p>
2678      *
2679      * @see MediaRouter#addCallback(int, Callback, int)
2680      * @see MediaRouter#removeCallback(Callback)
2681      */
2682     public static abstract class Callback {
2683         /**
2684          * Called when the supplied route becomes selected as the active route
2685          * for the given route type.
2686          *
2687          * @param router the MediaRouter reporting the event
2688          * @param type Type flag set indicating the routes that have been selected
2689          * @param info Route that has been selected for the given route types
2690          */
onRouteSelected(MediaRouter router, int type, RouteInfo info)2691         public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
2692 
2693         /**
2694          * Called when the supplied route becomes unselected as the active route
2695          * for the given route type.
2696          *
2697          * @param router the MediaRouter reporting the event
2698          * @param type Type flag set indicating the routes that have been unselected
2699          * @param info Route that has been unselected for the given route types
2700          */
onRouteUnselected(MediaRouter router, int type, RouteInfo info)2701         public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
2702 
2703         /**
2704          * Called when a route for the specified type was added.
2705          *
2706          * @param router the MediaRouter reporting the event
2707          * @param info Route that has become available for use
2708          */
onRouteAdded(MediaRouter router, RouteInfo info)2709         public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
2710 
2711         /**
2712          * Called when a route for the specified type was removed.
2713          *
2714          * @param router the MediaRouter reporting the event
2715          * @param info Route that has been removed from availability
2716          */
onRouteRemoved(MediaRouter router, RouteInfo info)2717         public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
2718 
2719         /**
2720          * Called when an aspect of the indicated route has changed.
2721          *
2722          * <p>This will not indicate that the types supported by this route have
2723          * changed, only that cosmetic info such as name or status have been updated.</p>
2724          *
2725          * @param router the MediaRouter reporting the event
2726          * @param info The route that was changed
2727          */
onRouteChanged(MediaRouter router, RouteInfo info)2728         public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
2729 
2730         /**
2731          * Called when a route is added to a group.
2732          *
2733          * @param router the MediaRouter reporting the event
2734          * @param info The route that was added
2735          * @param group The group the route was added to
2736          * @param index The route index within group that info was added at
2737          */
onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)2738         public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
2739                 int index);
2740 
2741         /**
2742          * Called when a route is removed from a group.
2743          *
2744          * @param router the MediaRouter reporting the event
2745          * @param info The route that was removed
2746          * @param group The group the route was removed from
2747          */
onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)2748         public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
2749 
2750         /**
2751          * Called when a route's volume changes.
2752          *
2753          * @param router the MediaRouter reporting the event
2754          * @param info The route with altered volume
2755          */
onRouteVolumeChanged(MediaRouter router, RouteInfo info)2756         public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
2757 
2758         /**
2759          * Called when a route's presentation display changes.
2760          * <p>
2761          * This method is called whenever the route's presentation display becomes
2762          * available, is removes or has changes to some of its properties (such as its size).
2763          * </p>
2764          *
2765          * @param router the MediaRouter reporting the event
2766          * @param info The route whose presentation display changed
2767          *
2768          * @see RouteInfo#getPresentationDisplay()
2769          */
onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info)2770         public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) {
2771         }
2772     }
2773 
2774     /**
2775      * Stub implementation of {@link MediaRouter.Callback}.
2776      * Each abstract method is defined as a no-op. Override just the ones
2777      * you need.
2778      */
2779     public static class SimpleCallback extends Callback {
2780 
2781         @Override
onRouteSelected(MediaRouter router, int type, RouteInfo info)2782         public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
2783         }
2784 
2785         @Override
onRouteUnselected(MediaRouter router, int type, RouteInfo info)2786         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
2787         }
2788 
2789         @Override
onRouteAdded(MediaRouter router, RouteInfo info)2790         public void onRouteAdded(MediaRouter router, RouteInfo info) {
2791         }
2792 
2793         @Override
onRouteRemoved(MediaRouter router, RouteInfo info)2794         public void onRouteRemoved(MediaRouter router, RouteInfo info) {
2795         }
2796 
2797         @Override
onRouteChanged(MediaRouter router, RouteInfo info)2798         public void onRouteChanged(MediaRouter router, RouteInfo info) {
2799         }
2800 
2801         @Override
onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)2802         public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
2803                 int index) {
2804         }
2805 
2806         @Override
onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)2807         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
2808         }
2809 
2810         @Override
onRouteVolumeChanged(MediaRouter router, RouteInfo info)2811         public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
2812         }
2813     }
2814 
2815     static class VolumeCallbackInfo {
2816         public final VolumeCallback vcb;
2817         public final RouteInfo route;
2818 
VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route)2819         public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
2820             this.vcb = vcb;
2821             this.route = route;
2822         }
2823     }
2824 
2825     /**
2826      * Interface for receiving events about volume changes.
2827      * All methods of this interface will be called from the application's main thread.
2828      *
2829      * <p>A VolumeCallback will only receive events relevant to routes that the callback
2830      * was registered for.</p>
2831      *
2832      * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
2833      */
2834     public static abstract class VolumeCallback {
2835         /**
2836          * Called when the volume for the route should be increased or decreased.
2837          * @param info the route affected by this event
2838          * @param direction an integer indicating whether the volume is to be increased
2839          *     (positive value) or decreased (negative value).
2840          *     For bundled changes, the absolute value indicates the number of changes
2841          *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
2842          */
onVolumeUpdateRequest(RouteInfo info, int direction)2843         public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
2844         /**
2845          * Called when the volume for the route should be set to the given value
2846          * @param info the route affected by this event
2847          * @param volume an integer indicating the new volume value that should be used, always
2848          *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
2849          */
onVolumeSetRequest(RouteInfo info, int volume)2850         public abstract void onVolumeSetRequest(RouteInfo info, int volume);
2851     }
2852 
2853     static class VolumeChangeReceiver extends BroadcastReceiver {
2854         @Override
onReceive(Context context, Intent intent)2855         public void onReceive(Context context, Intent intent) {
2856             if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
2857                 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
2858                         -1);
2859                 if (streamType != AudioManager.STREAM_MUSIC) {
2860                     return;
2861                 }
2862 
2863                 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
2864                 final int oldVolume = intent.getIntExtra(
2865                         AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
2866                 if (newVolume != oldVolume) {
2867                     systemVolumeChanged(newVolume);
2868                 }
2869             }
2870         }
2871     }
2872 
2873     static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver {
2874         @Override
onReceive(Context context, Intent intent)2875         public void onReceive(Context context, Intent intent) {
2876             if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
2877                 updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
2878                         DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
2879             }
2880         }
2881     }
2882 }
2883