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