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