/*
 * Copyright (C) 2014 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.session;

import android.annotation.DrawableRes;
import android.annotation.IntDef;
import android.annotation.LongDef;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.text.TextUtils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 * Playback state for a {@link MediaSession}. This includes a state like
 * {@link PlaybackState#STATE_PLAYING}, the current playback position,
 * and the current control capabilities.
 */
public final class PlaybackState implements Parcelable {
    private static final String TAG = "PlaybackState";

    /**
     * @hide
     */
    @LongDef(flag = true, value = {ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND,
            ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING,
            ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
            ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
            ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Actions {}

    /**
     * Indicates this session supports the stop command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_STOP = 1 << 0;

    /**
     * Indicates this session supports the pause command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PAUSE = 1 << 1;

    /**
     * Indicates this session supports the play command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PLAY = 1 << 2;

    /**
     * Indicates this session supports the rewind command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_REWIND = 1 << 3;

    /**
     * Indicates this session supports the previous command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4;

    /**
     * Indicates this session supports the next command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_SKIP_TO_NEXT = 1 << 5;

    /**
     * Indicates this session supports the fast forward command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_FAST_FORWARD = 1 << 6;

    /**
     * Indicates this session supports the set rating command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_SET_RATING = 1 << 7;

    /**
     * Indicates this session supports the seek to command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_SEEK_TO = 1 << 8;

    /**
     * Indicates this session supports the play/pause toggle command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PLAY_PAUSE = 1 << 9;

    /**
     * Indicates this session supports the play from media id command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PLAY_FROM_MEDIA_ID = 1 << 10;

    /**
     * Indicates this session supports the play from search command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PLAY_FROM_SEARCH = 1 << 11;

    /**
     * Indicates this session supports the skip to queue item command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_SKIP_TO_QUEUE_ITEM = 1 << 12;

    /**
     * Indicates this session supports the play from URI command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PLAY_FROM_URI = 1 << 13;

    /**
     * Indicates this session supports the prepare command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PREPARE = 1 << 14;

    /**
     * Indicates this session supports the prepare from media id command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PREPARE_FROM_MEDIA_ID = 1 << 15;

    /**
     * Indicates this session supports the prepare from search command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PREPARE_FROM_SEARCH = 1 << 16;

    /**
     * Indicates this session supports the prepare from URI command.
     *
     * @see Builder#setActions(long)
     */
    public static final long ACTION_PREPARE_FROM_URI = 1 << 17;

    /**
     * @hide
     */
    @IntDef({STATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING,
            STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING,
            STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM})
    @Retention(RetentionPolicy.SOURCE)
    public @interface State {}

    /**
     * This is the default playback state and indicates that no media has been
     * added yet, or the performer has been reset and has no content to play.
     *
     * @see Builder#setState(int, long, float)
     * @see Builder#setState(int, long, float, long)
     */
    public static final int STATE_NONE = 0;

    /**
     * State indicating this item is currently stopped.
     *
     * @see Builder#setState
     */
    public static final int STATE_STOPPED = 1;

    /**
     * State indicating this item is currently paused.
     *
     * @see Builder#setState
     */
    public static final int STATE_PAUSED = 2;

    /**
     * State indicating this item is currently playing.
     *
     * @see Builder#setState
     */
    public static final int STATE_PLAYING = 3;

    /**
     * State indicating this item is currently fast forwarding.
     *
     * @see Builder#setState
     */
    public static final int STATE_FAST_FORWARDING = 4;

    /**
     * State indicating this item is currently rewinding.
     *
     * @see Builder#setState
     */
    public static final int STATE_REWINDING = 5;

    /**
     * State indicating this item is currently buffering and will begin playing
     * when enough data has buffered.
     *
     * @see Builder#setState
     */
    public static final int STATE_BUFFERING = 6;

    /**
     * State indicating this item is currently in an error state. The error
     * message should also be set when entering this state.
     *
     * @see Builder#setState
     */
    public static final int STATE_ERROR = 7;

    /**
     * State indicating the class doing playback is currently connecting to a
     * new destination.  Depending on the implementation you may return to the previous
     * state when the connection finishes or enter {@link #STATE_NONE}.
     * If the connection failed {@link #STATE_ERROR} should be used.
     *
     * @see Builder#setState
     */
    public static final int STATE_CONNECTING = 8;

    /**
     * State indicating the player is currently skipping to the previous item.
     *
     * @see Builder#setState
     */
    public static final int STATE_SKIPPING_TO_PREVIOUS = 9;

    /**
     * State indicating the player is currently skipping to the next item.
     *
     * @see Builder#setState
     */
    public static final int STATE_SKIPPING_TO_NEXT = 10;

    /**
     * State indicating the player is currently skipping to a specific item in
     * the queue.
     *
     * @see Builder#setState
     */
    public static final int STATE_SKIPPING_TO_QUEUE_ITEM = 11;

    /**
     * Use this value for the position to indicate the position is not known.
     */
    public static final long PLAYBACK_POSITION_UNKNOWN = -1;

    private final int mState;
    private final long mPosition;
    private final long mBufferedPosition;
    private final float mSpeed;
    private final long mActions;
    private List<PlaybackState.CustomAction> mCustomActions;
    private final CharSequence mErrorMessage;
    private final long mUpdateTime;
    private final long mActiveItemId;
    private final Bundle mExtras;

    private PlaybackState(int state, long position, long updateTime, float speed,
            long bufferedPosition, long transportControls,
            List<PlaybackState.CustomAction> customActions, long activeItemId,
            CharSequence error, Bundle extras) {
        mState = state;
        mPosition = position;
        mSpeed = speed;
        mUpdateTime = updateTime;
        mBufferedPosition = bufferedPosition;
        mActions = transportControls;
        mCustomActions = new ArrayList<>(customActions);
        mActiveItemId = activeItemId;
        mErrorMessage = error;
        mExtras = extras;
    }

    private PlaybackState(Parcel in) {
        mState = in.readInt();
        mPosition = in.readLong();
        mSpeed = in.readFloat();
        mUpdateTime = in.readLong();
        mBufferedPosition = in.readLong();
        mActions = in.readLong();
        mCustomActions = in.createTypedArrayList(CustomAction.CREATOR);
        mActiveItemId = in.readLong();
        mErrorMessage = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
        mExtras = in.readBundle();
    }

    @Override
    public String toString() {
        StringBuilder bob = new StringBuilder("PlaybackState {");
        bob.append("state=").append(mState);
        bob.append(", position=").append(mPosition);
        bob.append(", buffered position=").append(mBufferedPosition);
        bob.append(", speed=").append(mSpeed);
        bob.append(", updated=").append(mUpdateTime);
        bob.append(", actions=").append(mActions);
        bob.append(", custom actions=").append(mCustomActions);
        bob.append(", active item id=").append(mActiveItemId);
        bob.append(", error=").append(mErrorMessage);
        bob.append("}");
        return bob.toString();
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mState);
        dest.writeLong(mPosition);
        dest.writeFloat(mSpeed);
        dest.writeLong(mUpdateTime);
        dest.writeLong(mBufferedPosition);
        dest.writeLong(mActions);
        dest.writeTypedList(mCustomActions);
        dest.writeLong(mActiveItemId);
        TextUtils.writeToParcel(mErrorMessage, dest, 0);
        dest.writeBundle(mExtras);
    }

    /**
     * Get the current state of playback. One of the following:
     * <ul>
     * <li> {@link PlaybackState#STATE_NONE}</li>
     * <li> {@link PlaybackState#STATE_STOPPED}</li>
     * <li> {@link PlaybackState#STATE_PLAYING}</li>
     * <li> {@link PlaybackState#STATE_PAUSED}</li>
     * <li> {@link PlaybackState#STATE_FAST_FORWARDING}</li>
     * <li> {@link PlaybackState#STATE_REWINDING}</li>
     * <li> {@link PlaybackState#STATE_BUFFERING}</li>
     * <li> {@link PlaybackState#STATE_ERROR}</li>
     * <li> {@link PlaybackState#STATE_CONNECTING}</li>
     * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li>
     * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li>
     * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li>
     * </ul>
     */
    @State
    public int getState() {
        return mState;
    }

    /**
     * Get the current playback position in ms.
     */
    public long getPosition() {
        return mPosition;
    }

    /**
     * Get the current buffered position in ms. This is the farthest playback
     * point that can be reached from the current position using only buffered
     * content.
     */
    public long getBufferedPosition() {
        return mBufferedPosition;
    }

    /**
     * Get the current playback speed as a multiple of normal playback. This
     * should be negative when rewinding. A value of 1 means normal playback and
     * 0 means paused.
     *
     * @return The current speed of playback.
     */
    public float getPlaybackSpeed() {
        return mSpeed;
    }

    /**
     * Get the current actions available on this session. This should use a
     * bitmask of the available actions.
     * <ul>
     * <li> {@link PlaybackState#ACTION_SKIP_TO_PREVIOUS}</li>
     * <li> {@link PlaybackState#ACTION_REWIND}</li>
     * <li> {@link PlaybackState#ACTION_PLAY}</li>
     * <li> {@link PlaybackState#ACTION_PAUSE}</li>
     * <li> {@link PlaybackState#ACTION_STOP}</li>
     * <li> {@link PlaybackState#ACTION_FAST_FORWARD}</li>
     * <li> {@link PlaybackState#ACTION_SKIP_TO_NEXT}</li>
     * <li> {@link PlaybackState#ACTION_SEEK_TO}</li>
     * <li> {@link PlaybackState#ACTION_SET_RATING}</li>
     * <li> {@link PlaybackState#ACTION_PLAY_PAUSE}</li>
     * <li> {@link PlaybackState#ACTION_PLAY_FROM_MEDIA_ID}</li>
     * <li> {@link PlaybackState#ACTION_PLAY_FROM_SEARCH}</li>
     * <li> {@link PlaybackState#ACTION_SKIP_TO_QUEUE_ITEM}</li>
     * <li> {@link PlaybackState#ACTION_PLAY_FROM_URI}</li>
     * <li> {@link PlaybackState#ACTION_PREPARE}</li>
     * <li> {@link PlaybackState#ACTION_PREPARE_FROM_MEDIA_ID}</li>
     * <li> {@link PlaybackState#ACTION_PREPARE_FROM_SEARCH}</li>
     * <li> {@link PlaybackState#ACTION_PREPARE_FROM_URI}</li>
     * </ul>
     */
    @Actions
    public long getActions() {
        return mActions;
    }

    /**
     * Get the list of custom actions.
     */
    public List<PlaybackState.CustomAction> getCustomActions() {
        return mCustomActions;
    }

    /**
     * Get a user readable error message. This should be set when the state is
     * {@link PlaybackState#STATE_ERROR}.
     */
    public CharSequence getErrorMessage() {
        return mErrorMessage;
    }

    /**
     * Get the elapsed real time at which position was last updated. If the
     * position has never been set this will return 0;
     *
     * @return The last time the position was updated.
     */
    public long getLastPositionUpdateTime() {
        return mUpdateTime;
    }

    /**
     * Get the id of the currently active item in the queue. If there is no
     * queue or a queue is not supported by the session this will be
     * {@link MediaSession.QueueItem#UNKNOWN_ID}.
     *
     * @return The id of the currently active item in the queue or
     *         {@link MediaSession.QueueItem#UNKNOWN_ID}.
     */
    public long getActiveQueueItemId() {
        return mActiveItemId;
    }

    /**
     * Get any custom extras that were set on this playback state.
     *
     * @return The extras for this state or null.
     */
    public @Nullable Bundle getExtras() {
        return mExtras;
    }

    public static final @android.annotation.NonNull Parcelable.Creator<PlaybackState> CREATOR =
            new Parcelable.Creator<PlaybackState>() {
        @Override
        public PlaybackState createFromParcel(Parcel in) {
            return new PlaybackState(in);
        }

        @Override
        public PlaybackState[] newArray(int size) {
            return new PlaybackState[size];
        }
    };

    /**
     * {@link PlaybackState.CustomAction CustomActions} can be used to extend the capabilities of
     * the standard transport controls by exposing app specific actions to
     * {@link MediaController MediaControllers}.
     */
    public static final class CustomAction implements Parcelable {
        private final String mAction;
        private final CharSequence mName;
        private final int mIcon;
        private final Bundle mExtras;

        /**
         * Use {@link PlaybackState.CustomAction.Builder#build()}.
         */
        private CustomAction(String action, CharSequence name, int icon, Bundle extras) {
            mAction = action;
            mName = name;
            mIcon = icon;
            mExtras = extras;
        }

        private CustomAction(Parcel in) {
            mAction = in.readString();
            mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            mIcon = in.readInt();
            mExtras = in.readBundle();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(mAction);
            TextUtils.writeToParcel(mName, dest, flags);
            dest.writeInt(mIcon);
            dest.writeBundle(mExtras);
        }

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

        public static final @android.annotation.NonNull Parcelable.Creator<PlaybackState.CustomAction> CREATOR =
                new Parcelable.Creator<PlaybackState.CustomAction>() {

                    @Override
                    public PlaybackState.CustomAction createFromParcel(Parcel p) {
                        return new PlaybackState.CustomAction(p);
                    }

                    @Override
                    public PlaybackState.CustomAction[] newArray(int size) {
                        return new PlaybackState.CustomAction[size];
                    }
                };

        /**
         * Returns the action of the {@link CustomAction}.
         *
         * @return The action of the {@link CustomAction}.
         */
        public String getAction() {
            return mAction;
        }

        /**
         * Returns the display name of this action. e.g. "Favorite"
         *
         * @return The display name of this {@link CustomAction}.
         */
        public CharSequence getName() {
            return mName;
        }

        /**
         * Returns the resource id of the icon in the {@link MediaSession MediaSession's} package.
         *
         * @return The resource id of the icon in the {@link MediaSession MediaSession's} package.
         */
        public int getIcon() {
            return mIcon;
        }

        /**
         * Returns extras which provide additional application-specific information about the
         * action, or null if none. These arguments are meant to be consumed by a
         * {@link MediaController} if it knows how to handle them.
         *
         * @return Optional arguments for the {@link CustomAction}.
         */
        public Bundle getExtras() {
            return mExtras;
        }

        @Override
        public String toString() {
            return "Action:" + "mName='" + mName + ", mIcon=" + mIcon + ", mExtras=" + mExtras;
        }

        /**
         * Builder for {@link CustomAction} objects.
         */
        public static final class Builder {
            private final String mAction;
            private final CharSequence mName;
            private final int mIcon;
            private Bundle mExtras;

            /**
             * Creates a {@link CustomAction} builder with the id, name, and icon set.
             *
             * @param action The action of the {@link CustomAction}.
             * @param name The display name of the {@link CustomAction}. This name will be displayed
             *             along side the action if the UI supports it.
             * @param icon The icon resource id of the {@link CustomAction}. This resource id
             *             must be in the same package as the {@link MediaSession}. It will be
             *             displayed with the custom action if the UI supports it.
             */
            public Builder(String action, CharSequence name, @DrawableRes int icon) {
                if (TextUtils.isEmpty(action)) {
                    throw new IllegalArgumentException(
                            "You must specify an action to build a CustomAction.");
                }
                if (TextUtils.isEmpty(name)) {
                    throw new IllegalArgumentException(
                            "You must specify a name to build a CustomAction.");
                }
                if (icon == 0) {
                    throw new IllegalArgumentException(
                            "You must specify an icon resource id to build a CustomAction.");
                }
                mAction = action;
                mName = name;
                mIcon = icon;
            }

            /**
             * Set optional extras for the {@link CustomAction}. These extras are meant to be
             * consumed by a {@link MediaController} if it knows how to handle them.
             * Keys should be fully qualified (e.g. "com.example.MY_ARG") to avoid collisions.
             *
             * @param extras Optional extras for the {@link CustomAction}.
             * @return this.
             */
            public Builder setExtras(Bundle extras) {
                mExtras = extras;
                return this;
            }

            /**
             * Build and return the {@link CustomAction} instance with the specified values.
             *
             * @return A new {@link CustomAction} instance.
             */
            public CustomAction build() {
                return new CustomAction(mAction, mName, mIcon, mExtras);
            }
        }
    }

    /**
     * Builder for {@link PlaybackState} objects.
     */
    public static final class Builder {
        private final List<PlaybackState.CustomAction> mCustomActions = new ArrayList<>();

        private int mState;
        private long mPosition;
        private long mBufferedPosition;
        private float mSpeed;
        private long mActions;
        private CharSequence mErrorMessage;
        private long mUpdateTime;
        private long mActiveItemId = MediaSession.QueueItem.UNKNOWN_ID;
        private Bundle mExtras;

        /**
         * Creates an initially empty state builder.
         */
        public Builder() {
        }

        /**
         * Creates a builder with the same initial values as those in the from
         * state.
         *
         * @param from The state to use for initializing the builder.
         */
        public Builder(PlaybackState from) {
            if (from == null) {
                return;
            }
            mState = from.mState;
            mPosition = from.mPosition;
            mBufferedPosition = from.mBufferedPosition;
            mSpeed = from.mSpeed;
            mActions = from.mActions;
            if (from.mCustomActions != null) {
                mCustomActions.addAll(from.mCustomActions);
            }
            mErrorMessage = from.mErrorMessage;
            mUpdateTime = from.mUpdateTime;
            mActiveItemId = from.mActiveItemId;
            mExtras = from.mExtras;
        }

        /**
         * Set the current state of playback.
         * <p>
         * The position must be in ms and indicates the current playback
         * position within the item. If the position is unknown use
         * {@link #PLAYBACK_POSITION_UNKNOWN}. When not using an unknown
         * position the time at which the position was updated must be provided.
         * It is okay to use {@link SystemClock#elapsedRealtime()} if the
         * current position was just retrieved.
         * <p>
         * The speed is a multiple of normal playback and should be 0 when
         * paused and negative when rewinding. Normal playback speed is 1.0.
         * <p>
         * The state must be one of the following:
         * <ul>
         * <li> {@link PlaybackState#STATE_NONE}</li>
         * <li> {@link PlaybackState#STATE_STOPPED}</li>
         * <li> {@link PlaybackState#STATE_PLAYING}</li>
         * <li> {@link PlaybackState#STATE_PAUSED}</li>
         * <li> {@link PlaybackState#STATE_FAST_FORWARDING}</li>
         * <li> {@link PlaybackState#STATE_REWINDING}</li>
         * <li> {@link PlaybackState#STATE_BUFFERING}</li>
         * <li> {@link PlaybackState#STATE_ERROR}</li>
         * <li> {@link PlaybackState#STATE_CONNECTING}</li>
         * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li>
         * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li>
         * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li>
         * </ul>
         *
         * @param state The current state of playback.
         * @param position The position in the current item in ms.
         * @param playbackSpeed The current speed of playback as a multiple of
         *            normal playback.
         * @param updateTime The time in the {@link SystemClock#elapsedRealtime}
         *            timebase that the position was updated at.
         * @return this
         */
        public Builder setState(@State int state, long position, float playbackSpeed,
                long updateTime) {
            mState = state;
            mPosition = position;
            mUpdateTime = updateTime;
            mSpeed = playbackSpeed;
            return this;
        }

        /**
         * Set the current state of playback.
         * <p>
         * The position must be in ms and indicates the current playback
         * position within the item. If the position is unknown use
         * {@link #PLAYBACK_POSITION_UNKNOWN}. The update time will be set to
         * the current {@link SystemClock#elapsedRealtime()}.
         * <p>
         * The speed is a multiple of normal playback and should be 0 when
         * paused and negative when rewinding. Normal playback speed is 1.0.
         * <p>
         * The state must be one of the following:
         * <ul>
         * <li> {@link PlaybackState#STATE_NONE}</li>
         * <li> {@link PlaybackState#STATE_STOPPED}</li>
         * <li> {@link PlaybackState#STATE_PLAYING}</li>
         * <li> {@link PlaybackState#STATE_PAUSED}</li>
         * <li> {@link PlaybackState#STATE_FAST_FORWARDING}</li>
         * <li> {@link PlaybackState#STATE_REWINDING}</li>
         * <li> {@link PlaybackState#STATE_BUFFERING}</li>
         * <li> {@link PlaybackState#STATE_ERROR}</li>
         * <li> {@link PlaybackState#STATE_CONNECTING}</li>
         * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li>
         * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li>
         * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li>
         * </ul>
         *
         * @param state The current state of playback.
         * @param position The position in the current item in ms.
         * @param playbackSpeed The current speed of playback as a multiple of
         *            normal playback.
         * @return this
         */
        public Builder setState(@State int state, long position, float playbackSpeed) {
            return setState(state, position, playbackSpeed, SystemClock.elapsedRealtime());
        }

        /**
         * Set the current actions available on this session. This should use a
         * bitmask of possible actions.
         * <ul>
         * <li> {@link PlaybackState#ACTION_SKIP_TO_PREVIOUS}</li>
         * <li> {@link PlaybackState#ACTION_REWIND}</li>
         * <li> {@link PlaybackState#ACTION_PLAY}</li>
         * <li> {@link PlaybackState#ACTION_PAUSE}</li>
         * <li> {@link PlaybackState#ACTION_STOP}</li>
         * <li> {@link PlaybackState#ACTION_FAST_FORWARD}</li>
         * <li> {@link PlaybackState#ACTION_SKIP_TO_NEXT}</li>
         * <li> {@link PlaybackState#ACTION_SEEK_TO}</li>
         * <li> {@link PlaybackState#ACTION_SET_RATING}</li>
         * <li> {@link PlaybackState#ACTION_PLAY_PAUSE}</li>
         * <li> {@link PlaybackState#ACTION_PLAY_FROM_MEDIA_ID}</li>
         * <li> {@link PlaybackState#ACTION_PLAY_FROM_SEARCH}</li>
         * <li> {@link PlaybackState#ACTION_SKIP_TO_QUEUE_ITEM}</li>
         * <li> {@link PlaybackState#ACTION_PLAY_FROM_URI}</li>
         * <li> {@link PlaybackState#ACTION_PREPARE}</li>
         * <li> {@link PlaybackState#ACTION_PREPARE_FROM_MEDIA_ID}</li>
         * <li> {@link PlaybackState#ACTION_PREPARE_FROM_SEARCH}</li>
         * <li> {@link PlaybackState#ACTION_PREPARE_FROM_URI}</li>
         * </ul>
         *
         * @param actions The set of actions allowed.
         * @return this
         */
        public Builder setActions(@Actions long actions) {
            mActions = actions;
            return this;
        }

        /**
         * Add a custom action to the playback state. Actions can be used to
         * expose additional functionality to {@link MediaController
         * MediaControllers} beyond what is offered by the standard transport
         * controls.
         * <p>
         * e.g. start a radio station based on the current item or skip ahead by
         * 30 seconds.
         *
         * @param action An identifier for this action. It can be sent back to
         *            the {@link MediaSession} through
         *            {@link MediaController.TransportControls#sendCustomAction(String, Bundle)}.
         * @param name The display name for the action. If text is shown with
         *            the action or used for accessibility, this is what should
         *            be used.
         * @param icon The resource action of the icon that should be displayed
         *            for the action. The resource should be in the package of
         *            the {@link MediaSession}.
         * @return this
         */
        public Builder addCustomAction(String action, String name, int icon) {
            return addCustomAction(new PlaybackState.CustomAction(action, name, icon, null));
        }

        /**
         * Add a custom action to the playback state. Actions can be used to expose additional
         * functionality to {@link MediaController MediaControllers} beyond what is offered by the
         * standard transport controls.
         * <p>
         * An example of an action would be to start a radio station based on the current item
         * or to skip ahead by 30 seconds.
         *
         * @param customAction The custom action to add to the {@link PlaybackState}.
         * @return this
         */
        public Builder addCustomAction(PlaybackState.CustomAction customAction) {
            if (customAction == null) {
                throw new IllegalArgumentException(
                        "You may not add a null CustomAction to PlaybackState.");
            }
            mCustomActions.add(customAction);
            return this;
        }

        /**
         * Set the current buffered position in ms. This is the farthest
         * playback point that can be reached from the current position using
         * only buffered content.
         *
         * @param bufferedPosition The position in ms that playback is buffered
         *            to.
         * @return this
         */
        public Builder setBufferedPosition(long bufferedPosition) {
            mBufferedPosition = bufferedPosition;
            return this;
        }

        /**
         * Set the active item in the play queue by specifying its id. The
         * default value is {@link MediaSession.QueueItem#UNKNOWN_ID}
         *
         * @param id The id of the active item.
         * @return this
         */
        public Builder setActiveQueueItemId(long id) {
            mActiveItemId = id;
            return this;
        }

        /**
         * Set a user readable error message. This should be set when the state
         * is {@link PlaybackState#STATE_ERROR}.
         *
         * @param error The error message for display to the user.
         * @return this
         */
        public Builder setErrorMessage(CharSequence error) {
            mErrorMessage = error;
            return this;
        }

        /**
         * Set any custom extras to be included with the playback state.
         *
         * @param extras The extras to include.
         * @return this
         */
        public Builder setExtras(Bundle extras) {
            mExtras = extras;
            return this;
        }

        /**
         * Build and return the {@link PlaybackState} instance with these
         * values.
         *
         * @return A new state instance.
         */
        public PlaybackState build() {
            return new PlaybackState(mState, mPosition, mUpdateTime, mSpeed, mBufferedPosition,
                    mActions, mCustomActions, mActiveItemId, mErrorMessage, mExtras);
        }
    }
}
