MediaPlaybackService.java revision e99341f9674462607f3fd5e8dbd100b34b973e1b
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 prev(); 579 } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) { 580 if (isPlaying()) { 581 pause(); 582 } else { 583 play(); 584 } 585 } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) { 586 pause(); 587 } else if (CMDSTOP.equals(cmd)) { 588 pause(); 589 seek(0); 590 } 591 } 592 593 // make sure the service will shut down on its own if it was 594 // just started but not bound to and nothing is playing 595 mDelayedStopHandler.removeCallbacksAndMessages(null); 596 Message msg = mDelayedStopHandler.obtainMessage(); 597 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 598 return START_STICKY; 599 } 600 601 @Override 602 public boolean onUnbind(Intent intent) { 603 mServiceInUse = false; 604 605 // Take a snapshot of the current playlist 606 saveQueue(true); 607 608 if (isPlaying() || mResumeAfterCall) { 609 // something is currently playing, or will be playing once 610 // an in-progress call ends, so don't stop the service now. 611 return true; 612 } 613 614 // If there is a playlist but playback is paused, then wait a while 615 // before stopping the service, so that pause/resume isn't slow. 616 // Also delay stopping the service if we're transitioning between tracks. 617 if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) { 618 Message msg = mDelayedStopHandler.obtainMessage(); 619 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 620 return true; 621 } 622 623 // No active playlist, OK to stop the service right now 624 stopSelf(mServiceStartId); 625 return true; 626 } 627 628 private Handler mDelayedStopHandler = new Handler() { 629 @Override 630 public void handleMessage(Message msg) { 631 // Check again to make sure nothing is playing right now 632 if (isPlaying() || mResumeAfterCall || mServiceInUse 633 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) { 634 return; 635 } 636 // save the queue again, because it might have changed 637 // since the user exited the music app (because of 638 // party-shuffle or because the play-position changed) 639 saveQueue(true); 640 stopSelf(mServiceStartId); 641 } 642 }; 643 644 /** 645 * Called when we receive a ACTION_MEDIA_EJECT notification. 646 * 647 * @param storagePath path to mount point for the removed media 648 */ 649 public void closeExternalStorageFiles(String storagePath) { 650 // stop playback and clean up if the SD card is going to be unmounted. 651 stop(true); 652 notifyChange(QUEUE_CHANGED); 653 notifyChange(META_CHANGED); 654 } 655 656 /** 657 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. 658 * The intent will call closeExternalStorageFiles() if the external media 659 * is going to be ejected, so applications can clean up any files they have open. 660 */ 661 public void registerExternalStorageListener() { 662 if (mUnmountReceiver == null) { 663 mUnmountReceiver = new BroadcastReceiver() { 664 @Override 665 public void onReceive(Context context, Intent intent) { 666 String action = intent.getAction(); 667 if (action.equals(Intent.ACTION_MEDIA_EJECT)) { 668 saveQueue(true); 669 mOneShot = true; // This makes us not save the state again later, 670 // which would be wrong because the song ids and 671 // card id might not match. 672 closeExternalStorageFiles(intent.getData().getPath()); 673 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 674 mMediaMountedCount++; 675 mCardId = FileUtils.getFatVolumeId(intent.getData().getPath()); 676 reloadQueue(); 677 notifyChange(QUEUE_CHANGED); 678 notifyChange(META_CHANGED); 679 } 680 } 681 }; 682 IntentFilter iFilter = new IntentFilter(); 683 iFilter.addAction(Intent.ACTION_MEDIA_EJECT); 684 iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); 685 iFilter.addDataScheme("file"); 686 registerReceiver(mUnmountReceiver, iFilter); 687 } 688 } 689 690 /** 691 * Notify the change-receivers that something has changed. 692 * The intent that is sent contains the following data 693 * for the currently playing track: 694 * "id" - Integer: the database row ID 695 * "artist" - String: the name of the artist 696 * "album" - String: the name of the album 697 * "track" - String: the name of the track 698 * The intent has an action that is one of 699 * "com.android.music.metachanged" 700 * "com.android.music.queuechanged", 701 * "com.android.music.playbackcomplete" 702 * "com.android.music.playstatechanged" 703 * respectively indicating that a new track has 704 * started playing, that the playback queue has 705 * changed, that playback has stopped because 706 * the last file in the list has been played, 707 * or that the play-state changed (paused/resumed). 708 */ 709 private void notifyChange(String what) { 710 711 Intent i = new Intent(what); 712 i.putExtra("id", Long.valueOf(getAudioId())); 713 i.putExtra("artist", getArtistName()); 714 i.putExtra("album",getAlbumName()); 715 i.putExtra("track", getTrackName()); 716 sendBroadcast(i); 717 718 if (what.equals(QUEUE_CHANGED)) { 719 saveQueue(true); 720 } else { 721 saveQueue(false); 722 } 723 724 // Share this notification directly with our widgets 725 mAppWidgetProvider.notifyChange(this, what); 726 } 727 728 private void ensurePlayListCapacity(int size) { 729 if (mPlayList == null || size > mPlayList.length) { 730 // reallocate at 2x requested size so we don't 731 // need to grow and copy the array for every 732 // insert 733 long [] newlist = new long[size * 2]; 734 int len = mPlayList != null ? mPlayList.length : mPlayListLen; 735 for (int i = 0; i < len; i++) { 736 newlist[i] = mPlayList[i]; 737 } 738 mPlayList = newlist; 739 } 740 // FIXME: shrink the array when the needed size is much smaller 741 // than the allocated size 742 } 743 744 // insert the list of songs at the specified position in the playlist 745 private void addToPlayList(long [] list, int position) { 746 int addlen = list.length; 747 if (position < 0) { // overwrite 748 mPlayListLen = 0; 749 position = 0; 750 } 751 ensurePlayListCapacity(mPlayListLen + addlen); 752 if (position > mPlayListLen) { 753 position = mPlayListLen; 754 } 755 756 // move part of list after insertion point 757 int tailsize = mPlayListLen - position; 758 for (int i = tailsize ; i > 0 ; i--) { 759 mPlayList[position + i] = mPlayList[position + i - addlen]; 760 } 761 762 // copy list into playlist 763 for (int i = 0; i < addlen; i++) { 764 mPlayList[position + i] = list[i]; 765 } 766 mPlayListLen += addlen; 767 } 768 769 /** 770 * Appends a list of tracks to the current playlist. 771 * If nothing is playing currently, playback will be started at 772 * the first track. 773 * If the action is NOW, playback will switch to the first of 774 * the new tracks immediately. 775 * @param list The list of tracks to append. 776 * @param action NOW, NEXT or LAST 777 */ 778 public void enqueue(long [] list, int action) { 779 synchronized(this) { 780 if (action == NEXT && mPlayPos + 1 < mPlayListLen) { 781 addToPlayList(list, mPlayPos + 1); 782 notifyChange(QUEUE_CHANGED); 783 } else { 784 // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen 785 addToPlayList(list, Integer.MAX_VALUE); 786 notifyChange(QUEUE_CHANGED); 787 if (action == NOW) { 788 mPlayPos = mPlayListLen - list.length; 789 openCurrent(); 790 play(); 791 notifyChange(META_CHANGED); 792 return; 793 } 794 } 795 if (mPlayPos < 0) { 796 mPlayPos = 0; 797 openCurrent(); 798 play(); 799 notifyChange(META_CHANGED); 800 } 801 } 802 } 803 804 /** 805 * Replaces the current playlist with a new list, 806 * and prepares for starting playback at the specified 807 * position in the list, or a random position if the 808 * specified position is 0. 809 * @param list The new list of tracks. 810 */ 811 public void open(long [] list, int position) { 812 synchronized (this) { 813 if (mShuffleMode == SHUFFLE_AUTO) { 814 mShuffleMode = SHUFFLE_NORMAL; 815 } 816 long oldId = getAudioId(); 817 int listlength = list.length; 818 boolean newlist = true; 819 if (mPlayListLen == listlength) { 820 // possible fast path: list might be the same 821 newlist = false; 822 for (int i = 0; i < listlength; i++) { 823 if (list[i] != mPlayList[i]) { 824 newlist = true; 825 break; 826 } 827 } 828 } 829 if (newlist) { 830 addToPlayList(list, -1); 831 notifyChange(QUEUE_CHANGED); 832 } 833 int oldpos = mPlayPos; 834 if (position >= 0) { 835 mPlayPos = position; 836 } else { 837 mPlayPos = mRand.nextInt(mPlayListLen); 838 } 839 mHistory.clear(); 840 841 saveBookmarkIfNeeded(); 842 openCurrent(); 843 if (oldId != getAudioId()) { 844 notifyChange(META_CHANGED); 845 } 846 } 847 } 848 849 /** 850 * Moves the item at index1 to index2. 851 * @param index1 852 * @param index2 853 */ 854 public void moveQueueItem(int index1, int index2) { 855 synchronized (this) { 856 if (index1 >= mPlayListLen) { 857 index1 = mPlayListLen - 1; 858 } 859 if (index2 >= mPlayListLen) { 860 index2 = mPlayListLen - 1; 861 } 862 if (index1 < index2) { 863 long tmp = mPlayList[index1]; 864 for (int i = index1; i < index2; i++) { 865 mPlayList[i] = mPlayList[i+1]; 866 } 867 mPlayList[index2] = tmp; 868 if (mPlayPos == index1) { 869 mPlayPos = index2; 870 } else if (mPlayPos >= index1 && mPlayPos <= index2) { 871 mPlayPos--; 872 } 873 } else if (index2 < index1) { 874 long tmp = mPlayList[index1]; 875 for (int i = index1; i > index2; i--) { 876 mPlayList[i] = mPlayList[i-1]; 877 } 878 mPlayList[index2] = tmp; 879 if (mPlayPos == index1) { 880 mPlayPos = index2; 881 } else if (mPlayPos >= index2 && mPlayPos <= index1) { 882 mPlayPos++; 883 } 884 } 885 notifyChange(QUEUE_CHANGED); 886 } 887 } 888 889 /** 890 * Returns the current play list 891 * @return An array of integers containing the IDs of the tracks in the play list 892 */ 893 public long [] getQueue() { 894 synchronized (this) { 895 int len = mPlayListLen; 896 long [] list = new long[len]; 897 for (int i = 0; i < len; i++) { 898 list[i] = mPlayList[i]; 899 } 900 return list; 901 } 902 } 903 904 private void openCurrent() { 905 synchronized (this) { 906 if (mCursor != null) { 907 mCursor.close(); 908 mCursor = null; 909 } 910 if (mPlayListLen == 0) { 911 return; 912 } 913 stop(false); 914 915 String id = String.valueOf(mPlayList[mPlayPos]); 916 917 mCursor = getContentResolver().query( 918 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 919 mCursorCols, "_id=" + id , null, null); 920 if (mCursor != null) { 921 mCursor.moveToFirst(); 922 open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id, false); 923 // go to bookmark if needed 924 if (isPodcast()) { 925 long bookmark = getBookmark(); 926 // Start playing a little bit before the bookmark, 927 // so it's easier to get back in to the narrative. 928 seek(bookmark - 5000); 929 } 930 } 931 } 932 } 933 934 public void openAsync(String path) { 935 synchronized (this) { 936 if (path == null) { 937 return; 938 } 939 940 mRepeatMode = REPEAT_NONE; 941 ensurePlayListCapacity(1); 942 mPlayListLen = 1; 943 mPlayPos = -1; 944 945 mFileToPlay = path; 946 mCursor = null; 947 mPlayer.setDataSourceAsync(mFileToPlay); 948 mOneShot = true; 949 } 950 } 951 952 /** 953 * Opens the specified file and readies it for playback. 954 * 955 * @param path The full path of the file to be opened. 956 * @param oneshot when set to true, playback will stop after this file completes, instead 957 * of moving on to the next track in the list 958 */ 959 public void open(String path, boolean oneshot) { 960 synchronized (this) { 961 if (path == null) { 962 return; 963 } 964 965 if (oneshot) { 966 mRepeatMode = REPEAT_NONE; 967 ensurePlayListCapacity(1); 968 mPlayListLen = 1; 969 mPlayPos = -1; 970 } 971 972 // if mCursor is null, try to associate path with a database cursor 973 if (mCursor == null) { 974 975 ContentResolver resolver = getContentResolver(); 976 Uri uri; 977 String where; 978 String selectionArgs[]; 979 if (path.startsWith("content://media/")) { 980 uri = Uri.parse(path); 981 where = null; 982 selectionArgs = null; 983 } else { 984 uri = MediaStore.Audio.Media.getContentUriForPath(path); 985 where = MediaStore.Audio.Media.DATA + "=?"; 986 selectionArgs = new String[] { path }; 987 } 988 989 try { 990 mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null); 991 if (mCursor != null) { 992 if (mCursor.getCount() == 0) { 993 mCursor.close(); 994 mCursor = null; 995 } else { 996 mCursor.moveToNext(); 997 ensurePlayListCapacity(1); 998 mPlayListLen = 1; 999 mPlayList[0] = mCursor.getLong(IDCOLIDX); 1000 mPlayPos = 0; 1001 } 1002 } 1003 } catch (UnsupportedOperationException ex) { 1004 } 1005 } 1006 mFileToPlay = path; 1007 mPlayer.setDataSource(mFileToPlay); 1008 mOneShot = oneshot; 1009 if (! mPlayer.isInitialized()) { 1010 stop(true); 1011 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) { 1012 // beware: this ends up being recursive because next() calls open() again. 1013 next(false); 1014 } 1015 if (! mPlayer.isInitialized() && mOpenFailedCounter != 0) { 1016 // need to make sure we only shows this once 1017 mOpenFailedCounter = 0; 1018 if (!mQuietMode) { 1019 Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show(); 1020 } 1021 Log.d(LOGTAG, "Failed to open file for playback"); 1022 } 1023 } else { 1024 mOpenFailedCounter = 0; 1025 } 1026 } 1027 } 1028 1029 /** 1030 * Starts playback of a previously opened file. 1031 */ 1032 public void play() { 1033 if (mPlayer.isInitialized()) { 1034 // if we are at the end of the song, go to the next song first 1035 long duration = mPlayer.duration(); 1036 if (mRepeatMode != REPEAT_CURRENT && duration > 2000 && 1037 mPlayer.position() >= duration - 2000) { 1038 next(true); 1039 } 1040 1041 mPlayer.start(); 1042 1043 RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar); 1044 views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer); 1045 if (getAudioId() < 0) { 1046 // streaming 1047 views.setTextViewText(R.id.trackname, getPath()); 1048 views.setTextViewText(R.id.artistalbum, null); 1049 } else { 1050 String artist = getArtistName(); 1051 views.setTextViewText(R.id.trackname, getTrackName()); 1052 if (artist == null || artist.equals(MediaFile.UNKNOWN_STRING)) { 1053 artist = getString(R.string.unknown_artist_name); 1054 } 1055 String album = getAlbumName(); 1056 if (album == null || album.equals(MediaFile.UNKNOWN_STRING)) { 1057 album = getString(R.string.unknown_album_name); 1058 } 1059 1060 views.setTextViewText(R.id.artistalbum, 1061 getString(R.string.notification_artist_album, artist, album) 1062 ); 1063 } 1064 1065 Notification status = new Notification(); 1066 status.contentView = views; 1067 status.flags |= Notification.FLAG_ONGOING_EVENT; 1068 status.icon = R.drawable.stat_notify_musicplayer; 1069 status.contentIntent = PendingIntent.getActivity(this, 0, 1070 new Intent("com.android.music.PLAYBACK_VIEWER"), 0); 1071 startForeground(PLAYBACKSERVICE_STATUS, status); 1072 if (!mIsSupposedToBePlaying) { 1073 mIsSupposedToBePlaying = true; 1074 notifyChange(PLAYSTATE_CHANGED); 1075 } 1076 1077 } else if (mPlayListLen <= 0) { 1078 // This is mostly so that if you press 'play' on a bluetooth headset 1079 // without every having played anything before, it will still play 1080 // something. 1081 setShuffleMode(SHUFFLE_AUTO); 1082 } 1083 } 1084 1085 private void stop(boolean remove_status_icon) { 1086 if (mPlayer.isInitialized()) { 1087 mPlayer.stop(); 1088 } 1089 mFileToPlay = null; 1090 if (mCursor != null) { 1091 mCursor.close(); 1092 mCursor = null; 1093 } 1094 if (remove_status_icon) { 1095 gotoIdleState(); 1096 } else { 1097 stopForeground(false); 1098 } 1099 if (remove_status_icon) { 1100 mIsSupposedToBePlaying = false; 1101 } 1102 } 1103 1104 /** 1105 * Stops playback. 1106 */ 1107 public void stop() { 1108 stop(true); 1109 } 1110 1111 /** 1112 * Pauses playback (call play() to resume) 1113 */ 1114 public void pause() { 1115 synchronized(this) { 1116 if (isPlaying()) { 1117 mPlayer.pause(); 1118 gotoIdleState(); 1119 mIsSupposedToBePlaying = false; 1120 notifyChange(PLAYSTATE_CHANGED); 1121 saveBookmarkIfNeeded(); 1122 } 1123 } 1124 } 1125 1126 /** Returns whether something is currently playing 1127 * 1128 * @return true if something is playing (or will be playing shortly, in case 1129 * we're currently transitioning between tracks), false if not. 1130 */ 1131 public boolean isPlaying() { 1132 return mIsSupposedToBePlaying; 1133 } 1134 1135 /* 1136 Desired behavior for prev/next/shuffle: 1137 1138 - NEXT will move to the next track in the list when not shuffling, and to 1139 a track randomly picked from the not-yet-played tracks when shuffling. 1140 If all tracks have already been played, pick from the full set, but 1141 avoid picking the previously played track if possible. 1142 - when shuffling, PREV will go to the previously played track. Hitting PREV 1143 again will go to the track played before that, etc. When the start of the 1144 history has been reached, PREV is a no-op. 1145 When not shuffling, PREV will go to the sequentially previous track (the 1146 difference with the shuffle-case is mainly that when not shuffling, the 1147 user can back up to tracks that are not in the history). 1148 1149 Example: 1150 When playing an album with 10 tracks from the start, and enabling shuffle 1151 while playing track 5, the remaining tracks (6-10) will be shuffled, e.g. 1152 the final play order might be 1-2-3-4-5-8-10-6-9-7. 1153 When hitting 'prev' 8 times while playing track 7 in this example, the 1154 user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next', 1155 a random track will be picked again. If at any time user disables shuffling 1156 the next/previous track will be picked in sequential order again. 1157 */ 1158 1159 public void prev() { 1160 synchronized (this) { 1161 if (mOneShot) { 1162 // we were playing a specific file not part of a playlist, so there is no 'previous' 1163 seek(0); 1164 play(); 1165 return; 1166 } 1167 if (mShuffleMode == SHUFFLE_NORMAL) { 1168 // go to previously-played track and remove it from the history 1169 int histsize = mHistory.size(); 1170 if (histsize == 0) { 1171 // prev is a no-op 1172 return; 1173 } 1174 Integer pos = mHistory.remove(histsize - 1); 1175 mPlayPos = pos.intValue(); 1176 } else { 1177 if (mPlayPos > 0) { 1178 mPlayPos--; 1179 } else { 1180 mPlayPos = mPlayListLen - 1; 1181 } 1182 } 1183 saveBookmarkIfNeeded(); 1184 stop(false); 1185 openCurrent(); 1186 play(); 1187 notifyChange(META_CHANGED); 1188 } 1189 } 1190 1191 public void next(boolean force) { 1192 synchronized (this) { 1193 if (mOneShot) { 1194 // we were playing a specific file not part of a playlist, so there is no 'next' 1195 seek(0); 1196 play(); 1197 return; 1198 } 1199 1200 if (mPlayListLen <= 0) { 1201 Log.d(LOGTAG, "No play queue"); 1202 return; 1203 } 1204 1205 // Store the current file in the history, but keep the history at a 1206 // reasonable size 1207 if (mPlayPos >= 0) { 1208 mHistory.add(Integer.valueOf(mPlayPos)); 1209 } 1210 if (mHistory.size() > MAX_HISTORY_SIZE) { 1211 mHistory.removeElementAt(0); 1212 } 1213 1214 if (mShuffleMode == SHUFFLE_NORMAL) { 1215 // Pick random next track from the not-yet-played ones 1216 // TODO: make it work right after adding/removing items in the queue. 1217 1218 int numTracks = mPlayListLen; 1219 int[] tracks = new int[numTracks]; 1220 for (int i=0;i < numTracks; i++) { 1221 tracks[i] = i; 1222 } 1223 1224 int numHistory = mHistory.size(); 1225 int numUnplayed = numTracks; 1226 for (int i=0;i < numHistory; i++) { 1227 int idx = mHistory.get(i).intValue(); 1228 if (idx < numTracks && tracks[idx] >= 0) { 1229 numUnplayed--; 1230 tracks[idx] = -1; 1231 } 1232 } 1233 1234 // 'numUnplayed' now indicates how many tracks have not yet 1235 // been played, and 'tracks' contains the indices of those 1236 // tracks. 1237 if (numUnplayed <=0) { 1238 // everything's already been played 1239 if (mRepeatMode == REPEAT_ALL || force) { 1240 //pick from full set 1241 numUnplayed = numTracks; 1242 for (int i=0;i < numTracks; i++) { 1243 tracks[i] = i; 1244 } 1245 } else { 1246 // all done 1247 gotoIdleState(); 1248 return; 1249 } 1250 } 1251 int skip = mRand.nextInt(numUnplayed); 1252 int cnt = -1; 1253 while (true) { 1254 while (tracks[++cnt] < 0) 1255 ; 1256 skip--; 1257 if (skip < 0) { 1258 break; 1259 } 1260 } 1261 mPlayPos = cnt; 1262 } else if (mShuffleMode == SHUFFLE_AUTO) { 1263 doAutoShuffleUpdate(); 1264 mPlayPos++; 1265 } else { 1266 if (mPlayPos >= mPlayListLen - 1) { 1267 // we're at the end of the list 1268 if (mRepeatMode == REPEAT_NONE && !force) { 1269 // all done 1270 gotoIdleState(); 1271 notifyChange(PLAYBACK_COMPLETE); 1272 mIsSupposedToBePlaying = false; 1273 return; 1274 } else if (mRepeatMode == REPEAT_ALL || force) { 1275 mPlayPos = 0; 1276 } 1277 } else { 1278 mPlayPos++; 1279 } 1280 } 1281 saveBookmarkIfNeeded(); 1282 stop(false); 1283 openCurrent(); 1284 play(); 1285 notifyChange(META_CHANGED); 1286 } 1287 } 1288 1289 private void gotoIdleState() { 1290 mDelayedStopHandler.removeCallbacksAndMessages(null); 1291 Message msg = mDelayedStopHandler.obtainMessage(); 1292 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 1293 stopForeground(true); 1294 } 1295 1296 private void saveBookmarkIfNeeded() { 1297 try { 1298 if (isPodcast()) { 1299 long pos = position(); 1300 long bookmark = getBookmark(); 1301 long duration = duration(); 1302 if ((pos < bookmark && (pos + 10000) > bookmark) || 1303 (pos > bookmark && (pos - 10000) < bookmark)) { 1304 // The existing bookmark is close to the current 1305 // position, so don't update it. 1306 return; 1307 } 1308 if (pos < 15000 || (pos + 10000) > duration) { 1309 // if we're near the start or end, clear the bookmark 1310 pos = 0; 1311 } 1312 1313 // write 'pos' to the bookmark field 1314 ContentValues values = new ContentValues(); 1315 values.put(MediaStore.Audio.Media.BOOKMARK, pos); 1316 Uri uri = ContentUris.withAppendedId( 1317 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX)); 1318 getContentResolver().update(uri, values, null, null); 1319 } 1320 } catch (SQLiteException ex) { 1321 } 1322 } 1323 1324 // Make sure there are at least 5 items after the currently playing item 1325 // and no more than 10 items before. 1326 private void doAutoShuffleUpdate() { 1327 boolean notify = false; 1328 // remove old entries 1329 if (mPlayPos > 10) { 1330 removeTracks(0, mPlayPos - 9); 1331 notify = true; 1332 } 1333 // add new entries if needed 1334 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos)); 1335 for (int i = 0; i < to_add; i++) { 1336 // pick something at random from the list 1337 int idx = mRand.nextInt(mAutoShuffleList.length); 1338 long which = mAutoShuffleList[idx]; 1339 ensurePlayListCapacity(mPlayListLen + 1); 1340 mPlayList[mPlayListLen++] = which; 1341 notify = true; 1342 } 1343 if (notify) { 1344 notifyChange(QUEUE_CHANGED); 1345 } 1346 } 1347 1348 // A simple variation of Random that makes sure that the 1349 // value it returns is not equal to the value it returned 1350 // previously, unless the interval is 1. 1351 private static class Shuffler { 1352 private int mPrevious; 1353 private Random mRandom = new Random(); 1354 public int nextInt(int interval) { 1355 int ret; 1356 do { 1357 ret = mRandom.nextInt(interval); 1358 } while (ret == mPrevious && interval > 1); 1359 mPrevious = ret; 1360 return ret; 1361 } 1362 }; 1363 1364 private boolean makeAutoShuffleList() { 1365 ContentResolver res = getContentResolver(); 1366 Cursor c = null; 1367 try { 1368 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1369 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1", 1370 null, null); 1371 if (c == null || c.getCount() == 0) { 1372 return false; 1373 } 1374 int len = c.getCount(); 1375 long [] list = new long[len]; 1376 for (int i = 0; i < len; i++) { 1377 c.moveToNext(); 1378 list[i] = c.getLong(0); 1379 } 1380 mAutoShuffleList = list; 1381 return true; 1382 } catch (RuntimeException ex) { 1383 } finally { 1384 if (c != null) { 1385 c.close(); 1386 } 1387 } 1388 return false; 1389 } 1390 1391 /** 1392 * Removes the range of tracks specified from the play list. If a file within the range is 1393 * the file currently being played, playback will move to the next file after the 1394 * range. 1395 * @param first The first file to be removed 1396 * @param last The last file to be removed 1397 * @return the number of tracks deleted 1398 */ 1399 public int removeTracks(int first, int last) { 1400 int numremoved = removeTracksInternal(first, last); 1401 if (numremoved > 0) { 1402 notifyChange(QUEUE_CHANGED); 1403 } 1404 return numremoved; 1405 } 1406 1407 private int removeTracksInternal(int first, int last) { 1408 synchronized (this) { 1409 if (last < first) return 0; 1410 if (first < 0) first = 0; 1411 if (last >= mPlayListLen) last = mPlayListLen - 1; 1412 1413 boolean gotonext = false; 1414 if (first <= mPlayPos && mPlayPos <= last) { 1415 mPlayPos = first; 1416 gotonext = true; 1417 } else if (mPlayPos > last) { 1418 mPlayPos -= (last - first + 1); 1419 } 1420 int num = mPlayListLen - last - 1; 1421 for (int i = 0; i < num; i++) { 1422 mPlayList[first + i] = mPlayList[last + 1 + i]; 1423 } 1424 mPlayListLen -= last - first + 1; 1425 1426 if (gotonext) { 1427 if (mPlayListLen == 0) { 1428 stop(true); 1429 mPlayPos = -1; 1430 } else { 1431 if (mPlayPos >= mPlayListLen) { 1432 mPlayPos = 0; 1433 } 1434 boolean wasPlaying = isPlaying(); 1435 stop(false); 1436 openCurrent(); 1437 if (wasPlaying) { 1438 play(); 1439 } 1440 } 1441 } 1442 return last - first + 1; 1443 } 1444 } 1445 1446 /** 1447 * Removes all instances of the track with the given id 1448 * from the playlist. 1449 * @param id The id to be removed 1450 * @return how many instances of the track were removed 1451 */ 1452 public int removeTrack(long id) { 1453 int numremoved = 0; 1454 synchronized (this) { 1455 for (int i = 0; i < mPlayListLen; i++) { 1456 if (mPlayList[i] == id) { 1457 numremoved += removeTracksInternal(i, i); 1458 i--; 1459 } 1460 } 1461 } 1462 if (numremoved > 0) { 1463 notifyChange(QUEUE_CHANGED); 1464 } 1465 return numremoved; 1466 } 1467 1468 public void setShuffleMode(int shufflemode) { 1469 synchronized(this) { 1470 if (mShuffleMode == shufflemode && mPlayListLen > 0) { 1471 return; 1472 } 1473 mShuffleMode = shufflemode; 1474 if (mShuffleMode == SHUFFLE_AUTO) { 1475 if (makeAutoShuffleList()) { 1476 mPlayListLen = 0; 1477 doAutoShuffleUpdate(); 1478 mPlayPos = 0; 1479 openCurrent(); 1480 play(); 1481 notifyChange(META_CHANGED); 1482 return; 1483 } else { 1484 // failed to build a list of files to shuffle 1485 mShuffleMode = SHUFFLE_NONE; 1486 } 1487 } 1488 saveQueue(false); 1489 } 1490 } 1491 public int getShuffleMode() { 1492 return mShuffleMode; 1493 } 1494 1495 public void setRepeatMode(int repeatmode) { 1496 synchronized(this) { 1497 mRepeatMode = repeatmode; 1498 saveQueue(false); 1499 } 1500 } 1501 public int getRepeatMode() { 1502 return mRepeatMode; 1503 } 1504 1505 public int getMediaMountedCount() { 1506 return mMediaMountedCount; 1507 } 1508 1509 /** 1510 * Returns the path of the currently playing file, or null if 1511 * no file is currently playing. 1512 */ 1513 public String getPath() { 1514 return mFileToPlay; 1515 } 1516 1517 /** 1518 * Returns the rowid of the currently playing file, or -1 if 1519 * no file is currently playing. 1520 */ 1521 public long getAudioId() { 1522 synchronized (this) { 1523 if (mPlayPos >= 0 && mPlayer.isInitialized()) { 1524 return mPlayList[mPlayPos]; 1525 } 1526 } 1527 return -1; 1528 } 1529 1530 /** 1531 * Returns the position in the queue 1532 * @return the position in the queue 1533 */ 1534 public int getQueuePosition() { 1535 synchronized(this) { 1536 return mPlayPos; 1537 } 1538 } 1539 1540 /** 1541 * Starts playing the track at the given position in the queue. 1542 * @param pos The position in the queue of the track that will be played. 1543 */ 1544 public void setQueuePosition(int pos) { 1545 synchronized(this) { 1546 stop(false); 1547 mPlayPos = pos; 1548 openCurrent(); 1549 play(); 1550 notifyChange(META_CHANGED); 1551 } 1552 } 1553 1554 public String getArtistName() { 1555 synchronized(this) { 1556 if (mCursor == null) { 1557 return null; 1558 } 1559 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); 1560 } 1561 } 1562 1563 public long getArtistId() { 1564 synchronized (this) { 1565 if (mCursor == null) { 1566 return -1; 1567 } 1568 return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID)); 1569 } 1570 } 1571 1572 public String getAlbumName() { 1573 synchronized (this) { 1574 if (mCursor == null) { 1575 return null; 1576 } 1577 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)); 1578 } 1579 } 1580 1581 public long getAlbumId() { 1582 synchronized (this) { 1583 if (mCursor == null) { 1584 return -1; 1585 } 1586 return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)); 1587 } 1588 } 1589 1590 public String getTrackName() { 1591 synchronized (this) { 1592 if (mCursor == null) { 1593 return null; 1594 } 1595 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); 1596 } 1597 } 1598 1599 private boolean isPodcast() { 1600 synchronized (this) { 1601 if (mCursor == null) { 1602 return false; 1603 } 1604 return (mCursor.getInt(PODCASTCOLIDX) > 0); 1605 } 1606 } 1607 1608 private long getBookmark() { 1609 synchronized (this) { 1610 if (mCursor == null) { 1611 return 0; 1612 } 1613 return mCursor.getLong(BOOKMARKCOLIDX); 1614 } 1615 } 1616 1617 /** 1618 * Returns the duration of the file in milliseconds. 1619 * Currently this method returns -1 for the duration of MIDI files. 1620 */ 1621 public long duration() { 1622 if (mPlayer.isInitialized()) { 1623 return mPlayer.duration(); 1624 } 1625 return -1; 1626 } 1627 1628 /** 1629 * Returns the current playback position in milliseconds 1630 */ 1631 public long position() { 1632 if (mPlayer.isInitialized()) { 1633 return mPlayer.position(); 1634 } 1635 return -1; 1636 } 1637 1638 /** 1639 * Seeks to the position specified. 1640 * 1641 * @param pos The position to seek to, in milliseconds 1642 */ 1643 public long seek(long pos) { 1644 if (mPlayer.isInitialized()) { 1645 if (pos < 0) pos = 0; 1646 if (pos > mPlayer.duration()) pos = mPlayer.duration(); 1647 return mPlayer.seek(pos); 1648 } 1649 return -1; 1650 } 1651 1652 /** 1653 * Provides a unified interface for dealing with midi files and 1654 * other media files. 1655 */ 1656 private class MultiPlayer { 1657 private MediaPlayer mMediaPlayer = new MediaPlayer(); 1658 private Handler mHandler; 1659 private boolean mIsInitialized = false; 1660 1661 public MultiPlayer() { 1662 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1663 } 1664 1665 public void setDataSourceAsync(String path) { 1666 try { 1667 mMediaPlayer.reset(); 1668 mMediaPlayer.setDataSource(path); 1669 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1670 mMediaPlayer.setOnPreparedListener(preparedlistener); 1671 mMediaPlayer.prepareAsync(); 1672 } catch (IOException ex) { 1673 // TODO: notify the user why the file couldn't be opened 1674 mIsInitialized = false; 1675 return; 1676 } catch (IllegalArgumentException ex) { 1677 // TODO: notify the user why the file couldn't be opened 1678 mIsInitialized = false; 1679 return; 1680 } 1681 mMediaPlayer.setOnCompletionListener(listener); 1682 mMediaPlayer.setOnErrorListener(errorListener); 1683 1684 mIsInitialized = true; 1685 } 1686 1687 public void setDataSource(String path) { 1688 try { 1689 mMediaPlayer.reset(); 1690 mMediaPlayer.setOnPreparedListener(null); 1691 if (path.startsWith("content://")) { 1692 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path)); 1693 } else { 1694 mMediaPlayer.setDataSource(path); 1695 } 1696 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1697 mMediaPlayer.prepare(); 1698 } catch (IOException ex) { 1699 // TODO: notify the user why the file couldn't be opened 1700 mIsInitialized = false; 1701 return; 1702 } catch (IllegalArgumentException ex) { 1703 // TODO: notify the user why the file couldn't be opened 1704 mIsInitialized = false; 1705 return; 1706 } 1707 mMediaPlayer.setOnCompletionListener(listener); 1708 mMediaPlayer.setOnErrorListener(errorListener); 1709 1710 mIsInitialized = true; 1711 } 1712 1713 public boolean isInitialized() { 1714 return mIsInitialized; 1715 } 1716 1717 public void start() { 1718 mMediaPlayer.start(); 1719 } 1720 1721 public void stop() { 1722 mMediaPlayer.reset(); 1723 mIsInitialized = false; 1724 } 1725 1726 /** 1727 * You CANNOT use this player anymore after calling release() 1728 */ 1729 public void release() { 1730 stop(); 1731 mMediaPlayer.release(); 1732 } 1733 1734 public void pause() { 1735 mMediaPlayer.pause(); 1736 } 1737 1738 public void setHandler(Handler handler) { 1739 mHandler = handler; 1740 } 1741 1742 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() { 1743 public void onCompletion(MediaPlayer mp) { 1744 // Acquire a temporary wakelock, since when we return from 1745 // this callback the MediaPlayer will release its wakelock 1746 // and allow the device to go to sleep. 1747 // This temporary wakelock is released when the RELEASE_WAKELOCK 1748 // message is processed, but just in case, put a timeout on it. 1749 mWakeLock.acquire(30000); 1750 mHandler.sendEmptyMessage(TRACK_ENDED); 1751 mHandler.sendEmptyMessage(RELEASE_WAKELOCK); 1752 } 1753 }; 1754 1755 MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() { 1756 public void onPrepared(MediaPlayer mp) { 1757 notifyChange(ASYNC_OPEN_COMPLETE); 1758 } 1759 }; 1760 1761 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() { 1762 public boolean onError(MediaPlayer mp, int what, int extra) { 1763 switch (what) { 1764 case MediaPlayer.MEDIA_ERROR_SERVER_DIED: 1765 mIsInitialized = false; 1766 mMediaPlayer.release(); 1767 // Creating a new MediaPlayer and settings its wakemode does not 1768 // require the media service, so it's OK to do this now, while the 1769 // service is still being restarted 1770 mMediaPlayer = new MediaPlayer(); 1771 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1772 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000); 1773 return true; 1774 default: 1775 Log.d("MultiPlayer", "Error: " + what + "," + extra); 1776 break; 1777 } 1778 return false; 1779 } 1780 }; 1781 1782 public long duration() { 1783 return mMediaPlayer.getDuration(); 1784 } 1785 1786 public long position() { 1787 return mMediaPlayer.getCurrentPosition(); 1788 } 1789 1790 public long seek(long whereto) { 1791 mMediaPlayer.seekTo((int) whereto); 1792 return whereto; 1793 } 1794 1795 public void setVolume(float vol) { 1796 mMediaPlayer.setVolume(vol, vol); 1797 } 1798 } 1799 1800 /* 1801 * By making this a static class with a WeakReference to the Service, we 1802 * ensure that the Service can be GCd even when the system process still 1803 * has a remote reference to the stub. 1804 */ 1805 static class ServiceStub extends IMediaPlaybackService.Stub { 1806 WeakReference<MediaPlaybackService> mService; 1807 1808 ServiceStub(MediaPlaybackService service) { 1809 mService = new WeakReference<MediaPlaybackService>(service); 1810 } 1811 1812 public void openFileAsync(String path) 1813 { 1814 mService.get().openAsync(path); 1815 } 1816 public void openFile(String path, boolean oneShot) 1817 { 1818 mService.get().open(path, oneShot); 1819 } 1820 public void open(long [] list, int position) { 1821 mService.get().open(list, position); 1822 } 1823 public int getQueuePosition() { 1824 return mService.get().getQueuePosition(); 1825 } 1826 public void setQueuePosition(int index) { 1827 mService.get().setQueuePosition(index); 1828 } 1829 public boolean isPlaying() { 1830 return mService.get().isPlaying(); 1831 } 1832 public void stop() { 1833 mService.get().stop(); 1834 } 1835 public void pause() { 1836 mService.get().pause(); 1837 } 1838 public void play() { 1839 mService.get().play(); 1840 } 1841 public void prev() { 1842 mService.get().prev(); 1843 } 1844 public void next() { 1845 mService.get().next(true); 1846 } 1847 public String getTrackName() { 1848 return mService.get().getTrackName(); 1849 } 1850 public String getAlbumName() { 1851 return mService.get().getAlbumName(); 1852 } 1853 public long getAlbumId() { 1854 return mService.get().getAlbumId(); 1855 } 1856 public String getArtistName() { 1857 return mService.get().getArtistName(); 1858 } 1859 public long getArtistId() { 1860 return mService.get().getArtistId(); 1861 } 1862 public void enqueue(long [] list , int action) { 1863 mService.get().enqueue(list, action); 1864 } 1865 public long [] getQueue() { 1866 return mService.get().getQueue(); 1867 } 1868 public void moveQueueItem(int from, int to) { 1869 mService.get().moveQueueItem(from, to); 1870 } 1871 public String getPath() { 1872 return mService.get().getPath(); 1873 } 1874 public long getAudioId() { 1875 return mService.get().getAudioId(); 1876 } 1877 public long position() { 1878 return mService.get().position(); 1879 } 1880 public long duration() { 1881 return mService.get().duration(); 1882 } 1883 public long seek(long pos) { 1884 return mService.get().seek(pos); 1885 } 1886 public void setShuffleMode(int shufflemode) { 1887 mService.get().setShuffleMode(shufflemode); 1888 } 1889 public int getShuffleMode() { 1890 return mService.get().getShuffleMode(); 1891 } 1892 public int removeTracks(int first, int last) { 1893 return mService.get().removeTracks(first, last); 1894 } 1895 public int removeTrack(long id) { 1896 return mService.get().removeTrack(id); 1897 } 1898 public void setRepeatMode(int repeatmode) { 1899 mService.get().setRepeatMode(repeatmode); 1900 } 1901 public int getRepeatMode() { 1902 return mService.get().getRepeatMode(); 1903 } 1904 public int getMediaMountedCount() { 1905 return mService.get().getMediaMountedCount(); 1906 } 1907 1908 } 1909 1910 private final IBinder mBinder = new ServiceStub(this); 1911} 1912