MediaPlaybackService.java revision 2b0b91376a7101fafdf8b007ae660d8b2a039530
1/* 2 * Copyright (C) 2007 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 17package com.android.music; 18 19import android.app.Notification; 20import android.app.NotificationManager; 21import android.app.PendingIntent; 22import android.app.Service; 23import android.appwidget.AppWidgetManager; 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.BroadcastReceiver; 31import android.content.SharedPreferences; 32import android.content.SharedPreferences.Editor; 33import android.database.Cursor; 34import android.database.sqlite.SQLiteException; 35import android.media.AudioManager; 36import android.media.MediaFile; 37import android.media.MediaPlayer; 38import android.net.Uri; 39import android.os.Environment; 40import android.os.FileUtils; 41import android.os.Handler; 42import android.os.IBinder; 43import android.os.Message; 44import android.os.PowerManager; 45import android.os.SystemClock; 46import android.os.PowerManager.WakeLock; 47import android.provider.MediaStore; 48import android.util.Log; 49import android.widget.RemoteViews; 50import android.widget.Toast; 51import com.android.internal.telephony.Phone; 52import com.android.internal.telephony.PhoneStateIntentReceiver; 53 54import java.io.IOException; 55import java.lang.ref.WeakReference; 56import java.util.Random; 57import java.util.Vector; 58 59/** 60 * Provides "background" audio playback capabilities, allowing the 61 * user to switch between activities without stopping playback. 62 */ 63public class MediaPlaybackService extends Service { 64 /** used to specify whether enqueue() should start playing 65 * the new list of files right away, next or once all the currently 66 * queued files have been played 67 */ 68 public static final int NOW = 1; 69 public static final int NEXT = 2; 70 public static final int LAST = 3; 71 public static final int PLAYBACKSERVICE_STATUS = 1; 72 73 public static final int SHUFFLE_NONE = 0; 74 public static final int SHUFFLE_NORMAL = 1; 75 public static final int SHUFFLE_AUTO = 2; 76 77 public static final int REPEAT_NONE = 0; 78 public static final int REPEAT_CURRENT = 1; 79 public static final int REPEAT_ALL = 2; 80 81 public static final String PLAYSTATE_CHANGED = "com.android.music.playstatechanged"; 82 public static final String META_CHANGED = "com.android.music.metachanged"; 83 public static final String QUEUE_CHANGED = "com.android.music.queuechanged"; 84 public static final String PLAYBACK_COMPLETE = "com.android.music.playbackcomplete"; 85 public static final String ASYNC_OPEN_COMPLETE = "com.android.music.asyncopencomplete"; 86 87 public static final String SERVICECMD = "com.android.music.musicservicecommand"; 88 public static final String CMDNAME = "command"; 89 public static final String CMDTOGGLEPAUSE = "togglepause"; 90 public static final String CMDSTOP = "stop"; 91 public static final String CMDPAUSE = "pause"; 92 public static final String CMDPREVIOUS = "previous"; 93 public static final String CMDNEXT = "next"; 94 95 public static final String TOGGLEPAUSE_ACTION = "com.android.music.musicservicecommand.togglepause"; 96 public static final String PAUSE_ACTION = "com.android.music.musicservicecommand.pause"; 97 public static final String PREVIOUS_ACTION = "com.android.music.musicservicecommand.previous"; 98 public static final String NEXT_ACTION = "com.android.music.musicservicecommand.next"; 99 100 private static final int PHONE_CHANGED = 1; 101 private static final int TRACK_ENDED = 1; 102 private static final int RELEASE_WAKELOCK = 2; 103 private static final int SERVER_DIED = 3; 104 private static final int FADEIN = 4; 105 private static final int MAX_HISTORY_SIZE = 10; 106 107 private MultiPlayer mPlayer; 108 private String mFileToPlay; 109 private PhoneStateIntentReceiver mPsir; 110 private int mShuffleMode = SHUFFLE_NONE; 111 private int mRepeatMode = REPEAT_NONE; 112 private int mMediaMountedCount = 0; 113 private int [] mAutoShuffleList = null; 114 private boolean mOneShot; 115 private int [] mPlayList = null; 116 private int mPlayListLen = 0; 117 private Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE); 118 private Cursor mCursor; 119 private int mPlayPos = -1; 120 private static final String LOGTAG = "MediaPlaybackService"; 121 private final Shuffler mRand = new Shuffler(); 122 private int mOpenFailedCounter = 0; 123 String[] mCursorCols = new String[] { 124 "audio._id AS _id", // index must match IDCOLIDX below 125 MediaStore.Audio.Media.ARTIST, 126 MediaStore.Audio.Media.ALBUM, 127 MediaStore.Audio.Media.TITLE, 128 MediaStore.Audio.Media.DATA, 129 MediaStore.Audio.Media.MIME_TYPE, 130 MediaStore.Audio.Media.ALBUM_ID, 131 MediaStore.Audio.Media.ARTIST_ID, 132 MediaStore.Audio.Media.IS_PODCAST, // index must match PODCASTCOLIDX below 133 MediaStore.Audio.Media.BOOKMARK // index must match BOOKMARKCOLIDX below 134 }; 135 private final static int IDCOLIDX = 0; 136 private final static int PODCASTCOLIDX = 8; 137 private final static int BOOKMARKCOLIDX = 9; 138 private BroadcastReceiver mUnmountReceiver = null; 139 private WakeLock mWakeLock; 140 private int mServiceStartId = -1; 141 private boolean mServiceInUse = false; 142 private boolean mResumeAfterCall = false; 143 private boolean mIsSupposedToBePlaying = false; 144 private boolean mQuietMode = false; 145 146 private SharedPreferences mPreferences; 147 // We use this to distinguish between different cards when saving/restoring playlists. 148 // This will have to change if we want to support multiple simultaneous cards. 149 private int mCardId; 150 151 private MediaAppWidgetProvider mAppWidgetProvider = MediaAppWidgetProvider.getInstance(); 152 153 // interval after which we stop the service when idle 154 private static final int IDLE_DELAY = 60000; 155 156 private Handler mPhoneHandler = new Handler() { 157 @Override 158 public void handleMessage(Message msg) { 159 switch (msg.what) { 160 case PHONE_CHANGED: 161 Phone.State state = mPsir.getPhoneState(); 162 if (state == Phone.State.RINGING) { 163 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 164 int ringvolume = audioManager.getStreamVolume(AudioManager.STREAM_RING); 165 if (ringvolume > 0) { 166 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0); 167 pause(); 168 } 169 } else if (state == Phone.State.OFFHOOK) { 170 // pause the music while a conversation is in progress 171 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0); 172 pause(); 173 } else if (state == Phone.State.IDLE) { 174 // start playing again 175 if (mResumeAfterCall) { 176 // resume playback only if music was playing 177 // when the call was answered 178 startAndFadeIn(); 179 mResumeAfterCall = false; 180 } 181 } 182 break; 183 default: 184 break; 185 } 186 } 187 }; 188 189 private void startAndFadeIn() { 190 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10); 191 } 192 193 private Handler mMediaplayerHandler = new Handler() { 194 float mCurrentVolume = 1.0f; 195 @Override 196 public void handleMessage(Message msg) { 197 switch (msg.what) { 198 case FADEIN: 199 if (!isPlaying()) { 200 mCurrentVolume = 0f; 201 mPlayer.setVolume(mCurrentVolume); 202 play(); 203 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10); 204 } else { 205 mCurrentVolume += 0.01f; 206 if (mCurrentVolume < 1.0f) { 207 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10); 208 } else { 209 mCurrentVolume = 1.0f; 210 } 211 mPlayer.setVolume(mCurrentVolume); 212 } 213 break; 214 case SERVER_DIED: 215 if (mIsSupposedToBePlaying) { 216 next(true); 217 } else { 218 // the server died when we were idle, so just 219 // reopen the same song (it will start again 220 // from the beginning though when the user 221 // restarts) 222 openCurrent(); 223 } 224 break; 225 case TRACK_ENDED: 226 if (mRepeatMode == REPEAT_CURRENT) { 227 seek(0); 228 play(); 229 } else if (!mOneShot) { 230 next(false); 231 } else { 232 notifyChange(PLAYBACK_COMPLETE); 233 mIsSupposedToBePlaying = false; 234 } 235 break; 236 case RELEASE_WAKELOCK: 237 mWakeLock.release(); 238 break; 239 default: 240 break; 241 } 242 } 243 }; 244 245 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 246 @Override 247 public void onReceive(Context context, Intent intent) { 248 String action = intent.getAction(); 249 String cmd = intent.getStringExtra("command"); 250 if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) { 251 next(true); 252 } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) { 253 prev(); 254 } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) { 255 if (isPlaying()) { 256 pause(); 257 } else { 258 play(); 259 } 260 } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) { 261 pause(); 262 } else if (CMDSTOP.equals(cmd)) { 263 pause(); 264 seek(0); 265 } else if (MediaAppWidgetProvider.CMDAPPWIDGETUPDATE.equals(cmd)) { 266 // Someone asked us to refresh a set of specific widgets, probably 267 // because they were just added. 268 int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); 269 mAppWidgetProvider.performUpdate(MediaPlaybackService.this, appWidgetIds); 270 } 271 } 272 }; 273 274 public MediaPlaybackService() { 275 mPsir = new PhoneStateIntentReceiver(this, mPhoneHandler); 276 mPsir.notifyPhoneCallState(PHONE_CHANGED); 277 } 278 279 @Override 280 public void onCreate() { 281 super.onCreate(); 282 283 mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE); 284 mCardId = FileUtils.getFatVolumeId(Environment.getExternalStorageDirectory().getPath()); 285 286 registerExternalStorageListener(); 287 288 // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes. 289 mPlayer = new MultiPlayer(); 290 mPlayer.setHandler(mMediaplayerHandler); 291 292 // Clear leftover notification in case this service previously got killed while playing 293 NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 294 nm.cancel(PLAYBACKSERVICE_STATUS); 295 296 reloadQueue(); 297 298 IntentFilter commandFilter = new IntentFilter(); 299 commandFilter.addAction(SERVICECMD); 300 commandFilter.addAction(TOGGLEPAUSE_ACTION); 301 commandFilter.addAction(PAUSE_ACTION); 302 commandFilter.addAction(NEXT_ACTION); 303 commandFilter.addAction(PREVIOUS_ACTION); 304 registerReceiver(mIntentReceiver, commandFilter); 305 306 mPsir.registerIntent(); 307 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 308 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); 309 mWakeLock.setReferenceCounted(false); 310 311 // If the service was idle, but got killed before it stopped itself, the 312 // system will relaunch it. Make sure it gets stopped again in that case. 313 Message msg = mDelayedStopHandler.obtainMessage(); 314 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 315 } 316 317 @Override 318 public void onDestroy() { 319 // Check that we're not being destroyed while something is still playing. 320 if (isPlaying()) { 321 Log.e("MediaPlaybackService", "Service being destroyed while still playing."); 322 } 323 // release all MediaPlayer resources, including the native player and wakelocks 324 mPlayer.release(); 325 mPlayer = null; 326 327 // make sure there aren't any other messages coming 328 mDelayedStopHandler.removeCallbacksAndMessages(null); 329 330 if (mCursor != null) { 331 mCursor.close(); 332 mCursor = null; 333 } 334 335 unregisterReceiver(mIntentReceiver); 336 if (mUnmountReceiver != null) { 337 unregisterReceiver(mUnmountReceiver); 338 mUnmountReceiver = null; 339 } 340 mPsir.unregisterIntent(); 341 mWakeLock.release(); 342 super.onDestroy(); 343 } 344 345 private final char hexdigits [] = new char [] { 346 '0', '1', '2', '3', 347 '4', '5', '6', '7', 348 '8', '9', 'a', 'b', 349 'c', 'd', 'e', 'f' 350 }; 351 352 private void saveQueue(boolean full) { 353 if (mOneShot) { 354 return; 355 } 356 Editor ed = mPreferences.edit(); 357 //long start = System.currentTimeMillis(); 358 if (full) { 359 StringBuilder q = new StringBuilder(); 360 361 // The current playlist is saved as a list of "reverse hexadecimal" 362 // numbers, which we can generate faster than normal decimal or 363 // hexadecimal numbers, which in turn allows us to save the playlist 364 // more often without worrying too much about performance. 365 // (saving the full state takes about 40 ms under no-load conditions 366 // on the phone) 367 int len = mPlayListLen; 368 for (int i = 0; i < len; i++) { 369 int n = mPlayList[i]; 370 if (n == 0) { 371 q.append("0;"); 372 } else { 373 while (n != 0) { 374 int digit = n & 0xf; 375 n >>= 4; 376 q.append(hexdigits[digit]); 377 } 378 q.append(";"); 379 } 380 } 381 //Log.i("@@@@ service", "created queue string in " + (System.currentTimeMillis() - start) + " ms"); 382 ed.putString("queue", q.toString()); 383 ed.putInt("cardid", mCardId); 384 } 385 ed.putInt("curpos", mPlayPos); 386 if (mPlayer.isInitialized()) { 387 ed.putLong("seekpos", mPlayer.position()); 388 } 389 ed.putInt("repeatmode", mRepeatMode); 390 ed.putInt("shufflemode", mShuffleMode); 391 ed.commit(); 392 393 //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms"); 394 } 395 396 private void reloadQueue() { 397 String q = null; 398 399 boolean newstyle = false; 400 int id = mCardId; 401 if (mPreferences.contains("cardid")) { 402 newstyle = true; 403 id = mPreferences.getInt("cardid", ~mCardId); 404 } 405 if (id == mCardId) { 406 // Only restore the saved playlist if the card is still 407 // the same one as when the playlist was saved 408 q = mPreferences.getString("queue", ""); 409 } 410 if (q != null && q.length() > 1) { 411 //Log.i("@@@@ service", "loaded queue: " + q); 412 String [] entries = q.split(";"); 413 int len = entries.length; 414 ensurePlayListCapacity(len); 415 for (int i = 0; i < len; i++) { 416 if (newstyle) { 417 String revhex = entries[i]; 418 int n = 0; 419 for (int j = revhex.length() - 1; j >= 0 ; j--) { 420 n <<= 4; 421 char c = revhex.charAt(j); 422 if (c >= '0' && c <= '9') { 423 n += (c - '0'); 424 } else if (c >= 'a' && c <= 'f') { 425 n += (10 + c - 'a'); 426 } else { 427 // bogus playlist data 428 len = 0; 429 break; 430 } 431 } 432 mPlayList[i] = n; 433 } else { 434 mPlayList[i] = Integer.parseInt(entries[i]); 435 } 436 } 437 mPlayListLen = len; 438 439 int pos = mPreferences.getInt("curpos", 0); 440 if (pos < 0 || pos >= len) { 441 // The saved playlist is bogus, discard it 442 mPlayListLen = 0; 443 return; 444 } 445 mPlayPos = pos; 446 447 // When reloadQueue is called in response to a card-insertion, 448 // we might not be able to query the media provider right away. 449 // To deal with this, try querying for the current file, and if 450 // that fails, wait a while and try again. If that too fails, 451 // assume there is a problem and don't restore the state. 452 Cursor c = MusicUtils.query(this, 453 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 454 new String [] {"_id"}, "_id=" + mPlayList[mPlayPos] , null, null); 455 if (c == null || c.getCount() == 0) { 456 // wait a bit and try again 457 SystemClock.sleep(3000); 458 c = getContentResolver().query( 459 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 460 mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null); 461 } 462 if (c != null) { 463 c.close(); 464 } 465 466 // Make sure we don't auto-skip to the next song, since that 467 // also starts playback. What could happen in that case is: 468 // - music is paused 469 // - go to UMS and delete some files, including the currently playing one 470 // - come back from UMS 471 // (time passes) 472 // - music app is killed for some reason (out of memory) 473 // - music service is restarted, service restores state, doesn't find 474 // the "current" file, goes to the next and: playback starts on its 475 // own, potentially at some random inconvenient time. 476 mOpenFailedCounter = 20; 477 mQuietMode = true; 478 openCurrent(); 479 mQuietMode = false; 480 if (!mPlayer.isInitialized()) { 481 // couldn't restore the saved state 482 mPlayListLen = 0; 483 return; 484 } 485 486 long seekpos = mPreferences.getLong("seekpos", 0); 487 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0); 488 489 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE); 490 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) { 491 repmode = REPEAT_NONE; 492 } 493 mRepeatMode = repmode; 494 495 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE); 496 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) { 497 shufmode = SHUFFLE_NONE; 498 } 499 if (shufmode == SHUFFLE_AUTO) { 500 if (! makeAutoShuffleList()) { 501 shufmode = SHUFFLE_NONE; 502 } 503 } 504 mShuffleMode = shufmode; 505 } 506 } 507 508 @Override 509 public IBinder onBind(Intent intent) { 510 mDelayedStopHandler.removeCallbacksAndMessages(null); 511 mServiceInUse = true; 512 return mBinder; 513 } 514 515 @Override 516 public void onRebind(Intent intent) { 517 mDelayedStopHandler.removeCallbacksAndMessages(null); 518 mServiceInUse = true; 519 } 520 521 @Override 522 public void onStart(Intent intent, int startId) { 523 mServiceStartId = startId; 524 mDelayedStopHandler.removeCallbacksAndMessages(null); 525 526 String action = intent.getAction(); 527 String cmd = intent.getStringExtra("command"); 528 529 if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) { 530 next(true); 531 } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) { 532 prev(); 533 } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) { 534 if (isPlaying()) { 535 pause(); 536 } else { 537 play(); 538 } 539 } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) { 540 pause(); 541 } else if (CMDSTOP.equals(cmd)) { 542 pause(); 543 seek(0); 544 } 545 546 // make sure the service will shut down on its own if it was 547 // just started but not bound to and nothing is playing 548 mDelayedStopHandler.removeCallbacksAndMessages(null); 549 Message msg = mDelayedStopHandler.obtainMessage(); 550 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 551 } 552 553 @Override 554 public boolean onUnbind(Intent intent) { 555 mServiceInUse = false; 556 557 // Take a snapshot of the current playlist 558 saveQueue(true); 559 560 if (isPlaying() || mResumeAfterCall) { 561 // something is currently playing, or will be playing once 562 // an in-progress call ends, so don't stop the service now. 563 return true; 564 } 565 566 // If there is a playlist but playback is paused, then wait a while 567 // before stopping the service, so that pause/resume isn't slow. 568 // Also delay stopping the service if we're transitioning between tracks. 569 if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) { 570 Message msg = mDelayedStopHandler.obtainMessage(); 571 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 572 return true; 573 } 574 575 // No active playlist, OK to stop the service right now 576 stopSelf(mServiceStartId); 577 return true; 578 } 579 580 private Handler mDelayedStopHandler = new Handler() { 581 @Override 582 public void handleMessage(Message msg) { 583 // Check again to make sure nothing is playing right now 584 if (isPlaying() || mResumeAfterCall || mServiceInUse 585 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) { 586 return; 587 } 588 // save the queue again, because it might have changed 589 // since the user exited the music app (because of 590 // party-shuffle or because the play-position changed) 591 saveQueue(true); 592 stopSelf(mServiceStartId); 593 } 594 }; 595 596 /** 597 * Called when we receive a ACTION_MEDIA_EJECT notification. 598 * 599 * @param storagePath path to mount point for the removed media 600 */ 601 public void closeExternalStorageFiles(String storagePath) { 602 // stop playback and clean up if the SD card is going to be unmounted. 603 stop(true); 604 notifyChange(QUEUE_CHANGED); 605 notifyChange(META_CHANGED); 606 } 607 608 /** 609 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. 610 * The intent will call closeExternalStorageFiles() if the external media 611 * is going to be ejected, so applications can clean up any files they have open. 612 */ 613 public void registerExternalStorageListener() { 614 if (mUnmountReceiver == null) { 615 mUnmountReceiver = new BroadcastReceiver() { 616 @Override 617 public void onReceive(Context context, Intent intent) { 618 String action = intent.getAction(); 619 if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 620 saveQueue(true); 621 mOneShot = true; // This makes us not save the state again later, 622 // which would be wrong because the song ids and 623 // card id might not match. 624 closeExternalStorageFiles(intent.getData().getPath()); 625 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 626 mMediaMountedCount++; 627 mCardId = FileUtils.getFatVolumeId(intent.getData().getPath()); 628 reloadQueue(); 629 notifyChange(QUEUE_CHANGED); 630 notifyChange(META_CHANGED); 631 } 632 } 633 }; 634 IntentFilter iFilter = new IntentFilter(); 635 iFilter.addAction(Intent.ACTION_MEDIA_EJECT); 636 iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); 637 iFilter.addDataScheme("file"); 638 registerReceiver(mUnmountReceiver, iFilter); 639 } 640 } 641 642 /** 643 * Notify the change-receivers that something has changed. 644 * The intent that is sent contains the following data 645 * for the currently playing track: 646 * "id" - Integer: the database row ID 647 * "artist" - String: the name of the artist 648 * "album" - String: the name of the album 649 * "track" - String: the name of the track 650 * The intent has an action that is one of 651 * "com.android.music.metachanged" 652 * "com.android.music.queuechanged", 653 * "com.android.music.playbackcomplete" 654 * "com.android.music.playstatechanged" 655 * respectively indicating that a new track has 656 * started playing, that the playback queue has 657 * changed, that playback has stopped because 658 * the last file in the list has been played, 659 * or that the play-state changed (paused/resumed). 660 */ 661 private void notifyChange(String what) { 662 663 Intent i = new Intent(what); 664 i.putExtra("id", Integer.valueOf(getAudioId())); 665 i.putExtra("artist", getArtistName()); 666 i.putExtra("album",getAlbumName()); 667 i.putExtra("track", getTrackName()); 668 sendBroadcast(i); 669 670 if (what.equals(QUEUE_CHANGED)) { 671 saveQueue(true); 672 } else { 673 saveQueue(false); 674 } 675 676 // Share this notification directly with our widgets 677 mAppWidgetProvider.notifyChange(this, what); 678 } 679 680 private void ensurePlayListCapacity(int size) { 681 if (mPlayList == null || size > mPlayList.length) { 682 // reallocate at 2x requested size so we don't 683 // need to grow and copy the array for every 684 // insert 685 int [] newlist = new int[size * 2]; 686 int len = mPlayListLen; 687 for (int i = 0; i < len; i++) { 688 newlist[i] = mPlayList[i]; 689 } 690 mPlayList = newlist; 691 } 692 // FIXME: shrink the array when the needed size is much smaller 693 // than the allocated size 694 } 695 696 // insert the list of songs at the specified position in the playlist 697 private void addToPlayList(int [] list, int position) { 698 int addlen = list.length; 699 if (position < 0) { // overwrite 700 mPlayListLen = 0; 701 position = 0; 702 } 703 ensurePlayListCapacity(mPlayListLen + addlen); 704 if (position > mPlayListLen) { 705 position = mPlayListLen; 706 } 707 708 // move part of list after insertion point 709 int tailsize = mPlayListLen - position; 710 for (int i = tailsize ; i > 0 ; i--) { 711 mPlayList[position + i] = mPlayList[position + i - addlen]; 712 } 713 714 // copy list into playlist 715 for (int i = 0; i < addlen; i++) { 716 mPlayList[position + i] = list[i]; 717 } 718 mPlayListLen += addlen; 719 } 720 721 /** 722 * Appends a list of tracks to the current playlist. 723 * If nothing is playing currently, playback will be started at 724 * the first track. 725 * If the action is NOW, playback will switch to the first of 726 * the new tracks immediately. 727 * @param list The list of tracks to append. 728 * @param action NOW, NEXT or LAST 729 */ 730 public void enqueue(int [] list, int action) { 731 synchronized(this) { 732 if (action == NEXT && mPlayPos + 1 < mPlayListLen) { 733 addToPlayList(list, mPlayPos + 1); 734 notifyChange(QUEUE_CHANGED); 735 } else { 736 // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen 737 addToPlayList(list, Integer.MAX_VALUE); 738 notifyChange(QUEUE_CHANGED); 739 if (action == NOW) { 740 mPlayPos = mPlayListLen - list.length; 741 openCurrent(); 742 play(); 743 notifyChange(META_CHANGED); 744 return; 745 } 746 } 747 if (mPlayPos < 0) { 748 mPlayPos = 0; 749 openCurrent(); 750 play(); 751 notifyChange(META_CHANGED); 752 } 753 } 754 } 755 756 /** 757 * Replaces the current playlist with a new list, 758 * and prepares for starting playback at the specified 759 * position in the list, or a random position if the 760 * specified position is 0. 761 * @param list The new list of tracks. 762 */ 763 public void open(int [] list, int position) { 764 synchronized (this) { 765 if (mShuffleMode == SHUFFLE_AUTO) { 766 mShuffleMode = SHUFFLE_NORMAL; 767 } 768 int oldId = getAudioId(); 769 int listlength = list.length; 770 boolean newlist = true; 771 if (mPlayListLen == listlength) { 772 // possible fast path: list might be the same 773 newlist = false; 774 for (int i = 0; i < listlength; i++) { 775 if (list[i] != mPlayList[i]) { 776 newlist = true; 777 break; 778 } 779 } 780 } 781 if (newlist) { 782 addToPlayList(list, -1); 783 notifyChange(QUEUE_CHANGED); 784 } 785 int oldpos = mPlayPos; 786 if (position >= 0) { 787 mPlayPos = position; 788 } else { 789 mPlayPos = mRand.nextInt(mPlayListLen); 790 } 791 mHistory.clear(); 792 793 saveBookmarkIfNeeded(); 794 openCurrent(); 795 if (oldId != getAudioId()) { 796 notifyChange(META_CHANGED); 797 } 798 } 799 } 800 801 /** 802 * Moves the item at index1 to index2. 803 * @param index1 804 * @param index2 805 */ 806 public void moveQueueItem(int index1, int index2) { 807 synchronized (this) { 808 if (index1 >= mPlayListLen) { 809 index1 = mPlayListLen - 1; 810 } 811 if (index2 >= mPlayListLen) { 812 index2 = mPlayListLen - 1; 813 } 814 if (index1 < index2) { 815 int tmp = mPlayList[index1]; 816 for (int i = index1; i < index2; i++) { 817 mPlayList[i] = mPlayList[i+1]; 818 } 819 mPlayList[index2] = tmp; 820 if (mPlayPos == index1) { 821 mPlayPos = index2; 822 } else if (mPlayPos >= index1 && mPlayPos <= index2) { 823 mPlayPos--; 824 } 825 } else if (index2 < index1) { 826 int tmp = mPlayList[index1]; 827 for (int i = index1; i > index2; i--) { 828 mPlayList[i] = mPlayList[i-1]; 829 } 830 mPlayList[index2] = tmp; 831 if (mPlayPos == index1) { 832 mPlayPos = index2; 833 } else if (mPlayPos >= index2 && mPlayPos <= index1) { 834 mPlayPos++; 835 } 836 } 837 notifyChange(QUEUE_CHANGED); 838 } 839 } 840 841 /** 842 * Returns the current play list 843 * @return An array of integers containing the IDs of the tracks in the play list 844 */ 845 public int [] getQueue() { 846 synchronized (this) { 847 int len = mPlayListLen; 848 int [] list = new int[len]; 849 for (int i = 0; i < len; i++) { 850 list[i] = mPlayList[i]; 851 } 852 return list; 853 } 854 } 855 856 private void openCurrent() { 857 synchronized (this) { 858 if (mCursor != null) { 859 mCursor.close(); 860 mCursor = null; 861 } 862 if (mPlayListLen == 0) { 863 return; 864 } 865 stop(false); 866 867 String id = String.valueOf(mPlayList[mPlayPos]); 868 869 mCursor = getContentResolver().query( 870 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 871 mCursorCols, "_id=" + id , null, null); 872 if (mCursor != null) { 873 mCursor.moveToFirst(); 874 open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id, false); 875 // go to bookmark if needed 876 if (isPodcast()) { 877 long bookmark = getBookmark(); 878 // Start playing a little bit before the bookmark, 879 // so it's easier to get back in to the narrative. 880 seek(bookmark - 5000); 881 } 882 } 883 } 884 } 885 886 public void openAsync(String path) { 887 synchronized (this) { 888 if (path == null) { 889 return; 890 } 891 892 mRepeatMode = REPEAT_NONE; 893 ensurePlayListCapacity(1); 894 mPlayListLen = 1; 895 mPlayPos = -1; 896 897 mFileToPlay = path; 898 mCursor = null; 899 mPlayer.setDataSourceAsync(mFileToPlay); 900 mOneShot = true; 901 } 902 } 903 904 /** 905 * Opens the specified file and readies it for playback. 906 * 907 * @param path The full path of the file to be opened. 908 * @param oneshot when set to true, playback will stop after this file completes, instead 909 * of moving on to the next track in the list 910 */ 911 public void open(String path, boolean oneshot) { 912 synchronized (this) { 913 if (path == null) { 914 return; 915 } 916 917 if (oneshot) { 918 mRepeatMode = REPEAT_NONE; 919 ensurePlayListCapacity(1); 920 mPlayListLen = 1; 921 mPlayPos = -1; 922 } 923 924 // if mCursor is null, try to associate path with a database cursor 925 if (mCursor == null) { 926 927 ContentResolver resolver = getContentResolver(); 928 Uri uri; 929 String where; 930 String selectionArgs[]; 931 if (path.startsWith("content://media/")) { 932 uri = Uri.parse(path); 933 where = null; 934 selectionArgs = null; 935 } else { 936 uri = MediaStore.Audio.Media.getContentUriForPath(path); 937 where = MediaStore.Audio.Media.DATA + "=?"; 938 selectionArgs = new String[] { path }; 939 } 940 941 try { 942 mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null); 943 if (mCursor != null) { 944 if (mCursor.getCount() == 0) { 945 mCursor.close(); 946 mCursor = null; 947 } else { 948 mCursor.moveToNext(); 949 ensurePlayListCapacity(1); 950 mPlayListLen = 1; 951 mPlayList[0] = mCursor.getInt(IDCOLIDX); 952 mPlayPos = 0; 953 } 954 } 955 } catch (UnsupportedOperationException ex) { 956 } 957 } 958 mFileToPlay = path; 959 mPlayer.setDataSource(mFileToPlay); 960 mOneShot = oneshot; 961 if (! mPlayer.isInitialized()) { 962 stop(true); 963 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) { 964 // beware: this ends up being recursive because next() calls open() again. 965 next(false); 966 } 967 if (! mPlayer.isInitialized() && mOpenFailedCounter != 0) { 968 // need to make sure we only shows this once 969 mOpenFailedCounter = 0; 970 if (!mQuietMode) { 971 Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show(); 972 } 973 } 974 } else { 975 mOpenFailedCounter = 0; 976 } 977 } 978 } 979 980 /** 981 * Starts playback of a previously opened file. 982 */ 983 public void play() { 984 if (mPlayer.isInitialized()) { 985 mPlayer.start(); 986 setForeground(true); 987 988 NotificationManager nm = (NotificationManager) 989 getSystemService(Context.NOTIFICATION_SERVICE); 990 991 RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar); 992 views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer); 993 if (getAudioId() < 0) { 994 // streaming 995 views.setTextViewText(R.id.trackname, getPath()); 996 views.setTextViewText(R.id.artistalbum, null); 997 } else { 998 String artist = getArtistName(); 999 views.setTextViewText(R.id.trackname, getTrackName()); 1000 if (artist == null || artist.equals(MediaFile.UNKNOWN_STRING)) { 1001 artist = getString(R.string.unknown_artist_name); 1002 } 1003 String album = getAlbumName(); 1004 if (album == null || album.equals(MediaFile.UNKNOWN_STRING)) { 1005 album = getString(R.string.unknown_album_name); 1006 } 1007 1008 views.setTextViewText(R.id.artistalbum, 1009 getString(R.string.notification_artist_album, artist, album) 1010 ); 1011 } 1012 1013 Notification status = new Notification(); 1014 status.contentView = views; 1015 status.flags |= Notification.FLAG_ONGOING_EVENT; 1016 status.icon = R.drawable.stat_notify_musicplayer; 1017 status.contentIntent = PendingIntent.getActivity(this, 0, 1018 new Intent("com.android.music.PLAYBACK_VIEWER"), 0); 1019 nm.notify(PLAYBACKSERVICE_STATUS, status); 1020 if (!mIsSupposedToBePlaying) { 1021 notifyChange(PLAYSTATE_CHANGED); 1022 } 1023 mIsSupposedToBePlaying = true; 1024 } else if (mPlayListLen <= 0) { 1025 // This is mostly so that if you press 'play' on a bluetooth headset 1026 // without every having played anything before, it will still play 1027 // something. 1028 setShuffleMode(SHUFFLE_AUTO); 1029 } 1030 } 1031 1032 private void stop(boolean remove_status_icon) { 1033 if (mPlayer.isInitialized()) { 1034 mPlayer.stop(); 1035 } 1036 mFileToPlay = null; 1037 if (mCursor != null) { 1038 mCursor.close(); 1039 mCursor = null; 1040 } 1041 if (remove_status_icon) { 1042 gotoIdleState(); 1043 } 1044 setForeground(false); 1045 if (remove_status_icon) { 1046 mIsSupposedToBePlaying = false; 1047 } 1048 } 1049 1050 /** 1051 * Stops playback. 1052 */ 1053 public void stop() { 1054 stop(true); 1055 } 1056 1057 /** 1058 * Pauses playback (call play() to resume) 1059 */ 1060 public void pause() { 1061 synchronized(this) { 1062 if (isPlaying()) { 1063 mPlayer.pause(); 1064 gotoIdleState(); 1065 setForeground(false); 1066 mIsSupposedToBePlaying = false; 1067 notifyChange(PLAYSTATE_CHANGED); 1068 saveBookmarkIfNeeded(); 1069 } 1070 } 1071 } 1072 1073 /** Returns whether something is currently playing 1074 * 1075 * @return true if something is playing (or will be playing shortly, in case 1076 * we're currently transitioning between tracks), false if not. 1077 */ 1078 public boolean isPlaying() { 1079 return mIsSupposedToBePlaying; 1080 } 1081 1082 /* 1083 Desired behavior for prev/next/shuffle: 1084 1085 - NEXT will move to the next track in the list when not shuffling, and to 1086 a track randomly picked from the not-yet-played tracks when shuffling. 1087 If all tracks have already been played, pick from the full set, but 1088 avoid picking the previously played track if possible. 1089 - when shuffling, PREV will go to the previously played track. Hitting PREV 1090 again will go to the track played before that, etc. When the start of the 1091 history has been reached, PREV is a no-op. 1092 When not shuffling, PREV will go to the sequentially previous track (the 1093 difference with the shuffle-case is mainly that when not shuffling, the 1094 user can back up to tracks that are not in the history). 1095 1096 Example: 1097 When playing an album with 10 tracks from the start, and enabling shuffle 1098 while playing track 5, the remaining tracks (6-10) will be shuffled, e.g. 1099 the final play order might be 1-2-3-4-5-8-10-6-9-7. 1100 When hitting 'prev' 8 times while playing track 7 in this example, the 1101 user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next', 1102 a random track will be picked again. If at any time user disables shuffling 1103 the next/previous track will be picked in sequential order again. 1104 */ 1105 1106 public void prev() { 1107 synchronized (this) { 1108 if (mOneShot) { 1109 // we were playing a specific file not part of a playlist, so there is no 'previous' 1110 seek(0); 1111 play(); 1112 return; 1113 } 1114 if (mShuffleMode == SHUFFLE_NORMAL) { 1115 // go to previously-played track and remove it from the history 1116 int histsize = mHistory.size(); 1117 if (histsize == 0) { 1118 // prev is a no-op 1119 return; 1120 } 1121 Integer pos = mHistory.remove(histsize - 1); 1122 mPlayPos = pos.intValue(); 1123 } else { 1124 if (mPlayPos > 0) { 1125 mPlayPos--; 1126 } else { 1127 mPlayPos = mPlayListLen - 1; 1128 } 1129 } 1130 saveBookmarkIfNeeded(); 1131 stop(false); 1132 openCurrent(); 1133 play(); 1134 notifyChange(META_CHANGED); 1135 } 1136 } 1137 1138 public void next(boolean force) { 1139 synchronized (this) { 1140 if (mOneShot) { 1141 // we were playing a specific file not part of a playlist, so there is no 'next' 1142 seek(0); 1143 play(); 1144 return; 1145 } 1146 1147 // Store the current file in the history, but keep the history at a 1148 // reasonable size 1149 if (mPlayPos >= 0) { 1150 mHistory.add(Integer.valueOf(mPlayPos)); 1151 } 1152 if (mHistory.size() > MAX_HISTORY_SIZE) { 1153 mHistory.removeElementAt(0); 1154 } 1155 1156 if (mShuffleMode == SHUFFLE_NORMAL) { 1157 // Pick random next track from the not-yet-played ones 1158 // TODO: make it work right after adding/removing items in the queue. 1159 1160 int numTracks = mPlayListLen; 1161 int[] tracks = new int[numTracks]; 1162 for (int i=0;i < numTracks; i++) { 1163 tracks[i] = i; 1164 } 1165 1166 int numHistory = mHistory.size(); 1167 int numUnplayed = numTracks; 1168 for (int i=0;i < numHistory; i++) { 1169 int idx = mHistory.get(i).intValue(); 1170 if (idx < numTracks && tracks[idx] >= 0) { 1171 numUnplayed--; 1172 tracks[idx] = -1; 1173 } 1174 } 1175 1176 // 'numUnplayed' now indicates how many tracks have not yet 1177 // been played, and 'tracks' contains the indices of those 1178 // tracks. 1179 if (numUnplayed <=0) { 1180 // everything's already been played 1181 if (mRepeatMode == REPEAT_ALL || force) { 1182 //pick from full set 1183 numUnplayed = numTracks; 1184 for (int i=0;i < numTracks; i++) { 1185 tracks[i] = i; 1186 } 1187 } else { 1188 // all done 1189 gotoIdleState(); 1190 return; 1191 } 1192 } 1193 int skip = mRand.nextInt(numUnplayed); 1194 int cnt = -1; 1195 while (true) { 1196 while (tracks[++cnt] < 0) 1197 ; 1198 skip--; 1199 if (skip < 0) { 1200 break; 1201 } 1202 } 1203 mPlayPos = cnt; 1204 } else if (mShuffleMode == SHUFFLE_AUTO) { 1205 doAutoShuffleUpdate(); 1206 mPlayPos++; 1207 } else { 1208 if (mPlayPos >= mPlayListLen - 1) { 1209 // we're at the end of the list 1210 if (mRepeatMode == REPEAT_NONE && !force) { 1211 // all done 1212 gotoIdleState(); 1213 notifyChange(PLAYBACK_COMPLETE); 1214 mIsSupposedToBePlaying = false; 1215 return; 1216 } else if (mRepeatMode == REPEAT_ALL || force) { 1217 mPlayPos = 0; 1218 } 1219 } else { 1220 mPlayPos++; 1221 } 1222 } 1223 saveBookmarkIfNeeded(); 1224 stop(false); 1225 openCurrent(); 1226 play(); 1227 notifyChange(META_CHANGED); 1228 } 1229 } 1230 1231 private void gotoIdleState() { 1232 NotificationManager nm = 1233 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 1234 nm.cancel(PLAYBACKSERVICE_STATUS); 1235 mDelayedStopHandler.removeCallbacksAndMessages(null); 1236 Message msg = mDelayedStopHandler.obtainMessage(); 1237 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 1238 } 1239 1240 private void saveBookmarkIfNeeded() { 1241 try { 1242 if (isPodcast()) { 1243 long pos = position(); 1244 long bookmark = getBookmark(); 1245 long duration = duration(); 1246 if ((pos < bookmark && (pos + 10000) > bookmark) || 1247 (pos > bookmark && (pos - 10000) < bookmark)) { 1248 // The existing bookmark is close to the current 1249 // position, so don't update it. 1250 return; 1251 } 1252 if (pos < 15000 || (pos + 10000) > duration) { 1253 // if we're near the start or end, clear the bookmark 1254 pos = 0; 1255 } 1256 1257 // write 'pos' to the bookmark field 1258 ContentValues values = new ContentValues(); 1259 values.put(MediaStore.Audio.Media.BOOKMARK, pos); 1260 Uri uri = ContentUris.withAppendedId( 1261 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX)); 1262 getContentResolver().update(uri, values, null, null); 1263 } 1264 } catch (SQLiteException ex) { 1265 } 1266 } 1267 1268 // Make sure there are at least 5 items after the currently playing item 1269 // and no more than 10 items before. 1270 private void doAutoShuffleUpdate() { 1271 boolean notify = false; 1272 // remove old entries 1273 if (mPlayPos > 10) { 1274 removeTracks(0, mPlayPos - 9); 1275 notify = true; 1276 } 1277 // add new entries if needed 1278 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos)); 1279 for (int i = 0; i < to_add; i++) { 1280 // pick something at random from the list 1281 int idx = mRand.nextInt(mAutoShuffleList.length); 1282 Integer which = mAutoShuffleList[idx]; 1283 ensurePlayListCapacity(mPlayListLen + 1); 1284 mPlayList[mPlayListLen++] = which; 1285 notify = true; 1286 } 1287 if (notify) { 1288 notifyChange(QUEUE_CHANGED); 1289 } 1290 } 1291 1292 // A simple variation of Random that makes sure that the 1293 // value it returns is not equal to the value it returned 1294 // previously, unless the interval is 1. 1295 private static class Shuffler { 1296 private int mPrevious; 1297 private Random mRandom = new Random(); 1298 public int nextInt(int interval) { 1299 int ret; 1300 do { 1301 ret = mRandom.nextInt(interval); 1302 } while (ret == mPrevious && interval > 1); 1303 mPrevious = ret; 1304 return ret; 1305 } 1306 }; 1307 1308 private boolean makeAutoShuffleList() { 1309 ContentResolver res = getContentResolver(); 1310 Cursor c = null; 1311 try { 1312 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1313 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1", 1314 null, null); 1315 if (c == null || c.getCount() == 0) { 1316 return false; 1317 } 1318 int len = c.getCount(); 1319 int[] list = new int[len]; 1320 for (int i = 0; i < len; i++) { 1321 c.moveToNext(); 1322 list[i] = c.getInt(0); 1323 } 1324 mAutoShuffleList = list; 1325 return true; 1326 } catch (RuntimeException ex) { 1327 } finally { 1328 if (c != null) { 1329 c.close(); 1330 } 1331 } 1332 return false; 1333 } 1334 1335 /** 1336 * Removes the range of tracks specified from the play list. If a file within the range is 1337 * the file currently being played, playback will move to the next file after the 1338 * range. 1339 * @param first The first file to be removed 1340 * @param last The last file to be removed 1341 * @return the number of tracks deleted 1342 */ 1343 public int removeTracks(int first, int last) { 1344 int numremoved = removeTracksInternal(first, last); 1345 if (numremoved > 0) { 1346 notifyChange(QUEUE_CHANGED); 1347 } 1348 return numremoved; 1349 } 1350 1351 private int removeTracksInternal(int first, int last) { 1352 synchronized (this) { 1353 if (last < first) return 0; 1354 if (first < 0) first = 0; 1355 if (last >= mPlayListLen) last = mPlayListLen - 1; 1356 1357 boolean gotonext = false; 1358 if (first <= mPlayPos && mPlayPos <= last) { 1359 mPlayPos = first; 1360 gotonext = true; 1361 } else if (mPlayPos > last) { 1362 mPlayPos -= (last - first + 1); 1363 } 1364 int num = mPlayListLen - last - 1; 1365 for (int i = 0; i < num; i++) { 1366 mPlayList[first + i] = mPlayList[last + 1 + i]; 1367 } 1368 mPlayListLen -= last - first + 1; 1369 1370 if (gotonext) { 1371 if (mPlayListLen == 0) { 1372 stop(true); 1373 mPlayPos = -1; 1374 } else { 1375 if (mPlayPos >= mPlayListLen) { 1376 mPlayPos = 0; 1377 } 1378 boolean wasPlaying = isPlaying(); 1379 stop(false); 1380 openCurrent(); 1381 if (wasPlaying) { 1382 play(); 1383 } 1384 } 1385 } 1386 return last - first + 1; 1387 } 1388 } 1389 1390 /** 1391 * Removes all instances of the track with the given id 1392 * from the playlist. 1393 * @param id The id to be removed 1394 * @return how many instances of the track were removed 1395 */ 1396 public int removeTrack(int id) { 1397 int numremoved = 0; 1398 synchronized (this) { 1399 for (int i = 0; i < mPlayListLen; i++) { 1400 if (mPlayList[i] == id) { 1401 numremoved += removeTracksInternal(i, i); 1402 i--; 1403 } 1404 } 1405 } 1406 if (numremoved > 0) { 1407 notifyChange(QUEUE_CHANGED); 1408 } 1409 return numremoved; 1410 } 1411 1412 public void setShuffleMode(int shufflemode) { 1413 synchronized(this) { 1414 if (mShuffleMode == shufflemode && mPlayListLen > 0) { 1415 return; 1416 } 1417 mShuffleMode = shufflemode; 1418 if (mShuffleMode == SHUFFLE_AUTO) { 1419 if (makeAutoShuffleList()) { 1420 mPlayListLen = 0; 1421 doAutoShuffleUpdate(); 1422 mPlayPos = 0; 1423 openCurrent(); 1424 play(); 1425 notifyChange(META_CHANGED); 1426 return; 1427 } else { 1428 // failed to build a list of files to shuffle 1429 mShuffleMode = SHUFFLE_NONE; 1430 } 1431 } 1432 saveQueue(false); 1433 } 1434 } 1435 public int getShuffleMode() { 1436 return mShuffleMode; 1437 } 1438 1439 public void setRepeatMode(int repeatmode) { 1440 synchronized(this) { 1441 mRepeatMode = repeatmode; 1442 saveQueue(false); 1443 } 1444 } 1445 public int getRepeatMode() { 1446 return mRepeatMode; 1447 } 1448 1449 public int getMediaMountedCount() { 1450 return mMediaMountedCount; 1451 } 1452 1453 /** 1454 * Returns the path of the currently playing file, or null if 1455 * no file is currently playing. 1456 */ 1457 public String getPath() { 1458 return mFileToPlay; 1459 } 1460 1461 /** 1462 * Returns the rowid of the currently playing file, or -1 if 1463 * no file is currently playing. 1464 */ 1465 public int getAudioId() { 1466 synchronized (this) { 1467 if (mPlayPos >= 0 && mPlayer.isInitialized()) { 1468 return mPlayList[mPlayPos]; 1469 } 1470 } 1471 return -1; 1472 } 1473 1474 /** 1475 * Returns the position in the queue 1476 * @return the position in the queue 1477 */ 1478 public int getQueuePosition() { 1479 synchronized(this) { 1480 return mPlayPos; 1481 } 1482 } 1483 1484 /** 1485 * Starts playing the track at the given position in the queue. 1486 * @param pos The position in the queue of the track that will be played. 1487 */ 1488 public void setQueuePosition(int pos) { 1489 synchronized(this) { 1490 stop(false); 1491 mPlayPos = pos; 1492 openCurrent(); 1493 play(); 1494 notifyChange(META_CHANGED); 1495 } 1496 } 1497 1498 public String getArtistName() { 1499 synchronized(this) { 1500 if (mCursor == null) { 1501 return null; 1502 } 1503 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); 1504 } 1505 } 1506 1507 public int getArtistId() { 1508 synchronized (this) { 1509 if (mCursor == null) { 1510 return -1; 1511 } 1512 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID)); 1513 } 1514 } 1515 1516 public String getAlbumName() { 1517 synchronized (this) { 1518 if (mCursor == null) { 1519 return null; 1520 } 1521 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)); 1522 } 1523 } 1524 1525 public int getAlbumId() { 1526 synchronized (this) { 1527 if (mCursor == null) { 1528 return -1; 1529 } 1530 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)); 1531 } 1532 } 1533 1534 public String getTrackName() { 1535 synchronized (this) { 1536 if (mCursor == null) { 1537 return null; 1538 } 1539 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); 1540 } 1541 } 1542 1543 private boolean isPodcast() { 1544 synchronized (this) { 1545 if (mCursor == null) { 1546 return false; 1547 } 1548 return (mCursor.getInt(PODCASTCOLIDX) > 0); 1549 } 1550 } 1551 1552 private long getBookmark() { 1553 synchronized (this) { 1554 if (mCursor == null) { 1555 return 0; 1556 } 1557 return mCursor.getLong(BOOKMARKCOLIDX); 1558 } 1559 } 1560 1561 /** 1562 * Returns the duration of the file in milliseconds. 1563 * Currently this method returns -1 for the duration of MIDI files. 1564 */ 1565 public long duration() { 1566 if (mPlayer.isInitialized()) { 1567 return mPlayer.duration(); 1568 } 1569 return -1; 1570 } 1571 1572 /** 1573 * Returns the current playback position in milliseconds 1574 */ 1575 public long position() { 1576 if (mPlayer.isInitialized()) { 1577 return mPlayer.position(); 1578 } 1579 return -1; 1580 } 1581 1582 /** 1583 * Seeks to the position specified. 1584 * 1585 * @param pos The position to seek to, in milliseconds 1586 */ 1587 public long seek(long pos) { 1588 if (mPlayer.isInitialized()) { 1589 if (pos < 0) pos = 0; 1590 if (pos > mPlayer.duration()) pos = mPlayer.duration(); 1591 return mPlayer.seek(pos); 1592 } 1593 return -1; 1594 } 1595 1596 /** 1597 * Provides a unified interface for dealing with midi files and 1598 * other media files. 1599 */ 1600 private class MultiPlayer { 1601 private MediaPlayer mMediaPlayer = new MediaPlayer(); 1602 private Handler mHandler; 1603 private boolean mIsInitialized = false; 1604 1605 public MultiPlayer() { 1606 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1607 } 1608 1609 public void setDataSourceAsync(String path) { 1610 try { 1611 mMediaPlayer.reset(); 1612 mMediaPlayer.setDataSource(path); 1613 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1614 mMediaPlayer.setOnPreparedListener(preparedlistener); 1615 mMediaPlayer.prepareAsync(); 1616 } catch (IOException ex) { 1617 // TODO: notify the user why the file couldn't be opened 1618 mIsInitialized = false; 1619 return; 1620 } catch (IllegalArgumentException ex) { 1621 // TODO: notify the user why the file couldn't be opened 1622 mIsInitialized = false; 1623 return; 1624 } 1625 mMediaPlayer.setOnCompletionListener(listener); 1626 mMediaPlayer.setOnErrorListener(errorListener); 1627 1628 mIsInitialized = true; 1629 } 1630 1631 public void setDataSource(String path) { 1632 try { 1633 mMediaPlayer.reset(); 1634 mMediaPlayer.setOnPreparedListener(null); 1635 if (path.startsWith("content://")) { 1636 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path)); 1637 } else { 1638 mMediaPlayer.setDataSource(path); 1639 } 1640 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1641 mMediaPlayer.prepare(); 1642 } catch (IOException ex) { 1643 // TODO: notify the user why the file couldn't be opened 1644 mIsInitialized = false; 1645 return; 1646 } catch (IllegalArgumentException ex) { 1647 // TODO: notify the user why the file couldn't be opened 1648 mIsInitialized = false; 1649 return; 1650 } 1651 mMediaPlayer.setOnCompletionListener(listener); 1652 mMediaPlayer.setOnErrorListener(errorListener); 1653 1654 mIsInitialized = true; 1655 } 1656 1657 public boolean isInitialized() { 1658 return mIsInitialized; 1659 } 1660 1661 public void start() { 1662 mMediaPlayer.start(); 1663 } 1664 1665 public void stop() { 1666 mMediaPlayer.reset(); 1667 mIsInitialized = false; 1668 } 1669 1670 /** 1671 * You CANNOT use this player anymore after calling release() 1672 */ 1673 public void release() { 1674 stop(); 1675 mMediaPlayer.release(); 1676 } 1677 1678 public void pause() { 1679 mMediaPlayer.pause(); 1680 } 1681 1682 public void setHandler(Handler handler) { 1683 mHandler = handler; 1684 } 1685 1686 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() { 1687 public void onCompletion(MediaPlayer mp) { 1688 // Acquire a temporary wakelock, since when we return from 1689 // this callback the MediaPlayer will release its wakelock 1690 // and allow the device to go to sleep. 1691 // This temporary wakelock is released when the RELEASE_WAKELOCK 1692 // message is processed, but just in case, put a timeout on it. 1693 mWakeLock.acquire(30000); 1694 mHandler.sendEmptyMessage(TRACK_ENDED); 1695 mHandler.sendEmptyMessage(RELEASE_WAKELOCK); 1696 } 1697 }; 1698 1699 MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() { 1700 public void onPrepared(MediaPlayer mp) { 1701 notifyChange(ASYNC_OPEN_COMPLETE); 1702 } 1703 }; 1704 1705 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() { 1706 public boolean onError(MediaPlayer mp, int what, int extra) { 1707 switch (what) { 1708 case MediaPlayer.MEDIA_ERROR_SERVER_DIED: 1709 mIsInitialized = false; 1710 mMediaPlayer.release(); 1711 // Creating a new MediaPlayer and settings its wakemode does not 1712 // require the media service, so it's OK to do this now, while the 1713 // service is still being restarted 1714 mMediaPlayer = new MediaPlayer(); 1715 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1716 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000); 1717 return true; 1718 default: 1719 break; 1720 } 1721 return false; 1722 } 1723 }; 1724 1725 public long duration() { 1726 return mMediaPlayer.getDuration(); 1727 } 1728 1729 public long position() { 1730 return mMediaPlayer.getCurrentPosition(); 1731 } 1732 1733 public long seek(long whereto) { 1734 mMediaPlayer.seekTo((int) whereto); 1735 return whereto; 1736 } 1737 1738 public void setVolume(float vol) { 1739 mMediaPlayer.setVolume(vol, vol); 1740 } 1741 } 1742 1743 /* 1744 * By making this a static class with a WeakReference to the Service, we 1745 * ensure that the Service can be GCd even when the system process still 1746 * has a remote reference to the stub. 1747 */ 1748 static class ServiceStub extends IMediaPlaybackService.Stub { 1749 WeakReference<MediaPlaybackService> mService; 1750 1751 ServiceStub(MediaPlaybackService service) { 1752 mService = new WeakReference<MediaPlaybackService>(service); 1753 } 1754 1755 public void openFileAsync(String path) 1756 { 1757 mService.get().openAsync(path); 1758 } 1759 public void openFile(String path, boolean oneShot) 1760 { 1761 mService.get().open(path, oneShot); 1762 } 1763 public void open(int [] list, int position) { 1764 mService.get().open(list, position); 1765 } 1766 public int getQueuePosition() { 1767 return mService.get().getQueuePosition(); 1768 } 1769 public void setQueuePosition(int index) { 1770 mService.get().setQueuePosition(index); 1771 } 1772 public boolean isPlaying() { 1773 return mService.get().isPlaying(); 1774 } 1775 public void stop() { 1776 mService.get().stop(); 1777 } 1778 public void pause() { 1779 mService.get().pause(); 1780 } 1781 public void play() { 1782 mService.get().play(); 1783 } 1784 public void prev() { 1785 mService.get().prev(); 1786 } 1787 public void next() { 1788 mService.get().next(true); 1789 } 1790 public String getTrackName() { 1791 return mService.get().getTrackName(); 1792 } 1793 public String getAlbumName() { 1794 return mService.get().getAlbumName(); 1795 } 1796 public int getAlbumId() { 1797 return mService.get().getAlbumId(); 1798 } 1799 public String getArtistName() { 1800 return mService.get().getArtistName(); 1801 } 1802 public int getArtistId() { 1803 return mService.get().getArtistId(); 1804 } 1805 public void enqueue(int [] list , int action) { 1806 mService.get().enqueue(list, action); 1807 } 1808 public int [] getQueue() { 1809 return mService.get().getQueue(); 1810 } 1811 public void moveQueueItem(int from, int to) { 1812 mService.get().moveQueueItem(from, to); 1813 } 1814 public String getPath() { 1815 return mService.get().getPath(); 1816 } 1817 public int getAudioId() { 1818 return mService.get().getAudioId(); 1819 } 1820 public long position() { 1821 return mService.get().position(); 1822 } 1823 public long duration() { 1824 return mService.get().duration(); 1825 } 1826 public long seek(long pos) { 1827 return mService.get().seek(pos); 1828 } 1829 public void setShuffleMode(int shufflemode) { 1830 mService.get().setShuffleMode(shufflemode); 1831 } 1832 public int getShuffleMode() { 1833 return mService.get().getShuffleMode(); 1834 } 1835 public int removeTracks(int first, int last) { 1836 return mService.get().removeTracks(first, last); 1837 } 1838 public int removeTrack(int id) { 1839 return mService.get().removeTrack(id); 1840 } 1841 public void setRepeatMode(int repeatmode) { 1842 mService.get().setRepeatMode(repeatmode); 1843 } 1844 public int getRepeatMode() { 1845 return mService.get().getRepeatMode(); 1846 } 1847 public int getMediaMountedCount() { 1848 return mService.get().getMediaMountedCount(); 1849 } 1850 1851 } 1852 1853 private final IBinder mBinder = new ServiceStub(this); 1854} 1855