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