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