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