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