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