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