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