/* * 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 android.annotation.NonNull; import android.annotation.Nullable; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; /** * Describes a routing session which is created when a media route is selected. */ public final class RoutingSessionInfo implements Parcelable { @NonNull public static final Creator CREATOR = new Creator() { @Override public RoutingSessionInfo createFromParcel(Parcel in) { return new RoutingSessionInfo(in); } @Override public RoutingSessionInfo[] newArray(int size) { return new RoutingSessionInfo[size]; } }; private static final String TAG = "RoutingSessionInfo"; final String mId; final CharSequence mName; final String mOwnerPackageName; final String mClientPackageName; @Nullable final String mProviderId; final List mSelectedRoutes; final List mSelectableRoutes; final List mDeselectableRoutes; final List mTransferableRoutes; final int mVolumeHandling; final int mVolumeMax; final int mVolume; @Nullable final Bundle mControlHints; final boolean mIsSystemSession; RoutingSessionInfo(@NonNull Builder builder) { Objects.requireNonNull(builder, "builder must not be null."); mId = builder.mId; mName = builder.mName; mOwnerPackageName = builder.mOwnerPackageName; mClientPackageName = builder.mClientPackageName; mProviderId = builder.mProviderId; mSelectedRoutes = Collections.unmodifiableList( convertToUniqueRouteIds(builder.mSelectedRoutes)); mSelectableRoutes = Collections.unmodifiableList( convertToUniqueRouteIds(builder.mSelectableRoutes)); mDeselectableRoutes = Collections.unmodifiableList( convertToUniqueRouteIds(builder.mDeselectableRoutes)); mTransferableRoutes = Collections.unmodifiableList( convertToUniqueRouteIds(builder.mTransferableRoutes)); mVolumeHandling = builder.mVolumeHandling; mVolumeMax = builder.mVolumeMax; mVolume = builder.mVolume; mControlHints = builder.mControlHints; mIsSystemSession = builder.mIsSystemSession; } RoutingSessionInfo(@NonNull Parcel src) { Objects.requireNonNull(src, "src must not be null."); mId = ensureString(src.readString()); mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src); mOwnerPackageName = src.readString(); mClientPackageName = ensureString(src.readString()); mProviderId = src.readString(); mSelectedRoutes = ensureList(src.createStringArrayList()); mSelectableRoutes = ensureList(src.createStringArrayList()); mDeselectableRoutes = ensureList(src.createStringArrayList()); mTransferableRoutes = ensureList(src.createStringArrayList()); mVolumeHandling = src.readInt(); mVolumeMax = src.readInt(); mVolume = src.readInt(); mControlHints = src.readBundle(); mIsSystemSession = src.readBoolean(); } private static String ensureString(String str) { return str != null ? str : ""; } private static List ensureList(List list) { if (list != null) { return Collections.unmodifiableList(list); } return Collections.emptyList(); } /** * Gets the id of the session. The sessions 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, String) */ @NonNull public String getId() { if (mProviderId != null) { return MediaRouter2Utils.toUniqueId(mProviderId, mId); } else { return mId; } } /** * Gets the user-visible name of the session. It may be {@code null}. */ @Nullable public CharSequence getName() { return mName; } /** * Gets the original id set by {@link Builder#Builder(String, String)}. * @hide */ @NonNull public String getOriginalId() { return mId; } /** * Gets the package name of the session owner. * @hide */ @Nullable public String getOwnerPackageName() { return mOwnerPackageName; } /** * Gets the client package name of the session */ @NonNull public String getClientPackageName() { return mClientPackageName; } /** * Gets the provider id of the session. * @hide */ @Nullable public String getProviderId() { return mProviderId; } /** * Gets the list of IDs of selected routes for the session. It shouldn't be empty. */ @NonNull public List getSelectedRoutes() { return mSelectedRoutes; } /** * Gets the list of IDs of selectable routes for the session. */ @NonNull public List getSelectableRoutes() { return mSelectableRoutes; } /** * Gets the list of IDs of deselectable routes for the session. */ @NonNull public List getDeselectableRoutes() { return mDeselectableRoutes; } /** * Gets the list of IDs of transferable routes for the session. */ @NonNull public List getTransferableRoutes() { return mTransferableRoutes; } /** * Gets the information about how volume is handled on the session. * * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}. */ @MediaRoute2Info.PlaybackVolume public int getVolumeHandling() { return mVolumeHandling; } /** * Gets the maximum volume of the session. */ public int getVolumeMax() { return mVolumeMax; } /** * Gets the current volume of the session. *

* When it's available, it represents the volume of routing session, which is a group * of selected routes. To get the volume of each route, use {@link MediaRoute2Info#getVolume()}. *

* @see MediaRoute2Info#getVolume() */ public int getVolume() { return mVolume; } /** * Gets the control hints */ @Nullable public Bundle getControlHints() { return mControlHints; } /** * Gets whether this session is in system media route provider. * @hide */ @Nullable public boolean isSystemSession() { return mIsSystemSession; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mId); dest.writeCharSequence(mName); dest.writeString(mOwnerPackageName); dest.writeString(mClientPackageName); dest.writeString(mProviderId); dest.writeStringList(mSelectedRoutes); dest.writeStringList(mSelectableRoutes); dest.writeStringList(mDeselectableRoutes); dest.writeStringList(mTransferableRoutes); dest.writeInt(mVolumeHandling); dest.writeInt(mVolumeMax); dest.writeInt(mVolume); dest.writeBundle(mControlHints); dest.writeBoolean(mIsSystemSession); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof RoutingSessionInfo)) { return false; } RoutingSessionInfo other = (RoutingSessionInfo) obj; return Objects.equals(mId, other.mId) && Objects.equals(mName, other.mName) && Objects.equals(mOwnerPackageName, other.mOwnerPackageName) && Objects.equals(mClientPackageName, other.mClientPackageName) && Objects.equals(mProviderId, other.mProviderId) && Objects.equals(mSelectedRoutes, other.mSelectedRoutes) && Objects.equals(mSelectableRoutes, other.mSelectableRoutes) && Objects.equals(mDeselectableRoutes, other.mDeselectableRoutes) && Objects.equals(mTransferableRoutes, other.mTransferableRoutes) && (mVolumeHandling == other.mVolumeHandling) && (mVolumeMax == other.mVolumeMax) && (mVolume == other.mVolume); } @Override public int hashCode() { return Objects.hash(mId, mName, mOwnerPackageName, mClientPackageName, mProviderId, mSelectedRoutes, mSelectableRoutes, mDeselectableRoutes, mTransferableRoutes, mVolumeMax, mVolumeHandling, mVolume); } @Override public String toString() { StringBuilder result = new StringBuilder() .append("RoutingSessionInfo{ ") .append("sessionId=").append(getId()) .append(", name=").append(getName()) .append(", clientPackageName=").append(getClientPackageName()) .append(", selectedRoutes={") .append(String.join(",", getSelectedRoutes())) .append("}") .append(", selectableRoutes={") .append(String.join(",", getSelectableRoutes())) .append("}") .append(", deselectableRoutes={") .append(String.join(",", getDeselectableRoutes())) .append("}") .append(", transferableRoutes={") .append(String.join(",", getTransferableRoutes())) .append("}") .append(", volumeHandling=").append(getVolumeHandling()) .append(", volumeMax=").append(getVolumeMax()) .append(", volume=").append(getVolume()) .append(" }"); return result.toString(); } private List convertToUniqueRouteIds(@NonNull List routeIds) { if (routeIds == null) { Log.w(TAG, "routeIds is null. Returning an empty list"); return Collections.emptyList(); } // mProviderId can be null if not set. Return the original list for this case. if (mProviderId == null) { return routeIds; } List result = new ArrayList<>(); for (String routeId : routeIds) { result.add(MediaRouter2Utils.toUniqueId(mProviderId, routeId)); } return result; } /** * Builder class for {@link RoutingSessionInfo}. */ public static final class Builder { // TODO: Reorder these (important ones first) final String mId; CharSequence mName; String mOwnerPackageName; String mClientPackageName; String mProviderId; final List mSelectedRoutes; final List mSelectableRoutes; final List mDeselectableRoutes; final List mTransferableRoutes; int mVolumeHandling = MediaRoute2Info.PLAYBACK_VOLUME_FIXED; int mVolumeMax; int mVolume; Bundle mControlHints; boolean mIsSystemSession; /** * Constructor for builder to create {@link RoutingSessionInfo}. *

* In order to ensure ID uniqueness in {@link MediaRouter2} side, the value of * {@link RoutingSessionInfo#getId()} can be different from what was set in * {@link MediaRoute2ProviderService}. *

* * @param id ID of the session. Must not be empty. * @param clientPackageName package name of the client app which uses this session. * If is is unknown, then just use an empty string. * @see MediaRoute2Info#getId() */ public Builder(@NonNull String id, @NonNull String clientPackageName) { if (TextUtils.isEmpty(id)) { throw new IllegalArgumentException("id must not be empty"); } mId = id; mClientPackageName = Objects.requireNonNull(clientPackageName, "clientPackageName must not be null"); mSelectedRoutes = new ArrayList<>(); mSelectableRoutes = new ArrayList<>(); mDeselectableRoutes = new ArrayList<>(); mTransferableRoutes = new ArrayList<>(); } /** * Constructor for builder to create {@link RoutingSessionInfo} with * existing {@link RoutingSessionInfo} instance. * * @param sessionInfo the existing instance to copy data from. */ public Builder(@NonNull RoutingSessionInfo sessionInfo) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); mId = sessionInfo.mId; mName = sessionInfo.mName; mClientPackageName = sessionInfo.mClientPackageName; mProviderId = sessionInfo.mProviderId; mSelectedRoutes = new ArrayList<>(sessionInfo.mSelectedRoutes); mSelectableRoutes = new ArrayList<>(sessionInfo.mSelectableRoutes); mDeselectableRoutes = new ArrayList<>(sessionInfo.mDeselectableRoutes); mTransferableRoutes = new ArrayList<>(sessionInfo.mTransferableRoutes); if (mProviderId != null) { // They must have unique IDs. mSelectedRoutes.replaceAll(MediaRouter2Utils::getOriginalId); mSelectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId); mDeselectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId); mTransferableRoutes.replaceAll(MediaRouter2Utils::getOriginalId); } mVolumeHandling = sessionInfo.mVolumeHandling; mVolumeMax = sessionInfo.mVolumeMax; mVolume = sessionInfo.mVolume; mControlHints = sessionInfo.mControlHints; mIsSystemSession = sessionInfo.mIsSystemSession; } /** * Sets the user-visible name of the session. */ @NonNull public Builder setName(@Nullable CharSequence name) { mName = name; return this; } /** * Sets the package name of the session owner. It is expected to be called by the system. * * @hide */ @NonNull public Builder setOwnerPackageName(@Nullable String packageName) { mOwnerPackageName = packageName; return this; } /** * Sets the client package name of the session. * * @hide */ @NonNull public Builder setClientPackageName(@Nullable String packageName) { mClientPackageName = packageName; return this; } /** * Sets the provider ID of the session. * * @hide */ @NonNull public Builder setProviderId(@NonNull String providerId) { if (TextUtils.isEmpty(providerId)) { throw new IllegalArgumentException("providerId must not be empty"); } mProviderId = providerId; return this; } /** * Clears the selected routes. */ @NonNull public Builder clearSelectedRoutes() { mSelectedRoutes.clear(); return this; } /** * Adds a route to the selected routes. The {@code routeId} must not be empty. */ @NonNull public Builder addSelectedRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mSelectedRoutes.add(routeId); return this; } /** * Removes a route from the selected routes. The {@code routeId} must not be empty. */ @NonNull public Builder removeSelectedRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mSelectedRoutes.remove(routeId); return this; } /** * Clears the selectable routes. */ @NonNull public Builder clearSelectableRoutes() { mSelectableRoutes.clear(); return this; } /** * Adds a route to the selectable routes. The {@code routeId} must not be empty. */ @NonNull public Builder addSelectableRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mSelectableRoutes.add(routeId); return this; } /** * Removes a route from the selectable routes. The {@code routeId} must not be empty. */ @NonNull public Builder removeSelectableRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mSelectableRoutes.remove(routeId); return this; } /** * Clears the deselectable routes. */ @NonNull public Builder clearDeselectableRoutes() { mDeselectableRoutes.clear(); return this; } /** * Adds a route to the deselectable routes. The {@code routeId} must not be empty. */ @NonNull public Builder addDeselectableRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mDeselectableRoutes.add(routeId); return this; } /** * Removes a route from the deselectable routes. The {@code routeId} must not be empty. */ @NonNull public Builder removeDeselectableRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mDeselectableRoutes.remove(routeId); return this; } /** * Clears the transferable routes. */ @NonNull public Builder clearTransferableRoutes() { mTransferableRoutes.clear(); return this; } /** * Adds a route to the transferable routes. The {@code routeId} must not be empty. */ @NonNull public Builder addTransferableRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mTransferableRoutes.add(routeId); return this; } /** * Removes a route from the transferable routes. The {@code routeId} must not be empty. */ @NonNull public Builder removeTransferableRoute(@NonNull String routeId) { if (TextUtils.isEmpty(routeId)) { throw new IllegalArgumentException("routeId must not be empty"); } mTransferableRoutes.remove(routeId); return this; } /** * Sets the session's volume handling. * {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}. */ @NonNull public RoutingSessionInfo.Builder setVolumeHandling( @MediaRoute2Info.PlaybackVolume int volumeHandling) { mVolumeHandling = volumeHandling; return this; } /** * Sets the session's maximum volume, or 0 if unknown. */ @NonNull public RoutingSessionInfo.Builder setVolumeMax(int volumeMax) { mVolumeMax = volumeMax; return this; } /** * Sets the session's current volume, or 0 if unknown. */ @NonNull public RoutingSessionInfo.Builder setVolume(int volume) { mVolume = volume; return this; } /** * Sets control hints. */ @NonNull public Builder setControlHints(@Nullable Bundle controlHints) { mControlHints = controlHints; return this; } /** * Sets whether this session is in system media route provider. * @hide */ @NonNull public Builder setSystemSession(boolean isSystemSession) { mIsSystemSession = isSystemSession; return this; } /** * Builds a routing session info. * * @throws IllegalArgumentException if no selected routes are added. */ @NonNull public RoutingSessionInfo build() { if (mSelectedRoutes.isEmpty()) { throw new IllegalArgumentException("selectedRoutes must not be empty"); } return new RoutingSessionInfo(this); } } }