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