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