MediaPlaybackService.java revision bf0ea148eefcffd43e6f5023a6f8365d74fb829f
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.length(); 404 if (q != null && 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 // Store the current file in the history, but keep the history at a 1147 // reasonable size 1148 if (mPlayPos >= 0) { 1149 mHistory.add(Integer.valueOf(mPlayPos)); 1150 } 1151 if (mHistory.size() > MAX_HISTORY_SIZE) { 1152 mHistory.removeElementAt(0); 1153 } 1154 1155 if (mShuffleMode == SHUFFLE_NORMAL) { 1156 // Pick random next track from the not-yet-played ones 1157 // TODO: make it work right after adding/removing items in the queue. 1158 1159 int numTracks = mPlayListLen; 1160 int[] tracks = new int[numTracks]; 1161 for (int i=0;i < numTracks; i++) { 1162 tracks[i] = i; 1163 } 1164 1165 int numHistory = mHistory.size(); 1166 int numUnplayed = numTracks; 1167 for (int i=0;i < numHistory; i++) { 1168 int idx = mHistory.get(i).intValue(); 1169 if (idx < numTracks && tracks[idx] >= 0) { 1170 numUnplayed--; 1171 tracks[idx] = -1; 1172 } 1173 } 1174 1175 // 'numUnplayed' now indicates how many tracks have not yet 1176 // been played, and 'tracks' contains the indices of those 1177 // tracks. 1178 if (numUnplayed <=0) { 1179 // everything's already been played 1180 if (mRepeatMode == REPEAT_ALL || force) { 1181 //pick from full set 1182 numUnplayed = numTracks; 1183 for (int i=0;i < numTracks; i++) { 1184 tracks[i] = i; 1185 } 1186 } else { 1187 // all done 1188 gotoIdleState(); 1189 return; 1190 } 1191 } 1192 int skip = mRand.nextInt(numUnplayed); 1193 int cnt = -1; 1194 while (true) { 1195 while (tracks[++cnt] < 0) 1196 ; 1197 skip--; 1198 if (skip < 0) { 1199 break; 1200 } 1201 } 1202 mPlayPos = cnt; 1203 } else if (mShuffleMode == SHUFFLE_AUTO) { 1204 doAutoShuffleUpdate(); 1205 mPlayPos++; 1206 } else { 1207 if (mPlayPos >= mPlayListLen - 1) { 1208 // we're at the end of the list 1209 if (mRepeatMode == REPEAT_NONE && !force) { 1210 // all done 1211 gotoIdleState(); 1212 notifyChange(PLAYBACK_COMPLETE); 1213 mIsSupposedToBePlaying = false; 1214 return; 1215 } else if (mRepeatMode == REPEAT_ALL || force) { 1216 mPlayPos = 0; 1217 } 1218 } else { 1219 mPlayPos++; 1220 } 1221 } 1222 saveBookmarkIfNeeded(); 1223 stop(false); 1224 openCurrent(); 1225 play(); 1226 notifyChange(META_CHANGED); 1227 } 1228 } 1229 1230 private void gotoIdleState() { 1231 NotificationManager nm = 1232 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 1233 nm.cancel(PLAYBACKSERVICE_STATUS); 1234 mDelayedStopHandler.removeCallbacksAndMessages(null); 1235 Message msg = mDelayedStopHandler.obtainMessage(); 1236 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 1237 } 1238 1239 private void saveBookmarkIfNeeded() { 1240 try { 1241 if (isPodcast()) { 1242 long pos = position(); 1243 long bookmark = getBookmark(); 1244 long duration = duration(); 1245 if ((pos < bookmark && (pos + 10000) > bookmark) || 1246 (pos > bookmark && (pos - 10000) < bookmark)) { 1247 // The existing bookmark is close to the current 1248 // position, so don't update it. 1249 return; 1250 } 1251 if (pos < 15000 || (pos + 10000) > duration) { 1252 // if we're near the start or end, clear the bookmark 1253 pos = 0; 1254 } 1255 1256 // write 'pos' to the bookmark field 1257 ContentValues values = new ContentValues(); 1258 values.put(MediaStore.Audio.Media.BOOKMARK, pos); 1259 Uri uri = ContentUris.withAppendedId( 1260 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX)); 1261 getContentResolver().update(uri, values, null, null); 1262 } 1263 } catch (SQLiteException ex) { 1264 } 1265 } 1266 1267 // Make sure there are at least 5 items after the currently playing item 1268 // and no more than 10 items before. 1269 private void doAutoShuffleUpdate() { 1270 boolean notify = false; 1271 // remove old entries 1272 if (mPlayPos > 10) { 1273 removeTracks(0, mPlayPos - 9); 1274 notify = true; 1275 } 1276 // add new entries if needed 1277 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos)); 1278 for (int i = 0; i < to_add; i++) { 1279 // pick something at random from the list 1280 int idx = mRand.nextInt(mAutoShuffleList.length); 1281 Integer which = mAutoShuffleList[idx]; 1282 ensurePlayListCapacity(mPlayListLen + 1); 1283 mPlayList[mPlayListLen++] = which; 1284 notify = true; 1285 } 1286 if (notify) { 1287 notifyChange(QUEUE_CHANGED); 1288 } 1289 } 1290 1291 // A simple variation of Random that makes sure that the 1292 // value it returns is not equal to the value it returned 1293 // previously, unless the interval is 1. 1294 private static class Shuffler { 1295 private int mPrevious; 1296 private Random mRandom = new Random(); 1297 public int nextInt(int interval) { 1298 int ret; 1299 do { 1300 ret = mRandom.nextInt(interval); 1301 } while (ret == mPrevious && interval > 1); 1302 mPrevious = ret; 1303 return ret; 1304 } 1305 }; 1306 1307 private boolean makeAutoShuffleList() { 1308 ContentResolver res = getContentResolver(); 1309 Cursor c = null; 1310 try { 1311 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1312 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1", 1313 null, null); 1314 if (c == null || c.getCount() == 0) { 1315 return false; 1316 } 1317 int len = c.getCount(); 1318 int[] list = new int[len]; 1319 for (int i = 0; i < len; i++) { 1320 c.moveToNext(); 1321 list[i] = c.getInt(0); 1322 } 1323 mAutoShuffleList = list; 1324 return true; 1325 } catch (RuntimeException ex) { 1326 } finally { 1327 if (c != null) { 1328 c.close(); 1329 } 1330 } 1331 return false; 1332 } 1333 1334 /** 1335 * Removes the range of tracks specified from the play list. If a file within the range is 1336 * the file currently being played, playback will move to the next file after the 1337 * range. 1338 * @param first The first file to be removed 1339 * @param last The last file to be removed 1340 * @return the number of tracks deleted 1341 */ 1342 public int removeTracks(int first, int last) { 1343 int numremoved = removeTracksInternal(first, last); 1344 if (numremoved > 0) { 1345 notifyChange(QUEUE_CHANGED); 1346 } 1347 return numremoved; 1348 } 1349 1350 private int removeTracksInternal(int first, int last) { 1351 synchronized (this) { 1352 if (last < first) return 0; 1353 if (first < 0) first = 0; 1354 if (last >= mPlayListLen) last = mPlayListLen - 1; 1355 1356 boolean gotonext = false; 1357 if (first <= mPlayPos && mPlayPos <= last) { 1358 mPlayPos = first; 1359 gotonext = true; 1360 } else if (mPlayPos > last) { 1361 mPlayPos -= (last - first + 1); 1362 } 1363 int num = mPlayListLen - last - 1; 1364 for (int i = 0; i < num; i++) { 1365 mPlayList[first + i] = mPlayList[last + 1 + i]; 1366 } 1367 mPlayListLen -= last - first + 1; 1368 1369 if (gotonext) { 1370 if (mPlayListLen == 0) { 1371 stop(true); 1372 mPlayPos = -1; 1373 } else { 1374 if (mPlayPos >= mPlayListLen) { 1375 mPlayPos = 0; 1376 } 1377 boolean wasPlaying = isPlaying(); 1378 stop(false); 1379 openCurrent(); 1380 if (wasPlaying) { 1381 play(); 1382 } 1383 } 1384 } 1385 return last - first + 1; 1386 } 1387 } 1388 1389 /** 1390 * Removes all instances of the track with the given id 1391 * from the playlist. 1392 * @param id The id to be removed 1393 * @return how many instances of the track were removed 1394 */ 1395 public int removeTrack(int id) { 1396 int numremoved = 0; 1397 synchronized (this) { 1398 for (int i = 0; i < mPlayListLen; i++) { 1399 if (mPlayList[i] == id) { 1400 numremoved += removeTracksInternal(i, i); 1401 i--; 1402 } 1403 } 1404 } 1405 if (numremoved > 0) { 1406 notifyChange(QUEUE_CHANGED); 1407 } 1408 return numremoved; 1409 } 1410 1411 public void setShuffleMode(int shufflemode) { 1412 synchronized(this) { 1413 if (mShuffleMode == shufflemode && mPlayListLen > 0) { 1414 return; 1415 } 1416 mShuffleMode = shufflemode; 1417 if (mShuffleMode == SHUFFLE_AUTO) { 1418 if (makeAutoShuffleList()) { 1419 mPlayListLen = 0; 1420 doAutoShuffleUpdate(); 1421 mPlayPos = 0; 1422 openCurrent(); 1423 play(); 1424 notifyChange(META_CHANGED); 1425 return; 1426 } else { 1427 // failed to build a list of files to shuffle 1428 mShuffleMode = SHUFFLE_NONE; 1429 } 1430 } 1431 saveQueue(false); 1432 } 1433 } 1434 public int getShuffleMode() { 1435 return mShuffleMode; 1436 } 1437 1438 public void setRepeatMode(int repeatmode) { 1439 synchronized(this) { 1440 mRepeatMode = repeatmode; 1441 saveQueue(false); 1442 } 1443 } 1444 public int getRepeatMode() { 1445 return mRepeatMode; 1446 } 1447 1448 public int getMediaMountedCount() { 1449 return mMediaMountedCount; 1450 } 1451 1452 /** 1453 * Returns the path of the currently playing file, or null if 1454 * no file is currently playing. 1455 */ 1456 public String getPath() { 1457 return mFileToPlay; 1458 } 1459 1460 /** 1461 * Returns the rowid of the currently playing file, or -1 if 1462 * no file is currently playing. 1463 */ 1464 public int getAudioId() { 1465 synchronized (this) { 1466 if (mPlayPos >= 0 && mPlayer.isInitialized()) { 1467 return mPlayList[mPlayPos]; 1468 } 1469 } 1470 return -1; 1471 } 1472 1473 /** 1474 * Returns the position in the queue 1475 * @return the position in the queue 1476 */ 1477 public int getQueuePosition() { 1478 synchronized(this) { 1479 return mPlayPos; 1480 } 1481 } 1482 1483 /** 1484 * Starts playing the track at the given position in the queue. 1485 * @param pos The position in the queue of the track that will be played. 1486 */ 1487 public void setQueuePosition(int pos) { 1488 synchronized(this) { 1489 stop(false); 1490 mPlayPos = pos; 1491 openCurrent(); 1492 play(); 1493 notifyChange(META_CHANGED); 1494 } 1495 } 1496 1497 public String getArtistName() { 1498 synchronized(this) { 1499 if (mCursor == null) { 1500 return null; 1501 } 1502 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); 1503 } 1504 } 1505 1506 public int getArtistId() { 1507 synchronized (this) { 1508 if (mCursor == null) { 1509 return -1; 1510 } 1511 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID)); 1512 } 1513 } 1514 1515 public String getAlbumName() { 1516 synchronized (this) { 1517 if (mCursor == null) { 1518 return null; 1519 } 1520 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)); 1521 } 1522 } 1523 1524 public int getAlbumId() { 1525 synchronized (this) { 1526 if (mCursor == null) { 1527 return -1; 1528 } 1529 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)); 1530 } 1531 } 1532 1533 public String getTrackName() { 1534 synchronized (this) { 1535 if (mCursor == null) { 1536 return null; 1537 } 1538 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); 1539 } 1540 } 1541 1542 private boolean isPodcast() { 1543 synchronized (this) { 1544 if (mCursor == null) { 1545 return false; 1546 } 1547 return (mCursor.getInt(PODCASTCOLIDX) > 0); 1548 } 1549 } 1550 1551 private long getBookmark() { 1552 synchronized (this) { 1553 if (mCursor == null) { 1554 return 0; 1555 } 1556 return mCursor.getLong(BOOKMARKCOLIDX); 1557 } 1558 } 1559 1560 /** 1561 * Returns the duration of the file in milliseconds. 1562 * Currently this method returns -1 for the duration of MIDI files. 1563 */ 1564 public long duration() { 1565 if (mPlayer.isInitialized()) { 1566 return mPlayer.duration(); 1567 } 1568 return -1; 1569 } 1570 1571 /** 1572 * Returns the current playback position in milliseconds 1573 */ 1574 public long position() { 1575 if (mPlayer.isInitialized()) { 1576 return mPlayer.position(); 1577 } 1578 return -1; 1579 } 1580 1581 /** 1582 * Seeks to the position specified. 1583 * 1584 * @param pos The position to seek to, in milliseconds 1585 */ 1586 public long seek(long pos) { 1587 if (mPlayer.isInitialized()) { 1588 if (pos < 0) pos = 0; 1589 if (pos > mPlayer.duration()) pos = mPlayer.duration(); 1590 return mPlayer.seek(pos); 1591 } 1592 return -1; 1593 } 1594 1595 /** 1596 * Provides a unified interface for dealing with midi files and 1597 * other media files. 1598 */ 1599 private class MultiPlayer { 1600 private MediaPlayer mMediaPlayer = new MediaPlayer(); 1601 private Handler mHandler; 1602 private boolean mIsInitialized = false; 1603 1604 public MultiPlayer() { 1605 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1606 } 1607 1608 public void setDataSourceAsync(String path) { 1609 try { 1610 mMediaPlayer.reset(); 1611 mMediaPlayer.setDataSource(path); 1612 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1613 mMediaPlayer.setOnPreparedListener(preparedlistener); 1614 mMediaPlayer.prepareAsync(); 1615 } catch (IOException ex) { 1616 // TODO: notify the user why the file couldn't be opened 1617 mIsInitialized = false; 1618 return; 1619 } catch (IllegalArgumentException ex) { 1620 // TODO: notify the user why the file couldn't be opened 1621 mIsInitialized = false; 1622 return; 1623 } 1624 mMediaPlayer.setOnCompletionListener(listener); 1625 mMediaPlayer.setOnErrorListener(errorListener); 1626 1627 mIsInitialized = true; 1628 } 1629 1630 public void setDataSource(String path) { 1631 try { 1632 mMediaPlayer.reset(); 1633 mMediaPlayer.setOnPreparedListener(null); 1634 if (path.startsWith("content://")) { 1635 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path)); 1636 } else { 1637 mMediaPlayer.setDataSource(path); 1638 } 1639 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1640 mMediaPlayer.prepare(); 1641 } catch (IOException ex) { 1642 // TODO: notify the user why the file couldn't be opened 1643 mIsInitialized = false; 1644 return; 1645 } catch (IllegalArgumentException ex) { 1646 // TODO: notify the user why the file couldn't be opened 1647 mIsInitialized = false; 1648 return; 1649 } 1650 mMediaPlayer.setOnCompletionListener(listener); 1651 mMediaPlayer.setOnErrorListener(errorListener); 1652 1653 mIsInitialized = true; 1654 } 1655 1656 public boolean isInitialized() { 1657 return mIsInitialized; 1658 } 1659 1660 public void start() { 1661 mMediaPlayer.start(); 1662 } 1663 1664 public void stop() { 1665 mMediaPlayer.reset(); 1666 mIsInitialized = false; 1667 } 1668 1669 /** 1670 * You CANNOT use this player anymore after calling release() 1671 */ 1672 public void release() { 1673 stop(); 1674 mMediaPlayer.release(); 1675 } 1676 1677 public void pause() { 1678 mMediaPlayer.pause(); 1679 } 1680 1681 public void setHandler(Handler handler) { 1682 mHandler = handler; 1683 } 1684 1685 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() { 1686 public void onCompletion(MediaPlayer mp) { 1687 // Acquire a temporary wakelock, since when we return from 1688 // this callback the MediaPlayer will release its wakelock 1689 // and allow the device to go to sleep. 1690 // This temporary wakelock is released when the RELEASE_WAKELOCK 1691 // message is processed, but just in case, put a timeout on it. 1692 mWakeLock.acquire(30000); 1693 mHandler.sendEmptyMessage(TRACK_ENDED); 1694 mHandler.sendEmptyMessage(RELEASE_WAKELOCK); 1695 } 1696 }; 1697 1698 MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() { 1699 public void onPrepared(MediaPlayer mp) { 1700 notifyChange(ASYNC_OPEN_COMPLETE); 1701 } 1702 }; 1703 1704 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() { 1705 public boolean onError(MediaPlayer mp, int what, int extra) { 1706 switch (what) { 1707 case MediaPlayer.MEDIA_ERROR_SERVER_DIED: 1708 mIsInitialized = false; 1709 mMediaPlayer.release(); 1710 // Creating a new MediaPlayer and settings its wakemode does not 1711 // require the media service, so it's OK to do this now, while the 1712 // service is still being restarted 1713 mMediaPlayer = new MediaPlayer(); 1714 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1715 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000); 1716 return true; 1717 default: 1718 break; 1719 } 1720 return false; 1721 } 1722 }; 1723 1724 public long duration() { 1725 return mMediaPlayer.getDuration(); 1726 } 1727 1728 public long position() { 1729 return mMediaPlayer.getCurrentPosition(); 1730 } 1731 1732 public long seek(long whereto) { 1733 mMediaPlayer.seekTo((int) whereto); 1734 return whereto; 1735 } 1736 1737 public void setVolume(float vol) { 1738 mMediaPlayer.setVolume(vol, vol); 1739 } 1740 } 1741 1742 /* 1743 * By making this a static class with a WeakReference to the Service, we 1744 * ensure that the Service can be GCd even when the system process still 1745 * has a remote reference to the stub. 1746 */ 1747 static class ServiceStub extends IMediaPlaybackService.Stub { 1748 WeakReference<MediaPlaybackService> mService; 1749 1750 ServiceStub(MediaPlaybackService service) { 1751 mService = new WeakReference<MediaPlaybackService>(service); 1752 } 1753 1754 public void openFileAsync(String path) 1755 { 1756 mService.get().openAsync(path); 1757 } 1758 public void openFile(String path, boolean oneShot) 1759 { 1760 mService.get().open(path, oneShot); 1761 } 1762 public void open(int [] list, int position) { 1763 mService.get().open(list, position); 1764 } 1765 public int getQueuePosition() { 1766 return mService.get().getQueuePosition(); 1767 } 1768 public void setQueuePosition(int index) { 1769 mService.get().setQueuePosition(index); 1770 } 1771 public boolean isPlaying() { 1772 return mService.get().isPlaying(); 1773 } 1774 public void stop() { 1775 mService.get().stop(); 1776 } 1777 public void pause() { 1778 mService.get().pause(); 1779 } 1780 public void play() { 1781 mService.get().play(); 1782 } 1783 public void prev() { 1784 mService.get().prev(); 1785 } 1786 public void next() { 1787 mService.get().next(true); 1788 } 1789 public String getTrackName() { 1790 return mService.get().getTrackName(); 1791 } 1792 public String getAlbumName() { 1793 return mService.get().getAlbumName(); 1794 } 1795 public int getAlbumId() { 1796 return mService.get().getAlbumId(); 1797 } 1798 public String getArtistName() { 1799 return mService.get().getArtistName(); 1800 } 1801 public int getArtistId() { 1802 return mService.get().getArtistId(); 1803 } 1804 public void enqueue(int [] list , int action) { 1805 mService.get().enqueue(list, action); 1806 } 1807 public int [] getQueue() { 1808 return mService.get().getQueue(); 1809 } 1810 public void moveQueueItem(int from, int to) { 1811 mService.get().moveQueueItem(from, to); 1812 } 1813 public String getPath() { 1814 return mService.get().getPath(); 1815 } 1816 public int getAudioId() { 1817 return mService.get().getAudioId(); 1818 } 1819 public long position() { 1820 return mService.get().position(); 1821 } 1822 public long duration() { 1823 return mService.get().duration(); 1824 } 1825 public long seek(long pos) { 1826 return mService.get().seek(pos); 1827 } 1828 public void setShuffleMode(int shufflemode) { 1829 mService.get().setShuffleMode(shufflemode); 1830 } 1831 public int getShuffleMode() { 1832 return mService.get().getShuffleMode(); 1833 } 1834 public int removeTracks(int first, int last) { 1835 return mService.get().removeTracks(first, last); 1836 } 1837 public int removeTrack(int id) { 1838 return mService.get().removeTrack(id); 1839 } 1840 public void setRepeatMode(int repeatmode) { 1841 mService.get().setRepeatMode(repeatmode); 1842 } 1843 public int getRepeatMode() { 1844 return mService.get().getRepeatMode(); 1845 } 1846 public int getMediaMountedCount() { 1847 return mService.get().getMediaMountedCount(); 1848 } 1849 1850 } 1851 1852 private final IBinder mBinder = new ServiceStub(this); 1853} 1854