/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; /** * Allows applications to customize the list of routes used for media routing (for example, in the * System UI Output Switcher). * * @see MediaRouter2#setRouteListingPreference * @see Item */ public final class RouteListingPreference implements Parcelable { /** * {@link Intent} action that the system uses to take the user the app when the user selects an * {@link Item} whose {@link Item#getSelectionBehavior() selection behavior} is {@link * Item#SELECTION_BEHAVIOR_GO_TO_APP}. * *

The launched intent will identify the selected item using the extra identified by {@link * #EXTRA_ROUTE_ID}. * * @see #getLinkedItemComponentName() * @see Item#SELECTION_BEHAVIOR_GO_TO_APP */ public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA"; /** * {@link Intent} string extra key that contains the {@link Item#getRouteId() id} of the route * to transfer to, as part of an {@link #ACTION_TRANSFER_MEDIA} intent. * * @see #getLinkedItemComponentName() * @see Item#SELECTION_BEHAVIOR_GO_TO_APP */ public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID"; @NonNull public static final Creator CREATOR = new Creator<>() { @Override public RouteListingPreference createFromParcel(Parcel in) { return new RouteListingPreference(in); } @Override public RouteListingPreference[] newArray(int size) { return new RouteListingPreference[size]; } }; @NonNull private final List mItems; private final boolean mUseSystemOrdering; @Nullable private final ComponentName mLinkedItemComponentName; private RouteListingPreference(Builder builder) { mItems = builder.mItems; mUseSystemOrdering = builder.mUseSystemOrdering; mLinkedItemComponentName = builder.mLinkedItemComponentName; } private RouteListingPreference(Parcel in) { List items = in.readParcelableList(new ArrayList<>(), Item.class.getClassLoader(), Item.class); mItems = List.copyOf(items); mUseSystemOrdering = in.readBoolean(); mLinkedItemComponentName = ComponentName.readFromParcel(in); } /** * Returns an unmodifiable list containing the {@link Item items} that the app wants to be * listed for media routing. */ @NonNull public List getItems() { return mItems; } /** * Returns true if the application would like media route listing to use the system's ordering * strategy, or false if the application would like route listing to respect the ordering * obtained from {@link #getItems()}. * *

The system's ordering strategy is implementation-dependent, but may take into account each * route's recency or frequency of use in order to rank them. */ public boolean getUseSystemOrdering() { return mUseSystemOrdering; } /** * Returns a {@link ComponentName} for navigating to the application. * *

Must not be null if any of the {@link #getItems() items} of this route listing preference * has {@link Item#getSelectionBehavior() selection behavior} {@link * Item#SELECTION_BEHAVIOR_GO_TO_APP}. * *

The system navigates to the application when the user selects {@link Item} with {@link * Item#SELECTION_BEHAVIOR_GO_TO_APP} by launching an intent to the returned {@link * ComponentName}, using action {@link #ACTION_TRANSFER_MEDIA}, with the extra {@link * #EXTRA_ROUTE_ID}. */ @Nullable public ComponentName getLinkedItemComponentName() { return mLinkedItemComponentName; } // RouteListingPreference Parcelable implementation. @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelableList(mItems, flags); dest.writeBoolean(mUseSystemOrdering); ComponentName.writeToParcel(mLinkedItemComponentName, dest); } // Equals and hashCode. @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof RouteListingPreference)) { return false; } RouteListingPreference that = (RouteListingPreference) other; return mItems.equals(that.mItems) && mUseSystemOrdering == that.mUseSystemOrdering && Objects.equals(mLinkedItemComponentName, that.mLinkedItemComponentName); } @Override public int hashCode() { return Objects.hash(mItems, mUseSystemOrdering, mLinkedItemComponentName); } /** Builder for {@link RouteListingPreference}. */ public static final class Builder { private List mItems; private boolean mUseSystemOrdering; private ComponentName mLinkedItemComponentName; /** Creates a new instance with default values (documented in the setters). */ public Builder() { mItems = List.of(); mUseSystemOrdering = true; } /** * See {@link #getItems()} * *

The default value is an empty list. */ @NonNull public Builder setItems(@NonNull List items) { mItems = List.copyOf(Objects.requireNonNull(items)); return this; } /** * See {@link #getUseSystemOrdering()} * *

The default value is {@code true}. */ // Lint requires "isUseSystemOrdering", but "getUseSystemOrdering" is a better name. @SuppressWarnings("MissingGetterMatchingBuilder") @NonNull public Builder setUseSystemOrdering(boolean useSystemOrdering) { mUseSystemOrdering = useSystemOrdering; return this; } /** * See {@link #getLinkedItemComponentName()}. * *

The default value is {@code null}. */ @NonNull public Builder setLinkedItemComponentName(@Nullable ComponentName linkedItemComponentName) { mLinkedItemComponentName = linkedItemComponentName; return this; } /** * Creates and returns a new {@link RouteListingPreference} instance with the given * parameters. */ @NonNull public RouteListingPreference build() { return new RouteListingPreference(this); } } /** Holds preference information for a specific route in a {@link RouteListingPreference}. */ public static final class Item implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"SELECTION_BEHAVIOR_"}, value = { SELECTION_BEHAVIOR_NONE, SELECTION_BEHAVIOR_TRANSFER, SELECTION_BEHAVIOR_GO_TO_APP }) public @interface SelectionBehavior {} /** The corresponding route is not selectable by the user. */ public static final int SELECTION_BEHAVIOR_NONE = 0; /** If the user selects the corresponding route, the media transfers to the said route. */ public static final int SELECTION_BEHAVIOR_TRANSFER = 1; /** * If the user selects the corresponding route, the system takes the user to the * application. * *

The system uses {@link #getLinkedItemComponentName()} in order to navigate to the app. */ public static final int SELECTION_BEHAVIOR_GO_TO_APP = 2; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, prefix = {"FLAG_"}, value = {FLAG_ONGOING_SESSION, FLAG_ONGOING_SESSION_MANAGED, FLAG_SUGGESTED}) public @interface Flags {} /** * The corresponding route is already hosting a session with the app that owns this listing * preference. */ public static final int FLAG_ONGOING_SESSION = 1; /** * Signals that the ongoing session on the corresponding route is managed by the current * user of the app. * *

The system can use this flag to provide visual indication that the route is not only * hosting a session, but also that the user has ownership over said session. * *

This flag is ignored if {@link #FLAG_ONGOING_SESSION} is not set, or if the * corresponding route is not currently selected. * *

This flag does not affect volume adjustment (see {@link VolumeProvider}, and {@link * MediaRoute2Info#getVolumeHandling()}), or any aspect other than the visual representation * of the corresponding item. */ public static final int FLAG_ONGOING_SESSION_MANAGED = 1 << 1; /** * The corresponding route is specially likely to be selected by the user. * *

A UI reflecting this preference may reserve a specific space for suggested routes, * making it more accessible to the user. If the number of suggested routes exceeds the * number supported by the UI, the routes listed first in {@link * RouteListingPreference#getItems()} will take priority. */ public static final int FLAG_SUGGESTED = 1 << 2; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"SUBTEXT_"}, value = { SUBTEXT_NONE, SUBTEXT_ERROR_UNKNOWN, SUBTEXT_SUBSCRIPTION_REQUIRED, SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED, SUBTEXT_AD_ROUTING_DISALLOWED, SUBTEXT_DEVICE_LOW_POWER, SUBTEXT_UNAUTHORIZED, SUBTEXT_TRACK_UNSUPPORTED, SUBTEXT_CUSTOM }) public @interface SubText {} /** The corresponding route has no associated subtext. */ public static final int SUBTEXT_NONE = 0; /** * The corresponding route's subtext must indicate that it is not available because of an * unknown error. */ public static final int SUBTEXT_ERROR_UNKNOWN = 1; /** * The corresponding route's subtext must indicate that it requires a special subscription * in order to be available for routing. */ public static final int SUBTEXT_SUBSCRIPTION_REQUIRED = 2; /** * The corresponding route's subtext must indicate that downloaded content cannot be routed * to it. */ public static final int SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED = 3; /** * The corresponding route's subtext must indicate that it is not available because an ad is * in progress. */ public static final int SUBTEXT_AD_ROUTING_DISALLOWED = 4; /** * The corresponding route's subtext must indicate that it is not available because the * device is in low-power mode. */ public static final int SUBTEXT_DEVICE_LOW_POWER = 5; /** * The corresponding route's subtext must indicate that it is not available because the user * is not authorized to route to it. */ public static final int SUBTEXT_UNAUTHORIZED = 6; /** * The corresponding route's subtext must indicate that it is not available because the * device does not support the current media track. */ public static final int SUBTEXT_TRACK_UNSUPPORTED = 7; /** * The corresponding route's subtext must be obtained from {@link * #getCustomSubtextMessage()}. * *

Applications should strongly prefer one of the other disable reasons (for the full * list, see {@link #getSubText()}) in order to guarantee correct localization and rendering * across all form factors. */ public static final int SUBTEXT_CUSTOM = 10000; @NonNull public static final Creator CREATOR = new Creator<>() { @Override public Item createFromParcel(Parcel in) { return new Item(in); } @Override public Item[] newArray(int size) { return new Item[size]; } }; @NonNull private final String mRouteId; @SelectionBehavior private final int mSelectionBehavior; @Flags private final int mFlags; @SubText private final int mSubText; @Nullable private final CharSequence mCustomSubtextMessage; private Item(@NonNull Builder builder) { mRouteId = builder.mRouteId; mSelectionBehavior = builder.mSelectionBehavior; mFlags = builder.mFlags; mSubText = builder.mSubText; mCustomSubtextMessage = builder.mCustomSubtextMessage; validateCustomMessageSubtext(); } private Item(Parcel in) { mRouteId = in.readString(); Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId)); mSelectionBehavior = in.readInt(); mFlags = in.readInt(); mSubText = in.readInt(); mCustomSubtextMessage = in.readCharSequence(); validateCustomMessageSubtext(); } /** * Returns the id of the route that corresponds to this route listing preference item. * * @see MediaRoute2Info#getId() */ @NonNull public String getRouteId() { return mRouteId; } /** * Returns the behavior that the corresponding route has if the user selects it. * * @see #SELECTION_BEHAVIOR_NONE * @see #SELECTION_BEHAVIOR_TRANSFER * @see #SELECTION_BEHAVIOR_GO_TO_APP */ public int getSelectionBehavior() { return mSelectionBehavior; } /** * Returns the flags associated to the route that corresponds to this item. * * @see #FLAG_ONGOING_SESSION * @see #FLAG_ONGOING_SESSION_MANAGED * @see #FLAG_SUGGESTED */ @Flags public int getFlags() { return mFlags; } /** * Returns the type of subtext associated to this route. * *

Subtext types other than {@link #SUBTEXT_NONE} and {@link #SUBTEXT_CUSTOM} must not * have {@link #SELECTION_BEHAVIOR_TRANSFER}. * *

If this method returns {@link #SUBTEXT_CUSTOM}, then the subtext is obtained form * {@link #getCustomSubtextMessage()}. * * @see #SUBTEXT_NONE * @see #SUBTEXT_ERROR_UNKNOWN * @see #SUBTEXT_SUBSCRIPTION_REQUIRED * @see #SUBTEXT_DOWNLOADED_CONTENT_ROUTING_DISALLOWED * @see #SUBTEXT_AD_ROUTING_DISALLOWED * @see #SUBTEXT_DEVICE_LOW_POWER * @see #SUBTEXT_UNAUTHORIZED * @see #SUBTEXT_TRACK_UNSUPPORTED * @see #SUBTEXT_CUSTOM */ @SubText public int getSubText() { return mSubText; } /** * Returns a human-readable {@link CharSequence} providing the subtext for the corresponding * route. * *

This value is ignored if the {@link #getSubText() subtext} for this item is not {@link * #SUBTEXT_CUSTOM}.. * *

Applications must provide a localized message that matches the system's locale. See * {@link Locale#getDefault()}. * *

Applications should avoid using custom messages (and instead use one of non-custom * subtexts listed in {@link #getSubText()} in order to guarantee correct visual * representation and localization on all form factors. */ @Nullable public CharSequence getCustomSubtextMessage() { return mCustomSubtextMessage; } // Item Parcelable implementation. @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mRouteId); dest.writeInt(mSelectionBehavior); dest.writeInt(mFlags); dest.writeInt(mSubText); dest.writeCharSequence(mCustomSubtextMessage); } // Equals and hashCode. @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof Item)) { return false; } Item item = (Item) other; return mRouteId.equals(item.mRouteId) && mSelectionBehavior == item.mSelectionBehavior && mFlags == item.mFlags && mSubText == item.mSubText && TextUtils.equals(mCustomSubtextMessage, item.mCustomSubtextMessage); } @Override public int hashCode() { return Objects.hash( mRouteId, mSelectionBehavior, mFlags, mSubText, mCustomSubtextMessage); } // Internal methods. private void validateCustomMessageSubtext() { Preconditions.checkArgument( mSubText != SUBTEXT_CUSTOM || mCustomSubtextMessage != null, "The custom subtext message cannot be null if subtext is SUBTEXT_CUSTOM."); } // Internal classes. /** Builder for {@link Item}. */ public static final class Builder { private final String mRouteId; private int mSelectionBehavior; private int mFlags; private int mSubText; private CharSequence mCustomSubtextMessage; /** * Constructor. * * @param routeId See {@link Item#getRouteId()}. */ public Builder(@NonNull String routeId) { Preconditions.checkArgument(!TextUtils.isEmpty(routeId)); mRouteId = routeId; mSelectionBehavior = SELECTION_BEHAVIOR_TRANSFER; mSubText = SUBTEXT_NONE; } /** * See {@link Item#getSelectionBehavior()}. * *

The default value is {@link #ACTION_TRANSFER_MEDIA}. */ @NonNull public Builder setSelectionBehavior(int selectionBehavior) { mSelectionBehavior = selectionBehavior; return this; } /** * See {@link Item#getFlags()}. * *

The default value is zero (no flags). */ @NonNull public Builder setFlags(int flags) { mFlags = flags; return this; } /** * See {@link Item#getSubText()}. * *

The default value is {@link #SUBTEXT_NONE}. */ @NonNull public Builder setSubText(int subText) { mSubText = subText; return this; } /** * See {@link Item#getCustomSubtextMessage()}. * *

The default value is {@code null}. */ @NonNull public Builder setCustomSubtextMessage(@Nullable CharSequence customSubtextMessage) { mCustomSubtextMessage = customSubtextMessage; return this; } /** Creates and returns a new {@link Item} with the given parameters. */ @NonNull public Item build() { return new Item(this); } } } }