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