|  | /* | 
|  | * 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 android.annotation.IntDef; | 
|  | import android.annotation.NonNull; | 
|  | 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 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_PIID_UNASSIGNED = 0; | 
|  | /** @hide */ | 
|  | public static final int PLAYER_UPID_INVALID = -1; | 
|  |  | 
|  | // 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. | 
|  | * 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_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 */ | 
|  | @IntDef({ | 
|  | PLAYER_STATE_UNKNOWN, | 
|  | PLAYER_STATE_RELEASED, | 
|  | PLAYER_STATE_IDLE, | 
|  | PLAYER_STATE_STARTED, | 
|  | PLAYER_STATE_PAUSED, | 
|  | PLAYER_STATE_STOPPED | 
|  | }) | 
|  | @Retention(RetentionPolicy.SOURCE) | 
|  | public @interface PlayerState {} | 
|  |  | 
|  | // 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 | 
|  |  | 
|  | /** | 
|  | * 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); } | 
|  | mPlayerIId = piid; | 
|  | mPlayerType = pic.mPlayerType; | 
|  | mClientUid = uid; | 
|  | mClientPid = pid; | 
|  | mPlayerState = PLAYER_STATE_IDLE; | 
|  | mPlayerAttr = pic.mAttributes; | 
|  | if ((sPlayerDeathMonitor != null) && (pic.mIPlayer != null)) { | 
|  | mIPlayerShell = new IPlayerShell(this, pic.mIPlayer); | 
|  | } else { | 
|  | mIPlayerShell = null; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @hide | 
|  | */ | 
|  | public void init() { | 
|  | synchronized (this) { | 
|  | if (mIPlayerShell != null) { | 
|  | mIPlayerShell.monitorDeath(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // 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 toSanitize | 
|  | */ | 
|  | 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 | 
|  | anonymCopy.mPlayerAttr = new AudioAttributes.Builder() | 
|  | .setUsage(in.mPlayerAttr.getUsage()) | 
|  | .setContentType(in.mPlayerAttr.getContentType()) | 
|  | .setFlags(in.mPlayerAttr.getFlags()) | 
|  | .build(); | 
|  | // anonymized data | 
|  | anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN; | 
|  | anonymCopy.mClientUid = PLAYER_UPID_INVALID; | 
|  | anonymCopy.mClientPid = PLAYER_UPID_INVALID; | 
|  | anonymCopy.mIPlayerShell = null; | 
|  | 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; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @hide | 
|  | * Return the type of player linked to this configuration. The return value is one of | 
|  | * {@link #PLAYER_TYPE_JAM_AUDIOTRACK}, {@link #PLAYER_TYPE_JAM_MEDIAPLAYER}, | 
|  | * {@link #PLAYER_TYPE_JAM_SOUNDPOOL}, {@link #PLAYER_TYPE_SLES_AUDIOPLAYER_BUFFERQUEUE}, | 
|  | * {@link #PLAYER_TYPE_SLES_AUDIOPLAYER_URI_FD}, or {@link #PLAYER_TYPE_UNKNOWN}. | 
|  | * <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_AAUDIO: | 
|  | 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 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 player state change | 
|  | * @param event | 
|  | * @return true if the state changed, false otherwise | 
|  | */ | 
|  | public boolean handleStateEvent(int event) { | 
|  | final boolean changed; | 
|  | synchronized (this) { | 
|  | changed = (mPlayerState != event); | 
|  | mPlayerState = event; | 
|  | 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); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @hide | 
|  | * Returns true if the player is considered "active", i.e. actively playing, and thus | 
|  | * in a state that should make it considered for the list public (sanitized) active playback | 
|  | * configurations | 
|  | * @return true if active | 
|  | */ | 
|  | public boolean isActive() { | 
|  | switch (mPlayerState) { | 
|  | case PLAYER_STATE_STARTED: | 
|  | return true; | 
|  | 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("  " + toLogFriendlyString(this)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @hide | 
|  | */ | 
|  | public static String toLogFriendlyString(AudioPlaybackConfiguration apc) { | 
|  | return new String("ID:" + apc.mPlayerIId | 
|  | + " -- type:" + toLogFriendlyPlayerType(apc.mPlayerType) | 
|  | + " -- u/pid:" + apc.mClientUid +"/" + apc.mClientPid | 
|  | + " -- state:" + toLogFriendlyPlayerState(apc.mPlayerState) | 
|  | + " -- attr:" + apc.mPlayerAttr); | 
|  | } | 
|  |  | 
|  | public static final 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() { | 
|  | return Objects.hash(mPlayerIId, mPlayerType, mClientUid, mClientPid); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int describeContents() { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void writeToParcel(Parcel dest, int flags) { | 
|  | dest.writeInt(mPlayerIId); | 
|  | 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()); | 
|  | } | 
|  |  | 
|  | private AudioPlaybackConfiguration(Parcel in) { | 
|  | mPlayerIId = 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); | 
|  | } | 
|  |  | 
|  | @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) | 
|  | && (mPlayerType == that.mPlayerType) | 
|  | && (mClientUid == that.mClientUid) | 
|  | && (mClientPid == that.mClientPid)); | 
|  | } | 
|  |  | 
|  | //===================================================================== | 
|  | // 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(); | 
|  | } | 
|  | } | 
|  |  | 
|  | //===================================================================== | 
|  | // 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"; | 
|  | default: | 
|  | return "unknown player state - FIXME"; | 
|  | } | 
|  | } | 
|  | } |