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