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