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