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