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