1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.content.res.Resources; 24 import android.graphics.drawable.Drawable; 25 import android.os.Handler; 26 import android.os.IBinder; 27 import android.os.RemoteException; 28 import android.os.ServiceManager; 29 import android.text.TextUtils; 30 import android.util.Log; 31 32 import java.util.ArrayList; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.concurrent.CopyOnWriteArrayList; 36 37 /** 38 * MediaRouter allows applications to control the routing of media channels 39 * and streams from the current device to external speakers and destination devices. 40 * 41 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String) 42 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE 43 * Context.MEDIA_ROUTER_SERVICE}. 44 * 45 * <p>The media router API is not thread-safe; all interactions with it must be 46 * done from the main thread of the process.</p> 47 */ 48 public class MediaRouter { 49 private static final String TAG = "MediaRouter"; 50 51 static class Static { 52 final Resources mResources; 53 final IAudioService mAudioService; 54 final Handler mHandler; 55 final CopyOnWriteArrayList<CallbackInfo> mCallbacks = 56 new CopyOnWriteArrayList<CallbackInfo>(); 57 58 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 59 final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>(); 60 61 final RouteCategory mSystemCategory; 62 63 final AudioRoutesInfo mCurRoutesInfo = new AudioRoutesInfo(); 64 65 RouteInfo mDefaultAudio; 66 RouteInfo mBluetoothA2dpRoute; 67 68 RouteInfo mSelectedRoute; 69 70 final IAudioRoutesObserver.Stub mRoutesObserver = new IAudioRoutesObserver.Stub() { 71 public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { 72 mHandler.post(new Runnable() { 73 @Override public void run() { 74 updateRoutes(newRoutes); 75 } 76 }); 77 } 78 }; 79 Static(Context appContext)80 Static(Context appContext) { 81 mResources = Resources.getSystem(); 82 mHandler = new Handler(appContext.getMainLooper()); 83 84 IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); 85 mAudioService = IAudioService.Stub.asInterface(b); 86 87 mSystemCategory = new RouteCategory( 88 com.android.internal.R.string.default_audio_route_category_name, 89 ROUTE_TYPE_LIVE_AUDIO, false); 90 } 91 92 // Called after sStatic is initialized startMonitoringRoutes(Context appContext)93 void startMonitoringRoutes(Context appContext) { 94 mDefaultAudio = new RouteInfo(mSystemCategory); 95 mDefaultAudio.mNameResId = com.android.internal.R.string.default_audio_route_name; 96 mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; 97 addRoute(mDefaultAudio); 98 99 appContext.registerReceiver(new VolumeChangeReceiver(), 100 new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION)); 101 102 AudioRoutesInfo newRoutes = null; 103 try { 104 newRoutes = mAudioService.startWatchingRoutes(mRoutesObserver); 105 } catch (RemoteException e) { 106 } 107 if (newRoutes != null) { 108 updateRoutes(newRoutes); 109 } 110 } 111 updateRoutes(AudioRoutesInfo newRoutes)112 void updateRoutes(AudioRoutesInfo newRoutes) { 113 if (newRoutes.mMainType != mCurRoutesInfo.mMainType) { 114 mCurRoutesInfo.mMainType = newRoutes.mMainType; 115 int name; 116 if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0 117 || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) { 118 name = com.android.internal.R.string.default_audio_route_name_headphones; 119 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { 120 name = com.android.internal.R.string.default_audio_route_name_dock_speakers; 121 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) { 122 name = com.android.internal.R.string.default_audio_route_name_hdmi; 123 } else { 124 name = com.android.internal.R.string.default_audio_route_name; 125 } 126 sStatic.mDefaultAudio.mNameResId = name; 127 dispatchRouteChanged(sStatic.mDefaultAudio); 128 } 129 if (!TextUtils.equals(newRoutes.mBluetoothName, mCurRoutesInfo.mBluetoothName)) { 130 mCurRoutesInfo.mBluetoothName = newRoutes.mBluetoothName; 131 if (mCurRoutesInfo.mBluetoothName != null) { 132 if (sStatic.mBluetoothA2dpRoute == null) { 133 final RouteInfo info = new RouteInfo(sStatic.mSystemCategory); 134 info.mName = mCurRoutesInfo.mBluetoothName; 135 info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; 136 sStatic.mBluetoothA2dpRoute = info; 137 addRoute(sStatic.mBluetoothA2dpRoute); 138 try { 139 if (mAudioService.isBluetoothA2dpOn()) { 140 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute); 141 } 142 } catch (RemoteException e) { 143 Log.e(TAG, "Error selecting Bluetooth A2DP route", e); 144 } 145 } else { 146 sStatic.mBluetoothA2dpRoute.mName = mCurRoutesInfo.mBluetoothName; 147 dispatchRouteChanged(sStatic.mBluetoothA2dpRoute); 148 } 149 } else if (sStatic.mBluetoothA2dpRoute != null) { 150 removeRoute(sStatic.mBluetoothA2dpRoute); 151 sStatic.mBluetoothA2dpRoute = null; 152 } 153 } 154 } 155 } 156 157 static Static sStatic; 158 159 /** 160 * Route type flag for live audio. 161 * 162 * <p>A device that supports live audio routing will allow the media audio stream 163 * to be routed to supported destinations. This can include internal speakers or 164 * audio jacks on the device itself, A2DP devices, and more.</p> 165 * 166 * <p>Once initiated this routing is transparent to the application. All audio 167 * played on the media stream will be routed to the selected destination.</p> 168 */ 169 public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; 170 171 /** 172 * Route type flag for application-specific usage. 173 * 174 * <p>Unlike other media route types, user routes are managed by the application. 175 * The MediaRouter will manage and dispatch events for user routes, but the application 176 * is expected to interpret the meaning of these events and perform the requested 177 * routing tasks.</p> 178 */ 179 public static final int ROUTE_TYPE_USER = 0x00800000; 180 181 // Maps application contexts 182 static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); 183 typesToString(int types)184 static String typesToString(int types) { 185 final StringBuilder result = new StringBuilder(); 186 if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) { 187 result.append("ROUTE_TYPE_LIVE_AUDIO "); 188 } 189 if ((types & ROUTE_TYPE_USER) != 0) { 190 result.append("ROUTE_TYPE_USER "); 191 } 192 return result.toString(); 193 } 194 195 /** @hide */ MediaRouter(Context context)196 public MediaRouter(Context context) { 197 synchronized (Static.class) { 198 if (sStatic == null) { 199 final Context appContext = context.getApplicationContext(); 200 sStatic = new Static(appContext); 201 sStatic.startMonitoringRoutes(appContext); 202 } 203 } 204 } 205 206 /** 207 * @hide for use by framework routing UI 208 */ getSystemAudioRoute()209 public RouteInfo getSystemAudioRoute() { 210 return sStatic.mDefaultAudio; 211 } 212 213 /** 214 * @hide for use by framework routing UI 215 */ getSystemAudioCategory()216 public RouteCategory getSystemAudioCategory() { 217 return sStatic.mSystemCategory; 218 } 219 220 /** 221 * Return the currently selected route for the given types 222 * 223 * @param type route types 224 * @return the selected route 225 */ getSelectedRoute(int type)226 public RouteInfo getSelectedRoute(int type) { 227 return sStatic.mSelectedRoute; 228 } 229 230 /** 231 * Add a callback to listen to events about specific kinds of media routes. 232 * If the specified callback is already registered, its registration will be updated for any 233 * additional route types specified. 234 * 235 * @param types Types of routes this callback is interested in 236 * @param cb Callback to add 237 */ addCallback(int types, Callback cb)238 public void addCallback(int types, Callback cb) { 239 final int count = sStatic.mCallbacks.size(); 240 for (int i = 0; i < count; i++) { 241 final CallbackInfo info = sStatic.mCallbacks.get(i); 242 if (info.cb == cb) { 243 info.type |= types; 244 return; 245 } 246 } 247 sStatic.mCallbacks.add(new CallbackInfo(cb, types, this)); 248 } 249 250 /** 251 * Remove the specified callback. It will no longer receive events about media routing. 252 * 253 * @param cb Callback to remove 254 */ removeCallback(Callback cb)255 public void removeCallback(Callback cb) { 256 final int count = sStatic.mCallbacks.size(); 257 for (int i = 0; i < count; i++) { 258 if (sStatic.mCallbacks.get(i).cb == cb) { 259 sStatic.mCallbacks.remove(i); 260 return; 261 } 262 } 263 Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); 264 } 265 266 /** 267 * Select the specified route to use for output of the given media types. 268 * 269 * @param types type flags indicating which types this route should be used for. 270 * The route must support at least a subset. 271 * @param route Route to select 272 */ selectRoute(int types, RouteInfo route)273 public void selectRoute(int types, RouteInfo route) { 274 // Applications shouldn't programmatically change anything but user routes. 275 types &= ROUTE_TYPE_USER; 276 selectRouteStatic(types, route); 277 } 278 279 /** 280 * @hide internal use 281 */ selectRouteInt(int types, RouteInfo route)282 public void selectRouteInt(int types, RouteInfo route) { 283 selectRouteStatic(types, route); 284 } 285 selectRouteStatic(int types, RouteInfo route)286 static void selectRouteStatic(int types, RouteInfo route) { 287 if (sStatic.mSelectedRoute == route) return; 288 if ((route.getSupportedTypes() & types) == 0) { 289 Log.w(TAG, "selectRoute ignored; cannot select route with supported types " + 290 typesToString(route.getSupportedTypes()) + " into route types " + 291 typesToString(types)); 292 return; 293 } 294 295 final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute; 296 if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 && 297 (route == btRoute || route == sStatic.mDefaultAudio)) { 298 try { 299 sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute); 300 } catch (RemoteException e) { 301 Log.e(TAG, "Error changing Bluetooth A2DP state", e); 302 } 303 } 304 305 if (sStatic.mSelectedRoute != null) { 306 // TODO filter types properly 307 dispatchRouteUnselected(types & sStatic.mSelectedRoute.getSupportedTypes(), 308 sStatic.mSelectedRoute); 309 } 310 sStatic.mSelectedRoute = route; 311 if (route != null) { 312 // TODO filter types properly 313 dispatchRouteSelected(types & route.getSupportedTypes(), route); 314 } 315 } 316 317 /** 318 * Add an app-specified route for media to the MediaRouter. 319 * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)} 320 * 321 * @param info Definition of the route to add 322 * @see #createUserRoute() 323 * @see #removeUserRoute(UserRouteInfo) 324 */ addUserRoute(UserRouteInfo info)325 public void addUserRoute(UserRouteInfo info) { 326 addRoute(info); 327 } 328 329 /** 330 * @hide Framework use only 331 */ addRouteInt(RouteInfo info)332 public void addRouteInt(RouteInfo info) { 333 addRoute(info); 334 } 335 addRoute(RouteInfo info)336 static void addRoute(RouteInfo info) { 337 final RouteCategory cat = info.getCategory(); 338 if (!sStatic.mCategories.contains(cat)) { 339 sStatic.mCategories.add(cat); 340 } 341 final boolean onlyRoute = sStatic.mRoutes.isEmpty(); 342 if (cat.isGroupable() && !(info instanceof RouteGroup)) { 343 // Enforce that any added route in a groupable category must be in a group. 344 final RouteGroup group = new RouteGroup(info.getCategory()); 345 group.mSupportedTypes = info.mSupportedTypes; 346 sStatic.mRoutes.add(group); 347 dispatchRouteAdded(group); 348 group.addRoute(info); 349 350 info = group; 351 } else { 352 sStatic.mRoutes.add(info); 353 dispatchRouteAdded(info); 354 } 355 356 if (onlyRoute) { 357 selectRouteStatic(info.getSupportedTypes(), info); 358 } 359 } 360 361 /** 362 * Remove an app-specified route for media from the MediaRouter. 363 * 364 * @param info Definition of the route to remove 365 * @see #addUserRoute(UserRouteInfo) 366 */ removeUserRoute(UserRouteInfo info)367 public void removeUserRoute(UserRouteInfo info) { 368 removeRoute(info); 369 } 370 371 /** 372 * Remove all app-specified routes from the MediaRouter. 373 * 374 * @see #removeUserRoute(UserRouteInfo) 375 */ clearUserRoutes()376 public void clearUserRoutes() { 377 for (int i = 0; i < sStatic.mRoutes.size(); i++) { 378 final RouteInfo info = sStatic.mRoutes.get(i); 379 // TODO Right now, RouteGroups only ever contain user routes. 380 // The code below will need to change if this assumption does. 381 if (info instanceof UserRouteInfo || info instanceof RouteGroup) { 382 removeRouteAt(i); 383 i--; 384 } 385 } 386 } 387 388 /** 389 * @hide internal use only 390 */ removeRouteInt(RouteInfo info)391 public void removeRouteInt(RouteInfo info) { 392 removeRoute(info); 393 } 394 removeRoute(RouteInfo info)395 static void removeRoute(RouteInfo info) { 396 if (sStatic.mRoutes.remove(info)) { 397 final RouteCategory removingCat = info.getCategory(); 398 final int count = sStatic.mRoutes.size(); 399 boolean found = false; 400 for (int i = 0; i < count; i++) { 401 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 402 if (removingCat == cat) { 403 found = true; 404 break; 405 } 406 } 407 if (info == sStatic.mSelectedRoute) { 408 // Removing the currently selected route? Select the default before we remove it. 409 // TODO: Be smarter about the route types here; this selects for all valid. 410 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio); 411 } 412 if (!found) { 413 sStatic.mCategories.remove(removingCat); 414 } 415 dispatchRouteRemoved(info); 416 } 417 } 418 removeRouteAt(int routeIndex)419 void removeRouteAt(int routeIndex) { 420 if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) { 421 final RouteInfo info = sStatic.mRoutes.remove(routeIndex); 422 final RouteCategory removingCat = info.getCategory(); 423 final int count = sStatic.mRoutes.size(); 424 boolean found = false; 425 for (int i = 0; i < count; i++) { 426 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 427 if (removingCat == cat) { 428 found = true; 429 break; 430 } 431 } 432 if (info == sStatic.mSelectedRoute) { 433 // Removing the currently selected route? Select the default before we remove it. 434 // TODO: Be smarter about the route types here; this selects for all valid. 435 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio); 436 } 437 if (!found) { 438 sStatic.mCategories.remove(removingCat); 439 } 440 dispatchRouteRemoved(info); 441 } 442 } 443 444 /** 445 * Return the number of {@link MediaRouter.RouteCategory categories} currently 446 * represented by routes known to this MediaRouter. 447 * 448 * @return the number of unique categories represented by this MediaRouter's known routes 449 */ getCategoryCount()450 public int getCategoryCount() { 451 return sStatic.mCategories.size(); 452 } 453 454 /** 455 * Return the {@link MediaRouter.RouteCategory category} at the given index. 456 * Valid indices are in the range [0-getCategoryCount). 457 * 458 * @param index which category to return 459 * @return the category at index 460 */ getCategoryAt(int index)461 public RouteCategory getCategoryAt(int index) { 462 return sStatic.mCategories.get(index); 463 } 464 465 /** 466 * Return the number of {@link MediaRouter.RouteInfo routes} currently known 467 * to this MediaRouter. 468 * 469 * @return the number of routes tracked by this router 470 */ getRouteCount()471 public int getRouteCount() { 472 return sStatic.mRoutes.size(); 473 } 474 475 /** 476 * Return the route at the specified index. 477 * 478 * @param index index of the route to return 479 * @return the route at index 480 */ getRouteAt(int index)481 public RouteInfo getRouteAt(int index) { 482 return sStatic.mRoutes.get(index); 483 } 484 getRouteCountStatic()485 static int getRouteCountStatic() { 486 return sStatic.mRoutes.size(); 487 } 488 getRouteAtStatic(int index)489 static RouteInfo getRouteAtStatic(int index) { 490 return sStatic.mRoutes.get(index); 491 } 492 493 /** 494 * Create a new user route that may be modified and registered for use by the application. 495 * 496 * @param category The category the new route will belong to 497 * @return A new UserRouteInfo for use by the application 498 * 499 * @see #addUserRoute(UserRouteInfo) 500 * @see #removeUserRoute(UserRouteInfo) 501 * @see #createRouteCategory(CharSequence) 502 */ createUserRoute(RouteCategory category)503 public UserRouteInfo createUserRoute(RouteCategory category) { 504 return new UserRouteInfo(category); 505 } 506 507 /** 508 * Create a new route category. Each route must belong to a category. 509 * 510 * @param name Name of the new category 511 * @param isGroupable true if routes in this category may be grouped with one another 512 * @return the new RouteCategory 513 */ createRouteCategory(CharSequence name, boolean isGroupable)514 public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) { 515 return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable); 516 } 517 518 /** 519 * Create a new route category. Each route must belong to a category. 520 * 521 * @param nameResId Resource ID of the name of the new category 522 * @param isGroupable true if routes in this category may be grouped with one another 523 * @return the new RouteCategory 524 */ createRouteCategory(int nameResId, boolean isGroupable)525 public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) { 526 return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable); 527 } 528 updateRoute(final RouteInfo info)529 static void updateRoute(final RouteInfo info) { 530 dispatchRouteChanged(info); 531 } 532 dispatchRouteSelected(int type, RouteInfo info)533 static void dispatchRouteSelected(int type, RouteInfo info) { 534 for (CallbackInfo cbi : sStatic.mCallbacks) { 535 if ((cbi.type & type) != 0) { 536 cbi.cb.onRouteSelected(cbi.router, type, info); 537 } 538 } 539 } 540 dispatchRouteUnselected(int type, RouteInfo info)541 static void dispatchRouteUnselected(int type, RouteInfo info) { 542 for (CallbackInfo cbi : sStatic.mCallbacks) { 543 if ((cbi.type & type) != 0) { 544 cbi.cb.onRouteUnselected(cbi.router, type, info); 545 } 546 } 547 } 548 dispatchRouteChanged(RouteInfo info)549 static void dispatchRouteChanged(RouteInfo info) { 550 for (CallbackInfo cbi : sStatic.mCallbacks) { 551 if ((cbi.type & info.mSupportedTypes) != 0) { 552 cbi.cb.onRouteChanged(cbi.router, info); 553 } 554 } 555 } 556 dispatchRouteAdded(RouteInfo info)557 static void dispatchRouteAdded(RouteInfo info) { 558 for (CallbackInfo cbi : sStatic.mCallbacks) { 559 if ((cbi.type & info.mSupportedTypes) != 0) { 560 cbi.cb.onRouteAdded(cbi.router, info); 561 } 562 } 563 } 564 dispatchRouteRemoved(RouteInfo info)565 static void dispatchRouteRemoved(RouteInfo info) { 566 for (CallbackInfo cbi : sStatic.mCallbacks) { 567 if ((cbi.type & info.mSupportedTypes) != 0) { 568 cbi.cb.onRouteRemoved(cbi.router, info); 569 } 570 } 571 } 572 dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index)573 static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) { 574 for (CallbackInfo cbi : sStatic.mCallbacks) { 575 if ((cbi.type & group.mSupportedTypes) != 0) { 576 cbi.cb.onRouteGrouped(cbi.router, info, group, index); 577 } 578 } 579 } 580 dispatchRouteUngrouped(RouteInfo info, RouteGroup group)581 static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) { 582 for (CallbackInfo cbi : sStatic.mCallbacks) { 583 if ((cbi.type & group.mSupportedTypes) != 0) { 584 cbi.cb.onRouteUngrouped(cbi.router, info, group); 585 } 586 } 587 } 588 dispatchRouteVolumeChanged(RouteInfo info)589 static void dispatchRouteVolumeChanged(RouteInfo info) { 590 for (CallbackInfo cbi : sStatic.mCallbacks) { 591 if ((cbi.type & info.mSupportedTypes) != 0) { 592 cbi.cb.onRouteVolumeChanged(cbi.router, info); 593 } 594 } 595 } 596 systemVolumeChanged(int newValue)597 static void systemVolumeChanged(int newValue) { 598 final RouteInfo selectedRoute = sStatic.mSelectedRoute; 599 if (selectedRoute == null) return; 600 601 if (selectedRoute == sStatic.mBluetoothA2dpRoute || 602 selectedRoute == sStatic.mDefaultAudio) { 603 dispatchRouteVolumeChanged(selectedRoute); 604 } else if (sStatic.mBluetoothA2dpRoute != null) { 605 try { 606 dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ? 607 sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudio); 608 } catch (RemoteException e) { 609 Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e); 610 } 611 } else { 612 dispatchRouteVolumeChanged(sStatic.mDefaultAudio); 613 } 614 } 615 616 /** 617 * Information about a media route. 618 */ 619 public static class RouteInfo { 620 CharSequence mName; 621 int mNameResId; 622 private CharSequence mStatus; 623 int mSupportedTypes; 624 RouteGroup mGroup; 625 final RouteCategory mCategory; 626 Drawable mIcon; 627 // playback information 628 int mPlaybackType = PLAYBACK_TYPE_LOCAL; 629 int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 630 int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 631 int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING; 632 int mPlaybackStream = AudioManager.STREAM_MUSIC; 633 VolumeCallbackInfo mVcb; 634 635 private Object mTag; 636 637 /** 638 * The default playback type, "local", indicating the presentation of the media is happening 639 * on the same device (e.g. a phone, a tablet) as where it is controlled from. 640 * @see #setPlaybackType(int) 641 */ 642 public final static int PLAYBACK_TYPE_LOCAL = 0; 643 /** 644 * A playback type indicating the presentation of the media is happening on 645 * a different device (i.e. the remote device) than where it is controlled from. 646 * @see #setPlaybackType(int) 647 */ 648 public final static int PLAYBACK_TYPE_REMOTE = 1; 649 /** 650 * Playback information indicating the playback volume is fixed, i.e. it cannot be 651 * controlled from this object. An example of fixed playback volume is a remote player, 652 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 653 * than attenuate at the source. 654 * @see #setVolumeHandling(int) 655 */ 656 public final static int PLAYBACK_VOLUME_FIXED = 0; 657 /** 658 * Playback information indicating the playback volume is variable and can be controlled 659 * from this object. 660 */ 661 public final static int PLAYBACK_VOLUME_VARIABLE = 1; 662 RouteInfo(RouteCategory category)663 RouteInfo(RouteCategory category) { 664 mCategory = category; 665 } 666 667 /** 668 * @return The user-friendly name of a media route. This is the string presented 669 * to users who may select this as the active route. 670 */ getName()671 public CharSequence getName() { 672 return getName(sStatic.mResources); 673 } 674 675 /** 676 * Return the properly localized/resource selected name of this route. 677 * 678 * @param context Context used to resolve the correct configuration to load 679 * @return The user-friendly name of the media route. This is the string presented 680 * to users who may select this as the active route. 681 */ getName(Context context)682 public CharSequence getName(Context context) { 683 return getName(context.getResources()); 684 } 685 getName(Resources res)686 CharSequence getName(Resources res) { 687 if (mNameResId != 0) { 688 return mName = res.getText(mNameResId); 689 } 690 return mName; 691 } 692 693 /** 694 * @return The user-friendly status for a media route. This may include a description 695 * of the currently playing media, if available. 696 */ getStatus()697 public CharSequence getStatus() { 698 return mStatus; 699 } 700 701 /** 702 * @return A media type flag set describing which types this route supports. 703 */ getSupportedTypes()704 public int getSupportedTypes() { 705 return mSupportedTypes; 706 } 707 708 /** 709 * @return The group that this route belongs to. 710 */ getGroup()711 public RouteGroup getGroup() { 712 return mGroup; 713 } 714 715 /** 716 * @return the category this route belongs to. 717 */ getCategory()718 public RouteCategory getCategory() { 719 return mCategory; 720 } 721 722 /** 723 * Get the icon representing this route. 724 * This icon will be used in picker UIs if available. 725 * 726 * @return the icon representing this route or null if no icon is available 727 */ getIconDrawable()728 public Drawable getIconDrawable() { 729 return mIcon; 730 } 731 732 /** 733 * Set an application-specific tag object for this route. 734 * The application may use this to store arbitrary data associated with the 735 * route for internal tracking. 736 * 737 * <p>Note that the lifespan of a route may be well past the lifespan of 738 * an Activity or other Context; take care that objects you store here 739 * will not keep more data in memory alive than you intend.</p> 740 * 741 * @param tag Arbitrary, app-specific data for this route to hold for later use 742 */ setTag(Object tag)743 public void setTag(Object tag) { 744 mTag = tag; 745 routeUpdated(); 746 } 747 748 /** 749 * @return The tag object previously set by the application 750 * @see #setTag(Object) 751 */ getTag()752 public Object getTag() { 753 return mTag; 754 } 755 756 /** 757 * @return the type of playback associated with this route 758 * @see UserRouteInfo#setPlaybackType(int) 759 */ getPlaybackType()760 public int getPlaybackType() { 761 return mPlaybackType; 762 } 763 764 /** 765 * @return the stream over which the playback associated with this route is performed 766 * @see UserRouteInfo#setPlaybackStream(int) 767 */ getPlaybackStream()768 public int getPlaybackStream() { 769 return mPlaybackStream; 770 } 771 772 /** 773 * Return the current volume for this route. Depending on the route, this may only 774 * be valid if the route is currently selected. 775 * 776 * @return the volume at which the playback associated with this route is performed 777 * @see UserRouteInfo#setVolume(int) 778 */ getVolume()779 public int getVolume() { 780 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 781 int vol = 0; 782 try { 783 vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream); 784 } catch (RemoteException e) { 785 Log.e(TAG, "Error getting local stream volume", e); 786 } 787 return vol; 788 } else { 789 return mVolume; 790 } 791 } 792 793 /** 794 * Request a volume change for this route. 795 * @param volume value between 0 and getVolumeMax 796 */ requestSetVolume(int volume)797 public void requestSetVolume(int volume) { 798 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 799 try { 800 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 801 } catch (RemoteException e) { 802 Log.e(TAG, "Error setting local stream volume", e); 803 } 804 } else { 805 Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " + 806 "Non-local volume playback on system route? " + 807 "Could not request volume change."); 808 } 809 } 810 811 /** 812 * Request an incremental volume update for this route. 813 * @param direction Delta to apply to the current volume 814 */ requestUpdateVolume(int direction)815 public void requestUpdateVolume(int direction) { 816 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 817 try { 818 final int volume = 819 Math.max(0, Math.min(getVolume() + direction, getVolumeMax())); 820 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 821 } catch (RemoteException e) { 822 Log.e(TAG, "Error setting local stream volume", e); 823 } 824 } else { 825 Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " + 826 "Non-local volume playback on system route? " + 827 "Could not request volume change."); 828 } 829 } 830 831 /** 832 * @return the maximum volume at which the playback associated with this route is performed 833 * @see UserRouteInfo#setVolumeMax(int) 834 */ getVolumeMax()835 public int getVolumeMax() { 836 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 837 int volMax = 0; 838 try { 839 volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream); 840 } catch (RemoteException e) { 841 Log.e(TAG, "Error getting local stream volume", e); 842 } 843 return volMax; 844 } else { 845 return mVolumeMax; 846 } 847 } 848 849 /** 850 * @return how volume is handling on the route 851 * @see UserRouteInfo#setVolumeHandling(int) 852 */ getVolumeHandling()853 public int getVolumeHandling() { 854 return mVolumeHandling; 855 } 856 setStatusInt(CharSequence status)857 void setStatusInt(CharSequence status) { 858 if (!status.equals(mStatus)) { 859 mStatus = status; 860 if (mGroup != null) { 861 mGroup.memberStatusChanged(this, status); 862 } 863 routeUpdated(); 864 } 865 } 866 867 final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { 868 public void dispatchRemoteVolumeUpdate(final int direction, final int value) { 869 sStatic.mHandler.post(new Runnable() { 870 @Override 871 public void run() { 872 //Log.d(TAG, "dispatchRemoteVolumeUpdate dir=" + direction + " val=" + value); 873 if (mVcb != null) { 874 if (direction != 0) { 875 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); 876 } else { 877 mVcb.vcb.onVolumeSetRequest(mVcb.route, value); 878 } 879 } 880 } 881 }); 882 } 883 }; 884 routeUpdated()885 void routeUpdated() { 886 updateRoute(this); 887 } 888 889 @Override toString()890 public String toString() { 891 String supportedTypes = typesToString(getSupportedTypes()); 892 return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() + 893 " category=" + getCategory() + 894 " supportedTypes=" + supportedTypes + "}"; 895 } 896 } 897 898 /** 899 * Information about a route that the application may define and modify. 900 * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and 901 * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}. 902 * 903 * @see MediaRouter.RouteInfo 904 */ 905 public static class UserRouteInfo extends RouteInfo { 906 RemoteControlClient mRcc; 907 UserRouteInfo(RouteCategory category)908 UserRouteInfo(RouteCategory category) { 909 super(category); 910 mSupportedTypes = ROUTE_TYPE_USER; 911 mPlaybackType = PLAYBACK_TYPE_REMOTE; 912 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 913 } 914 915 /** 916 * Set the user-visible name of this route. 917 * @param name Name to display to the user to describe this route 918 */ setName(CharSequence name)919 public void setName(CharSequence name) { 920 mName = name; 921 routeUpdated(); 922 } 923 924 /** 925 * Set the user-visible name of this route. 926 * @param resId Resource ID of the name to display to the user to describe this route 927 */ setName(int resId)928 public void setName(int resId) { 929 mNameResId = resId; 930 mName = null; 931 routeUpdated(); 932 } 933 934 /** 935 * Set the current user-visible status for this route. 936 * @param status Status to display to the user to describe what the endpoint 937 * of this route is currently doing 938 */ setStatus(CharSequence status)939 public void setStatus(CharSequence status) { 940 setStatusInt(status); 941 } 942 943 /** 944 * Set the RemoteControlClient responsible for reporting playback info for this 945 * user route. 946 * 947 * <p>If this route manages remote playback, the data exposed by this 948 * RemoteControlClient will be used to reflect and update information 949 * such as route volume info in related UIs.</p> 950 * 951 * <p>The RemoteControlClient must have been previously registered with 952 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p> 953 * 954 * @param rcc RemoteControlClient associated with this route 955 */ setRemoteControlClient(RemoteControlClient rcc)956 public void setRemoteControlClient(RemoteControlClient rcc) { 957 mRcc = rcc; 958 updatePlaybackInfoOnRcc(); 959 } 960 961 /** 962 * Retrieve the RemoteControlClient associated with this route, if one has been set. 963 * 964 * @return the RemoteControlClient associated with this route 965 * @see #setRemoteControlClient(RemoteControlClient) 966 */ getRemoteControlClient()967 public RemoteControlClient getRemoteControlClient() { 968 return mRcc; 969 } 970 971 /** 972 * Set an icon that will be used to represent this route. 973 * The system may use this icon in picker UIs or similar. 974 * 975 * @param icon icon drawable to use to represent this route 976 */ setIconDrawable(Drawable icon)977 public void setIconDrawable(Drawable icon) { 978 mIcon = icon; 979 } 980 981 /** 982 * Set an icon that will be used to represent this route. 983 * The system may use this icon in picker UIs or similar. 984 * 985 * @param resId Resource ID of an icon drawable to use to represent this route 986 */ setIconResource(int resId)987 public void setIconResource(int resId) { 988 setIconDrawable(sStatic.mResources.getDrawable(resId)); 989 } 990 991 /** 992 * Set a callback to be notified of volume update requests 993 * @param vcb 994 */ setVolumeCallback(VolumeCallback vcb)995 public void setVolumeCallback(VolumeCallback vcb) { 996 mVcb = new VolumeCallbackInfo(vcb, this); 997 } 998 999 /** 1000 * Defines whether playback associated with this route is "local" 1001 * ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote" 1002 * ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}). 1003 * @param type 1004 */ setPlaybackType(int type)1005 public void setPlaybackType(int type) { 1006 if (mPlaybackType != type) { 1007 mPlaybackType = type; 1008 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type); 1009 } 1010 } 1011 1012 /** 1013 * Defines whether volume for the playback associated with this route is fixed 1014 * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified 1015 * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}). 1016 * @param volumeHandling 1017 */ setVolumeHandling(int volumeHandling)1018 public void setVolumeHandling(int volumeHandling) { 1019 if (mVolumeHandling != volumeHandling) { 1020 mVolumeHandling = volumeHandling; 1021 setPlaybackInfoOnRcc( 1022 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling); 1023 } 1024 } 1025 1026 /** 1027 * Defines at what volume the playback associated with this route is performed (for user 1028 * feedback purposes). This information is only used when the playback is not local. 1029 * @param volume 1030 */ setVolume(int volume)1031 public void setVolume(int volume) { 1032 volume = Math.max(0, Math.min(volume, getVolumeMax())); 1033 if (mVolume != volume) { 1034 mVolume = volume; 1035 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume); 1036 dispatchRouteVolumeChanged(this); 1037 if (mGroup != null) { 1038 mGroup.memberVolumeChanged(this); 1039 } 1040 } 1041 } 1042 1043 @Override requestSetVolume(int volume)1044 public void requestSetVolume(int volume) { 1045 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1046 if (mVcb == null) { 1047 Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set"); 1048 return; 1049 } 1050 mVcb.vcb.onVolumeSetRequest(this, volume); 1051 } 1052 } 1053 1054 @Override requestUpdateVolume(int direction)1055 public void requestUpdateVolume(int direction) { 1056 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1057 if (mVcb == null) { 1058 Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set"); 1059 return; 1060 } 1061 mVcb.vcb.onVolumeUpdateRequest(this, direction); 1062 } 1063 } 1064 1065 /** 1066 * Defines the maximum volume at which the playback associated with this route is performed 1067 * (for user feedback purposes). This information is only used when the playback is not 1068 * local. 1069 * @param volumeMax 1070 */ setVolumeMax(int volumeMax)1071 public void setVolumeMax(int volumeMax) { 1072 if (mVolumeMax != volumeMax) { 1073 mVolumeMax = volumeMax; 1074 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax); 1075 } 1076 } 1077 1078 /** 1079 * Defines over what stream type the media is presented. 1080 * @param stream 1081 */ setPlaybackStream(int stream)1082 public void setPlaybackStream(int stream) { 1083 if (mPlaybackStream != stream) { 1084 mPlaybackStream = stream; 1085 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream); 1086 } 1087 } 1088 updatePlaybackInfoOnRcc()1089 private void updatePlaybackInfoOnRcc() { 1090 if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) { 1091 mRcc.setPlaybackInformation( 1092 RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax); 1093 mRcc.setPlaybackInformation( 1094 RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume); 1095 mRcc.setPlaybackInformation( 1096 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling); 1097 mRcc.setPlaybackInformation( 1098 RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream); 1099 mRcc.setPlaybackInformation( 1100 RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType); 1101 // let AudioService know whom to call when remote volume needs to be updated 1102 try { 1103 sStatic.mAudioService.registerRemoteVolumeObserverForRcc( 1104 mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */); 1105 } catch (RemoteException e) { 1106 Log.e(TAG, "Error registering remote volume observer", e); 1107 } 1108 } 1109 } 1110 setPlaybackInfoOnRcc(int what, int value)1111 private void setPlaybackInfoOnRcc(int what, int value) { 1112 if (mRcc != null) { 1113 mRcc.setPlaybackInformation(what, value); 1114 } 1115 } 1116 } 1117 1118 /** 1119 * Information about a route that consists of multiple other routes in a group. 1120 */ 1121 public static class RouteGroup extends RouteInfo { 1122 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 1123 private boolean mUpdateName; 1124 RouteGroup(RouteCategory category)1125 RouteGroup(RouteCategory category) { 1126 super(category); 1127 mGroup = this; 1128 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1129 } 1130 getName(Resources res)1131 CharSequence getName(Resources res) { 1132 if (mUpdateName) updateName(); 1133 return super.getName(res); 1134 } 1135 1136 /** 1137 * Add a route to this group. The route must not currently belong to another group. 1138 * 1139 * @param route route to add to this group 1140 */ addRoute(RouteInfo route)1141 public void addRoute(RouteInfo route) { 1142 if (route.getGroup() != null) { 1143 throw new IllegalStateException("Route " + route + " is already part of a group."); 1144 } 1145 if (route.getCategory() != mCategory) { 1146 throw new IllegalArgumentException( 1147 "Route cannot be added to a group with a different category. " + 1148 "(Route category=" + route.getCategory() + 1149 " group category=" + mCategory + ")"); 1150 } 1151 final int at = mRoutes.size(); 1152 mRoutes.add(route); 1153 route.mGroup = this; 1154 mUpdateName = true; 1155 updateVolume(); 1156 routeUpdated(); 1157 dispatchRouteGrouped(route, this, at); 1158 } 1159 1160 /** 1161 * Add a route to this group before the specified index. 1162 * 1163 * @param route route to add 1164 * @param insertAt insert the new route before this index 1165 */ addRoute(RouteInfo route, int insertAt)1166 public void addRoute(RouteInfo route, int insertAt) { 1167 if (route.getGroup() != null) { 1168 throw new IllegalStateException("Route " + route + " is already part of a group."); 1169 } 1170 if (route.getCategory() != mCategory) { 1171 throw new IllegalArgumentException( 1172 "Route cannot be added to a group with a different category. " + 1173 "(Route category=" + route.getCategory() + 1174 " group category=" + mCategory + ")"); 1175 } 1176 mRoutes.add(insertAt, route); 1177 route.mGroup = this; 1178 mUpdateName = true; 1179 updateVolume(); 1180 routeUpdated(); 1181 dispatchRouteGrouped(route, this, insertAt); 1182 } 1183 1184 /** 1185 * Remove a route from this group. 1186 * 1187 * @param route route to remove 1188 */ removeRoute(RouteInfo route)1189 public void removeRoute(RouteInfo route) { 1190 if (route.getGroup() != this) { 1191 throw new IllegalArgumentException("Route " + route + 1192 " is not a member of this group."); 1193 } 1194 mRoutes.remove(route); 1195 route.mGroup = null; 1196 mUpdateName = true; 1197 updateVolume(); 1198 dispatchRouteUngrouped(route, this); 1199 routeUpdated(); 1200 } 1201 1202 /** 1203 * Remove the route at the specified index from this group. 1204 * 1205 * @param index index of the route to remove 1206 */ removeRoute(int index)1207 public void removeRoute(int index) { 1208 RouteInfo route = mRoutes.remove(index); 1209 route.mGroup = null; 1210 mUpdateName = true; 1211 updateVolume(); 1212 dispatchRouteUngrouped(route, this); 1213 routeUpdated(); 1214 } 1215 1216 /** 1217 * @return The number of routes in this group 1218 */ getRouteCount()1219 public int getRouteCount() { 1220 return mRoutes.size(); 1221 } 1222 1223 /** 1224 * Return the route in this group at the specified index 1225 * 1226 * @param index Index to fetch 1227 * @return The route at index 1228 */ getRouteAt(int index)1229 public RouteInfo getRouteAt(int index) { 1230 return mRoutes.get(index); 1231 } 1232 1233 /** 1234 * Set an icon that will be used to represent this group. 1235 * The system may use this icon in picker UIs or similar. 1236 * 1237 * @param icon icon drawable to use to represent this group 1238 */ setIconDrawable(Drawable icon)1239 public void setIconDrawable(Drawable icon) { 1240 mIcon = icon; 1241 } 1242 1243 /** 1244 * Set an icon that will be used to represent this group. 1245 * The system may use this icon in picker UIs or similar. 1246 * 1247 * @param resId Resource ID of an icon drawable to use to represent this group 1248 */ setIconResource(int resId)1249 public void setIconResource(int resId) { 1250 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1251 } 1252 1253 @Override requestSetVolume(int volume)1254 public void requestSetVolume(int volume) { 1255 final int maxVol = getVolumeMax(); 1256 if (maxVol == 0) { 1257 return; 1258 } 1259 1260 final float scaledVolume = (float) volume / maxVol; 1261 final int routeCount = getRouteCount(); 1262 for (int i = 0; i < routeCount; i++) { 1263 final RouteInfo route = getRouteAt(i); 1264 final int routeVol = (int) (scaledVolume * route.getVolumeMax()); 1265 route.requestSetVolume(routeVol); 1266 } 1267 if (volume != mVolume) { 1268 mVolume = volume; 1269 dispatchRouteVolumeChanged(this); 1270 } 1271 } 1272 1273 @Override requestUpdateVolume(int direction)1274 public void requestUpdateVolume(int direction) { 1275 final int maxVol = getVolumeMax(); 1276 if (maxVol == 0) { 1277 return; 1278 } 1279 1280 final int routeCount = getRouteCount(); 1281 int volume = 0; 1282 for (int i = 0; i < routeCount; i++) { 1283 final RouteInfo route = getRouteAt(i); 1284 route.requestUpdateVolume(direction); 1285 final int routeVol = route.getVolume(); 1286 if (routeVol > volume) { 1287 volume = routeVol; 1288 } 1289 } 1290 if (volume != mVolume) { 1291 mVolume = volume; 1292 dispatchRouteVolumeChanged(this); 1293 } 1294 } 1295 memberNameChanged(RouteInfo info, CharSequence name)1296 void memberNameChanged(RouteInfo info, CharSequence name) { 1297 mUpdateName = true; 1298 routeUpdated(); 1299 } 1300 memberStatusChanged(RouteInfo info, CharSequence status)1301 void memberStatusChanged(RouteInfo info, CharSequence status) { 1302 setStatusInt(status); 1303 } 1304 memberVolumeChanged(RouteInfo info)1305 void memberVolumeChanged(RouteInfo info) { 1306 updateVolume(); 1307 } 1308 updateVolume()1309 void updateVolume() { 1310 // A group always represents the highest component volume value. 1311 final int routeCount = getRouteCount(); 1312 int volume = 0; 1313 for (int i = 0; i < routeCount; i++) { 1314 final int routeVol = getRouteAt(i).getVolume(); 1315 if (routeVol > volume) { 1316 volume = routeVol; 1317 } 1318 } 1319 if (volume != mVolume) { 1320 mVolume = volume; 1321 dispatchRouteVolumeChanged(this); 1322 } 1323 } 1324 1325 @Override routeUpdated()1326 void routeUpdated() { 1327 int types = 0; 1328 final int count = mRoutes.size(); 1329 if (count == 0) { 1330 // Don't keep empty groups in the router. 1331 MediaRouter.removeRoute(this); 1332 return; 1333 } 1334 1335 int maxVolume = 0; 1336 boolean isLocal = true; 1337 boolean isFixedVolume = true; 1338 for (int i = 0; i < count; i++) { 1339 final RouteInfo route = mRoutes.get(i); 1340 types |= route.mSupportedTypes; 1341 final int routeMaxVolume = route.getVolumeMax(); 1342 if (routeMaxVolume > maxVolume) { 1343 maxVolume = routeMaxVolume; 1344 } 1345 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL; 1346 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED; 1347 } 1348 mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE; 1349 mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE; 1350 mSupportedTypes = types; 1351 mVolumeMax = maxVolume; 1352 mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null; 1353 super.routeUpdated(); 1354 } 1355 updateName()1356 void updateName() { 1357 final StringBuilder sb = new StringBuilder(); 1358 final int count = mRoutes.size(); 1359 for (int i = 0; i < count; i++) { 1360 final RouteInfo info = mRoutes.get(i); 1361 // TODO: There's probably a much more correct way to localize this. 1362 if (i > 0) sb.append(", "); 1363 sb.append(info.mName); 1364 } 1365 mName = sb.toString(); 1366 mUpdateName = false; 1367 } 1368 1369 @Override toString()1370 public String toString() { 1371 StringBuilder sb = new StringBuilder(super.toString()); 1372 sb.append('['); 1373 final int count = mRoutes.size(); 1374 for (int i = 0; i < count; i++) { 1375 if (i > 0) sb.append(", "); 1376 sb.append(mRoutes.get(i)); 1377 } 1378 sb.append(']'); 1379 return sb.toString(); 1380 } 1381 } 1382 1383 /** 1384 * Definition of a category of routes. All routes belong to a category. 1385 */ 1386 public static class RouteCategory { 1387 CharSequence mName; 1388 int mNameResId; 1389 int mTypes; 1390 final boolean mGroupable; 1391 RouteCategory(CharSequence name, int types, boolean groupable)1392 RouteCategory(CharSequence name, int types, boolean groupable) { 1393 mName = name; 1394 mTypes = types; 1395 mGroupable = groupable; 1396 } 1397 RouteCategory(int nameResId, int types, boolean groupable)1398 RouteCategory(int nameResId, int types, boolean groupable) { 1399 mNameResId = nameResId; 1400 mTypes = types; 1401 mGroupable = groupable; 1402 } 1403 1404 /** 1405 * @return the name of this route category 1406 */ getName()1407 public CharSequence getName() { 1408 return getName(sStatic.mResources); 1409 } 1410 1411 /** 1412 * Return the properly localized/configuration dependent name of this RouteCategory. 1413 * 1414 * @param context Context to resolve name resources 1415 * @return the name of this route category 1416 */ getName(Context context)1417 public CharSequence getName(Context context) { 1418 return getName(context.getResources()); 1419 } 1420 getName(Resources res)1421 CharSequence getName(Resources res) { 1422 if (mNameResId != 0) { 1423 return res.getText(mNameResId); 1424 } 1425 return mName; 1426 } 1427 1428 /** 1429 * Return the current list of routes in this category that have been added 1430 * to the MediaRouter. 1431 * 1432 * <p>This list will not include routes that are nested within RouteGroups. 1433 * A RouteGroup is treated as a single route within its category.</p> 1434 * 1435 * @param out a List to fill with the routes in this category. If this parameter is 1436 * non-null, it will be cleared, filled with the current routes with this 1437 * category, and returned. If this parameter is null, a new List will be 1438 * allocated to report the category's current routes. 1439 * @return A list with the routes in this category that have been added to the MediaRouter. 1440 */ getRoutes(List<RouteInfo> out)1441 public List<RouteInfo> getRoutes(List<RouteInfo> out) { 1442 if (out == null) { 1443 out = new ArrayList<RouteInfo>(); 1444 } else { 1445 out.clear(); 1446 } 1447 1448 final int count = getRouteCountStatic(); 1449 for (int i = 0; i < count; i++) { 1450 final RouteInfo route = getRouteAtStatic(i); 1451 if (route.mCategory == this) { 1452 out.add(route); 1453 } 1454 } 1455 return out; 1456 } 1457 1458 /** 1459 * @return Flag set describing the route types supported by this category 1460 */ getSupportedTypes()1461 public int getSupportedTypes() { 1462 return mTypes; 1463 } 1464 1465 /** 1466 * Return whether or not this category supports grouping. 1467 * 1468 * <p>If this method returns true, all routes obtained from this category 1469 * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> 1470 * 1471 * @return true if this category supports 1472 */ isGroupable()1473 public boolean isGroupable() { 1474 return mGroupable; 1475 } 1476 toString()1477 public String toString() { 1478 return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + 1479 " groupable=" + mGroupable + " }"; 1480 } 1481 } 1482 1483 static class CallbackInfo { 1484 public int type; 1485 public final Callback cb; 1486 public final MediaRouter router; 1487 CallbackInfo(Callback cb, int type, MediaRouter router)1488 public CallbackInfo(Callback cb, int type, MediaRouter router) { 1489 this.cb = cb; 1490 this.type = type; 1491 this.router = router; 1492 } 1493 } 1494 1495 /** 1496 * Interface for receiving events about media routing changes. 1497 * All methods of this interface will be called from the application's main thread. 1498 * 1499 * <p>A Callback will only receive events relevant to routes that the callback 1500 * was registered for.</p> 1501 * 1502 * @see MediaRouter#addCallback(int, Callback) 1503 * @see MediaRouter#removeCallback(Callback) 1504 */ 1505 public static abstract class Callback { 1506 /** 1507 * Called when the supplied route becomes selected as the active route 1508 * for the given route type. 1509 * 1510 * @param router the MediaRouter reporting the event 1511 * @param type Type flag set indicating the routes that have been selected 1512 * @param info Route that has been selected for the given route types 1513 */ onRouteSelected(MediaRouter router, int type, RouteInfo info)1514 public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info); 1515 1516 /** 1517 * Called when the supplied route becomes unselected as the active route 1518 * for the given route type. 1519 * 1520 * @param router the MediaRouter reporting the event 1521 * @param type Type flag set indicating the routes that have been unselected 1522 * @param info Route that has been unselected for the given route types 1523 */ onRouteUnselected(MediaRouter router, int type, RouteInfo info)1524 public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info); 1525 1526 /** 1527 * Called when a route for the specified type was added. 1528 * 1529 * @param router the MediaRouter reporting the event 1530 * @param info Route that has become available for use 1531 */ onRouteAdded(MediaRouter router, RouteInfo info)1532 public abstract void onRouteAdded(MediaRouter router, RouteInfo info); 1533 1534 /** 1535 * Called when a route for the specified type was removed. 1536 * 1537 * @param router the MediaRouter reporting the event 1538 * @param info Route that has been removed from availability 1539 */ onRouteRemoved(MediaRouter router, RouteInfo info)1540 public abstract void onRouteRemoved(MediaRouter router, RouteInfo info); 1541 1542 /** 1543 * Called when an aspect of the indicated route has changed. 1544 * 1545 * <p>This will not indicate that the types supported by this route have 1546 * changed, only that cosmetic info such as name or status have been updated.</p> 1547 * 1548 * @param router the MediaRouter reporting the event 1549 * @param info The route that was changed 1550 */ onRouteChanged(MediaRouter router, RouteInfo info)1551 public abstract void onRouteChanged(MediaRouter router, RouteInfo info); 1552 1553 /** 1554 * Called when a route is added to a group. 1555 * 1556 * @param router the MediaRouter reporting the event 1557 * @param info The route that was added 1558 * @param group The group the route was added to 1559 * @param index The route index within group that info was added at 1560 */ onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)1561 public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1562 int index); 1563 1564 /** 1565 * Called when a route is removed from a group. 1566 * 1567 * @param router the MediaRouter reporting the event 1568 * @param info The route that was removed 1569 * @param group The group the route was removed from 1570 */ onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)1571 public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); 1572 1573 /** 1574 * Called when a route's volume changes. 1575 * 1576 * @param router the MediaRouter reporting the event 1577 * @param info The route with altered volume 1578 */ onRouteVolumeChanged(MediaRouter router, RouteInfo info)1579 public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info); 1580 } 1581 1582 /** 1583 * Stub implementation of {@link MediaRouter.Callback}. 1584 * Each abstract method is defined as a no-op. Override just the ones 1585 * you need. 1586 */ 1587 public static class SimpleCallback extends Callback { 1588 1589 @Override onRouteSelected(MediaRouter router, int type, RouteInfo info)1590 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 1591 } 1592 1593 @Override onRouteUnselected(MediaRouter router, int type, RouteInfo info)1594 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 1595 } 1596 1597 @Override onRouteAdded(MediaRouter router, RouteInfo info)1598 public void onRouteAdded(MediaRouter router, RouteInfo info) { 1599 } 1600 1601 @Override onRouteRemoved(MediaRouter router, RouteInfo info)1602 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 1603 } 1604 1605 @Override onRouteChanged(MediaRouter router, RouteInfo info)1606 public void onRouteChanged(MediaRouter router, RouteInfo info) { 1607 } 1608 1609 @Override onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)1610 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1611 int index) { 1612 } 1613 1614 @Override onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)1615 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 1616 } 1617 1618 @Override onRouteVolumeChanged(MediaRouter router, RouteInfo info)1619 public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) { 1620 } 1621 } 1622 1623 static class VolumeCallbackInfo { 1624 public final VolumeCallback vcb; 1625 public final RouteInfo route; 1626 VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route)1627 public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) { 1628 this.vcb = vcb; 1629 this.route = route; 1630 } 1631 } 1632 1633 /** 1634 * Interface for receiving events about volume changes. 1635 * All methods of this interface will be called from the application's main thread. 1636 * 1637 * <p>A VolumeCallback will only receive events relevant to routes that the callback 1638 * was registered for.</p> 1639 * 1640 * @see UserRouteInfo#setVolumeCallback(VolumeCallback) 1641 */ 1642 public static abstract class VolumeCallback { 1643 /** 1644 * Called when the volume for the route should be increased or decreased. 1645 * @param info the route affected by this event 1646 * @param direction an integer indicating whether the volume is to be increased 1647 * (positive value) or decreased (negative value). 1648 * For bundled changes, the absolute value indicates the number of changes 1649 * in the same direction, e.g. +3 corresponds to three "volume up" changes. 1650 */ onVolumeUpdateRequest(RouteInfo info, int direction)1651 public abstract void onVolumeUpdateRequest(RouteInfo info, int direction); 1652 /** 1653 * Called when the volume for the route should be set to the given value 1654 * @param info the route affected by this event 1655 * @param volume an integer indicating the new volume value that should be used, always 1656 * between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}. 1657 */ onVolumeSetRequest(RouteInfo info, int volume)1658 public abstract void onVolumeSetRequest(RouteInfo info, int volume); 1659 } 1660 1661 static class VolumeChangeReceiver extends BroadcastReceiver { 1662 1663 @Override onReceive(Context context, Intent intent)1664 public void onReceive(Context context, Intent intent) { 1665 if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) { 1666 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, 1667 -1); 1668 if (streamType != AudioManager.STREAM_MUSIC) { 1669 return; 1670 } 1671 1672 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); 1673 final int oldVolume = intent.getIntExtra( 1674 AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); 1675 if (newVolume != oldVolume) { 1676 systemVolumeChanged(newVolume); 1677 } 1678 } 1679 } 1680 1681 } 1682 } 1683