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