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