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