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