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