/*
 * 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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Binder;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.text.TextUtils;

import com.android.internal.util.Preconditions;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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<RoutingSessionInfo> CREATOR =
            new Creator<RoutingSessionInfo>() {
                @Override
                public RoutingSessionInfo createFromParcel(Parcel in) {
                    return new RoutingSessionInfo(in);
                }
                @Override
                public RoutingSessionInfo[] newArray(int size) {
                    return new RoutingSessionInfo[size];
                }
            };

    private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE";
    private static final String KEY_VOLUME_HANDLING = "volumeHandling";

    /**
     * Indicates that the transfer happened by the default logic without explicit system's or user's
     * request.
     *
     * <p>For example, an automatically connected Bluetooth device will have this transfer reason.
     */
    public static final int TRANSFER_REASON_FALLBACK = 0;

    /** Indicates that the transfer happened from within a privileged application. */
    public static final int TRANSFER_REASON_SYSTEM_REQUEST = 1;

    /** Indicates that the transfer happened from a non-privileged app. */
    public static final int TRANSFER_REASON_APP = 2;

    /**
     * Indicates the transfer reason.
     *
     * @hide
     */
    @IntDef(value = {TRANSFER_REASON_FALLBACK, TRANSFER_REASON_SYSTEM_REQUEST, TRANSFER_REASON_APP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface TransferReason {}

    /**
     * Indicates release is unsupported on the session.
     *
     * @hide
     */
    public static final int RELEASE_UNSUPPORTED = 0;

    /**
     * Indicates the release type of the session is to stop casting.
     *
     * @hide
     */
    public static final int RELEASE_TYPE_CASTING = 1;

    /**
     * Indicates the release type of the session is to stop audio sharing.
     *
     * @hide
     */
    public static final int RELEASE_TYPE_SHARING = 2;

    /**
     * Indicates the release type of the session.
     *
     * @hide
     */
    @IntDef({RELEASE_UNSUPPORTED, RELEASE_TYPE_CASTING, RELEASE_TYPE_SHARING})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ReleaseType {}

    @NonNull final String mOriginalId;
    @Nullable
    final CharSequence mName;
    @Nullable
    final String mOwnerPackageName;
    @NonNull
    final String mClientPackageName;
    @Nullable
    final String mProviderId;
    @NonNull
    final List<String> mSelectedRoutes;
    @NonNull
    final List<String> mSelectableRoutes;
    @NonNull
    final List<String> mDeselectableRoutes;
    @NonNull
    final List<String> mTransferableRoutes;

    @MediaRoute2Info.PlaybackVolume final int mVolumeHandling;
    final int mVolumeMax;
    final int mVolume;

    @Nullable
    final Bundle mControlHints;
    final boolean mIsSystemSession;

    @TransferReason final int mTransferReason;

    @Nullable final UserHandle mTransferInitiatorUserHandle;
    @Nullable final String mTransferInitiatorPackageName;

    @ReleaseType private final int mReleaseType;

    RoutingSessionInfo(@NonNull Builder builder) {
        Objects.requireNonNull(builder, "builder must not be null.");

        mOriginalId = builder.mOriginalId;
        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));

        mVolumeMax = builder.mVolumeMax;
        mVolume = builder.mVolume;

        mIsSystemSession = builder.mIsSystemSession;
        mVolumeHandling = builder.mVolumeHandling;

        mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling);
        mTransferReason = builder.mTransferReason;
        mTransferInitiatorUserHandle = builder.mTransferInitiatorUserHandle;
        mTransferInitiatorPackageName = builder.mTransferInitiatorPackageName;
        mReleaseType = builder.mReleaseType;
    }

    RoutingSessionInfo(@NonNull Parcel src) {
        mOriginalId = src.readString();
        Preconditions.checkArgument(!TextUtils.isEmpty(mOriginalId));

        mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src);
        mOwnerPackageName = src.readString();
        mClientPackageName = ensureString(src.readString());
        mProviderId = src.readString();

        mSelectedRoutes = ensureList(src.createStringArrayList());
        Preconditions.checkArgument(!mSelectedRoutes.isEmpty());

        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();
        mTransferReason = src.readInt();
        mTransferInitiatorUserHandle = UserHandle.readFromParcel(src);
        mTransferInitiatorPackageName = src.readString();
        mReleaseType = src.readInt();
    }

    @Nullable
    private static Bundle updateVolumeHandlingInHints(@Nullable Bundle controlHints,
            int volumeHandling) {
        // Workaround to preserve retro-compatibility with androidx.
        // See b/228021646 for more details.
        if (controlHints != null && controlHints.containsKey(KEY_GROUP_ROUTE)) {
            Bundle groupRoute = controlHints.getBundle(KEY_GROUP_ROUTE);

            if (groupRoute != null && groupRoute.containsKey(KEY_VOLUME_HANDLING)
                    && volumeHandling != groupRoute.getInt(KEY_VOLUME_HANDLING)) {
                //Creating copy of controlHints with updated value.
                Bundle newGroupRoute = new Bundle(groupRoute);
                newGroupRoute.putInt(KEY_VOLUME_HANDLING, volumeHandling);
                Bundle newControlHints = new Bundle(controlHints);
                newControlHints.putBundle(KEY_GROUP_ROUTE, newGroupRoute);
                return newControlHints;
            }
        }
        //Return same Bundle.
        return controlHints;
    }

    @NonNull
    private static String ensureString(@Nullable String str) {
        return str != null ? str : "";
    }

    @NonNull
    private static <T> List<T> ensureList(@Nullable List<? extends T> 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.
     * <p>
     * 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 (!TextUtils.isEmpty(mProviderId)) {
            return MediaRouter2Utils.toUniqueId(mProviderId, mOriginalId);
        } else {
            return mOriginalId;
        }
    }

    /**
     * Gets the user-visible name of the session. It may be {@code null}.
     */
    @Nullable
    public CharSequence getName() {
        return mName;
    }

    /**
     * Gets the original id as assigned by the {@link MediaRoute2ProviderService route provider}.
     *
     * <p>This may be different from {@link #getId()}, which may convert this original id into a
     * unique one by adding information about the provider that created this session info.
     *
     * @hide
     */
    @NonNull
    public String getOriginalId() {
        return mOriginalId;
    }

    /**
     * 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.
     *
     * <p>Selected routes are the routes that this session is actively routing media to.
     *
     * <p>The behavior of a routing session with multiple selected routes is ultimately defined by
     * the {@link MediaRoute2ProviderService} implementation. However, typically, it's expected that
     * all the selected routes of a routing session are playing the same media in sync.
     *
     * @return A non-empty list of selected route ids.
     */
    @NonNull
    public List<String> getSelectedRoutes() {
        return mSelectedRoutes;
    }

    /**
     * Gets the list of IDs of selectable routes for the session.
     *
     * <p>Selectable routes can be added to a routing session (via {@link
     * MediaRouter2.RoutingController#selectRoute}) in order to add them to the {@link
     * #getSelectedRoutes() selected routes}, so that media plays on the newly selected route along
     * with the other selected routes.
     *
     * <p>Not to be confused with {@link #getTransferableRoutes() transferable routes}. Transferring
     * to a route makes it the sole selected route.
     *
     * @return A possibly empty list of selectable route ids.
     */
    @NonNull
    public List<String> getSelectableRoutes() {
        return mSelectableRoutes;
    }

    /**
     * Gets the list of IDs of deselectable routes for the session.
     *
     * <p>Deselectable routes can be removed from the {@link #getSelectedRoutes() selected routes},
     * so that the routing session stops routing to the newly deselected route, but continues on any
     * remaining selected routes.
     *
     * <p>Deselectable routes should be a subset of the {@link #getSelectedRoutes() selected
     * routes}, meaning not all of the selected routes might be deselectable. For example, one of
     * the selected routes may be a leader device coordinating group playback, which must always
     * remain selected while the session is active.
     *
     * @return A possibly empty list of deselectable route ids.
     */
    @NonNull
    public List<String> getDeselectableRoutes() {
        return mDeselectableRoutes;
    }

    /**
     * Gets the list of IDs of transferable routes for the session.
     *
     * <p>Transferring to a route (for example, using {@link MediaRouter2#transferTo}) replaces the
     * list of {@link #getSelectedRoutes() selected routes} with the target route, causing playback
     * to move from one route to another.
     *
     * <p>Note that this is different from {@link #getSelectableRoutes() selectable routes}, because
     * selecting a route makes it part of the selected routes, while transferring to a route makes
     * it the selected route. A route can be both transferable and selectable.
     *
     * <p>Note that playback may transfer across routes without the target route being in the list
     * of transferable routes. This can happen by creating a new routing session to the target
     * route, and releasing the routing session being transferred from. The difference is that a
     * transfer to a route in the transferable list can happen with no intervention from the app,
     * with the route provider taking care of the entire operation. A transfer to a route that is
     * not in the list of transferable routes (by creating a new session) requires the app to move
     * the playback state from one device to the other.
     *
     * @return A possibly empty list of transferable route ids.
     */
    @NonNull
    public List<String> 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.
     * <p>
     * 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()}.
     * </p>
     * @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;
    }

    /** Returns the transfer reason for this routing session. */
    @TransferReason
    public int getTransferReason() {
        return mTransferReason;
    }

    /** @hide */
    @Nullable
    public UserHandle getTransferInitiatorUserHandle() {
        return mTransferInitiatorUserHandle;
    }

    /** @hide */
    @Nullable
    public String getTransferInitiatorPackageName() {
        return mTransferInitiatorPackageName;
    }

    /**
     * Returns the release type of the session.
     *
     * @hide
     */
    @ReleaseType
    public int getReleaseType() {
        return mReleaseType;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeString(mOriginalId);
        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);
        dest.writeInt(mTransferReason);
        UserHandle.writeToParcel(mTransferInitiatorUserHandle, dest);
        dest.writeString(mTransferInitiatorPackageName);
        dest.writeInt(mReleaseType);
    }

    /**
     * Dumps current state of the instance. Use with {@code dumpsys}.
     *
     * <p>See {@link Binder#dump(FileDescriptor, PrintWriter, String[])}.
     *
     * @hide
     */
    public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
        pw.println(prefix + "RoutingSessionInfo");

        String indent = prefix + "  ";

        pw.println(indent + "mOriginalId=" + mOriginalId);
        pw.println(indent + "mName=" + mName);
        pw.println(indent + "mOwnerPackageName=" + mOwnerPackageName);
        pw.println(indent + "mClientPackageName=" + mClientPackageName);
        pw.println(indent + "mProviderId=" + mProviderId);
        pw.println(indent + "mSelectedRoutes=" + mSelectedRoutes);
        pw.println(indent + "mSelectableRoutes=" + mSelectableRoutes);
        pw.println(indent + "mDeselectableRoutes=" + mDeselectableRoutes);
        pw.println(indent + "mTransferableRoutes=" + mTransferableRoutes);
        pw.println(indent + MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling));
        pw.println(indent + "mControlHints=" + mControlHints);
        pw.println(indent + "mIsSystemSession=" + mIsSystemSession);
        pw.println(indent + "mTransferReason=" + mTransferReason);
        pw.println(indent + "mtransferInitiatorUserHandle=" + mTransferInitiatorUserHandle);
        pw.println(indent + "mtransferInitiatorPackageName=" + mTransferInitiatorPackageName);
        pw.println(indent + "mReleaseType=" + getHumanReadableReleaseType(mReleaseType));
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj == null) {
            return false;
        }

        if (this == obj) {
            return true;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }

        RoutingSessionInfo other = (RoutingSessionInfo) obj;
        return Objects.equals(mOriginalId, other.mOriginalId)
                && 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)
                && (mTransferReason == other.mTransferReason)
                && Objects.equals(mTransferInitiatorUserHandle, other.mTransferInitiatorUserHandle)
                && Objects.equals(
                        mTransferInitiatorPackageName, other.mTransferInitiatorPackageName)
                && mReleaseType == other.mReleaseType;
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                mOriginalId,
                mName,
                mOwnerPackageName,
                mClientPackageName,
                mProviderId,
                mSelectedRoutes,
                mSelectableRoutes,
                mDeselectableRoutes,
                mTransferableRoutes,
                mVolumeMax,
                mVolumeHandling,
                mVolume,
                mTransferReason,
                mTransferInitiatorUserHandle,
                mTransferInitiatorPackageName,
                mReleaseType);
    }

    @Override
    public String toString() {
        return 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(", ")
                .append(MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling))
                .append(", transferReason=")
                .append(getTransferReason())
                .append(", transferInitiatorUserHandle=")
                .append(getTransferInitiatorUserHandle())
                .append(", transferInitiatorPackageName=")
                .append(getTransferInitiatorPackageName())
                .append(", releaseType=")
                .append(getHumanReadableReleaseType(getReleaseType()))
                .append(" }")
                .toString();
    }

    @NonNull
    private static String getHumanReadableReleaseType(@ReleaseType int releaseType) {
        return switch (releaseType) {
            case RELEASE_TYPE_SHARING -> "RELEASE_TYPE_SHARING";
            case RELEASE_TYPE_CASTING -> "RELEASE_TYPE_CASTING";
            case RELEASE_UNSUPPORTED -> "RELEASE_UNSUPPORTED";
            default -> "";
        };
    }

    /**
     * Provides a new list with unique route IDs if {@link #mProviderId} is set, or the original IDs
     * otherwise.
     *
     * @param routeIds list of route IDs to convert
     * @return new list with unique IDs or original IDs
     */

    @NonNull
    private List<String> convertToUniqueRouteIds(@NonNull List<String> routeIds) {
        Objects.requireNonNull(routeIds, "RouteIds cannot be null.");

        // mProviderId can be null if not set. Return the original list for this case.
        if (TextUtils.isEmpty(mProviderId)) {
            return new ArrayList<>(routeIds);
        }

        List<String> 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 {
        @NonNull private final String mOriginalId;
        @Nullable
        private CharSequence mName;
        @Nullable
        private String mOwnerPackageName;
        @NonNull
        private String mClientPackageName;
        @Nullable
        private String mProviderId;
        @NonNull
        private final List<String> mSelectedRoutes;
        @NonNull
        private final List<String> mSelectableRoutes;
        @NonNull
        private final List<String> mDeselectableRoutes;
        @NonNull
        private final List<String> mTransferableRoutes;
        @MediaRoute2Info.PlaybackVolume
        private int mVolumeHandling = MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
        private int mVolumeMax;
        private int mVolume;
        @Nullable
        private Bundle mControlHints;
        private boolean mIsSystemSession;
        @TransferReason private int mTransferReason = TRANSFER_REASON_FALLBACK;
        @Nullable private UserHandle mTransferInitiatorUserHandle;
        @Nullable private String mTransferInitiatorPackageName;
        @ReleaseType private int mReleaseType = RELEASE_TYPE_CASTING;

        /**
         * Constructor for builder to create {@link RoutingSessionInfo}.
         *
         * <p>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 originalId 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 originalId, @NonNull String clientPackageName) {
            if (TextUtils.isEmpty(originalId)) {
                throw new IllegalArgumentException("id must not be empty");
            }

            mOriginalId = originalId;
            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) {
            this(sessionInfo, sessionInfo.getOriginalId());
        }

        /**
         * Builds upon the given {@code sessionInfo}, using the given {@link #getOriginalId()} for
         * the id.
         *
         * @hide
         */
        public Builder(@NonNull RoutingSessionInfo sessionInfo, String originalId) {
            Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");

            mOriginalId = originalId;
            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;
            mTransferReason = sessionInfo.mTransferReason;
            mTransferInitiatorUserHandle = sessionInfo.mTransferInitiatorUserHandle;
            mTransferInitiatorPackageName = sessionInfo.mTransferInitiatorPackageName;
            mReleaseType = sessionInfo.mReleaseType;
        }

        /**
         * 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 Builder setVolumeHandling(@MediaRoute2Info.PlaybackVolume int volumeHandling) {
            mVolumeHandling = volumeHandling;
            return this;
        }

        /** Sets the session's maximum volume, or 0 if unknown. */
        @NonNull
        public Builder setVolumeMax(int volumeMax) {
            mVolumeMax = volumeMax;
            return this;
        }

        /** Sets the session's current volume, or 0 if unknown. */
        @NonNull
        public 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;
        }

        /**
         * Sets transfer reason for the current session.
         *
         * <p>By default the transfer reason is set to {@link
         * RoutingSessionInfo#TRANSFER_REASON_FALLBACK}.
         */
        @NonNull
        public Builder setTransferReason(@TransferReason int transferReason) {
            mTransferReason = transferReason;
            return this;
        }

        /**
         * Gets the release type of the current session.
         *
         * <p>By default the release type is set to {@link RoutingSessionInfo#RELEASE_TYPE_CASTING}.
         *
         * @hide
         */
        @NonNull
        public Builder setReleaseType(@ReleaseType int releaseType) {
            mReleaseType = releaseType;
            return this;
        }

        /**
         * Sets the user handle and package name of the process that initiated the transfer.
         *
         * <p>By default the transfer initiation user handle and package name are set to {@code
         * null}.
         */
        // The UserHandleName warning suggests the name should be "doFooAsUser". But the UserHandle
        // parameter of this function is stored in a field, and not used to execute an operation on
        // a specific user.
        // The MissingGetterMatchingBuilder requires a getTransferInitiator function. But said
        // getter is not included because the returned package name and user handle is always either
        // null or the values that correspond to the calling app, and that information is obtainable
        // via RoutingController#wasTransferInitiatedBySelf.
        @SuppressWarnings({"UserHandleName", "MissingGetterMatchingBuilder"})
        @NonNull
        public Builder setTransferInitiator(
                @Nullable UserHandle transferInitiatorUserHandle,
                @Nullable String transferInitiatorPackageName) {
            mTransferInitiatorUserHandle = transferInitiatorUserHandle;
            mTransferInitiatorPackageName = transferInitiatorPackageName;
            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);
        }
    }
}
