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