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