/*
 * Copyright (C) 2016 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.AudioAttributes.ALLOW_CAPTURE_BY_ALL;
import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE;

import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;

/**
 * The AudioPlaybackConfiguration class collects the information describing an audio playback
 * session.
 */
public final class AudioPlaybackConfiguration implements Parcelable {
    private static final String TAG = new String("AudioPlaybackConfiguration");

    private static final boolean DEBUG = false;

    /** @hide */
    public static final int PLAYER_PIID_INVALID = -1;
    /** @hide */
    public static final int PLAYER_UPID_INVALID = -1;
    /** @hide */
    public static final int PLAYER_DEVICEID_INVALID = 0;

    // information about the implementation
    /**
     * @hide
     * An unknown type of player
     */
    @SystemApi
    public static final int PLAYER_TYPE_UNKNOWN = -1;
    /**
     * @hide
     * Player backed by a java android.media.AudioTrack player
     */
    @SystemApi
    public static final int PLAYER_TYPE_JAM_AUDIOTRACK = 1;
    /**
     * @hide
     * Player backed by a java android.media.MediaPlayer player
     */
    @SystemApi
    public static final int PLAYER_TYPE_JAM_MEDIAPLAYER = 2;
    /**
     * @hide
     * Player backed by a java android.media.SoundPool player
     */
    @SystemApi
    public static final int PLAYER_TYPE_JAM_SOUNDPOOL = 3;
    /**
     * @hide
     * Player backed by a C OpenSL ES AudioPlayer player with a BufferQueue source
     */
    @SystemApi
    public static final int PLAYER_TYPE_SLES_AUDIOPLAYER_BUFFERQUEUE = 11;
    /**
     * @hide
     * Player backed by a C OpenSL ES AudioPlayer player with a URI or FD source
     */
    @SystemApi
    public static final int PLAYER_TYPE_SLES_AUDIOPLAYER_URI_FD = 12;

    /**
     * @hide
     * Player backed an AAudio player.
     */
    @SystemApi
    public static final int PLAYER_TYPE_AAUDIO = 13;

    /**
     * @hide
     * Player backed a hardware source, whose state is visible in the Android audio policy manager.
     * Note this type is not in System API so it will not be returned in public API calls
     */
    // TODO unhide for SystemApi, update getPlayerType()
    public static final int PLAYER_TYPE_HW_SOURCE = 14;

    /**
     * @hide
     * Player is a proxy for an audio player whose audio and state doesn't go through the Android
     * audio framework.
     * Note this type is not in System API so it will not be returned in public API calls
     */
    // TODO unhide for SystemApi, update getPlayerType()
    public static final int PLAYER_TYPE_EXTERNAL_PROXY = 15;

    /** @hide */
    @IntDef({
        PLAYER_TYPE_UNKNOWN,
        PLAYER_TYPE_JAM_AUDIOTRACK,
        PLAYER_TYPE_JAM_MEDIAPLAYER,
        PLAYER_TYPE_JAM_SOUNDPOOL,
        PLAYER_TYPE_SLES_AUDIOPLAYER_BUFFERQUEUE,
        PLAYER_TYPE_SLES_AUDIOPLAYER_URI_FD,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PlayerType {}

    /**
     * @hide
     * An unknown player state
     */
    @SystemApi
    public static final int PLAYER_STATE_UNKNOWN = -1;
    /**
     * @hide
     * The resources of the player have been released, it cannot play anymore
     */
    @SystemApi
    public static final int PLAYER_STATE_RELEASED = 0;
    /**
     * @hide
     * The state of a player when it's created
     */
    @SystemApi
    public static final int PLAYER_STATE_IDLE = 1;
    /**
     * @hide
     * The state of a player that is actively playing
     */
    @SystemApi
    public static final int PLAYER_STATE_STARTED = 2;
    /**
     * @hide
     * The state of a player where playback is paused
     */
    @SystemApi
    public static final int PLAYER_STATE_PAUSED = 3;
    /**
     * @hide
     * The state of a player where playback is stopped
     */
    @SystemApi
    public static final int PLAYER_STATE_STOPPED = 4;
    /**
     * @hide
     * The state used to update device id, does not actually change the state of the player
     */
    public static final int PLAYER_UPDATE_DEVICE_ID = 5;
    /**
     * @hide
     * The state used to update port id, does not actually change the state of the player
     */
    public static final int PLAYER_UPDATE_PORT_ID = 6;
    /**
     * @hide
     * Used to update the mute state of a player through its port id
     */
    public static final int PLAYER_UPDATE_MUTED = 7;
    /**
     * @hide
     * Used to update the spatialization status and format of a player through its port id
     */
    public static final int PLAYER_UPDATE_FORMAT = 8;

    /** @hide */
    @IntDef({
        PLAYER_STATE_UNKNOWN,
        PLAYER_STATE_RELEASED,
        PLAYER_STATE_IDLE,
        PLAYER_STATE_STARTED,
        PLAYER_STATE_PAUSED,
        PLAYER_STATE_STOPPED,
        PLAYER_UPDATE_DEVICE_ID,
        PLAYER_UPDATE_PORT_ID,
        PLAYER_UPDATE_MUTED,
        PLAYER_UPDATE_FORMAT,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PlayerState {}

    /** @hide */
    public static String playerStateToString(@PlayerState int state) {
        switch (state) {
            case PLAYER_STATE_UNKNOWN: return "PLAYER_STATE_UNKNOWN";
            case PLAYER_STATE_RELEASED: return "PLAYER_STATE_RELEASED";
            case PLAYER_STATE_IDLE: return "PLAYER_STATE_IDLE";
            case PLAYER_STATE_STARTED: return "PLAYER_STATE_STARTED";
            case PLAYER_STATE_PAUSED: return "PLAYER_STATE_PAUSED";
            case PLAYER_STATE_STOPPED: return "PLAYER_STATE_STOPPED";
            case PLAYER_UPDATE_DEVICE_ID: return "PLAYER_UPDATE_DEVICE_ID";
            case PLAYER_UPDATE_PORT_ID: return "PLAYER_UPDATE_PORT_ID";
            case PLAYER_UPDATE_MUTED: return "PLAYER_UPDATE_MUTED";
            case PLAYER_UPDATE_FORMAT: return "PLAYER_UPDATE_FORMAT";
            default:
                return "invalid state " + state;
        }
    }

    /**
     * @hide
     * Used to update the spatialization status of a player through its port ID. Must be kept in
     * sync with frameworks/native/include/audiomanager/AudioManager.h
     */
    public static final String EXTRA_PLAYER_EVENT_SPATIALIZED =
            "android.media.extra.PLAYER_EVENT_SPATIALIZED";
    /**
     * @hide
     * Used to update the sample rate of a player through its port ID. Must be kept in sync with
     * frameworks/native/include/audiomanager/AudioManager.h
     */
    public static final String EXTRA_PLAYER_EVENT_SAMPLE_RATE =
            "android.media.extra.PLAYER_EVENT_SAMPLE_RATE";
    /**
     * @hide
     * Used to update the channel mask of a player through its port ID. Must be kept in sync with
     * frameworks/native/include/audiomanager/AudioManager.h
     */
    public static final String EXTRA_PLAYER_EVENT_CHANNEL_MASK =
            "android.media.extra.PLAYER_EVENT_CHANNEL_MASK";
    /**
     * @hide
     * Used to update the mute state of a player through its port ID. Must be kept in sync with
     * frameworks/native/include/audiomanager/AudioManager.h
     */
    public static final String EXTRA_PLAYER_EVENT_MUTE =
            "android.media.extra.PLAYER_EVENT_MUTE";

    /**
     * @hide
     * Flag used when muted by master volume.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    public static final int MUTED_BY_MASTER = (1 << 0);
    /**
     * @hide
     * Flag used when muted by stream volume.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    public static final int MUTED_BY_STREAM_VOLUME = (1 << 1);
    /**
     * @hide
     * Flag used when muted by stream mute.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    public static final int MUTED_BY_STREAM_MUTED = (1 << 2);
    /**
     * @hide
     * Flag used when playback is muted by AppOpsManager#OP_PLAY_AUDIO.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    public static final int MUTED_BY_APP_OPS = (1 << 3);
    /**
     * @hide
     * Flag used when muted by client volume.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    public static final int MUTED_BY_CLIENT_VOLUME = (1 << 4);
    /**
     * @hide
     * Flag used when muted by volume shaper.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    public static final int MUTED_BY_VOLUME_SHAPER = (1 << 5);

    /** @hide */
    @IntDef(
            flag = true,
            value = {MUTED_BY_MASTER, MUTED_BY_STREAM_VOLUME, MUTED_BY_STREAM_MUTED,
                    MUTED_BY_APP_OPS, MUTED_BY_CLIENT_VOLUME, MUTED_BY_VOLUME_SHAPER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface PlayerMuteEvent {
    }

    // immutable data
    private final int mPlayerIId;

    // not final due to anonymization step
    private int mPlayerType;
    private int mClientUid;
    private int mClientPid;
    // the IPlayer reference and death monitor
    private IPlayerShell mIPlayerShell;

    private int mPlayerState;
    private AudioAttributes mPlayerAttr; // never null

    // lock for updateable properties
    private final Object mUpdateablePropLock = new Object();

    @GuardedBy("mUpdateablePropLock")
    private int mDeviceId;
    @GuardedBy("mUpdateablePropLock")
    private int mSessionId;
    @GuardedBy("mUpdateablePropLock")
    private @NonNull FormatInfo mFormatInfo;
    @GuardedBy("mUpdateablePropLock")
    @PlayerMuteEvent private int mMutedState;

    /**
     * Never use without initializing parameters afterwards
     */
    private AudioPlaybackConfiguration(int piid) {
        mPlayerIId = piid;
        mIPlayerShell = null;
    }

    /**
     * @hide
     */
    public AudioPlaybackConfiguration(PlayerBase.PlayerIdCard pic, int piid, int uid, int pid) {
        if (DEBUG) {
            Log.d(TAG, "new: piid=" + piid + " iplayer=" + pic.mIPlayer
                    + " sessionId=" + pic.mSessionId);
        }
        mPlayerIId = piid;
        mPlayerType = pic.mPlayerType;
        mClientUid = uid;
        mClientPid = pid;
        mMutedState = 0;
        mDeviceId = PLAYER_DEVICEID_INVALID;
        mPlayerState = PLAYER_STATE_IDLE;
        mPlayerAttr = pic.mAttributes;
        if ((sPlayerDeathMonitor != null) && (pic.mIPlayer != null)) {
            mIPlayerShell = new IPlayerShell(this, pic.mIPlayer);
        } else {
            mIPlayerShell = null;
        }
        mSessionId = pic.mSessionId;
        mFormatInfo = FormatInfo.DEFAULT;
    }

    /**
     * @hide
     */
    public void init() {
        synchronized (this) {
            if (mIPlayerShell != null) {
                mIPlayerShell.monitorDeath();
            }
        }
    }

    // sets the fields that are updateable and require synchronization
    private void setUpdateableFields(int deviceId, int sessionId, int mutedState, FormatInfo format)
    {
        synchronized (mUpdateablePropLock) {
            mDeviceId = deviceId;
            mSessionId = sessionId;
            mMutedState = mutedState;
            mFormatInfo = format;
        }
    }
    // Note that this method is called server side, so no "privileged" information is ever sent
    // to a client that is not supposed to have access to it.
    /**
     * @hide
     * Creates a copy of the playback configuration that is stripped of any data enabling
     * identification of which application it is associated with ("anonymized").
     * @param in the instance to copy from
     */
    public static AudioPlaybackConfiguration anonymizedCopy(AudioPlaybackConfiguration in) {
        final AudioPlaybackConfiguration anonymCopy = new AudioPlaybackConfiguration(in.mPlayerIId);
        anonymCopy.mPlayerState = in.mPlayerState;
        // do not reuse the full attributes: only usage, content type and public flags are allowed
        AudioAttributes.Builder builder = new AudioAttributes.Builder()
                .setContentType(in.mPlayerAttr.getContentType())
                .setFlags(in.mPlayerAttr.getFlags())
                .setAllowedCapturePolicy(
                        in.mPlayerAttr.getAllowedCapturePolicy() == ALLOW_CAPTURE_BY_ALL
                                ? ALLOW_CAPTURE_BY_ALL : ALLOW_CAPTURE_BY_NONE);
        if (AudioAttributes.isSystemUsage(in.mPlayerAttr.getSystemUsage())) {
            builder.setSystemUsage(in.mPlayerAttr.getSystemUsage());
        } else {
            builder.setUsage(in.mPlayerAttr.getUsage());
        }
        anonymCopy.mPlayerAttr = builder.build();
        // anonymized data
        anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN;
        anonymCopy.mClientUid = PLAYER_UPID_INVALID;
        anonymCopy.mClientPid = PLAYER_UPID_INVALID;
        anonymCopy.mIPlayerShell = null;
        anonymCopy.setUpdateableFields(
                /*deviceId*/ PLAYER_DEVICEID_INVALID,
                /*sessionId*/ AudioSystem.AUDIO_SESSION_ALLOCATE,
                /*mutedState*/ 0,
                FormatInfo.DEFAULT);
        return anonymCopy;
    }

    /**
     * Return the {@link AudioAttributes} of the corresponding player.
     * @return the audio attributes of the player
     */
    public AudioAttributes getAudioAttributes() {
        return mPlayerAttr;
    }

    /**
     * @hide
     * Return the uid of the client application that created this player.
     * @return the uid of the client
     */
    @SystemApi
    public int getClientUid() {
        return mClientUid;
    }

    /**
     * @hide
     * Return the pid of the client application that created this player.
     * @return the pid of the client
     */
    @SystemApi
    public int getClientPid() {
        return mClientPid;
    }

    /**
     * Returns information about the {@link AudioDeviceInfo} used for this playback.
     * @return the audio playback device or null if the device is not available at the time of query
     */
    public @Nullable AudioDeviceInfo getAudioDeviceInfo() {
        final int deviceId;
        synchronized (mUpdateablePropLock) {
            deviceId = mDeviceId;
        }
        if (deviceId == PLAYER_DEVICEID_INVALID) {
            return null;
        }
        return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_OUTPUTS);
    }

    /**
     * @hide
     * Return the audio session ID associated with this player.
     * See {@link AudioManager#generateAudioSessionId()}.
     * @return an audio session ID
     */
    @SystemApi
    public @IntRange(from = 0) int getSessionId() {
        synchronized (mUpdateablePropLock) {
            return mSessionId;
        }
    }

    /**
     * @hide
     * Used for determining if the current player is muted.
     * <br>Note that if this result is true then {@link #getMutedBy} will be > 0.
     * @return {@code true} if the player associated with this configuration has been muted (by any
     * given MUTED_BY_* source event) or {@code false} otherwise.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    public boolean isMuted() {
        synchronized (mUpdateablePropLock) {
            return mMutedState != 0;
        }
    }

    /**
     * @hide
     * Returns a bitmask expressing the mute state as a combination of MUTED_BY_* flags.
     * <br>A value of 0 corresponds to an unmuted player.
     * @return the mute state.
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
    @PlayerMuteEvent public int getMutedBy() {
        synchronized (mUpdateablePropLock) {
            return mMutedState;
        }
    }

    /**
     * @hide
     * Return the type of player linked to this configuration.
     * <br>Note that player types not exposed in the system API will be represented as
     * {@link #PLAYER_TYPE_UNKNOWN}.
     * @return the type of the player.
     */
    @SystemApi
    public @PlayerType int getPlayerType() {
        switch (mPlayerType) {
            case PLAYER_TYPE_HW_SOURCE:
            case PLAYER_TYPE_EXTERNAL_PROXY:
                return PLAYER_TYPE_UNKNOWN;
            default:
                return mPlayerType;
        }
    }

    /**
     * @hide
     * Return the current state of the player linked to this configuration. The return value is one
     * of {@link #PLAYER_STATE_IDLE}, {@link #PLAYER_STATE_PAUSED}, {@link #PLAYER_STATE_STARTED},
     * {@link #PLAYER_STATE_STOPPED}, {@link #PLAYER_STATE_RELEASED} or
     * {@link #PLAYER_STATE_UNKNOWN}.
     * @return the state of the player.
     */
    @SystemApi
    public @PlayerState int getPlayerState() {
        return mPlayerState;
    }

    /**
     * @hide
     * Return an identifier unique for the lifetime of the player.
     * @return a player interface identifier
     */
    @SystemApi
    public int getPlayerInterfaceId() {
        return mPlayerIId;
    }

    /**
     * @hide
     * Return a proxy for the player associated with this playback configuration
     * @return a proxy player
     */
    @SystemApi
    public PlayerProxy getPlayerProxy() {
        final IPlayerShell ips;
        synchronized (this) {
            ips = mIPlayerShell;
        }
        return ips == null ? null : new PlayerProxy(this);
    }

    /**
     * @hide
     * Return whether this player's output is being processed by the spatializer effect backing
     * the {@link android.media.Spatializer} implementation.
     * @return true if spatialized, false if not or playback hasn't started
     */
    @SystemApi
    public boolean isSpatialized() {
        synchronized (mUpdateablePropLock) {
            return mFormatInfo.mIsSpatialized;
        }
    }

    /**
     * @hide
     * Return the sample rate in Hz of the content being played.
     * @return the sample rate in Hertz, or 0 if playback hasn't started
     */
    @SystemApi
    public @IntRange(from = 0) int getSampleRate() {
        synchronized (mUpdateablePropLock) {
            return mFormatInfo.mSampleRate;
        }
    }

    /**
     * @hide
     * Return the player's channel mask
     * @return the channel mask, or 0 if playback hasn't started. See {@link AudioFormat} and
     *     the definitions for the <code>CHANNEL_OUT_*</code> values used for the mask's bitfield
     */
    @SystemApi
    public @AudioFormat.ChannelOut int getChannelMask() {
        synchronized (mUpdateablePropLock) {
            return (AudioFormat.convertNativeChannelMaskToOutMask(mFormatInfo.mNativeChannelMask));
        }
    }

    /**
     * @hide
     * @return the IPlayer interface for the associated player
     */
    IPlayer getIPlayer() {
        final IPlayerShell ips;
        synchronized (this) {
            ips = mIPlayerShell;
        }
        return ips == null ? null : ips.getIPlayer();
    }

    /**
     * @hide
     * Handle a change of audio attributes
     * @param attr
     */
    public boolean handleAudioAttributesEvent(@NonNull AudioAttributes attr) {
        final boolean changed = !attr.equals(mPlayerAttr);
        mPlayerAttr = attr;
        return changed;
    }

    /**
     * @hide
     * Handle a change of audio session ID
     * @param sessionId the audio session ID
     */
    public boolean handleSessionIdEvent(int sessionId) {
        synchronized (mUpdateablePropLock) {
            final boolean changed = sessionId != mSessionId;
            mSessionId = sessionId;
            return changed;
        }
    }

    /**
     * @hide
     * Handle a change of the muted state
     * @param mutedState the mute reason as a combination of {@link PlayerMuteEvent} flags
     * @return true if the state changed, false otherwise
     */
    public boolean handleMutedEvent(@PlayerMuteEvent int mutedState) {
        synchronized (mUpdateablePropLock) {
            final boolean changed = mMutedState != mutedState;
            mMutedState = mutedState;
            return changed;
        }
    }

    /**
     * @hide
     * Handle a change of playback format
     * @param fi the latest format information
     * @return true if the format changed, false otherwise
     */
    public boolean handleFormatEvent(@NonNull FormatInfo fi) {
        synchronized (mUpdateablePropLock) {
            final boolean changed = !mFormatInfo.equals(fi);
            mFormatInfo = fi;
            return changed;
        }
    }

    /**
     * @hide
     * Handle a player state change
     * @param event
     * @param deviceId active device id or {@Code PLAYER_DEVICEID_INVALID}
     * <br>Note device id is valid for {@code PLAYER_UPDATE_DEVICE_ID} or
     * <br>{@code PLAYER_STATE_STARTED} events, as the device id will be reset to none when
     * <br>pausing or stopping playback. It will be set to active device when playback starts or
     * <br>it will be changed when PLAYER_UPDATE_DEVICE_ID is sent. The latter can happen if the
     * <br>device changes in the middle of playback.
     * @return true if the state changed, false otherwise
     */
    public boolean handleStateEvent(int event, int deviceId) {
        boolean changed = false;
        synchronized (mUpdateablePropLock) {

            // Do not update if it is only device id update
            if (event != PLAYER_UPDATE_DEVICE_ID) {
                changed = (mPlayerState != event);
                mPlayerState = event;
            }

            if (event == PLAYER_STATE_STARTED || event == PLAYER_UPDATE_DEVICE_ID) {
                changed = changed || (mDeviceId != deviceId);
                mDeviceId = deviceId;
            }

            if (changed && (event == PLAYER_STATE_RELEASED) && (mIPlayerShell != null)) {
                mIPlayerShell.release();
                mIPlayerShell = null;
            }
        }
        return changed;
    }

    // To report IPlayer death from death recipient
    /** @hide */
    public interface PlayerDeathMonitor {
        public void playerDeath(int piid);
    }
    /** @hide */
    public static PlayerDeathMonitor sPlayerDeathMonitor;

    private void playerDied() {
        if (sPlayerDeathMonitor != null) {
            sPlayerDeathMonitor.playerDeath(mPlayerIId);
        }
    }

    private boolean isMuteAffectingActiveState() {
        return (mMutedState & MUTED_BY_CLIENT_VOLUME) != 0
                || (mMutedState & MUTED_BY_VOLUME_SHAPER) != 0
                || (mMutedState & MUTED_BY_APP_OPS) != 0;
    }

    /**
     * @hide
     * Returns true if the player is considered "active", i.e. actively playing with unmuted
     * volume, and thus in a state that should make it considered for the list public (sanitized)
     * active playback configurations
     * @return true if active
     */
    @SystemApi
    public boolean isActive() {
        switch (mPlayerState) {
            case PLAYER_STATE_STARTED:
                return !isMuteAffectingActiveState();
            case PLAYER_STATE_UNKNOWN:
            case PLAYER_STATE_RELEASED:
            case PLAYER_STATE_IDLE:
            case PLAYER_STATE_PAUSED:
            case PLAYER_STATE_STOPPED:
            default:
                return false;
        }
    }

    /**
     * @hide
     * For AudioService dump
     * @param pw
     */
    public void dump(PrintWriter pw) {
        pw.println("  " + this);
    }

    public static final @android.annotation.NonNull Parcelable.Creator<AudioPlaybackConfiguration> CREATOR
            = new Parcelable.Creator<AudioPlaybackConfiguration>() {
        /**
         * Rebuilds an AudioPlaybackConfiguration previously stored with writeToParcel().
         * @param p Parcel object to read the AudioPlaybackConfiguration from
         * @return a new AudioPlaybackConfiguration created from the data in the parcel
         */
        public AudioPlaybackConfiguration createFromParcel(Parcel p) {
            return new AudioPlaybackConfiguration(p);
        }
        public AudioPlaybackConfiguration[] newArray(int size) {
            return new AudioPlaybackConfiguration[size];
        }
    };

    @Override
    public int hashCode() {
        synchronized (mUpdateablePropLock) {
            return Objects.hash(mPlayerIId, mDeviceId, mMutedState, mPlayerType, mClientUid,
                    mClientPid, mSessionId);
        }
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        synchronized (mUpdateablePropLock) {
            dest.writeInt(mPlayerIId);
            dest.writeInt(mDeviceId);
            dest.writeInt(mMutedState);
            dest.writeInt(mPlayerType);
            dest.writeInt(mClientUid);
            dest.writeInt(mClientPid);
            dest.writeInt(mPlayerState);
            mPlayerAttr.writeToParcel(dest, 0);
            final IPlayerShell ips;
            synchronized (this) {
                ips = mIPlayerShell;
            }
            dest.writeStrongInterface(ips == null ? null : ips.getIPlayer());
            dest.writeInt(mSessionId);
            mFormatInfo.writeToParcel(dest, 0);
        }
    }

    private AudioPlaybackConfiguration(Parcel in) {
        mPlayerIId = in.readInt();
        mDeviceId = in.readInt();
        mMutedState = in.readInt();
        mPlayerType = in.readInt();
        mClientUid = in.readInt();
        mClientPid = in.readInt();
        mPlayerState = in.readInt();
        mPlayerAttr = AudioAttributes.CREATOR.createFromParcel(in);
        final IPlayer p = IPlayer.Stub.asInterface(in.readStrongBinder());
        mIPlayerShell = (p == null) ? null : new IPlayerShell(null, p);
        mSessionId = in.readInt();
        mFormatInfo = FormatInfo.CREATOR.createFromParcel(in);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || !(o instanceof AudioPlaybackConfiguration)) return false;

        AudioPlaybackConfiguration that = (AudioPlaybackConfiguration) o;

        return ((mPlayerIId == that.mPlayerIId)
                && (mDeviceId == that.mDeviceId)
                && (mMutedState == that.mMutedState)
                && (mPlayerType == that.mPlayerType)
                && (mClientUid == that.mClientUid)
                && (mClientPid == that.mClientPid))
                && (mSessionId == that.mSessionId);
    }

    @Override
    public String toString() {
        StringBuilder apcToString = new StringBuilder();
        synchronized (mUpdateablePropLock) {
            apcToString.append("AudioPlaybackConfiguration piid:").append(mPlayerIId).append(
                    " deviceId:").append(mDeviceId).append(" type:").append(
                    toLogFriendlyPlayerType(mPlayerType)).append(" u/pid:").append(
                    mClientUid).append(
                    "/").append(mClientPid).append(" state:").append(
                    toLogFriendlyPlayerState(mPlayerState)).append(" attr:").append(
                    mPlayerAttr).append(
                    " sessionId:").append(mSessionId).append(" mutedState:");
            if (mMutedState == 0) {
                apcToString.append("none ");
            } else {
                if ((mMutedState & MUTED_BY_MASTER) != 0) {
                    apcToString.append("master ");
                }
                if ((mMutedState & MUTED_BY_STREAM_VOLUME) != 0) {
                    apcToString.append("streamVolume ");
                }
                if ((mMutedState & MUTED_BY_STREAM_MUTED) != 0) {
                    apcToString.append("streamMute ");
                }
                if ((mMutedState & MUTED_BY_APP_OPS) != 0) {
                    apcToString.append("appOps ");
                }
                if ((mMutedState & MUTED_BY_CLIENT_VOLUME) != 0) {
                    apcToString.append("clientVolume ");
                }
                if ((mMutedState & MUTED_BY_VOLUME_SHAPER) != 0) {
                    apcToString.append("volumeShaper ");
                }
            }
            apcToString.append(" ").append(mFormatInfo);
        }
        return apcToString.toString();
    }

    //=====================================================================
    // Inner class for corresponding IPlayer and its death monitoring
    static final class IPlayerShell implements IBinder.DeathRecipient {

        final AudioPlaybackConfiguration mMonitor; // never null
        private volatile IPlayer mIPlayer;

        IPlayerShell(@NonNull AudioPlaybackConfiguration monitor, @NonNull IPlayer iplayer) {
            mMonitor = monitor;
            mIPlayer = iplayer;
        }

        synchronized void monitorDeath() {
            if (mIPlayer == null) {
                return;
            }
            try {
                mIPlayer.asBinder().linkToDeath(this, 0);
            } catch (RemoteException e) {
                if (mMonitor != null) {
                    Log.w(TAG, "Could not link to client death for piid=" + mMonitor.mPlayerIId, e);
                } else {
                    Log.w(TAG, "Could not link to client death", e);
                }
            }
        }

        IPlayer getIPlayer() {
            return mIPlayer;
        }

        public void binderDied() {
            if (mMonitor != null) {
                if (DEBUG) { Log.i(TAG, "IPlayerShell binderDied for piid=" + mMonitor.mPlayerIId);}
                mMonitor.playerDied();
            } else if (DEBUG) { Log.i(TAG, "IPlayerShell binderDied"); }
        }

        synchronized void release() {
            if (mIPlayer == null) {
                return;
            }
            mIPlayer.asBinder().unlinkToDeath(this, 0);
            mIPlayer = null;
            Binder.flushPendingCommands();
        }
    }

    //=====================================================================

    /**
     * @hide
     * Class to store sample rate, channel mask, and spatialization status
     */
    public static final class FormatInfo implements Parcelable {
        static final FormatInfo DEFAULT = new FormatInfo(
                /*spatialized*/ false, /*channel mask*/ 0, /*sample rate*/ 0);
        final boolean mIsSpatialized;
        final int mNativeChannelMask;
        final int mSampleRate;

        public FormatInfo(boolean isSpatialized, int nativeChannelMask, int sampleRate) {
            mIsSpatialized = isSpatialized;
            mNativeChannelMask = nativeChannelMask;
            mSampleRate = sampleRate;
        }

        @Override
        public String toString() {
            return "FormatInfo{"
                    + "isSpatialized=" + mIsSpatialized
                    + ", channelMask=0x" + Integer.toHexString(mNativeChannelMask)
                    + ", sampleRate=" + mSampleRate
                    + '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof FormatInfo)) return false;
            FormatInfo that = (FormatInfo) o;
            return mIsSpatialized == that.mIsSpatialized
                    && mNativeChannelMask == that.mNativeChannelMask
                    && mSampleRate == that.mSampleRate;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mIsSpatialized, mNativeChannelMask, mSampleRate);
        }

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

        @Override
        public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
            dest.writeBoolean(mIsSpatialized);
            dest.writeInt(mNativeChannelMask);
            dest.writeInt(mSampleRate);
        }

        private FormatInfo(Parcel in) {
            this(
                    in.readBoolean(), // isSpatialized
                    in.readInt(),     // channelMask
                    in.readInt()      // sampleRate
            );
        }

        public static final @NonNull Parcelable.Creator<FormatInfo> CREATOR =
                new Parcelable.Creator<FormatInfo>() {
            public FormatInfo createFromParcel(Parcel p) {
                return new FormatInfo(p);
            }
            public FormatInfo[] newArray(int size) {
                return new FormatInfo[size];
            }
        };
    }
    //=====================================================================
    // Utilities

    /** @hide */
    public static String toLogFriendlyPlayerType(int type) {
        switch (type) {
            case PLAYER_TYPE_UNKNOWN: return "unknown";
            case PLAYER_TYPE_JAM_AUDIOTRACK: return "android.media.AudioTrack";
            case PLAYER_TYPE_JAM_MEDIAPLAYER: return "android.media.MediaPlayer";
            case PLAYER_TYPE_JAM_SOUNDPOOL:   return "android.media.SoundPool";
            case PLAYER_TYPE_SLES_AUDIOPLAYER_BUFFERQUEUE:
                return "OpenSL ES AudioPlayer (Buffer Queue)";
            case PLAYER_TYPE_SLES_AUDIOPLAYER_URI_FD:
                return "OpenSL ES AudioPlayer (URI/FD)";
            case PLAYER_TYPE_AAUDIO: return "AAudio";
            case PLAYER_TYPE_HW_SOURCE: return "hardware source";
            case PLAYER_TYPE_EXTERNAL_PROXY: return "external proxy";
            default:
                return "unknown player type " + type + " - FIXME";
        }
    }

    /** @hide */
    public static String toLogFriendlyPlayerState(int state) {
        switch (state) {
            case PLAYER_STATE_UNKNOWN: return "unknown";
            case PLAYER_STATE_RELEASED: return "released";
            case PLAYER_STATE_IDLE: return "idle";
            case PLAYER_STATE_STARTED: return "started";
            case PLAYER_STATE_PAUSED: return "paused";
            case PLAYER_STATE_STOPPED: return "stopped";
            case PLAYER_UPDATE_DEVICE_ID: return "device updated";
            case PLAYER_UPDATE_PORT_ID: return "port updated";
            case PLAYER_UPDATE_MUTED: return "muted updated";
            default:
                return "unknown player state - FIXME";
        }
    }
}
