/* * Copyright 2019 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 static android.media.MediaRouter2Utils.toUniqueId; import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER; import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; import static com.android.media.flags.Flags.FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; /** * Describes the properties of a route. */ public final class MediaRoute2Info implements Parcelable { @NonNull public static final Creator CREATOR = new Creator() { @Override public MediaRoute2Info createFromParcel(Parcel in) { return new MediaRoute2Info(in); } @Override public MediaRoute2Info[] newArray(int size) { return new MediaRoute2Info[size]; } }; /** * The {@link #getOriginalId() original id} of the route that represents the built-in media * route. * *

A route with this id will only be visible to apps with permission to do system routing, * which means having {@link android.Manifest.permission#BLUETOOTH_CONNECT} and {@link * android.Manifest.permission#BLUETOOTH_SCAN}, or {@link * android.Manifest.permission#MODIFY_AUDIO_ROUTING}. * * @hide */ public static final String ROUTE_ID_DEVICE = "DEVICE_ROUTE"; /** * The {@link #getOriginalId() original id} of the route that represents the default system * media route. * *

A route with this id will be visible to apps with no permission over system routing. See * {@link #ROUTE_ID_DEVICE} for details. * * @hide */ public static final String ROUTE_ID_DEFAULT = "DEFAULT_ROUTE"; /** @hide */ @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING, CONNECTION_STATE_CONNECTED}) @Retention(RetentionPolicy.SOURCE) public @interface ConnectionState {} /** * The default connection state indicating the route is disconnected. * * @see #getConnectionState */ public static final int CONNECTION_STATE_DISCONNECTED = 0; /** * A connection state indicating the route is in the process of connecting and is not yet * ready for use. * * @see #getConnectionState */ public static final int CONNECTION_STATE_CONNECTING = 1; /** * A connection state indicating the route is connected. * * @see #getConnectionState */ public static final int CONNECTION_STATE_CONNECTED = 2; /** @hide */ @IntDef({PLAYBACK_VOLUME_FIXED, PLAYBACK_VOLUME_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface PlaybackVolume {} /** * Playback information indicating the playback volume is fixed, i.e. it cannot be * controlled from this object. An example of fixed playback volume is a remote player, * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather * than attenuate at the source. * * @see #getVolumeHandling() */ public static final int PLAYBACK_VOLUME_FIXED = 0; /** * Playback information indicating the playback volume is variable and can be controlled * from this object. * * @see #getVolumeHandling() */ public static final int PLAYBACK_VOLUME_VARIABLE = 1; /** @hide */ @IntDef( prefix = {"TYPE_"}, value = { TYPE_UNKNOWN, TYPE_BUILTIN_SPEAKER, TYPE_WIRED_HEADSET, TYPE_WIRED_HEADPHONES, TYPE_BLUETOOTH_A2DP, TYPE_HDMI, TYPE_HDMI_ARC, TYPE_HDMI_EARC, TYPE_USB_DEVICE, TYPE_USB_ACCESSORY, TYPE_DOCK, TYPE_USB_HEADSET, TYPE_HEARING_AID, TYPE_BLE_HEADSET, TYPE_REMOTE_TV, TYPE_REMOTE_SPEAKER, TYPE_REMOTE_AUDIO_VIDEO_RECEIVER, TYPE_REMOTE_TABLET, TYPE_REMOTE_TABLET_DOCKED, TYPE_REMOTE_COMPUTER, TYPE_REMOTE_GAME_CONSOLE, TYPE_REMOTE_CAR, TYPE_REMOTE_SMARTWATCH, TYPE_REMOTE_SMARTPHONE, TYPE_GROUP }) @Retention(RetentionPolicy.SOURCE) public @interface Type {} /** * Indicates the route's type is unknown or undefined. * * @see #getType */ public static final int TYPE_UNKNOWN = 0; /** * Indicates the route is the speaker system (i.e. a mono speaker or stereo speakers) built into * the device. * * @see #getType */ public static final int TYPE_BUILTIN_SPEAKER = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; /** * Indicates the route is a headset, which is the combination of a headphones and a microphone. * * @see #getType */ public static final int TYPE_WIRED_HEADSET = AudioDeviceInfo.TYPE_WIRED_HEADSET; /** * Indicates the route is a pair of wired headphones. * * @see #getType */ public static final int TYPE_WIRED_HEADPHONES = AudioDeviceInfo.TYPE_WIRED_HEADPHONES; /** * Indicates the route is a bluetooth device, such as a bluetooth speaker or headphones. * * @see #getType */ public static final int TYPE_BLUETOOTH_A2DP = AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; /** * Indicates the route is an HDMI connection. * * @see #getType */ public static final int TYPE_HDMI = AudioDeviceInfo.TYPE_HDMI; /** * Indicates the route is an Audio Return Channel of an HDMI connection. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) public static final int TYPE_HDMI_ARC = AudioDeviceInfo.TYPE_HDMI_ARC; /** * Indicates the route is an Enhanced Audio Return Channel of an HDMI connection. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) public static final int TYPE_HDMI_EARC = AudioDeviceInfo.TYPE_HDMI_EARC; /** * Indicates the route is a USB audio device. * * @see #getType */ public static final int TYPE_USB_DEVICE = AudioDeviceInfo.TYPE_USB_DEVICE; /** * Indicates the route is a USB audio device in accessory mode. * * @see #getType */ public static final int TYPE_USB_ACCESSORY = AudioDeviceInfo.TYPE_USB_ACCESSORY; /** * Indicates the route is the audio device associated with a dock. * * @see #getType */ public static final int TYPE_DOCK = AudioDeviceInfo.TYPE_DOCK; /** * Indicates the route is a USB audio headset. * * @see #getType */ public static final int TYPE_USB_HEADSET = AudioDeviceInfo.TYPE_USB_HEADSET; /** * Indicates the route is a hearing aid. * * @see #getType */ public static final int TYPE_HEARING_AID = AudioDeviceInfo.TYPE_HEARING_AID; /** * Indicates the route is a Bluetooth Low Energy (BLE) HEADSET. * * @see #getType */ public static final int TYPE_BLE_HEADSET = AudioDeviceInfo.TYPE_BLE_HEADSET; /** * Indicates the route is a remote TV. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ public static final int TYPE_REMOTE_TV = 1001; /** * Indicates the route is a remote speaker. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ public static final int TYPE_REMOTE_SPEAKER = 1002; /** * Indicates the route is a remote Audio/Video Receiver (AVR). * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ public static final int TYPE_REMOTE_AUDIO_VIDEO_RECEIVER = 1003; /** * Indicates the route is a remote tablet. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES) public static final int TYPE_REMOTE_TABLET = 1004; /** * Indicates the route is a remote docked tablet. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES) public static final int TYPE_REMOTE_TABLET_DOCKED = 1005; /** * Indicates the route is a remote computer. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES) public static final int TYPE_REMOTE_COMPUTER = 1006; /** * Indicates the route is a remote gaming console. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES) public static final int TYPE_REMOTE_GAME_CONSOLE = 1007; /** * Indicates the route is a remote car. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES) public static final int TYPE_REMOTE_CAR = 1008; /** * Indicates the route is a remote smartwatch. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES) public static final int TYPE_REMOTE_SMARTWATCH = 1009; /** * Indicates the route is a remote smartphone. * *

A remote device uses a routing protocol managed by the application, as opposed to the * routing being done by the system. * * @see #getType */ @FlaggedApi(FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES) public static final int TYPE_REMOTE_SMARTPHONE = 1010; /** * Indicates the route is a group of devices. * * @see #getType */ public static final int TYPE_GROUP = 2000; /** * Route feature: Live audio. *

* A route that supports live audio routing will allow the media audio stream * to be sent to supported destinations. This can include internal speakers or * audio jacks on the device itself, A2DP devices, and more. *

* When a live audio route is selected, audio routing is transparent to the application. * All audio played on the media stream will be routed to the selected destination. *

* Refer to the class documentation for details about live audio routes. *

*/ public static final String FEATURE_LIVE_AUDIO = "android.media.route.feature.LIVE_AUDIO"; /** * Route feature: Live video. *

* A route that supports live video routing will allow a mirrored version * of the device's primary display or a customized * {@link android.app.Presentation Presentation} to be sent to supported * destinations. *

* When a live video route is selected, audio and video routing is transparent * to the application. By default, audio and video is routed to the selected * destination. For certain live video routes, the application may also use a * {@link android.app.Presentation Presentation} to replace the mirrored view * on the external display with different content. *

* Refer to the class documentation for details about live video routes. *

* * @see android.app.Presentation */ public static final String FEATURE_LIVE_VIDEO = "android.media.route.feature.LIVE_VIDEO"; /** * Route feature: Local playback. * @hide */ public static final String FEATURE_LOCAL_PLAYBACK = "android.media.route.feature.LOCAL_PLAYBACK"; /** * Route feature: Remote playback. *

* A route that supports remote playback routing will allow an application to send * requests to play content remotely to supported destinations. * A route may only support {@link #FEATURE_REMOTE_AUDIO_PLAYBACK audio playback} or * {@link #FEATURE_REMOTE_VIDEO_PLAYBACK video playback}. *

* Remote playback routes destinations operate independently of the local device. * When a remote playback route is selected, the application can control the content * playing on the destination using {@link MediaRouter2.RoutingController#getControlHints()}. * The application may also receive status updates from the route regarding remote playback. *

* Refer to the class documentation for details about remote playback routes. *

* @see #FEATURE_REMOTE_AUDIO_PLAYBACK * @see #FEATURE_REMOTE_VIDEO_PLAYBACK */ public static final String FEATURE_REMOTE_PLAYBACK = "android.media.route.feature.REMOTE_PLAYBACK"; /** * Route feature: Remote audio playback. *

* A route that supports remote audio playback routing will allow an application to send * requests to play audio content remotely to supported destinations. * * @see #FEATURE_REMOTE_PLAYBACK * @see #FEATURE_REMOTE_VIDEO_PLAYBACK */ public static final String FEATURE_REMOTE_AUDIO_PLAYBACK = "android.media.route.feature.REMOTE_AUDIO_PLAYBACK"; /** * Route feature: Remote video playback. *

* A route that supports remote video playback routing will allow an application to send * requests to play video content remotely to supported destinations. * * @see #FEATURE_REMOTE_PLAYBACK * @see #FEATURE_REMOTE_AUDIO_PLAYBACK */ public static final String FEATURE_REMOTE_VIDEO_PLAYBACK = "android.media.route.feature.REMOTE_VIDEO_PLAYBACK"; /** * Route feature: Remote group playback. *

* @hide */ public static final String FEATURE_REMOTE_GROUP_PLAYBACK = "android.media.route.feature.REMOTE_GROUP_PLAYBACK"; /** Indicates the route is always suitable for media playback. */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) public static final int SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER = 0; /** * Indicates that the route is suitable for media playback only after explicit user selection. */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) public static final int SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER = 1; /** Indicates that the route is never suitable for media playback. */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) public static final int SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER = 2; /** * Route suitability status. * *

Signals whether the route is suitable to play media. * * @hide */ @IntDef( value = { SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER, SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER, SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER }) @Retention(RetentionPolicy.SOURCE) @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) public @interface SuitabilityStatus {} private final String mId; private final CharSequence mName; private final List mFeatures; @Type private final int mType; private final boolean mIsSystem; private final Uri mIconUri; private final CharSequence mDescription; @ConnectionState private final int mConnectionState; private final String mClientPackageName; private final String mPackageName; private final int mVolumeHandling; private final int mVolumeMax; private final int mVolume; private final String mAddress; private final Set mDeduplicationIds; private final Bundle mExtras; private final String mProviderId; private final boolean mIsVisibilityRestricted; private final Set mAllowedPackages; @SuitabilityStatus private final int mSuitabilityStatus; MediaRoute2Info(@NonNull Builder builder) { mId = builder.mId; mName = builder.mName; mFeatures = builder.mFeatures; mType = builder.mType; mIsSystem = builder.mIsSystem; mIconUri = builder.mIconUri; mDescription = builder.mDescription; mConnectionState = builder.mConnectionState; mClientPackageName = builder.mClientPackageName; mPackageName = builder.mPackageName; mVolumeHandling = builder.mVolumeHandling; mVolumeMax = builder.mVolumeMax; mVolume = builder.mVolume; mAddress = builder.mAddress; mDeduplicationIds = builder.mDeduplicationIds; mExtras = builder.mExtras; mProviderId = builder.mProviderId; mIsVisibilityRestricted = builder.mIsVisibilityRestricted; mAllowedPackages = builder.mAllowedPackages; mSuitabilityStatus = builder.mSuitabilityStatus; } MediaRoute2Info(@NonNull Parcel in) { mId = in.readString(); Preconditions.checkArgument(!TextUtils.isEmpty(mId)); mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mFeatures = in.createStringArrayList(); mType = in.readInt(); mIsSystem = in.readBoolean(); mIconUri = in.readParcelable(null, android.net.Uri.class); mDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mConnectionState = in.readInt(); mClientPackageName = in.readString(); mPackageName = in.readString(); mVolumeHandling = in.readInt(); mVolumeMax = in.readInt(); mVolume = in.readInt(); mAddress = in.readString(); mDeduplicationIds = Set.of(in.readStringArray()); mExtras = in.readBundle(); mProviderId = in.readString(); mIsVisibilityRestricted = in.readBoolean(); mAllowedPackages = Set.of(in.createString8Array()); mSuitabilityStatus = in.readInt(); } /** * Gets the id of the route. The routes which are given by {@link MediaRouter2} will have * unique IDs. *

* In order to ensure uniqueness in {@link MediaRouter2} side, the value of this method * can be different from what was set in {@link MediaRoute2ProviderService}. * * @see Builder#Builder(String, CharSequence) */ @NonNull public String getId() { if (!TextUtils.isEmpty(mProviderId)) { return toUniqueId(mProviderId, mId); } else { return mId; } } /** * Gets the user-visible name of the route. */ @NonNull public CharSequence getName() { return mName; } /** * Gets the supported features of the route. */ @NonNull public List getFeatures() { return mFeatures; } /** * Returns the type of this route. */ @Type public int getType() { return mType; } /** * Returns whether the route is a system route or not. *

* System routes are media routes directly controlled by the system * such as phone speaker, wired headset, and Bluetooth devices. *

*/ public boolean isSystemRoute() { return mIsSystem; } /** * Gets the URI of the icon representing this route. *

* This icon will be used in picker UIs if available. * * @return The URI of the icon representing this route, or null if none. */ @Nullable public Uri getIconUri() { return mIconUri; } /** * Gets the user-visible description of the route. */ @Nullable public CharSequence getDescription() { return mDescription; } /** * Gets the connection state of the route. * * @return The connection state of this route: {@link #CONNECTION_STATE_DISCONNECTED}, * {@link #CONNECTION_STATE_CONNECTING}, or {@link #CONNECTION_STATE_CONNECTED}. */ @ConnectionState public int getConnectionState() { return mConnectionState; } /** * Gets the package name of the app using the route. * Returns null if no apps are using this route. */ @Nullable public String getClientPackageName() { return mClientPackageName; } /** * Gets the package name of the provider that published the route. *

* It is set by the system service. * @hide */ @Nullable public String getPackageName() { return mPackageName; } /** * Gets information about how volume is handled on the route. * * @return {@link #PLAYBACK_VOLUME_FIXED} or {@link #PLAYBACK_VOLUME_VARIABLE} */ @PlaybackVolume public int getVolumeHandling() { return mVolumeHandling; } /** * Gets the maximum volume of the route. */ public int getVolumeMax() { return mVolumeMax; } /** * Gets the current volume of the route. This may be invalid if the route is not selected. */ public int getVolume() { return mVolume; } /** * Gets the hardware address of the route if available. * @hide */ @Nullable public String getAddress() { return mAddress; } /** * Gets the deduplication IDs associated to the route. * *

Two routes with a matching deduplication ID originate from the same receiver device. */ @NonNull public Set getDeduplicationIds() { return mDeduplicationIds; } /** * Gets an optional bundle with extra data. */ @Nullable public Bundle getExtras() { return mExtras == null ? null : new Bundle(mExtras); } /** * Gets the original id set by {@link Builder#Builder(String, CharSequence)}. * @hide */ @NonNull @TestApi public String getOriginalId() { return mId; } /** * Gets the provider id of the route. It is assigned automatically by * {@link com.android.server.media.MediaRouterService}. * * @return provider id of the route or null if it's not set. * @hide */ @Nullable public String getProviderId() { return mProviderId; } /** * Returns if the route has at least one of the specified route features. * * @param features the list of route features to consider * @return {@code true} if the route has at least one feature in the list * @hide */ public boolean hasAnyFeatures(@NonNull Collection features) { Objects.requireNonNull(features, "features must not be null"); for (String feature : features) { if (getFeatures().contains(feature)) { return true; } } return false; } /** * Returns if the route has all the specified route features. * * @hide */ public boolean hasAllFeatures(@NonNull Collection features) { Objects.requireNonNull(features, "features must not be null"); for (String feature : features) { if (!getFeatures().contains(feature)) { return false; } } return true; } /** * Returns true if the route info has all of the required field. * A route is valid if and only if it is obtained from * {@link com.android.server.media.MediaRouterService}. * @hide */ public boolean isValid() { if (TextUtils.isEmpty(getId()) || TextUtils.isEmpty(getName()) || TextUtils.isEmpty(getProviderId())) { return false; } return true; } /** * Returns whether this route is visible to the package with the given name. * @hide */ public boolean isVisibleTo(String packageName) { return !mIsVisibilityRestricted || getPackageName().equals(packageName) || mAllowedPackages.contains(packageName); } /** Returns the route suitability status. */ @SuitabilityStatus @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) public int getSuitabilityStatus() { return mSuitabilityStatus; } /** * Dumps the current state of the object to the given {@code pw} as a human-readable string. * *

Used in the context of dumpsys.

* * @hide */ public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(prefix + "MediaRoute2Info"); String indent = prefix + " "; pw.println(indent + "mId=" + mId); pw.println(indent + "mName=" + mName); pw.println(indent + "mFeatures=" + mFeatures); pw.println(indent + "mType=" + getDeviceTypeString(mType)); pw.println(indent + "mIsSystem=" + mIsSystem); pw.println(indent + "mIconUri=" + mIconUri); pw.println(indent + "mDescription=" + mDescription); pw.println(indent + "mConnectionState=" + mConnectionState); pw.println(indent + "mClientPackageName=" + mClientPackageName); pw.println(indent + "mPackageName=" + mPackageName); dumpVolume(pw, indent); pw.println(indent + "mAddress=" + mAddress); pw.println(indent + "mDeduplicationIds=" + mDeduplicationIds); pw.println(indent + "mExtras=" + mExtras); pw.println(indent + "mProviderId=" + mProviderId); pw.println(indent + "mIsVisibilityRestricted=" + mIsVisibilityRestricted); pw.println(indent + "mAllowedPackages=" + mAllowedPackages); pw.println(indent + "mSuitabilityStatus=" + mSuitabilityStatus); } private void dumpVolume(@NonNull PrintWriter pw, @NonNull String prefix) { String volumeHandlingName; switch (mVolumeHandling) { case PLAYBACK_VOLUME_FIXED: volumeHandlingName = "FIXED"; break; case PLAYBACK_VOLUME_VARIABLE: volumeHandlingName = "VARIABLE"; break; default: volumeHandlingName = "UNKNOWN"; break; } String volume = String.format(Locale.US, "volume(current=%d, max=%d, handling=%s(%d))", mVolume, mVolumeMax, volumeHandlingName, mVolumeHandling); pw.println(prefix + volume); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof MediaRoute2Info)) { return false; } MediaRoute2Info other = (MediaRoute2Info) obj; // Note: mExtras is not included. return Objects.equals(mId, other.mId) && Objects.equals(mName, other.mName) && Objects.equals(mFeatures, other.mFeatures) && (mType == other.mType) && (mIsSystem == other.mIsSystem) && Objects.equals(mIconUri, other.mIconUri) && Objects.equals(mDescription, other.mDescription) && (mConnectionState == other.mConnectionState) && Objects.equals(mClientPackageName, other.mClientPackageName) && Objects.equals(mPackageName, other.mPackageName) && (mVolumeHandling == other.mVolumeHandling) && (mVolumeMax == other.mVolumeMax) && (mVolume == other.mVolume) && Objects.equals(mAddress, other.mAddress) && Objects.equals(mDeduplicationIds, other.mDeduplicationIds) && Objects.equals(mProviderId, other.mProviderId) && (mIsVisibilityRestricted == other.mIsVisibilityRestricted) && Objects.equals(mAllowedPackages, other.mAllowedPackages) && mSuitabilityStatus == other.mSuitabilityStatus; } @Override public int hashCode() { // Note: mExtras is not included. return Objects.hash( mId, mName, mFeatures, mType, mIsSystem, mIconUri, mDescription, mConnectionState, mClientPackageName, mPackageName, mVolumeHandling, mVolumeMax, mVolume, mAddress, mDeduplicationIds, mProviderId, mIsVisibilityRestricted, mAllowedPackages, mSuitabilityStatus); } @Override public String toString() { // Note: mExtras is not printed here. StringBuilder result = new StringBuilder() .append("MediaRoute2Info{ ") .append("id=") .append(getId()) .append(", name=") .append(getName()) .append(", type=") .append(getDeviceTypeString(getType())) .append(", isSystem=") .append(isSystemRoute()) .append(", features=") .append(getFeatures()) .append(", iconUri=") .append(getIconUri()) .append(", description=") .append(getDescription()) .append(", connectionState=") .append(getConnectionState()) .append(", clientPackageName=") .append(getClientPackageName()) .append(", volumeHandling=") .append(getVolumeHandling()) .append(", volumeMax=") .append(getVolumeMax()) .append(", volume=") .append(getVolume()) .append(", address=") .append(getAddress()) .append(", deduplicationIds=") .append(String.join(",", getDeduplicationIds())) .append(", providerId=") .append(getProviderId()) .append(", isVisibilityRestricted=") .append(mIsVisibilityRestricted) .append(", allowedPackages=") .append(String.join(",", mAllowedPackages)) .append(", suitabilityStatus=") .append(mSuitabilityStatus) .append(" }"); return result.toString(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mId); TextUtils.writeToParcel(mName, dest, flags); dest.writeStringList(mFeatures); dest.writeInt(mType); dest.writeBoolean(mIsSystem); dest.writeParcelable(mIconUri, flags); TextUtils.writeToParcel(mDescription, dest, flags); dest.writeInt(mConnectionState); dest.writeString(mClientPackageName); dest.writeString(mPackageName); dest.writeInt(mVolumeHandling); dest.writeInt(mVolumeMax); dest.writeInt(mVolume); dest.writeString(mAddress); dest.writeStringArray(mDeduplicationIds.toArray(new String[mDeduplicationIds.size()])); dest.writeBundle(mExtras); dest.writeString(mProviderId); dest.writeBoolean(mIsVisibilityRestricted); dest.writeString8Array(mAllowedPackages.toArray(new String[0])); dest.writeInt(mSuitabilityStatus); } private static String getDeviceTypeString(@Type int deviceType) { switch (deviceType) { case TYPE_BUILTIN_SPEAKER: return "BUILTIN_SPEAKER"; case TYPE_WIRED_HEADSET: return "WIRED_HEADSET"; case TYPE_WIRED_HEADPHONES: return "WIRED_HEADPHONES"; case TYPE_BLUETOOTH_A2DP: return "BLUETOOTH_A2DP"; case TYPE_HDMI: return "HDMI"; case TYPE_HDMI_ARC: return "HDMI_ARC"; case TYPE_HDMI_EARC: return "HDMI_EARC"; case TYPE_DOCK: return "DOCK"; case TYPE_USB_DEVICE: return "USB_DEVICE"; case TYPE_USB_ACCESSORY: return "USB_ACCESSORY"; case TYPE_USB_HEADSET: return "USB_HEADSET"; case TYPE_HEARING_AID: return "HEARING_AID"; case TYPE_REMOTE_TV: return "REMOTE_TV"; case TYPE_REMOTE_SPEAKER: return "REMOTE_SPEAKER"; case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER: return "REMOTE_AUDIO_VIDEO_RECEIVER"; case TYPE_REMOTE_TABLET: return "REMOTE_TABLET"; case TYPE_REMOTE_TABLET_DOCKED: return "REMOTE_TABLET_DOCKED"; case TYPE_REMOTE_COMPUTER: return "REMOTE_COMPUTER"; case TYPE_REMOTE_GAME_CONSOLE: return "REMOTE_GAME_CONSOLE"; case TYPE_REMOTE_CAR: return "REMOTE_CAR"; case TYPE_REMOTE_SMARTWATCH: return "REMOTE_SMARTWATCH"; case TYPE_REMOTE_SMARTPHONE: return "REMOTE_SMARTPHONE"; case TYPE_GROUP: return "GROUP"; case TYPE_UNKNOWN: default: return TextUtils.formatSimple("UNKNOWN(%d)", deviceType); } } /** * Builder for {@link MediaRoute2Info media route info}. */ public static final class Builder { private final String mId; private final CharSequence mName; private final List mFeatures; @Type private int mType = TYPE_UNKNOWN; private boolean mIsSystem; private Uri mIconUri; private CharSequence mDescription; @ConnectionState private int mConnectionState; private String mClientPackageName; private String mPackageName; private int mVolumeHandling = PLAYBACK_VOLUME_FIXED; private int mVolumeMax; private int mVolume; private String mAddress; private Set mDeduplicationIds; private Bundle mExtras; private String mProviderId; private boolean mIsVisibilityRestricted; private Set mAllowedPackages; @SuitabilityStatus private int mSuitabilityStatus; /** * Constructor for builder to create {@link MediaRoute2Info}. *

* In order to ensure ID uniqueness, the {@link MediaRoute2Info#getId() ID} of a route info * obtained from {@link MediaRouter2} can be different from what was set in * {@link MediaRoute2ProviderService}. *

* @param id The ID of the route. Must not be empty. * @param name The user-visible name of the route. */ public Builder(@NonNull String id, @NonNull CharSequence name) { if (TextUtils.isEmpty(id)) { throw new IllegalArgumentException("id must not be empty"); } if (TextUtils.isEmpty(name)) { throw new IllegalArgumentException("name must not be empty"); } mId = id; mName = name; mFeatures = new ArrayList<>(); mDeduplicationIds = Set.of(); mAllowedPackages = Set.of(); mSuitabilityStatus = SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER; } /** * Constructor for builder to create {@link MediaRoute2Info} with existing * {@link MediaRoute2Info} instance. * * @param routeInfo the existing instance to copy data from. */ public Builder(@NonNull MediaRoute2Info routeInfo) { this(routeInfo.mId, routeInfo); } /** * Constructor for builder to create {@link MediaRoute2Info} with existing * {@link MediaRoute2Info} instance and replace ID with the given {@code id}. * * @param id The ID of the new route. Must not be empty. * @param routeInfo the existing instance to copy data from. * @hide */ public Builder(@NonNull String id, @NonNull MediaRoute2Info routeInfo) { if (TextUtils.isEmpty(id)) { throw new IllegalArgumentException("id must not be empty"); } Objects.requireNonNull(routeInfo, "routeInfo must not be null"); mId = id; mName = routeInfo.mName; mFeatures = new ArrayList<>(routeInfo.mFeatures); mType = routeInfo.mType; mIsSystem = routeInfo.mIsSystem; mIconUri = routeInfo.mIconUri; mDescription = routeInfo.mDescription; mConnectionState = routeInfo.mConnectionState; mClientPackageName = routeInfo.mClientPackageName; mPackageName = routeInfo.mPackageName; mVolumeHandling = routeInfo.mVolumeHandling; mVolumeMax = routeInfo.mVolumeMax; mVolume = routeInfo.mVolume; mAddress = routeInfo.mAddress; mDeduplicationIds = Set.copyOf(routeInfo.mDeduplicationIds); if (routeInfo.mExtras != null) { mExtras = new Bundle(routeInfo.mExtras); } mProviderId = routeInfo.mProviderId; mIsVisibilityRestricted = routeInfo.mIsVisibilityRestricted; mAllowedPackages = routeInfo.mAllowedPackages; mSuitabilityStatus = routeInfo.mSuitabilityStatus; } /** * Adds a feature for the route. * @param feature a feature that the route has. May be one of predefined features * such as {@link #FEATURE_LIVE_AUDIO}, {@link #FEATURE_LIVE_VIDEO} or * {@link #FEATURE_REMOTE_PLAYBACK} or a custom feature defined by * a provider. * * @see #addFeatures(Collection) */ @NonNull public Builder addFeature(@NonNull String feature) { if (TextUtils.isEmpty(feature)) { throw new IllegalArgumentException("feature must not be null or empty"); } mFeatures.add(feature); return this; } /** * Adds features for the route. A route must support at least one route type. * @param features features that the route has. May include predefined features * such as {@link #FEATURE_LIVE_AUDIO}, {@link #FEATURE_LIVE_VIDEO} or * {@link #FEATURE_REMOTE_PLAYBACK} or custom features defined by * a provider. * * @see #addFeature(String) */ @NonNull public Builder addFeatures(@NonNull Collection features) { Objects.requireNonNull(features, "features must not be null"); for (String feature : features) { addFeature(feature); } return this; } /** * Clears the features of the route. A route must support at least one route type. */ @NonNull public Builder clearFeatures() { mFeatures.clear(); return this; } /** * Sets the route's type. * * @see MediaRoute2Info#getType() */ @NonNull public Builder setType(@Type int type) { mType = type; return this; } /** * Sets whether the route is a system route or not. * @hide */ @NonNull public Builder setSystemRoute(boolean isSystem) { mIsSystem = isSystem; return this; } /** * Sets the URI of the icon representing this route. *

* This icon will be used in picker UIs if available. *

* The URI must be one of the following formats: *

    *
  • content ({@link android.content.ContentResolver#SCHEME_CONTENT})
  • *
  • android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) *
  • *
  • file ({@link android.content.ContentResolver#SCHEME_FILE})
  • *
*

*/ @NonNull public Builder setIconUri(@Nullable Uri iconUri) { mIconUri = iconUri; return this; } /** * Sets the user-visible description of the route. */ @NonNull public Builder setDescription(@Nullable CharSequence description) { mDescription = description; return this; } /** * Sets the route's connection state. * * {@link #CONNECTION_STATE_DISCONNECTED}, * {@link #CONNECTION_STATE_CONNECTING}, or * {@link #CONNECTION_STATE_CONNECTED}. */ @NonNull public Builder setConnectionState(@ConnectionState int connectionState) { mConnectionState = connectionState; return this; } /** * Sets the package name of the app using the route. */ @NonNull public Builder setClientPackageName(@Nullable String packageName) { mClientPackageName = packageName; return this; } /** * Sets the package name of the route. * @hide */ @NonNull public Builder setPackageName(@NonNull String packageName) { mPackageName = packageName; return this; } /** * Sets the route's volume handling. */ @NonNull public Builder setVolumeHandling(@PlaybackVolume int volumeHandling) { mVolumeHandling = volumeHandling; return this; } /** * Sets the route's maximum volume, or 0 if unknown. */ @NonNull public Builder setVolumeMax(int volumeMax) { mVolumeMax = volumeMax; return this; } /** * Sets the route's current volume, or 0 if unknown. */ @NonNull public Builder setVolume(int volume) { mVolume = volume; return this; } /** * Sets the hardware address of the route. * @hide */ @NonNull public Builder setAddress(String address) { mAddress = address; return this; } /** * Sets the {@link MediaRoute2Info#getDeduplicationIds() deduplication IDs} of the route. */ @NonNull public Builder setDeduplicationIds(@NonNull Set id) { mDeduplicationIds = Set.copyOf(id); return this; } /** * Sets a bundle of extras for the route. *

* Note: The extras will not affect the result of {@link MediaRoute2Info#equals(Object)}. */ @NonNull public Builder setExtras(@Nullable Bundle extras) { if (extras == null) { mExtras = null; return this; } mExtras = new Bundle(extras); return this; } /** * Sets the provider id of the route. * @hide */ @NonNull public Builder setProviderId(@NonNull String providerId) { if (TextUtils.isEmpty(providerId)) { throw new IllegalArgumentException("providerId must not be null or empty"); } mProviderId = providerId; return this; } /** * Sets the visibility of this route to public. * *

By default, unless you call {@link #setVisibilityRestricted}, the new route will be * public. * *

Public routes are visible to any application with a matching {@link * RouteDiscoveryPreference#getPreferredFeatures feature}. * *

Calls to this method override previous calls to {@link #setVisibilityPublic} and * {@link #setVisibilityRestricted}. */ @NonNull public Builder setVisibilityPublic() { mIsVisibilityRestricted = false; mAllowedPackages = Set.of(); return this; } /** * Sets the visibility of this route to restricted. * *

Routes with restricted visibility are only visible to its publisher application and * applications whose package name is included in the provided {@code allowedPackages} set * with a matching {@link RouteDiscoveryPreference#getPreferredFeatures feature}. * *

Calls to this method override previous calls to {@link #setVisibilityPublic} and * {@link #setVisibilityRestricted}. * * @see #setVisibilityPublic * @param allowedPackages set of package names which are allowed to see this route. */ @NonNull public Builder setVisibilityRestricted(@NonNull Set allowedPackages) { mIsVisibilityRestricted = true; mAllowedPackages = Set.copyOf(allowedPackages); return this; } /** * Sets route suitability status. * *

The default value is {@link * MediaRoute2Info#SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER}. * *

Apps are not supposed to set {@link * MediaRoute2Info#SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER}. Publishing a non-system * route with such status throws {@link SecurityException}. */ @NonNull @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) public Builder setSuitabilityStatus(@SuitabilityStatus int suitabilityStatus) { mSuitabilityStatus = suitabilityStatus; return this; } /** * Builds the {@link MediaRoute2Info media route info}. * * @throws IllegalArgumentException if no features are added. */ @NonNull public MediaRoute2Info build() { if (mFeatures.isEmpty()) { throw new IllegalArgumentException("features must not be empty!"); } return new MediaRoute2Info(this); } } }