1 /* 2 * Copyright 2019 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 static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; 20 21 import android.annotation.FlaggedApi; 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.os.Bundle; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.os.UserHandle; 29 import android.text.TextUtils; 30 31 import com.android.internal.util.Preconditions; 32 33 import java.io.FileDescriptor; 34 import java.io.PrintWriter; 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.Objects; 41 42 /** 43 * Describes a routing session which is created when a media route is selected. 44 */ 45 public final class RoutingSessionInfo implements Parcelable { 46 @NonNull 47 public static final Creator<RoutingSessionInfo> CREATOR = 48 new Creator<RoutingSessionInfo>() { 49 @Override 50 public RoutingSessionInfo createFromParcel(Parcel in) { 51 return new RoutingSessionInfo(in); 52 } 53 @Override 54 public RoutingSessionInfo[] newArray(int size) { 55 return new RoutingSessionInfo[size]; 56 } 57 }; 58 59 private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE"; 60 private static final String KEY_VOLUME_HANDLING = "volumeHandling"; 61 62 /** 63 * Indicates that the transfer happened by the default logic without explicit system's or user's 64 * request. 65 * 66 * <p>For example, an automatically connected Bluetooth device will have this transfer reason. 67 */ 68 @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) 69 public static final int TRANSFER_REASON_FALLBACK = 0; 70 71 /** Indicates that the transfer happened from within a privileged application. */ 72 @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) 73 public static final int TRANSFER_REASON_SYSTEM_REQUEST = 1; 74 75 /** Indicates that the transfer happened from a non-privileged app. */ 76 @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) 77 public static final int TRANSFER_REASON_APP = 2; 78 79 /** 80 * Indicates the transfer reason. 81 * 82 * @hide 83 */ 84 @IntDef(value = {TRANSFER_REASON_FALLBACK, TRANSFER_REASON_SYSTEM_REQUEST, TRANSFER_REASON_APP}) 85 @Retention(RetentionPolicy.SOURCE) 86 public @interface TransferReason {} 87 88 @NonNull final String mOriginalId; 89 @Nullable 90 final CharSequence mName; 91 @Nullable 92 final String mOwnerPackageName; 93 @NonNull 94 final String mClientPackageName; 95 @Nullable 96 final String mProviderId; 97 @NonNull 98 final List<String> mSelectedRoutes; 99 @NonNull 100 final List<String> mSelectableRoutes; 101 @NonNull 102 final List<String> mDeselectableRoutes; 103 @NonNull 104 final List<String> mTransferableRoutes; 105 106 @MediaRoute2Info.PlaybackVolume final int mVolumeHandling; 107 final int mVolumeMax; 108 final int mVolume; 109 110 @Nullable 111 final Bundle mControlHints; 112 final boolean mIsSystemSession; 113 114 @TransferReason final int mTransferReason; 115 116 @Nullable final UserHandle mTransferInitiatorUserHandle; 117 @Nullable final String mTransferInitiatorPackageName; 118 RoutingSessionInfo(@onNull Builder builder)119 RoutingSessionInfo(@NonNull Builder builder) { 120 Objects.requireNonNull(builder, "builder must not be null."); 121 122 mOriginalId = builder.mOriginalId; 123 mName = builder.mName; 124 mOwnerPackageName = builder.mOwnerPackageName; 125 mClientPackageName = builder.mClientPackageName; 126 mProviderId = builder.mProviderId; 127 128 mSelectedRoutes = Collections.unmodifiableList( 129 convertToUniqueRouteIds(builder.mSelectedRoutes)); 130 mSelectableRoutes = Collections.unmodifiableList( 131 convertToUniqueRouteIds(builder.mSelectableRoutes)); 132 mDeselectableRoutes = Collections.unmodifiableList( 133 convertToUniqueRouteIds(builder.mDeselectableRoutes)); 134 mTransferableRoutes = Collections.unmodifiableList( 135 convertToUniqueRouteIds(builder.mTransferableRoutes)); 136 137 mVolumeMax = builder.mVolumeMax; 138 mVolume = builder.mVolume; 139 140 mIsSystemSession = builder.mIsSystemSession; 141 mVolumeHandling = builder.mVolumeHandling; 142 143 mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling); 144 mTransferReason = builder.mTransferReason; 145 mTransferInitiatorUserHandle = builder.mTransferInitiatorUserHandle; 146 mTransferInitiatorPackageName = builder.mTransferInitiatorPackageName; 147 } 148 RoutingSessionInfo(@onNull Parcel src)149 RoutingSessionInfo(@NonNull Parcel src) { 150 mOriginalId = src.readString(); 151 Preconditions.checkArgument(!TextUtils.isEmpty(mOriginalId)); 152 153 mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src); 154 mOwnerPackageName = src.readString(); 155 mClientPackageName = ensureString(src.readString()); 156 mProviderId = src.readString(); 157 158 mSelectedRoutes = ensureList(src.createStringArrayList()); 159 Preconditions.checkArgument(!mSelectedRoutes.isEmpty()); 160 161 mSelectableRoutes = ensureList(src.createStringArrayList()); 162 mDeselectableRoutes = ensureList(src.createStringArrayList()); 163 mTransferableRoutes = ensureList(src.createStringArrayList()); 164 165 mVolumeHandling = src.readInt(); 166 mVolumeMax = src.readInt(); 167 mVolume = src.readInt(); 168 169 mControlHints = src.readBundle(); 170 mIsSystemSession = src.readBoolean(); 171 mTransferReason = src.readInt(); 172 mTransferInitiatorUserHandle = UserHandle.readFromParcel(src); 173 mTransferInitiatorPackageName = src.readString(); 174 } 175 176 @Nullable updateVolumeHandlingInHints(@ullable Bundle controlHints, int volumeHandling)177 private static Bundle updateVolumeHandlingInHints(@Nullable Bundle controlHints, 178 int volumeHandling) { 179 // Workaround to preserve retro-compatibility with androidx. 180 // See b/228021646 for more details. 181 if (controlHints != null && controlHints.containsKey(KEY_GROUP_ROUTE)) { 182 Bundle groupRoute = controlHints.getBundle(KEY_GROUP_ROUTE); 183 184 if (groupRoute != null && groupRoute.containsKey(KEY_VOLUME_HANDLING) 185 && volumeHandling != groupRoute.getInt(KEY_VOLUME_HANDLING)) { 186 //Creating copy of controlHints with updated value. 187 Bundle newGroupRoute = new Bundle(groupRoute); 188 newGroupRoute.putInt(KEY_VOLUME_HANDLING, volumeHandling); 189 Bundle newControlHints = new Bundle(controlHints); 190 newControlHints.putBundle(KEY_GROUP_ROUTE, newGroupRoute); 191 return newControlHints; 192 } 193 } 194 //Return same Bundle. 195 return controlHints; 196 } 197 198 @NonNull ensureString(@ullable String str)199 private static String ensureString(@Nullable String str) { 200 return str != null ? str : ""; 201 } 202 203 @NonNull ensureList(@ullable List<? extends T> list)204 private static <T> List<T> ensureList(@Nullable List<? extends T> list) { 205 if (list != null) { 206 return Collections.unmodifiableList(list); 207 } 208 return Collections.emptyList(); 209 } 210 211 /** 212 * Gets the id of the session. The sessions which are given by {@link MediaRouter2} will have 213 * unique IDs. 214 * <p> 215 * In order to ensure uniqueness in {@link MediaRouter2} side, the value of this method 216 * can be different from what was set in {@link MediaRoute2ProviderService}. 217 * 218 * @see Builder#Builder(String, String) 219 */ 220 @NonNull getId()221 public String getId() { 222 if (!TextUtils.isEmpty(mProviderId)) { 223 return MediaRouter2Utils.toUniqueId(mProviderId, mOriginalId); 224 } else { 225 return mOriginalId; 226 } 227 } 228 229 /** 230 * Gets the user-visible name of the session. It may be {@code null}. 231 */ 232 @Nullable getName()233 public CharSequence getName() { 234 return mName; 235 } 236 237 /** 238 * Gets the original id as assigned by the {@link MediaRoute2ProviderService route provider}. 239 * 240 * <p>This may be different from {@link #getId()}, which may convert this original id into a 241 * unique one by adding information about the provider that created this session info. 242 * 243 * @hide 244 */ 245 @NonNull getOriginalId()246 public String getOriginalId() { 247 return mOriginalId; 248 } 249 250 /** 251 * Gets the package name of the session owner. 252 * @hide 253 */ 254 @Nullable getOwnerPackageName()255 public String getOwnerPackageName() { 256 return mOwnerPackageName; 257 } 258 259 /** 260 * Gets the client package name of the session 261 */ 262 @NonNull getClientPackageName()263 public String getClientPackageName() { 264 return mClientPackageName; 265 } 266 267 /** 268 * Gets the provider ID of the session. 269 * 270 * @hide 271 */ 272 @Nullable getProviderId()273 public String getProviderId() { 274 return mProviderId; 275 } 276 277 /** 278 * Gets the list of IDs of selected routes for the session. 279 * 280 * <p>Selected routes are the routes that this session is actively routing media to. 281 * 282 * <p>The behavior of a routing session with multiple selected routes is ultimately defined by 283 * the {@link MediaRoute2ProviderService} implementation. However, typically, it's expected that 284 * all the selected routes of a routing session are playing the same media in sync. 285 * 286 * @return A non-empty list of selected route ids. 287 */ 288 @NonNull getSelectedRoutes()289 public List<String> getSelectedRoutes() { 290 return mSelectedRoutes; 291 } 292 293 /** 294 * Gets the list of IDs of selectable routes for the session. 295 * 296 * <p>Selectable routes can be added to a routing session (via {@link 297 * MediaRouter2.RoutingController#selectRoute}) in order to add them to the {@link 298 * #getSelectedRoutes() selected routes}, so that media plays on the newly selected route along 299 * with the other selected routes. 300 * 301 * <p>Not to be confused with {@link #getTransferableRoutes() transferable routes}. Transferring 302 * to a route makes it the sole selected route. 303 * 304 * @return A possibly empty list of selectable route ids. 305 */ 306 @NonNull getSelectableRoutes()307 public List<String> getSelectableRoutes() { 308 return mSelectableRoutes; 309 } 310 311 /** 312 * Gets the list of IDs of deselectable routes for the session. 313 * 314 * <p>Deselectable routes can be removed from the {@link #getSelectedRoutes() selected routes}, 315 * so that the routing session stops routing to the newly deselected route, but continues on any 316 * remaining selected routes. 317 * 318 * <p>Deselectable routes should be a subset of the {@link #getSelectedRoutes() selected 319 * routes}, meaning not all of the selected routes might be deselectable. For example, one of 320 * the selected routes may be a leader device coordinating group playback, which must always 321 * remain selected while the session is active. 322 * 323 * @return A possibly empty list of deselectable route ids. 324 */ 325 @NonNull getDeselectableRoutes()326 public List<String> getDeselectableRoutes() { 327 return mDeselectableRoutes; 328 } 329 330 /** 331 * Gets the list of IDs of transferable routes for the session. 332 * 333 * <p>Transferring to a route (for example, using {@link MediaRouter2#transferTo}) replaces the 334 * list of {@link #getSelectedRoutes() selected routes} with the target route, causing playback 335 * to move from one route to another. 336 * 337 * <p>Note that this is different from {@link #getSelectableRoutes() selectable routes}, because 338 * selecting a route makes it part of the selected routes, while transferring to a route makes 339 * it the selected route. A route can be both transferable and selectable. 340 * 341 * <p>Note that playback may transfer across routes without the target route being in the list 342 * of transferable routes. This can happen by creating a new routing session to the target 343 * route, and releasing the routing session being transferred from. The difference is that a 344 * transfer to a route in the transferable list can happen with no intervention from the app, 345 * with the route provider taking care of the entire operation. A transfer to a route that is 346 * not in the list of transferable routes (by creating a new session) requires the app to move 347 * the playback state from one device to the other. 348 * 349 * @return A possibly empty list of transferable route ids. 350 */ 351 @NonNull getTransferableRoutes()352 public List<String> getTransferableRoutes() { 353 return mTransferableRoutes; 354 } 355 356 /** 357 * Gets the information about how volume is handled on the session. 358 * 359 * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or 360 * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}. 361 */ 362 @MediaRoute2Info.PlaybackVolume getVolumeHandling()363 public int getVolumeHandling() { 364 return mVolumeHandling; 365 } 366 367 /** 368 * Gets the maximum volume of the session. 369 */ getVolumeMax()370 public int getVolumeMax() { 371 return mVolumeMax; 372 } 373 374 /** 375 * Gets the current volume of the session. 376 * <p> 377 * When it's available, it represents the volume of routing session, which is a group 378 * of selected routes. To get the volume of each route, use {@link MediaRoute2Info#getVolume()}. 379 * </p> 380 * @see MediaRoute2Info#getVolume() 381 */ getVolume()382 public int getVolume() { 383 return mVolume; 384 } 385 386 /** 387 * Gets the control hints 388 */ 389 @Nullable getControlHints()390 public Bundle getControlHints() { 391 return mControlHints; 392 } 393 394 /** 395 * Gets whether this session is in system media route provider. 396 * @hide 397 */ 398 @Nullable isSystemSession()399 public boolean isSystemSession() { 400 return mIsSystemSession; 401 } 402 403 /** Returns the transfer reason for this routing session. */ 404 @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) 405 @TransferReason getTransferReason()406 public int getTransferReason() { 407 return mTransferReason; 408 } 409 410 /** @hide */ 411 @Nullable getTransferInitiatorUserHandle()412 public UserHandle getTransferInitiatorUserHandle() { 413 return mTransferInitiatorUserHandle; 414 } 415 416 /** @hide */ 417 @Nullable getTransferInitiatorPackageName()418 public String getTransferInitiatorPackageName() { 419 return mTransferInitiatorPackageName; 420 } 421 422 @Override describeContents()423 public int describeContents() { 424 return 0; 425 } 426 427 @Override writeToParcel(@onNull Parcel dest, int flags)428 public void writeToParcel(@NonNull Parcel dest, int flags) { 429 dest.writeString(mOriginalId); 430 dest.writeCharSequence(mName); 431 dest.writeString(mOwnerPackageName); 432 dest.writeString(mClientPackageName); 433 dest.writeString(mProviderId); 434 dest.writeStringList(mSelectedRoutes); 435 dest.writeStringList(mSelectableRoutes); 436 dest.writeStringList(mDeselectableRoutes); 437 dest.writeStringList(mTransferableRoutes); 438 dest.writeInt(mVolumeHandling); 439 dest.writeInt(mVolumeMax); 440 dest.writeInt(mVolume); 441 dest.writeBundle(mControlHints); 442 dest.writeBoolean(mIsSystemSession); 443 dest.writeInt(mTransferReason); 444 UserHandle.writeToParcel(mTransferInitiatorUserHandle, dest); 445 dest.writeString(mTransferInitiatorPackageName); 446 } 447 448 /** 449 * Dumps current state of the instance. Use with {@code dumpsys}. 450 * 451 * See {@link android.os.Binder#dump(FileDescriptor, PrintWriter, String[])}. 452 * 453 * @hide 454 */ dump(@onNull PrintWriter pw, @NonNull String prefix)455 public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { 456 pw.println(prefix + "RoutingSessionInfo"); 457 458 String indent = prefix + " "; 459 460 pw.println(indent + "mOriginalId=" + mOriginalId); 461 pw.println(indent + "mName=" + mName); 462 pw.println(indent + "mOwnerPackageName=" + mOwnerPackageName); 463 pw.println(indent + "mClientPackageName=" + mClientPackageName); 464 pw.println(indent + "mProviderId=" + mProviderId); 465 pw.println(indent + "mSelectedRoutes=" + mSelectedRoutes); 466 pw.println(indent + "mSelectableRoutes=" + mSelectableRoutes); 467 pw.println(indent + "mDeselectableRoutes=" + mDeselectableRoutes); 468 pw.println(indent + "mTransferableRoutes=" + mTransferableRoutes); 469 pw.println(indent + MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling)); 470 pw.println(indent + "mControlHints=" + mControlHints); 471 pw.println(indent + "mIsSystemSession=" + mIsSystemSession); 472 pw.println(indent + "mTransferReason=" + mTransferReason); 473 pw.println(indent + "mtransferInitiatorUserHandle=" + mTransferInitiatorUserHandle); 474 pw.println(indent + "mtransferInitiatorPackageName=" + mTransferInitiatorPackageName); 475 } 476 477 @Override equals(@ullable Object obj)478 public boolean equals(@Nullable Object obj) { 479 if (obj == null) { 480 return false; 481 } 482 483 if (this == obj) { 484 return true; 485 } 486 if (getClass() != obj.getClass()) { 487 return false; 488 } 489 490 RoutingSessionInfo other = (RoutingSessionInfo) obj; 491 return Objects.equals(mOriginalId, other.mOriginalId) 492 && Objects.equals(mName, other.mName) 493 && Objects.equals(mOwnerPackageName, other.mOwnerPackageName) 494 && Objects.equals(mClientPackageName, other.mClientPackageName) 495 && Objects.equals(mProviderId, other.mProviderId) 496 && Objects.equals(mSelectedRoutes, other.mSelectedRoutes) 497 && Objects.equals(mSelectableRoutes, other.mSelectableRoutes) 498 && Objects.equals(mDeselectableRoutes, other.mDeselectableRoutes) 499 && Objects.equals(mTransferableRoutes, other.mTransferableRoutes) 500 && (mVolumeHandling == other.mVolumeHandling) 501 && (mVolumeMax == other.mVolumeMax) 502 && (mVolume == other.mVolume) 503 && (mTransferReason == other.mTransferReason) 504 && Objects.equals(mTransferInitiatorUserHandle, other.mTransferInitiatorUserHandle) 505 && Objects.equals( 506 mTransferInitiatorPackageName, other.mTransferInitiatorPackageName); 507 } 508 509 @Override hashCode()510 public int hashCode() { 511 return Objects.hash( 512 mOriginalId, 513 mName, 514 mOwnerPackageName, 515 mClientPackageName, 516 mProviderId, 517 mSelectedRoutes, 518 mSelectableRoutes, 519 mDeselectableRoutes, 520 mTransferableRoutes, 521 mVolumeMax, 522 mVolumeHandling, 523 mVolume, 524 mTransferReason, 525 mTransferInitiatorUserHandle, 526 mTransferInitiatorPackageName); 527 } 528 529 @Override toString()530 public String toString() { 531 return new StringBuilder() 532 .append("RoutingSessionInfo{ ") 533 .append("sessionId=") 534 .append(getId()) 535 .append(", name=") 536 .append(getName()) 537 .append(", clientPackageName=") 538 .append(getClientPackageName()) 539 .append(", selectedRoutes={") 540 .append(String.join(",", getSelectedRoutes())) 541 .append("}") 542 .append(", selectableRoutes={") 543 .append(String.join(",", getSelectableRoutes())) 544 .append("}") 545 .append(", deselectableRoutes={") 546 .append(String.join(",", getDeselectableRoutes())) 547 .append("}") 548 .append(", transferableRoutes={") 549 .append(String.join(",", getTransferableRoutes())) 550 .append("}") 551 .append(", ") 552 .append(MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling)) 553 .append(", transferReason=") 554 .append(getTransferReason()) 555 .append(", transferInitiatorUserHandle=") 556 .append(getTransferInitiatorUserHandle()) 557 .append(", transferInitiatorPackageName=") 558 .append(getTransferInitiatorPackageName()) 559 .append(" }") 560 .toString(); 561 } 562 563 /** 564 * Provides a new list with unique route IDs if {@link #mProviderId} is set, or the original IDs 565 * otherwise. 566 * 567 * @param routeIds list of route IDs to convert 568 * @return new list with unique IDs or original IDs 569 */ 570 571 @NonNull convertToUniqueRouteIds(@onNull List<String> routeIds)572 private List<String> convertToUniqueRouteIds(@NonNull List<String> routeIds) { 573 Objects.requireNonNull(routeIds, "RouteIds cannot be null."); 574 575 // mProviderId can be null if not set. Return the original list for this case. 576 if (TextUtils.isEmpty(mProviderId)) { 577 return new ArrayList<>(routeIds); 578 } 579 580 List<String> result = new ArrayList<>(); 581 for (String routeId : routeIds) { 582 result.add(MediaRouter2Utils.toUniqueId(mProviderId, routeId)); 583 } 584 return result; 585 } 586 587 /** 588 * Builder class for {@link RoutingSessionInfo}. 589 */ 590 public static final class Builder { 591 @NonNull private final String mOriginalId; 592 @Nullable 593 private CharSequence mName; 594 @Nullable 595 private String mOwnerPackageName; 596 @NonNull 597 private String mClientPackageName; 598 @Nullable 599 private String mProviderId; 600 @NonNull 601 private final List<String> mSelectedRoutes; 602 @NonNull 603 private final List<String> mSelectableRoutes; 604 @NonNull 605 private final List<String> mDeselectableRoutes; 606 @NonNull 607 private final List<String> mTransferableRoutes; 608 @MediaRoute2Info.PlaybackVolume 609 private int mVolumeHandling = MediaRoute2Info.PLAYBACK_VOLUME_FIXED; 610 private int mVolumeMax; 611 private int mVolume; 612 @Nullable 613 private Bundle mControlHints; 614 private boolean mIsSystemSession; 615 @TransferReason private int mTransferReason = TRANSFER_REASON_FALLBACK; 616 @Nullable private UserHandle mTransferInitiatorUserHandle; 617 @Nullable private String mTransferInitiatorPackageName; 618 619 /** 620 * Constructor for builder to create {@link RoutingSessionInfo}. 621 * 622 * <p>In order to ensure ID uniqueness in {@link MediaRouter2} side, the value of {@link 623 * RoutingSessionInfo#getId()} can be different from what was set in {@link 624 * MediaRoute2ProviderService}. 625 * 626 * @param originalId ID of the session. Must not be empty. 627 * @param clientPackageName package name of the client app which uses this session. If is is 628 * unknown, then just use an empty string. 629 * @see MediaRoute2Info#getId() 630 */ Builder(@onNull String originalId, @NonNull String clientPackageName)631 public Builder(@NonNull String originalId, @NonNull String clientPackageName) { 632 if (TextUtils.isEmpty(originalId)) { 633 throw new IllegalArgumentException("id must not be empty"); 634 } 635 636 mOriginalId = originalId; 637 mClientPackageName = 638 Objects.requireNonNull(clientPackageName, "clientPackageName must not be null"); 639 mSelectedRoutes = new ArrayList<>(); 640 mSelectableRoutes = new ArrayList<>(); 641 mDeselectableRoutes = new ArrayList<>(); 642 mTransferableRoutes = new ArrayList<>(); 643 } 644 645 /** 646 * Constructor for builder to create {@link RoutingSessionInfo} with 647 * existing {@link RoutingSessionInfo} instance. 648 * 649 * @param sessionInfo the existing instance to copy data from. 650 */ Builder(@onNull RoutingSessionInfo sessionInfo)651 public Builder(@NonNull RoutingSessionInfo sessionInfo) { 652 this(sessionInfo, sessionInfo.getOriginalId()); 653 } 654 655 /** 656 * Builds upon the given {@code sessionInfo}, using the given {@link #getOriginalId()} for 657 * the id. 658 * 659 * @hide 660 */ Builder(@onNull RoutingSessionInfo sessionInfo, String originalId)661 public Builder(@NonNull RoutingSessionInfo sessionInfo, String originalId) { 662 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 663 664 mOriginalId = originalId; 665 mName = sessionInfo.mName; 666 mClientPackageName = sessionInfo.mClientPackageName; 667 mProviderId = sessionInfo.mProviderId; 668 669 mSelectedRoutes = new ArrayList<>(sessionInfo.mSelectedRoutes); 670 mSelectableRoutes = new ArrayList<>(sessionInfo.mSelectableRoutes); 671 mDeselectableRoutes = new ArrayList<>(sessionInfo.mDeselectableRoutes); 672 mTransferableRoutes = new ArrayList<>(sessionInfo.mTransferableRoutes); 673 674 if (mProviderId != null) { 675 // They must have unique IDs. 676 mSelectedRoutes.replaceAll(MediaRouter2Utils::getOriginalId); 677 mSelectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId); 678 mDeselectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId); 679 mTransferableRoutes.replaceAll(MediaRouter2Utils::getOriginalId); 680 } 681 682 mVolumeHandling = sessionInfo.mVolumeHandling; 683 mVolumeMax = sessionInfo.mVolumeMax; 684 mVolume = sessionInfo.mVolume; 685 686 mControlHints = sessionInfo.mControlHints; 687 mIsSystemSession = sessionInfo.mIsSystemSession; 688 mTransferReason = sessionInfo.mTransferReason; 689 mTransferInitiatorUserHandle = sessionInfo.mTransferInitiatorUserHandle; 690 mTransferInitiatorPackageName = sessionInfo.mTransferInitiatorPackageName; 691 } 692 693 /** 694 * Sets the user-visible name of the session. 695 */ 696 @NonNull setName(@ullable CharSequence name)697 public Builder setName(@Nullable CharSequence name) { 698 mName = name; 699 return this; 700 } 701 702 /** 703 * Sets the package name of the session owner. It is expected to be called by the system. 704 * 705 * @hide 706 */ 707 @NonNull setOwnerPackageName(@ullable String packageName)708 public Builder setOwnerPackageName(@Nullable String packageName) { 709 mOwnerPackageName = packageName; 710 return this; 711 } 712 713 /** 714 * Sets the client package name of the session. 715 * 716 * @hide 717 */ 718 @NonNull setClientPackageName(@ullable String packageName)719 public Builder setClientPackageName(@Nullable String packageName) { 720 mClientPackageName = packageName; 721 return this; 722 } 723 724 /** 725 * Sets the provider ID of the session. 726 * 727 * @hide 728 */ 729 @NonNull setProviderId(@onNull String providerId)730 public Builder setProviderId(@NonNull String providerId) { 731 if (TextUtils.isEmpty(providerId)) { 732 throw new IllegalArgumentException("providerId must not be empty"); 733 } 734 mProviderId = providerId; 735 return this; 736 } 737 738 /** 739 * Clears the selected routes. 740 */ 741 @NonNull clearSelectedRoutes()742 public Builder clearSelectedRoutes() { 743 mSelectedRoutes.clear(); 744 return this; 745 } 746 747 /** 748 * Adds a route to the selected routes. The {@code routeId} must not be empty. 749 */ 750 @NonNull addSelectedRoute(@onNull String routeId)751 public Builder addSelectedRoute(@NonNull String routeId) { 752 if (TextUtils.isEmpty(routeId)) { 753 throw new IllegalArgumentException("routeId must not be empty"); 754 } 755 mSelectedRoutes.add(routeId); 756 return this; 757 } 758 759 /** 760 * Removes a route from the selected routes. The {@code routeId} must not be empty. 761 */ 762 @NonNull removeSelectedRoute(@onNull String routeId)763 public Builder removeSelectedRoute(@NonNull String routeId) { 764 if (TextUtils.isEmpty(routeId)) { 765 throw new IllegalArgumentException("routeId must not be empty"); 766 } 767 mSelectedRoutes.remove(routeId); 768 return this; 769 } 770 771 /** 772 * Clears the selectable routes. 773 */ 774 @NonNull clearSelectableRoutes()775 public Builder clearSelectableRoutes() { 776 mSelectableRoutes.clear(); 777 return this; 778 } 779 780 /** 781 * Adds a route to the selectable routes. The {@code routeId} must not be empty. 782 */ 783 @NonNull addSelectableRoute(@onNull String routeId)784 public Builder addSelectableRoute(@NonNull String routeId) { 785 if (TextUtils.isEmpty(routeId)) { 786 throw new IllegalArgumentException("routeId must not be empty"); 787 } 788 mSelectableRoutes.add(routeId); 789 return this; 790 } 791 792 /** 793 * Removes a route from the selectable routes. The {@code routeId} must not be empty. 794 */ 795 @NonNull removeSelectableRoute(@onNull String routeId)796 public Builder removeSelectableRoute(@NonNull String routeId) { 797 if (TextUtils.isEmpty(routeId)) { 798 throw new IllegalArgumentException("routeId must not be empty"); 799 } 800 mSelectableRoutes.remove(routeId); 801 return this; 802 } 803 804 /** 805 * Clears the deselectable routes. 806 */ 807 @NonNull clearDeselectableRoutes()808 public Builder clearDeselectableRoutes() { 809 mDeselectableRoutes.clear(); 810 return this; 811 } 812 813 /** 814 * Adds a route to the deselectable routes. The {@code routeId} must not be empty. 815 */ 816 @NonNull addDeselectableRoute(@onNull String routeId)817 public Builder addDeselectableRoute(@NonNull String routeId) { 818 if (TextUtils.isEmpty(routeId)) { 819 throw new IllegalArgumentException("routeId must not be empty"); 820 } 821 mDeselectableRoutes.add(routeId); 822 return this; 823 } 824 825 /** 826 * Removes a route from the deselectable routes. The {@code routeId} must not be empty. 827 */ 828 @NonNull removeDeselectableRoute(@onNull String routeId)829 public Builder removeDeselectableRoute(@NonNull String routeId) { 830 if (TextUtils.isEmpty(routeId)) { 831 throw new IllegalArgumentException("routeId must not be empty"); 832 } 833 mDeselectableRoutes.remove(routeId); 834 return this; 835 } 836 837 /** 838 * Clears the transferable routes. 839 */ 840 @NonNull clearTransferableRoutes()841 public Builder clearTransferableRoutes() { 842 mTransferableRoutes.clear(); 843 return this; 844 } 845 846 /** 847 * Adds a route to the transferable routes. The {@code routeId} must not be empty. 848 */ 849 @NonNull addTransferableRoute(@onNull String routeId)850 public Builder addTransferableRoute(@NonNull String routeId) { 851 if (TextUtils.isEmpty(routeId)) { 852 throw new IllegalArgumentException("routeId must not be empty"); 853 } 854 mTransferableRoutes.add(routeId); 855 return this; 856 } 857 858 /** 859 * Removes a route from the transferable routes. The {@code routeId} must not be empty. 860 */ 861 @NonNull removeTransferableRoute(@onNull String routeId)862 public Builder removeTransferableRoute(@NonNull String routeId) { 863 if (TextUtils.isEmpty(routeId)) { 864 throw new IllegalArgumentException("routeId must not be empty"); 865 } 866 mTransferableRoutes.remove(routeId); 867 return this; 868 } 869 870 /** 871 * Sets the session's volume handling. 872 * {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or 873 * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}. 874 */ 875 @NonNull setVolumeHandling( @ediaRoute2Info.PlaybackVolume int volumeHandling)876 public RoutingSessionInfo.Builder setVolumeHandling( 877 @MediaRoute2Info.PlaybackVolume int volumeHandling) { 878 mVolumeHandling = volumeHandling; 879 return this; 880 } 881 882 /** 883 * Sets the session's maximum volume, or 0 if unknown. 884 */ 885 @NonNull setVolumeMax(int volumeMax)886 public RoutingSessionInfo.Builder setVolumeMax(int volumeMax) { 887 mVolumeMax = volumeMax; 888 return this; 889 } 890 891 /** 892 * Sets the session's current volume, or 0 if unknown. 893 */ 894 @NonNull setVolume(int volume)895 public RoutingSessionInfo.Builder setVolume(int volume) { 896 mVolume = volume; 897 return this; 898 } 899 900 /** 901 * Sets control hints. 902 */ 903 @NonNull setControlHints(@ullable Bundle controlHints)904 public Builder setControlHints(@Nullable Bundle controlHints) { 905 mControlHints = controlHints; 906 return this; 907 } 908 909 /** 910 * Sets whether this session is in system media route provider. 911 * @hide 912 */ 913 @NonNull setSystemSession(boolean isSystemSession)914 public Builder setSystemSession(boolean isSystemSession) { 915 mIsSystemSession = isSystemSession; 916 return this; 917 } 918 919 /** 920 * Sets transfer reason for the current session. 921 * 922 * <p>By default the transfer reason is set to {@link 923 * RoutingSessionInfo#TRANSFER_REASON_FALLBACK}. 924 */ 925 @NonNull 926 @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) setTransferReason(@ransferReason int transferReason)927 public Builder setTransferReason(@TransferReason int transferReason) { 928 mTransferReason = transferReason; 929 return this; 930 } 931 932 /** 933 * Sets the user handle and package name of the process that initiated the transfer. 934 * 935 * <p>By default the transfer initiation user handle and package name are set to {@code 936 * null}. 937 */ 938 // The UserHandleName warning suggests the name should be "doFooAsUser". But the UserHandle 939 // parameter of this function is stored in a field, and not used to execute an operation on 940 // a specific user. 941 // The MissingGetterMatchingBuilder requires a getTransferInitiator function. But said 942 // getter is not included because the returned package name and user handle is always either 943 // null or the values that correspond to the calling app, and that information is obtainable 944 // via RoutingController#wasTransferInitiatedBySelf. 945 @SuppressWarnings({"UserHandleName", "MissingGetterMatchingBuilder"}) 946 @NonNull 947 @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) setTransferInitiator( @ullable UserHandle transferInitiatorUserHandle, @Nullable String transferInitiatorPackageName)948 public Builder setTransferInitiator( 949 @Nullable UserHandle transferInitiatorUserHandle, 950 @Nullable String transferInitiatorPackageName) { 951 mTransferInitiatorUserHandle = transferInitiatorUserHandle; 952 mTransferInitiatorPackageName = transferInitiatorPackageName; 953 return this; 954 } 955 956 /** 957 * Builds a routing session info. 958 * 959 * @throws IllegalArgumentException if no selected routes are added. 960 */ 961 @NonNull build()962 public RoutingSessionInfo build() { 963 if (mSelectedRoutes.isEmpty()) { 964 throw new IllegalArgumentException("selectedRoutes must not be empty"); 965 } 966 return new RoutingSessionInfo(this); 967 } 968 } 969 } 970