1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media; 18 19 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL; 20 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE; 21 22 import android.annotation.IntDef; 23 import android.annotation.IntRange; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.annotation.SystemApi; 27 import android.os.Binder; 28 import android.os.IBinder; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.os.RemoteException; 32 import android.util.Log; 33 34 import java.io.PrintWriter; 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 import java.util.Objects; 38 39 /** 40 * The AudioPlaybackConfiguration class collects the information describing an audio playback 41 * session. 42 */ 43 public final class AudioPlaybackConfiguration implements Parcelable { 44 private static final String TAG = new String("AudioPlaybackConfiguration"); 45 46 private static final boolean DEBUG = false; 47 48 /** @hide */ 49 public static final int PLAYER_PIID_INVALID = -1; 50 /** @hide */ 51 public static final int PLAYER_UPID_INVALID = -1; 52 /** @hide */ 53 public static final int PLAYER_DEVICEID_INVALID = 0; 54 55 // information about the implementation 56 /** 57 * @hide 58 * An unknown type of player 59 */ 60 @SystemApi 61 public static final int PLAYER_TYPE_UNKNOWN = -1; 62 /** 63 * @hide 64 * Player backed by a java android.media.AudioTrack player 65 */ 66 @SystemApi 67 public static final int PLAYER_TYPE_JAM_AUDIOTRACK = 1; 68 /** 69 * @hide 70 * Player backed by a java android.media.MediaPlayer player 71 */ 72 @SystemApi 73 public static final int PLAYER_TYPE_JAM_MEDIAPLAYER = 2; 74 /** 75 * @hide 76 * Player backed by a java android.media.SoundPool player 77 */ 78 @SystemApi 79 public static final int PLAYER_TYPE_JAM_SOUNDPOOL = 3; 80 /** 81 * @hide 82 * Player backed by a C OpenSL ES AudioPlayer player with a BufferQueue source 83 */ 84 @SystemApi 85 public static final int PLAYER_TYPE_SLES_AUDIOPLAYER_BUFFERQUEUE = 11; 86 /** 87 * @hide 88 * Player backed by a C OpenSL ES AudioPlayer player with a URI or FD source 89 */ 90 @SystemApi 91 public static final int PLAYER_TYPE_SLES_AUDIOPLAYER_URI_FD = 12; 92 93 /** 94 * @hide 95 * Player backed an AAudio player. 96 */ 97 @SystemApi 98 public static final int PLAYER_TYPE_AAUDIO = 13; 99 100 /** 101 * @hide 102 * Player backed a hardware source, whose state is visible in the Android audio policy manager. 103 * Note this type is not in System API so it will not be returned in public API calls 104 */ 105 // TODO unhide for SystemApi, update getPlayerType() 106 public static final int PLAYER_TYPE_HW_SOURCE = 14; 107 108 /** 109 * @hide 110 * Player is a proxy for an audio player whose audio and state doesn't go through the Android 111 * audio framework. 112 * Note this type is not in System API so it will not be returned in public API calls 113 */ 114 // TODO unhide for SystemApi, update getPlayerType() 115 public static final int PLAYER_TYPE_EXTERNAL_PROXY = 15; 116 117 /** @hide */ 118 @IntDef({ 119 PLAYER_TYPE_UNKNOWN, 120 PLAYER_TYPE_JAM_AUDIOTRACK, 121 PLAYER_TYPE_JAM_MEDIAPLAYER, 122 PLAYER_TYPE_JAM_SOUNDPOOL, 123 PLAYER_TYPE_SLES_AUDIOPLAYER_BUFFERQUEUE, 124 PLAYER_TYPE_SLES_AUDIOPLAYER_URI_FD, 125 }) 126 @Retention(RetentionPolicy.SOURCE) 127 public @interface PlayerType {} 128 129 /** 130 * @hide 131 * An unknown player state 132 */ 133 @SystemApi 134 public static final int PLAYER_STATE_UNKNOWN = -1; 135 /** 136 * @hide 137 * The resources of the player have been released, it cannot play anymore 138 */ 139 @SystemApi 140 public static final int PLAYER_STATE_RELEASED = 0; 141 /** 142 * @hide 143 * The state of a player when it's created 144 */ 145 @SystemApi 146 public static final int PLAYER_STATE_IDLE = 1; 147 /** 148 * @hide 149 * The state of a player that is actively playing 150 */ 151 @SystemApi 152 public static final int PLAYER_STATE_STARTED = 2; 153 /** 154 * @hide 155 * The state of a player where playback is paused 156 */ 157 @SystemApi 158 public static final int PLAYER_STATE_PAUSED = 3; 159 /** 160 * @hide 161 * The state of a player where playback is stopped 162 */ 163 @SystemApi 164 public static final int PLAYER_STATE_STOPPED = 4; 165 /** 166 * @hide 167 * The state used to update device id, does not actually change the state of the player 168 */ 169 public static final int PLAYER_UPDATE_DEVICE_ID = 5; 170 171 /** @hide */ 172 @IntDef({ 173 PLAYER_STATE_UNKNOWN, 174 PLAYER_STATE_RELEASED, 175 PLAYER_STATE_IDLE, 176 PLAYER_STATE_STARTED, 177 PLAYER_STATE_PAUSED, 178 PLAYER_STATE_STOPPED, 179 PLAYER_UPDATE_DEVICE_ID 180 }) 181 @Retention(RetentionPolicy.SOURCE) 182 public @interface PlayerState {} 183 184 /** @hide */ playerStateToString(@layerState int state)185 public static String playerStateToString(@PlayerState int state) { 186 switch (state) { 187 case PLAYER_STATE_UNKNOWN: return "PLAYER_STATE_UNKNOWN"; 188 case PLAYER_STATE_RELEASED: return "PLAYER_STATE_RELEASED"; 189 case PLAYER_STATE_IDLE: return "PLAYER_STATE_IDLE"; 190 case PLAYER_STATE_STARTED: return "PLAYER_STATE_STARTED"; 191 case PLAYER_STATE_PAUSED: return "PLAYER_STATE_PAUSED"; 192 case PLAYER_STATE_STOPPED: return "PLAYER_STATE_STOPPED"; 193 case PLAYER_UPDATE_DEVICE_ID: return "PLAYER_UPDATE_DEVICE_ID"; 194 default: 195 return "invalid state " + state; 196 } 197 } 198 199 // immutable data 200 private final int mPlayerIId; 201 202 // not final due to anonymization step 203 private int mPlayerType; 204 private int mClientUid; 205 private int mClientPid; 206 // the IPlayer reference and death monitor 207 private IPlayerShell mIPlayerShell; 208 209 private int mPlayerState; 210 private AudioAttributes mPlayerAttr; // never null 211 212 private int mDeviceId; 213 214 private int mSessionId; 215 216 /** 217 * Never use without initializing parameters afterwards 218 */ AudioPlaybackConfiguration(int piid)219 private AudioPlaybackConfiguration(int piid) { 220 mPlayerIId = piid; 221 mIPlayerShell = null; 222 } 223 224 /** 225 * @hide 226 */ AudioPlaybackConfiguration(PlayerBase.PlayerIdCard pic, int piid, int uid, int pid)227 public AudioPlaybackConfiguration(PlayerBase.PlayerIdCard pic, int piid, int uid, int pid) { 228 if (DEBUG) { 229 Log.d(TAG, "new: piid=" + piid + " iplayer=" + pic.mIPlayer 230 + " sessionId=" + pic.mSessionId); 231 } 232 mPlayerIId = piid; 233 mPlayerType = pic.mPlayerType; 234 mClientUid = uid; 235 mClientPid = pid; 236 mDeviceId = PLAYER_DEVICEID_INVALID; 237 mPlayerState = PLAYER_STATE_IDLE; 238 mPlayerAttr = pic.mAttributes; 239 if ((sPlayerDeathMonitor != null) && (pic.mIPlayer != null)) { 240 mIPlayerShell = new IPlayerShell(this, pic.mIPlayer); 241 } else { 242 mIPlayerShell = null; 243 } 244 mSessionId = pic.mSessionId; 245 } 246 247 /** 248 * @hide 249 */ init()250 public void init() { 251 synchronized (this) { 252 if (mIPlayerShell != null) { 253 mIPlayerShell.monitorDeath(); 254 } 255 } 256 } 257 258 // Note that this method is called server side, so no "privileged" information is ever sent 259 // to a client that is not supposed to have access to it. 260 /** 261 * @hide 262 * Creates a copy of the playback configuration that is stripped of any data enabling 263 * identification of which application it is associated with ("anonymized"). 264 * @param toSanitize 265 */ anonymizedCopy(AudioPlaybackConfiguration in)266 public static AudioPlaybackConfiguration anonymizedCopy(AudioPlaybackConfiguration in) { 267 final AudioPlaybackConfiguration anonymCopy = new AudioPlaybackConfiguration(in.mPlayerIId); 268 anonymCopy.mPlayerState = in.mPlayerState; 269 // do not reuse the full attributes: only usage, content type and public flags are allowed 270 AudioAttributes.Builder builder = new AudioAttributes.Builder() 271 .setContentType(in.mPlayerAttr.getContentType()) 272 .setFlags(in.mPlayerAttr.getFlags()) 273 .setAllowedCapturePolicy( 274 in.mPlayerAttr.getAllowedCapturePolicy() == ALLOW_CAPTURE_BY_ALL 275 ? ALLOW_CAPTURE_BY_ALL : ALLOW_CAPTURE_BY_NONE); 276 if (AudioAttributes.isSystemUsage(in.mPlayerAttr.getSystemUsage())) { 277 builder.setSystemUsage(in.mPlayerAttr.getSystemUsage()); 278 } else { 279 builder.setUsage(in.mPlayerAttr.getUsage()); 280 } 281 anonymCopy.mPlayerAttr = builder.build(); 282 anonymCopy.mDeviceId = in.mDeviceId; 283 // anonymized data 284 anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN; 285 anonymCopy.mClientUid = PLAYER_UPID_INVALID; 286 anonymCopy.mClientPid = PLAYER_UPID_INVALID; 287 anonymCopy.mIPlayerShell = null; 288 anonymCopy.mSessionId = AudioSystem.AUDIO_SESSION_ALLOCATE; 289 return anonymCopy; 290 } 291 292 /** 293 * Return the {@link AudioAttributes} of the corresponding player. 294 * @return the audio attributes of the player 295 */ getAudioAttributes()296 public AudioAttributes getAudioAttributes() { 297 return mPlayerAttr; 298 } 299 300 /** 301 * @hide 302 * Return the uid of the client application that created this player. 303 * @return the uid of the client 304 */ 305 @SystemApi getClientUid()306 public int getClientUid() { 307 return mClientUid; 308 } 309 310 /** 311 * @hide 312 * Return the pid of the client application that created this player. 313 * @return the pid of the client 314 */ 315 @SystemApi getClientPid()316 public int getClientPid() { 317 return mClientPid; 318 } 319 320 /** 321 * Returns information about the {@link AudioDeviceInfo} used for this playback. 322 * @return the audio playback device or null if the device is not available at the time of query 323 */ getAudioDeviceInfo()324 public @Nullable AudioDeviceInfo getAudioDeviceInfo() { 325 if (mDeviceId == PLAYER_DEVICEID_INVALID) { 326 return null; 327 } 328 return AudioManager.getDeviceForPortId(mDeviceId, AudioManager.GET_DEVICES_OUTPUTS); 329 } 330 331 /** 332 * @hide 333 * Return the audio session ID associated with this player. 334 * See {@link AudioManager#generateAudioSessionId()}. 335 * @return an audio session ID 336 */ 337 @SystemApi getSessionId()338 public @IntRange(from = 0) int getSessionId() { 339 return mSessionId; 340 } 341 342 /** 343 * @hide 344 * Return the type of player linked to this configuration. 345 * <br>Note that player types not exposed in the system API will be represented as 346 * {@link #PLAYER_TYPE_UNKNOWN}. 347 * @return the type of the player. 348 */ 349 @SystemApi getPlayerType()350 public @PlayerType int getPlayerType() { 351 switch (mPlayerType) { 352 case PLAYER_TYPE_HW_SOURCE: 353 case PLAYER_TYPE_EXTERNAL_PROXY: 354 return PLAYER_TYPE_UNKNOWN; 355 default: 356 return mPlayerType; 357 } 358 } 359 360 /** 361 * @hide 362 * Return the current state of the player linked to this configuration. The return value is one 363 * of {@link #PLAYER_STATE_IDLE}, {@link #PLAYER_STATE_PAUSED}, {@link #PLAYER_STATE_STARTED}, 364 * {@link #PLAYER_STATE_STOPPED}, {@link #PLAYER_STATE_RELEASED} or 365 * {@link #PLAYER_STATE_UNKNOWN}. 366 * @return the state of the player. 367 */ 368 @SystemApi getPlayerState()369 public @PlayerState int getPlayerState() { 370 return mPlayerState; 371 } 372 373 /** 374 * @hide 375 * Return an identifier unique for the lifetime of the player. 376 * @return a player interface identifier 377 */ 378 @SystemApi getPlayerInterfaceId()379 public int getPlayerInterfaceId() { 380 return mPlayerIId; 381 } 382 383 /** 384 * @hide 385 * Return a proxy for the player associated with this playback configuration 386 * @return a proxy player 387 */ 388 @SystemApi getPlayerProxy()389 public PlayerProxy getPlayerProxy() { 390 final IPlayerShell ips; 391 synchronized (this) { 392 ips = mIPlayerShell; 393 } 394 return ips == null ? null : new PlayerProxy(this); 395 } 396 397 /** 398 * @hide 399 * @return the IPlayer interface for the associated player 400 */ getIPlayer()401 IPlayer getIPlayer() { 402 final IPlayerShell ips; 403 synchronized (this) { 404 ips = mIPlayerShell; 405 } 406 return ips == null ? null : ips.getIPlayer(); 407 } 408 409 /** 410 * @hide 411 * Handle a change of audio attributes 412 * @param attr 413 */ handleAudioAttributesEvent(@onNull AudioAttributes attr)414 public boolean handleAudioAttributesEvent(@NonNull AudioAttributes attr) { 415 final boolean changed = !attr.equals(mPlayerAttr); 416 mPlayerAttr = attr; 417 return changed; 418 } 419 420 /** 421 * @hide 422 * Handle a change of audio session Id 423 * @param sessionId the audio session ID 424 */ handleSessionIdEvent(int sessionId)425 public boolean handleSessionIdEvent(int sessionId) { 426 final boolean changed = sessionId != mSessionId; 427 mSessionId = sessionId; 428 return changed; 429 } 430 431 /** 432 * @hide 433 * Handle a player state change 434 * @param event 435 * @param deviceId active device id or {@Code PLAYER_DEVICEID_INVALID} 436 * <br>Note device id is valid for {@code PLAYER_UPDATE_DEVICE_ID} or 437 * <br>{@code PLAYER_STATE_STARTED} events, as the device id will be reset to none when 438 * <br>pausing or stopping playback. It will be set to active device when playback starts or 439 * <br>it will be changed when PLAYER_UPDATE_DEVICE_ID is sent. The latter can happen if the 440 * <br>device changes in the middle of playback. 441 * @return true if the state changed, false otherwise 442 */ handleStateEvent(int event, int deviceId)443 public boolean handleStateEvent(int event, int deviceId) { 444 boolean changed = false; 445 synchronized (this) { 446 447 // Do not update if it is only device id update 448 if (event != PLAYER_UPDATE_DEVICE_ID) { 449 changed = (mPlayerState != event); 450 mPlayerState = event; 451 } 452 453 if (event == PLAYER_STATE_STARTED || event == PLAYER_UPDATE_DEVICE_ID) { 454 changed = changed || (mDeviceId != deviceId); 455 mDeviceId = deviceId; 456 } 457 458 if (changed && (event == PLAYER_STATE_RELEASED) && (mIPlayerShell != null)) { 459 mIPlayerShell.release(); 460 mIPlayerShell = null; 461 } 462 } 463 return changed; 464 } 465 466 // To report IPlayer death from death recipient 467 /** @hide */ 468 public interface PlayerDeathMonitor { playerDeath(int piid)469 public void playerDeath(int piid); 470 } 471 /** @hide */ 472 public static PlayerDeathMonitor sPlayerDeathMonitor; 473 playerDied()474 private void playerDied() { 475 if (sPlayerDeathMonitor != null) { 476 sPlayerDeathMonitor.playerDeath(mPlayerIId); 477 } 478 } 479 480 /** 481 * @hide 482 * Returns true if the player is considered "active", i.e. actively playing, and thus 483 * in a state that should make it considered for the list public (sanitized) active playback 484 * configurations 485 * @return true if active 486 */ 487 @SystemApi isActive()488 public boolean isActive() { 489 switch (mPlayerState) { 490 case PLAYER_STATE_STARTED: 491 return true; 492 case PLAYER_STATE_UNKNOWN: 493 case PLAYER_STATE_RELEASED: 494 case PLAYER_STATE_IDLE: 495 case PLAYER_STATE_PAUSED: 496 case PLAYER_STATE_STOPPED: 497 default: 498 return false; 499 } 500 } 501 502 /** 503 * @hide 504 * For AudioService dump 505 * @param pw 506 */ dump(PrintWriter pw)507 public void dump(PrintWriter pw) { 508 pw.println(" " + this); 509 } 510 511 public static final @android.annotation.NonNull Parcelable.Creator<AudioPlaybackConfiguration> CREATOR 512 = new Parcelable.Creator<AudioPlaybackConfiguration>() { 513 /** 514 * Rebuilds an AudioPlaybackConfiguration previously stored with writeToParcel(). 515 * @param p Parcel object to read the AudioPlaybackConfiguration from 516 * @return a new AudioPlaybackConfiguration created from the data in the parcel 517 */ 518 public AudioPlaybackConfiguration createFromParcel(Parcel p) { 519 return new AudioPlaybackConfiguration(p); 520 } 521 public AudioPlaybackConfiguration[] newArray(int size) { 522 return new AudioPlaybackConfiguration[size]; 523 } 524 }; 525 526 @Override hashCode()527 public int hashCode() { 528 return Objects.hash(mPlayerIId, mDeviceId, mPlayerType, mClientUid, mClientPid, 529 mSessionId); 530 } 531 532 @Override describeContents()533 public int describeContents() { 534 return 0; 535 } 536 537 @Override writeToParcel(Parcel dest, int flags)538 public void writeToParcel(Parcel dest, int flags) { 539 dest.writeInt(mPlayerIId); 540 dest.writeInt(mDeviceId); 541 dest.writeInt(mPlayerType); 542 dest.writeInt(mClientUid); 543 dest.writeInt(mClientPid); 544 dest.writeInt(mPlayerState); 545 mPlayerAttr.writeToParcel(dest, 0); 546 final IPlayerShell ips; 547 synchronized (this) { 548 ips = mIPlayerShell; 549 } 550 dest.writeStrongInterface(ips == null ? null : ips.getIPlayer()); 551 dest.writeInt(mSessionId); 552 } 553 AudioPlaybackConfiguration(Parcel in)554 private AudioPlaybackConfiguration(Parcel in) { 555 mPlayerIId = in.readInt(); 556 mDeviceId = in.readInt(); 557 mPlayerType = in.readInt(); 558 mClientUid = in.readInt(); 559 mClientPid = in.readInt(); 560 mPlayerState = in.readInt(); 561 mPlayerAttr = AudioAttributes.CREATOR.createFromParcel(in); 562 final IPlayer p = IPlayer.Stub.asInterface(in.readStrongBinder()); 563 mIPlayerShell = (p == null) ? null : new IPlayerShell(null, p); 564 mSessionId = in.readInt(); 565 } 566 567 @Override equals(Object o)568 public boolean equals(Object o) { 569 if (this == o) return true; 570 if (o == null || !(o instanceof AudioPlaybackConfiguration)) return false; 571 572 AudioPlaybackConfiguration that = (AudioPlaybackConfiguration) o; 573 574 return ((mPlayerIId == that.mPlayerIId) 575 && (mDeviceId == that.mDeviceId) 576 && (mPlayerType == that.mPlayerType) 577 && (mClientUid == that.mClientUid) 578 && (mClientPid == that.mClientPid)) 579 && (mSessionId == that.mSessionId); 580 } 581 582 @Override toString()583 public String toString() { 584 return "AudioPlaybackConfiguration piid:" + mPlayerIId 585 + " deviceId:" + mDeviceId 586 + " type:" + toLogFriendlyPlayerType(mPlayerType) 587 + " u/pid:" + mClientUid + "/" + mClientPid 588 + " state:" + toLogFriendlyPlayerState(mPlayerState) 589 + " attr:" + mPlayerAttr 590 + " sessionId:" + mSessionId; 591 } 592 593 //===================================================================== 594 // Inner class for corresponding IPlayer and its death monitoring 595 static final class IPlayerShell implements IBinder.DeathRecipient { 596 597 final AudioPlaybackConfiguration mMonitor; // never null 598 private volatile IPlayer mIPlayer; 599 IPlayerShell(@onNull AudioPlaybackConfiguration monitor, @NonNull IPlayer iplayer)600 IPlayerShell(@NonNull AudioPlaybackConfiguration monitor, @NonNull IPlayer iplayer) { 601 mMonitor = monitor; 602 mIPlayer = iplayer; 603 } 604 monitorDeath()605 synchronized void monitorDeath() { 606 if (mIPlayer == null) { 607 return; 608 } 609 try { 610 mIPlayer.asBinder().linkToDeath(this, 0); 611 } catch (RemoteException e) { 612 if (mMonitor != null) { 613 Log.w(TAG, "Could not link to client death for piid=" + mMonitor.mPlayerIId, e); 614 } else { 615 Log.w(TAG, "Could not link to client death", e); 616 } 617 } 618 } 619 getIPlayer()620 IPlayer getIPlayer() { 621 return mIPlayer; 622 } 623 binderDied()624 public void binderDied() { 625 if (mMonitor != null) { 626 if (DEBUG) { Log.i(TAG, "IPlayerShell binderDied for piid=" + mMonitor.mPlayerIId);} 627 mMonitor.playerDied(); 628 } else if (DEBUG) { Log.i(TAG, "IPlayerShell binderDied"); } 629 } 630 release()631 synchronized void release() { 632 if (mIPlayer == null) { 633 return; 634 } 635 mIPlayer.asBinder().unlinkToDeath(this, 0); 636 mIPlayer = null; 637 Binder.flushPendingCommands(); 638 } 639 } 640 641 //===================================================================== 642 // Utilities 643 644 /** @hide */ toLogFriendlyPlayerType(int type)645 public static String toLogFriendlyPlayerType(int type) { 646 switch (type) { 647 case PLAYER_TYPE_UNKNOWN: return "unknown"; 648 case PLAYER_TYPE_JAM_AUDIOTRACK: return "android.media.AudioTrack"; 649 case PLAYER_TYPE_JAM_MEDIAPLAYER: return "android.media.MediaPlayer"; 650 case PLAYER_TYPE_JAM_SOUNDPOOL: return "android.media.SoundPool"; 651 case PLAYER_TYPE_SLES_AUDIOPLAYER_BUFFERQUEUE: 652 return "OpenSL ES AudioPlayer (Buffer Queue)"; 653 case PLAYER_TYPE_SLES_AUDIOPLAYER_URI_FD: 654 return "OpenSL ES AudioPlayer (URI/FD)"; 655 case PLAYER_TYPE_AAUDIO: return "AAudio"; 656 case PLAYER_TYPE_HW_SOURCE: return "hardware source"; 657 case PLAYER_TYPE_EXTERNAL_PROXY: return "external proxy"; 658 default: 659 return "unknown player type " + type + " - FIXME"; 660 } 661 } 662 663 /** @hide */ toLogFriendlyPlayerState(int state)664 public static String toLogFriendlyPlayerState(int state) { 665 switch (state) { 666 case PLAYER_STATE_UNKNOWN: return "unknown"; 667 case PLAYER_STATE_RELEASED: return "released"; 668 case PLAYER_STATE_IDLE: return "idle"; 669 case PLAYER_STATE_STARTED: return "started"; 670 case PLAYER_STATE_PAUSED: return "paused"; 671 case PLAYER_STATE_STOPPED: return "stopped"; 672 case PLAYER_UPDATE_DEVICE_ID: return "device"; 673 default: 674 return "unknown player state - FIXME"; 675 } 676 } 677 } 678