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