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