1 /* 2 * Copyright (C) 2022 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.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.content.ComponentName; 23 import android.content.Intent; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.text.TextUtils; 27 28 import com.android.internal.util.Preconditions; 29 30 import java.lang.annotation.Retention; 31 import java.lang.annotation.RetentionPolicy; 32 import java.util.ArrayList; 33 import java.util.List; 34 import java.util.Locale; 35 import java.util.Objects; 36 37 /** 38 * Allows applications to customize the list of routes used for media routing (for example, in the 39 * System UI Output Switcher). 40 * 41 * @see MediaRouter2#setRouteListingPreference 42 * @see Item 43 */ 44 public final class RouteListingPreference implements Parcelable { 45 46 /** 47 * {@link Intent} action that the system uses to take the user the app when the user selects an 48 * {@link Item} whose {@link Item#getSelectionBehavior() selection behavior} is {@link 49 * Item#SELECTION_BEHAVIOR_GO_TO_APP}. 50 * 51 * <p>The launched intent will identify the selected item using the extra identified by {@link 52 * #EXTRA_ROUTE_ID}. 53 * 54 * @see #getLinkedItemComponentName() 55 * @see Item#SELECTION_BEHAVIOR_GO_TO_APP 56 */ 57 public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA"; 58 59 /** 60 * {@link Intent} string extra key that contains the {@link Item#getRouteId() id} of the route 61 * to transfer to, as part of an {@link #ACTION_TRANSFER_MEDIA} intent. 62 * 63 * @see #getLinkedItemComponentName() 64 * @see Item#SELECTION_BEHAVIOR_GO_TO_APP 65 */ 66 public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID"; 67 68 @NonNull 69 public static final Creator<RouteListingPreference> CREATOR = 70 new Creator<>() { 71 @Override 72 public RouteListingPreference createFromParcel(Parcel in) { 73 return new RouteListingPreference(in); 74 } 75 76 @Override 77 public RouteListingPreference[] newArray(int size) { 78 return new RouteListingPreference[size]; 79 } 80 }; 81 82 @NonNull private final List<Item> mItems; 83 private final boolean mUseSystemOrdering; 84 @Nullable private final ComponentName mLinkedItemComponentName; 85 RouteListingPreference(Builder builder)86 private RouteListingPreference(Builder builder) { 87 mItems = builder.mItems; 88 mUseSystemOrdering = builder.mUseSystemOrdering; 89 mLinkedItemComponentName = builder.mLinkedItemComponentName; 90 } 91 RouteListingPreference(Parcel in)92 private RouteListingPreference(Parcel in) { 93 List<Item> items = 94 in.readParcelableList(new ArrayList<>(), Item.class.getClassLoader(), Item.class); 95 mItems = List.copyOf(items); 96 mUseSystemOrdering = in.readBoolean(); 97 mLinkedItemComponentName = ComponentName.readFromParcel(in); 98 } 99 100 /** 101 * Returns an unmodifiable list containing the {@link Item items} that the app wants to be 102 * listed for media routing. 103 */ 104 @NonNull getItems()105 public List<Item> getItems() { 106 return mItems; 107 } 108 109 /** 110 * Returns true if the application would like media route listing to use the system's ordering 111 * strategy, or false if the application would like route listing to respect the ordering 112 * obtained from {@link #getItems()}. 113 * 114 * <p>The system's ordering strategy is implementation-dependent, but may take into account each 115 * route's recency or frequency of use in order to rank them. 116 */ getUseSystemOrdering()117 public boolean getUseSystemOrdering() { 118 return mUseSystemOrdering; 119 } 120 121 /** 122 * Returns a {@link ComponentName} for navigating to the application. 123 * 124 * <p>Must not be null if any of the {@link #getItems() items} of this route listing preference 125 * has {@link Item#getSelectionBehavior() selection behavior} {@link 126 * Item#SELECTION_BEHAVIOR_GO_TO_APP}. 127 * 128 * <p>The system navigates to the application when the user selects {@link Item} with {@link 129 * Item#SELECTION_BEHAVIOR_GO_TO_APP} by launching an intent to the returned {@link 130 * ComponentName}, using action {@link #ACTION_TRANSFER_MEDIA}, with the extra {@link 131 * #EXTRA_ROUTE_ID}. 132 */ 133 @Nullable getLinkedItemComponentName()134 public ComponentName getLinkedItemComponentName() { 135 return mLinkedItemComponentName; 136 } 137 138 // RouteListingPreference Parcelable implementation. 139 140 @Override describeContents()141 public int describeContents() { 142 return 0; 143 } 144 145 @Override writeToParcel(@onNull Parcel dest, int flags)146 public void writeToParcel(@NonNull Parcel dest, int flags) { 147 dest.writeParcelableList(mItems, flags); 148 dest.writeBoolean(mUseSystemOrdering); 149 ComponentName.writeToParcel(mLinkedItemComponentName, dest); 150 } 151 152 // Equals and hashCode. 153 154 @Override equals(Object other)155 public boolean equals(Object other) { 156 if (this == other) { 157 return true; 158 } 159 if (!(other instanceof RouteListingPreference)) { 160 return false; 161 } 162 RouteListingPreference that = (RouteListingPreference) other; 163 return mItems.equals(that.mItems) 164 && mUseSystemOrdering == that.mUseSystemOrdering 165 && Objects.equals(mLinkedItemComponentName, that.mLinkedItemComponentName); 166 } 167 168 @Override hashCode()169 public int hashCode() { 170 return Objects.hash(mItems, mUseSystemOrdering, mLinkedItemComponentName); 171 } 172 173 /** Builder for {@link RouteListingPreference}. */ 174 public static final class Builder { 175 176 private List<Item> mItems; 177 private boolean mUseSystemOrdering; 178 private ComponentName mLinkedItemComponentName; 179 180 /** Creates a new instance with default values (documented in the setters). */ Builder()181 public Builder() { 182 mItems = List.of(); 183 mUseSystemOrdering = true; 184 } 185 186 /** 187 * See {@link #getItems()} 188 * 189 * <p>The default value is an empty list. 190 */ 191 @NonNull setItems(@onNull List<Item> items)192 public Builder setItems(@NonNull List<Item> items) { 193 mItems = List.copyOf(Objects.requireNonNull(items)); 194 return this; 195 } 196 197 /** 198 * See {@link #getUseSystemOrdering()} 199 * 200 * <p>The default value is {@code true}. 201 */ 202 // Lint requires "isUseSystemOrdering", but "getUseSystemOrdering" is a better name. 203 @SuppressWarnings("MissingGetterMatchingBuilder") 204 @NonNull setUseSystemOrdering(boolean useSystemOrdering)205 public Builder setUseSystemOrdering(boolean useSystemOrdering) { 206 mUseSystemOrdering = useSystemOrdering; 207 return this; 208 } 209 210 /** 211 * See {@link #getLinkedItemComponentName()}. 212 * 213 * <p>The default value is {@code null}. 214 */ 215 @NonNull setLinkedItemComponentName(@ullable ComponentName linkedItemComponentName)216 public Builder setLinkedItemComponentName(@Nullable ComponentName linkedItemComponentName) { 217 mLinkedItemComponentName = linkedItemComponentName; 218 return this; 219 } 220 221 /** 222 * Creates and returns a new {@link RouteListingPreference} instance with the given 223 * parameters. 224 */ 225 @NonNull build()226 public RouteListingPreference build() { 227 return new RouteListingPreference(this); 228 } 229 } 230 231 /** Holds preference information for a specific route in a {@link RouteListingPreference}. */ 232 public static final class Item implements Parcelable { 233 234 /** @hide */ 235 @Retention(RetentionPolicy.SOURCE) 236 @IntDef( 237 prefix = {"SELECTION_BEHAVIOR_"}, 238 value = { 239 SELECTION_BEHAVIOR_NONE, 240 SELECTION_BEHAVIOR_TRANSFER, 241 SELECTION_BEHAVIOR_GO_TO_APP 242 }) 243 public @interface SelectionBehavior {} 244 245 /** The corresponding route is not selectable by the user. */ 246 public static final int SELECTION_BEHAVIOR_NONE = 0; 247 /** If the user selects the corresponding route, the media transfers to the said route. */ 248 public static final int SELECTION_BEHAVIOR_TRANSFER = 1; 249 /** 250 * If the user selects the corresponding route, the system takes the user to the 251 * application. 252 * 253 * <p>The system uses {@link #getLinkedItemComponentName()} in order to navigate to the app. 254 */ 255 public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; 256 257 /** @hide */ 258 @Retention(RetentionPolicy.SOURCE) 259 @IntDef( 260 flag = true, 261 prefix = {"FLAG_"}, 262 value = {FLAG_ONGOING_SESSION, FLAG_ONGOING_SESSION_MANAGED, FLAG_SUGGESTED}) 263 public @interface Flags {} 264 265 /** 266 * The corresponding route is already hosting a session with the app that owns this listing 267 * preference. 268 */ 269 public static final int FLAG_ONGOING_SESSION = 1; 270 271 /** 272 * Signals that the ongoing session on the corresponding route is managed by the current 273 * user of the app. 274 * 275 * <p>The system can use this flag to provide visual indication that the route is not only 276 * hosting a session, but also that the user has ownership over said session. 277 * 278 * <p>This flag is ignored if {@link #FLAG_ONGOING_SESSION} is not set, or if the 279 * corresponding route is not currently selected. 280 * 281 * <p>This flag does not affect volume adjustment (see {@link VolumeProvider}, and {@link 282 * MediaRoute2Info#getVolumeHandling()}), or any aspect other than the visual representation 283 * of the corresponding item. 284 */ 285 public static final int FLAG_ONGOING_SESSION_MANAGED = 1 << 1; 286 287 /** 288 * The corresponding route is specially likely to be selected by the user. 289 * 290 * <p>A UI reflecting this preference may reserve a specific space for suggested routes, 291 * making it more accessible to the user. If the number of suggested routes exceeds the 292 * number supported by the UI, the routes listed first in {@link 293 * RouteListingPreference#getItems()} will take priority. 294 */ 295 public static final int FLAG_SUGGESTED = 1 << 2; 296 297 /** @hide */ 298 @Retention(RetentionPolicy.SOURCE) 299 @IntDef( 300 prefix = {"SUBTEXT_"}, 301 value = { 302 SUBTEXT_NONE, 303 SUBTEXT_ERROR_UNKNOWN, 304 SUBTEXT_SUBSCRIPTION_REQUIRED, 305 SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED, 306 SUBTEXT_AD_ROUTING_DISALLOWED, 307 SUBTEXT_DEVICE_LOW_POWER, 308 SUBTEXT_UNAUTHORIZED, 309 SUBTEXT_TRACK_UNSUPPORTED, 310 SUBTEXT_CUSTOM 311 }) 312 public @interface SubText {} 313 314 /** The corresponding route has no associated subtext. */ 315 public static final int SUBTEXT_NONE = 0; 316 /** 317 * The corresponding route's subtext must indicate that it is not available because of an 318 * unknown error. 319 */ 320 public static final int SUBTEXT_ERROR_UNKNOWN = 1; 321 /** 322 * The corresponding route's subtext must indicate that it requires a special subscription 323 * in order to be available for routing. 324 */ 325 public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 2; 326 /** 327 * The corresponding route's subtext must indicate that downloaded content cannot be routed 328 * to it. 329 */ 330 public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 3; 331 /** 332 * The corresponding route's subtext must indicate that it is not available because an ad is 333 * in progress. 334 */ 335 public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 4; 336 /** 337 * The corresponding route's subtext must indicate that it is not available because the 338 * device is in low-power mode. 339 */ 340 public static final int SUBTEXT_DEVICE_LOW_POWER = 5; 341 /** 342 * The corresponding route's subtext must indicate that it is not available because the user 343 * is not authorized to route to it. 344 */ 345 public static final int SUBTEXT_UNAUTHORIZED = 6; 346 /** 347 * The corresponding route's subtext must indicate that it is not available because the 348 * device does not support the current media track. 349 */ 350 public static final int SUBTEXT_TRACK_UNSUPPORTED = 7; 351 /** 352 * The corresponding route's subtext must be obtained from {@link 353 * #getCustomSubtextMessage()}. 354 * 355 * <p>Applications should strongly prefer one of the other disable reasons (for the full 356 * list, see {@link #getSubText()}) in order to guarantee correct localization and rendering 357 * across all form factors. 358 */ 359 public static final int SUBTEXT_CUSTOM = 10000; 360 361 @NonNull 362 public static final Creator<Item> CREATOR = 363 new Creator<>() { 364 @Override 365 public Item createFromParcel(Parcel in) { 366 return new Item(in); 367 } 368 369 @Override 370 public Item[] newArray(int size) { 371 return new Item[size]; 372 } 373 }; 374 375 @NonNull private final String mRouteId; 376 @SelectionBehavior private final int mSelectionBehavior; 377 @Flags private final int mFlags; 378 @SubText private final int mSubText; 379 380 @Nullable private final CharSequence mCustomSubtextMessage; 381 Item(@onNull Builder builder)382 private Item(@NonNull Builder builder) { 383 mRouteId = builder.mRouteId; 384 mSelectionBehavior = builder.mSelectionBehavior; 385 mFlags = builder.mFlags; 386 mSubText = builder.mSubText; 387 mCustomSubtextMessage = builder.mCustomSubtextMessage; 388 validateCustomMessageSubtext(); 389 } 390 Item(Parcel in)391 private Item(Parcel in) { 392 mRouteId = in.readString(); 393 Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId)); 394 mSelectionBehavior = in.readInt(); 395 mFlags = in.readInt(); 396 mSubText = in.readInt(); 397 mCustomSubtextMessage = in.readCharSequence(); 398 validateCustomMessageSubtext(); 399 } 400 401 /** 402 * Returns the id of the route that corresponds to this route listing preference item. 403 * 404 * @see MediaRoute2Info#getId() 405 */ 406 @NonNull getRouteId()407 public String getRouteId() { 408 return mRouteId; 409 } 410 411 /** 412 * Returns the behavior that the corresponding route has if the user selects it. 413 * 414 * @see #SELECTION_BEHAVIOR_NONE 415 * @see #SELECTION_BEHAVIOR_TRANSFER 416 * @see #SELECTION_BEHAVIOR_GO_TO_APP 417 */ getSelectionBehavior()418 public int getSelectionBehavior() { 419 return mSelectionBehavior; 420 } 421 422 /** 423 * Returns the flags associated to the route that corresponds to this item. 424 * 425 * @see #FLAG_ONGOING_SESSION 426 * @see #FLAG_ONGOING_SESSION_MANAGED 427 * @see #FLAG_SUGGESTED 428 */ 429 @Flags getFlags()430 public int getFlags() { 431 return mFlags; 432 } 433 434 /** 435 * Returns the type of subtext associated to this route. 436 * 437 * <p>Subtext types other than {@link #SUBTEXT_NONE} and {@link #SUBTEXT_CUSTOM} must not 438 * have {@link #SELECTION_BEHAVIOR_TRANSFER}. 439 * 440 * <p>If this method returns {@link #SUBTEXT_CUSTOM}, then the subtext is obtained form 441 * {@link #getCustomSubtextMessage()}. 442 * 443 * @see #SUBTEXT_NONE 444 * @see #SUBTEXT_ERROR_UNKNOWN 445 * @see #SUBTEXT_SUBSCRIPTION_REQUIRED 446 * @see #SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED 447 * @see #SUBTEXT_AD_ROUTING_DISALLOWED 448 * @see #SUBTEXT_DEVICE_LOW_POWER 449 * @see #SUBTEXT_UNAUTHORIZED 450 * @see #SUBTEXT_TRACK_UNSUPPORTED 451 * @see #SUBTEXT_CUSTOM 452 */ 453 @SubText getSubText()454 public int getSubText() { 455 return mSubText; 456 } 457 458 /** 459 * Returns a human-readable {@link CharSequence} providing the subtext for the corresponding 460 * route. 461 * 462 * <p>This value is ignored if the {@link #getSubText() subtext} for this item is not {@link 463 * #SUBTEXT_CUSTOM}.. 464 * 465 * <p>Applications must provide a localized message that matches the system's locale. See 466 * {@link Locale#getDefault()}. 467 * 468 * <p>Applications should avoid using custom messages (and instead use one of non-custom 469 * subtexts listed in {@link #getSubText()} in order to guarantee correct visual 470 * representation and localization on all form factors. 471 */ 472 @Nullable getCustomSubtextMessage()473 public CharSequence getCustomSubtextMessage() { 474 return mCustomSubtextMessage; 475 } 476 477 // Item Parcelable implementation. 478 479 @Override describeContents()480 public int describeContents() { 481 return 0; 482 } 483 484 @Override writeToParcel(@onNull Parcel dest, int flags)485 public void writeToParcel(@NonNull Parcel dest, int flags) { 486 dest.writeString(mRouteId); 487 dest.writeInt(mSelectionBehavior); 488 dest.writeInt(mFlags); 489 dest.writeInt(mSubText); 490 dest.writeCharSequence(mCustomSubtextMessage); 491 } 492 493 // Equals and hashCode. 494 495 @Override equals(Object other)496 public boolean equals(Object other) { 497 if (this == other) { 498 return true; 499 } 500 if (!(other instanceof Item)) { 501 return false; 502 } 503 Item item = (Item) other; 504 return mRouteId.equals(item.mRouteId) 505 && mSelectionBehavior == item.mSelectionBehavior 506 && mFlags == item.mFlags 507 && mSubText == item.mSubText 508 && TextUtils.equals(mCustomSubtextMessage, item.mCustomSubtextMessage); 509 } 510 511 @Override hashCode()512 public int hashCode() { 513 return Objects.hash( 514 mRouteId, mSelectionBehavior, mFlags, mSubText, mCustomSubtextMessage); 515 } 516 517 // Internal methods. 518 validateCustomMessageSubtext()519 private void validateCustomMessageSubtext() { 520 Preconditions.checkArgument( 521 mSubText != SUBTEXT_CUSTOM || mCustomSubtextMessage != null, 522 "The custom subtext message cannot be null if subtext is SUBTEXT_CUSTOM."); 523 } 524 525 // Internal classes. 526 527 /** Builder for {@link Item}. */ 528 public static final class Builder { 529 530 private final String mRouteId; 531 private int mSelectionBehavior; 532 private int mFlags; 533 private int mSubText; 534 private CharSequence mCustomSubtextMessage; 535 536 /** 537 * Constructor. 538 * 539 * @param routeId See {@link Item#getRouteId()}. 540 */ Builder(@onNull String routeId)541 public Builder(@NonNull String routeId) { 542 Preconditions.checkArgument(!TextUtils.isEmpty(routeId)); 543 mRouteId = routeId; 544 mSelectionBehavior = SELECTION_BEHAVIOR_TRANSFER; 545 mSubText = SUBTEXT_NONE; 546 } 547 548 /** 549 * See {@link Item#getSelectionBehavior()}. 550 * 551 * <p>The default value is {@link #ACTION_TRANSFER_MEDIA}. 552 */ 553 @NonNull setSelectionBehavior(int selectionBehavior)554 public Builder setSelectionBehavior(int selectionBehavior) { 555 mSelectionBehavior = selectionBehavior; 556 return this; 557 } 558 559 /** 560 * See {@link Item#getFlags()}. 561 * 562 * <p>The default value is zero (no flags). 563 */ 564 @NonNull setFlags(int flags)565 public Builder setFlags(int flags) { 566 mFlags = flags; 567 return this; 568 } 569 570 /** 571 * See {@link Item#getSubText()}. 572 * 573 * <p>The default value is {@link #SUBTEXT_NONE}. 574 */ 575 @NonNull setSubText(int subText)576 public Builder setSubText(int subText) { 577 mSubText = subText; 578 return this; 579 } 580 581 /** 582 * See {@link Item#getCustomSubtextMessage()}. 583 * 584 * <p>The default value is {@code null}. 585 */ 586 @NonNull setCustomSubtextMessage(@ullable CharSequence customSubtextMessage)587 public Builder setCustomSubtextMessage(@Nullable CharSequence customSubtextMessage) { 588 mCustomSubtextMessage = customSubtextMessage; 589 return this; 590 } 591 592 /** Creates and returns a new {@link Item} with the given parameters. */ 593 @NonNull build()594 public Item build() { 595 return new Item(this); 596 } 597 } 598 } 599 } 600