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