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 17 package com.android.music; 18 19 import android.app.Notification; 20 import android.app.PendingIntent; 21 import android.app.Service; 22 import android.appwidget.AppWidgetManager; 23 import android.content.ComponentName; 24 import android.content.ContentResolver; 25 import android.content.ContentUris; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.BroadcastReceiver; 31 import android.content.SharedPreferences; 32 import android.content.SharedPreferences.Editor; 33 import android.database.Cursor; 34 import android.database.sqlite.SQLiteException; 35 import android.media.audiofx.AudioEffect; 36 import android.media.AudioManager; 37 import android.media.AudioManager.OnAudioFocusChangeListener; 38 import android.media.MediaPlayer; 39 import android.net.Uri; 40 import android.os.Handler; 41 import android.os.IBinder; 42 import android.os.Message; 43 import android.os.PowerManager; 44 import android.os.SystemClock; 45 import android.os.PowerManager.WakeLock; 46 import android.provider.MediaStore; 47 import android.util.Log; 48 import android.widget.RemoteViews; 49 import android.widget.Toast; 50 51 import java.io.FileDescriptor; 52 import java.io.IOException; 53 import java.io.PrintWriter; 54 import java.lang.ref.WeakReference; 55 import java.util.Random; 56 import java.util.Vector; 57 58 /** 59 * Provides "background" audio playback capabilities, allowing the 60 * user to switch between activities without stopping playback. 61 */ 62 public 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 mPlayer.pause(); 1127 gotoIdleState(); 1128 mIsSupposedToBePlaying = false; 1129 notifyChange(PLAYSTATE_CHANGED); 1130 saveBookmarkIfNeeded(); 1131 } 1132 } 1133 } 1134 1135 /** Returns whether something is currently playing 1136 * 1137 * @return true if something is playing (or will be playing shortly, in case 1138 * we're currently transitioning between tracks), false if not. 1139 */ 1140 public boolean isPlaying() { 1141 return mIsSupposedToBePlaying; 1142 } 1143 1144 /* 1145 Desired behavior for prev/next/shuffle: 1146 1147 - NEXT will move to the next track in the list when not shuffling, and to 1148 a track randomly picked from the not-yet-played tracks when shuffling. 1149 If all tracks have already been played, pick from the full set, but 1150 avoid picking the previously played track if possible. 1151 - when shuffling, PREV will go to the previously played track. Hitting PREV 1152 again will go to the track played before that, etc. When the start of the 1153 history has been reached, PREV is a no-op. 1154 When not shuffling, PREV will go to the sequentially previous track (the 1155 difference with the shuffle-case is mainly that when not shuffling, the 1156 user can back up to tracks that are not in the history). 1157 1158 Example: 1159 When playing an album with 10 tracks from the start, and enabling shuffle 1160 while playing track 5, the remaining tracks (6-10) will be shuffled, e.g. 1161 the final play order might be 1-2-3-4-5-8-10-6-9-7. 1162 When hitting 'prev' 8 times while playing track 7 in this example, the 1163 user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next', 1164 a random track will be picked again. If at any time user disables shuffling 1165 the next/previous track will be picked in sequential order again. 1166 */ 1167 1168 public void prev() { 1169 synchronized (this) { 1170 if (mShuffleMode == SHUFFLE_NORMAL) { 1171 // go to previously-played track and remove it from the history 1172 int histsize = mHistory.size(); 1173 if (histsize == 0) { 1174 // prev is a no-op 1175 return; 1176 } 1177 Integer pos = mHistory.remove(histsize - 1); 1178 mPlayPos = pos.intValue(); 1179 } else { 1180 if (mPlayPos > 0) { 1181 mPlayPos--; 1182 } else { 1183 mPlayPos = mPlayListLen - 1; 1184 } 1185 } 1186 saveBookmarkIfNeeded(); 1187 stop(false); 1188 openCurrent(); 1189 play(); 1190 notifyChange(META_CHANGED); 1191 } 1192 } 1193 1194 public void next(boolean force) { 1195 synchronized (this) { 1196 if (mPlayListLen <= 0) { 1197 Log.d(LOGTAG, "No play queue"); 1198 return; 1199 } 1200 1201 if (mShuffleMode == SHUFFLE_NORMAL) { 1202 // Pick random next track from the not-yet-played ones 1203 // TODO: make it work right after adding/removing items in the queue. 1204 1205 // Store the current file in the history, but keep the history at a 1206 // reasonable size 1207 if (mPlayPos >= 0) { 1208 mHistory.add(mPlayPos); 1209 } 1210 if (mHistory.size() > MAX_HISTORY_SIZE) { 1211 mHistory.removeElementAt(0); 1212 } 1213 1214 int numTracks = mPlayListLen; 1215 int[] tracks = new int[numTracks]; 1216 for (int i=0;i < numTracks; i++) { 1217 tracks[i] = i; 1218 } 1219 1220 int numHistory = mHistory.size(); 1221 int numUnplayed = numTracks; 1222 for (int i=0;i < numHistory; i++) { 1223 int idx = mHistory.get(i).intValue(); 1224 if (idx < numTracks && tracks[idx] >= 0) { 1225 numUnplayed--; 1226 tracks[idx] = -1; 1227 } 1228 } 1229 1230 // 'numUnplayed' now indicates how many tracks have not yet 1231 // been played, and 'tracks' contains the indices of those 1232 // tracks. 1233 if (numUnplayed <=0) { 1234 // everything's already been played 1235 if (mRepeatMode == REPEAT_ALL || force) { 1236 //pick from full set 1237 numUnplayed = numTracks; 1238 for (int i=0;i < numTracks; i++) { 1239 tracks[i] = i; 1240 } 1241 } else { 1242 // all done 1243 gotoIdleState(); 1244 if (mIsSupposedToBePlaying) { 1245 mIsSupposedToBePlaying = false; 1246 notifyChange(PLAYSTATE_CHANGED); 1247 } 1248 return; 1249 } 1250 } 1251 int skip = mRand.nextInt(numUnplayed); 1252 int cnt = -1; 1253 while (true) { 1254 while (tracks[++cnt] < 0) 1255 ; 1256 skip--; 1257 if (skip < 0) { 1258 break; 1259 } 1260 } 1261 mPlayPos = cnt; 1262 } else if (mShuffleMode == SHUFFLE_AUTO) { 1263 doAutoShuffleUpdate(); 1264 mPlayPos++; 1265 } else { 1266 if (mPlayPos >= mPlayListLen - 1) { 1267 // we're at the end of the list 1268 if (mRepeatMode == REPEAT_NONE && !force) { 1269 // all done 1270 gotoIdleState(); 1271 mIsSupposedToBePlaying = false; 1272 notifyChange(PLAYSTATE_CHANGED); 1273 return; 1274 } else if (mRepeatMode == REPEAT_ALL || force) { 1275 mPlayPos = 0; 1276 } 1277 } else { 1278 mPlayPos++; 1279 } 1280 } 1281 saveBookmarkIfNeeded(); 1282 stop(false); 1283 openCurrent(); 1284 play(); 1285 notifyChange(META_CHANGED); 1286 } 1287 } 1288 1289 private void gotoIdleState() { 1290 mDelayedStopHandler.removeCallbacksAndMessages(null); 1291 Message msg = mDelayedStopHandler.obtainMessage(); 1292 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 1293 stopForeground(true); 1294 } 1295 1296 private void saveBookmarkIfNeeded() { 1297 try { 1298 if (isPodcast()) { 1299 long pos = position(); 1300 long bookmark = getBookmark(); 1301 long duration = duration(); 1302 if ((pos < bookmark && (pos + 10000) > bookmark) || 1303 (pos > bookmark && (pos - 10000) < bookmark)) { 1304 // The existing bookmark is close to the current 1305 // position, so don't update it. 1306 return; 1307 } 1308 if (pos < 15000 || (pos + 10000) > duration) { 1309 // if we're near the start or end, clear the bookmark 1310 pos = 0; 1311 } 1312 1313 // write 'pos' to the bookmark field 1314 ContentValues values = new ContentValues(); 1315 values.put(MediaStore.Audio.Media.BOOKMARK, pos); 1316 Uri uri = ContentUris.withAppendedId( 1317 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX)); 1318 getContentResolver().update(uri, values, null, null); 1319 } 1320 } catch (SQLiteException ex) { 1321 } 1322 } 1323 1324 // Make sure there are at least 5 items after the currently playing item 1325 // and no more than 10 items before. 1326 private void doAutoShuffleUpdate() { 1327 boolean notify = false; 1328 1329 // remove old entries 1330 if (mPlayPos > 10) { 1331 removeTracks(0, mPlayPos - 9); 1332 notify = true; 1333 } 1334 // add new entries if needed 1335 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos)); 1336 for (int i = 0; i < to_add; i++) { 1337 // pick something at random from the list 1338 1339 int lookback = mHistory.size(); 1340 int idx = -1; 1341 while(true) { 1342 idx = mRand.nextInt(mAutoShuffleList.length); 1343 if (!wasRecentlyUsed(idx, lookback)) { 1344 break; 1345 } 1346 lookback /= 2; 1347 } 1348 mHistory.add(idx); 1349 if (mHistory.size() > MAX_HISTORY_SIZE) { 1350 mHistory.remove(0); 1351 } 1352 ensurePlayListCapacity(mPlayListLen + 1); 1353 mPlayList[mPlayListLen++] = mAutoShuffleList[idx]; 1354 notify = true; 1355 } 1356 if (notify) { 1357 notifyChange(QUEUE_CHANGED); 1358 } 1359 } 1360 1361 // check that the specified idx is not in the history (but only look at at 1362 // most lookbacksize entries in the history) 1363 private boolean wasRecentlyUsed(int idx, int lookbacksize) { 1364 1365 // early exit to prevent infinite loops in case idx == mPlayPos 1366 if (lookbacksize == 0) { 1367 return false; 1368 } 1369 1370 int histsize = mHistory.size(); 1371 if (histsize < lookbacksize) { 1372 Log.d(LOGTAG, "lookback too big"); 1373 lookbacksize = histsize; 1374 } 1375 int maxidx = histsize - 1; 1376 for (int i = 0; i < lookbacksize; i++) { 1377 long entry = mHistory.get(maxidx - i); 1378 if (entry == idx) { 1379 return true; 1380 } 1381 } 1382 return false; 1383 } 1384 1385 // A simple variation of Random that makes sure that the 1386 // value it returns is not equal to the value it returned 1387 // previously, unless the interval is 1. 1388 private static class Shuffler { 1389 private int mPrevious; 1390 private Random mRandom = new Random(); 1391 public int nextInt(int interval) { 1392 int ret; 1393 do { 1394 ret = mRandom.nextInt(interval); 1395 } while (ret == mPrevious && interval > 1); 1396 mPrevious = ret; 1397 return ret; 1398 } 1399 }; 1400 1401 private boolean makeAutoShuffleList() { 1402 ContentResolver res = getContentResolver(); 1403 Cursor c = null; 1404 try { 1405 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1406 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1", 1407 null, null); 1408 if (c == null || c.getCount() == 0) { 1409 return false; 1410 } 1411 int len = c.getCount(); 1412 long [] list = new long[len]; 1413 for (int i = 0; i < len; i++) { 1414 c.moveToNext(); 1415 list[i] = c.getLong(0); 1416 } 1417 mAutoShuffleList = list; 1418 return true; 1419 } catch (RuntimeException ex) { 1420 } finally { 1421 if (c != null) { 1422 c.close(); 1423 } 1424 } 1425 return false; 1426 } 1427 1428 /** 1429 * Removes the range of tracks specified from the play list. If a file within the range is 1430 * the file currently being played, playback will move to the next file after the 1431 * range. 1432 * @param first The first file to be removed 1433 * @param last The last file to be removed 1434 * @return the number of tracks deleted 1435 */ 1436 public int removeTracks(int first, int last) { 1437 int numremoved = removeTracksInternal(first, last); 1438 if (numremoved > 0) { 1439 notifyChange(QUEUE_CHANGED); 1440 } 1441 return numremoved; 1442 } 1443 1444 private int removeTracksInternal(int first, int last) { 1445 synchronized (this) { 1446 if (last < first) return 0; 1447 if (first < 0) first = 0; 1448 if (last >= mPlayListLen) last = mPlayListLen - 1; 1449 1450 boolean gotonext = false; 1451 if (first <= mPlayPos && mPlayPos <= last) { 1452 mPlayPos = first; 1453 gotonext = true; 1454 } else if (mPlayPos > last) { 1455 mPlayPos -= (last - first + 1); 1456 } 1457 int num = mPlayListLen - last - 1; 1458 for (int i = 0; i < num; i++) { 1459 mPlayList[first + i] = mPlayList[last + 1 + i]; 1460 } 1461 mPlayListLen -= last - first + 1; 1462 1463 if (gotonext) { 1464 if (mPlayListLen == 0) { 1465 stop(true); 1466 mPlayPos = -1; 1467 if (mCursor != null) { 1468 mCursor.close(); 1469 mCursor = null; 1470 } 1471 } else { 1472 if (mPlayPos >= mPlayListLen) { 1473 mPlayPos = 0; 1474 } 1475 boolean wasPlaying = isPlaying(); 1476 stop(false); 1477 openCurrent(); 1478 if (wasPlaying) { 1479 play(); 1480 } 1481 } 1482 notifyChange(META_CHANGED); 1483 } 1484 return last - first + 1; 1485 } 1486 } 1487 1488 /** 1489 * Removes all instances of the track with the given id 1490 * from the playlist. 1491 * @param id The id to be removed 1492 * @return how many instances of the track were removed 1493 */ 1494 public int removeTrack(long id) { 1495 int numremoved = 0; 1496 synchronized (this) { 1497 for (int i = 0; i < mPlayListLen; i++) { 1498 if (mPlayList[i] == id) { 1499 numremoved += removeTracksInternal(i, i); 1500 i--; 1501 } 1502 } 1503 } 1504 if (numremoved > 0) { 1505 notifyChange(QUEUE_CHANGED); 1506 } 1507 return numremoved; 1508 } 1509 1510 public void setShuffleMode(int shufflemode) { 1511 synchronized(this) { 1512 if (mShuffleMode == shufflemode && mPlayListLen > 0) { 1513 return; 1514 } 1515 mShuffleMode = shufflemode; 1516 if (mShuffleMode == SHUFFLE_AUTO) { 1517 if (makeAutoShuffleList()) { 1518 mPlayListLen = 0; 1519 doAutoShuffleUpdate(); 1520 mPlayPos = 0; 1521 openCurrent(); 1522 play(); 1523 notifyChange(META_CHANGED); 1524 return; 1525 } else { 1526 // failed to build a list of files to shuffle 1527 mShuffleMode = SHUFFLE_NONE; 1528 } 1529 } 1530 saveQueue(false); 1531 } 1532 } 1533 public int getShuffleMode() { 1534 return mShuffleMode; 1535 } 1536 1537 public void setRepeatMode(int repeatmode) { 1538 synchronized(this) { 1539 mRepeatMode = repeatmode; 1540 saveQueue(false); 1541 } 1542 } 1543 public int getRepeatMode() { 1544 return mRepeatMode; 1545 } 1546 1547 public int getMediaMountedCount() { 1548 return mMediaMountedCount; 1549 } 1550 1551 /** 1552 * Returns the path of the currently playing file, or null if 1553 * no file is currently playing. 1554 */ 1555 public String getPath() { 1556 return mFileToPlay; 1557 } 1558 1559 /** 1560 * Returns the rowid of the currently playing file, or -1 if 1561 * no file is currently playing. 1562 */ 1563 public long getAudioId() { 1564 synchronized (this) { 1565 if (mPlayPos >= 0 && mPlayer.isInitialized()) { 1566 return mPlayList[mPlayPos]; 1567 } 1568 } 1569 return -1; 1570 } 1571 1572 /** 1573 * Returns the position in the queue 1574 * @return the position in the queue 1575 */ 1576 public int getQueuePosition() { 1577 synchronized(this) { 1578 return mPlayPos; 1579 } 1580 } 1581 1582 /** 1583 * Starts playing the track at the given position in the queue. 1584 * @param pos The position in the queue of the track that will be played. 1585 */ 1586 public void setQueuePosition(int pos) { 1587 synchronized(this) { 1588 stop(false); 1589 mPlayPos = pos; 1590 openCurrent(); 1591 play(); 1592 notifyChange(META_CHANGED); 1593 if (mShuffleMode == SHUFFLE_AUTO) { 1594 doAutoShuffleUpdate(); 1595 } 1596 } 1597 } 1598 1599 public String getArtistName() { 1600 synchronized(this) { 1601 if (mCursor == null) { 1602 return null; 1603 } 1604 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); 1605 } 1606 } 1607 1608 public long getArtistId() { 1609 synchronized (this) { 1610 if (mCursor == null) { 1611 return -1; 1612 } 1613 return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID)); 1614 } 1615 } 1616 1617 public String getAlbumName() { 1618 synchronized (this) { 1619 if (mCursor == null) { 1620 return null; 1621 } 1622 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)); 1623 } 1624 } 1625 1626 public long getAlbumId() { 1627 synchronized (this) { 1628 if (mCursor == null) { 1629 return -1; 1630 } 1631 return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)); 1632 } 1633 } 1634 1635 public String getTrackName() { 1636 synchronized (this) { 1637 if (mCursor == null) { 1638 return null; 1639 } 1640 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); 1641 } 1642 } 1643 1644 private boolean isPodcast() { 1645 synchronized (this) { 1646 if (mCursor == null) { 1647 return false; 1648 } 1649 return (mCursor.getInt(PODCASTCOLIDX) > 0); 1650 } 1651 } 1652 1653 private long getBookmark() { 1654 synchronized (this) { 1655 if (mCursor == null) { 1656 return 0; 1657 } 1658 return mCursor.getLong(BOOKMARKCOLIDX); 1659 } 1660 } 1661 1662 /** 1663 * Returns the duration of the file in milliseconds. 1664 * Currently this method returns -1 for the duration of MIDI files. 1665 */ 1666 public long duration() { 1667 if (mPlayer.isInitialized()) { 1668 return mPlayer.duration(); 1669 } 1670 return -1; 1671 } 1672 1673 /** 1674 * Returns the current playback position in milliseconds 1675 */ 1676 public long position() { 1677 if (mPlayer.isInitialized()) { 1678 return mPlayer.position(); 1679 } 1680 return -1; 1681 } 1682 1683 /** 1684 * Seeks to the position specified. 1685 * 1686 * @param pos The position to seek to, in milliseconds 1687 */ 1688 public long seek(long pos) { 1689 if (mPlayer.isInitialized()) { 1690 if (pos < 0) pos = 0; 1691 if (pos > mPlayer.duration()) pos = mPlayer.duration(); 1692 return mPlayer.seek(pos); 1693 } 1694 return -1; 1695 } 1696 1697 /** 1698 * Sets the audio session ID. 1699 * 1700 * @param sessionId: the audio session ID. 1701 */ 1702 public void setAudioSessionId(int sessionId) { 1703 synchronized (this) { 1704 mPlayer.setAudioSessionId(sessionId); 1705 } 1706 } 1707 1708 /** 1709 * Returns the audio session ID. 1710 */ 1711 public int getAudioSessionId() { 1712 synchronized (this) { 1713 return mPlayer.getAudioSessionId(); 1714 } 1715 } 1716 1717 /** 1718 * Provides a unified interface for dealing with midi files and 1719 * other media files. 1720 */ 1721 private class MultiPlayer { 1722 private MediaPlayer mMediaPlayer = new MediaPlayer(); 1723 private Handler mHandler; 1724 private boolean mIsInitialized = false; 1725 1726 public MultiPlayer() { 1727 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1728 } 1729 1730 public void setDataSource(String path) { 1731 try { 1732 mMediaPlayer.reset(); 1733 mMediaPlayer.setOnPreparedListener(null); 1734 if (path.startsWith("content://")) { 1735 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path)); 1736 } else { 1737 mMediaPlayer.setDataSource(path); 1738 } 1739 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1740 mMediaPlayer.prepare(); 1741 } catch (IOException ex) { 1742 // TODO: notify the user why the file couldn't be opened 1743 mIsInitialized = false; 1744 return; 1745 } catch (IllegalArgumentException ex) { 1746 // TODO: notify the user why the file couldn't be opened 1747 mIsInitialized = false; 1748 return; 1749 } 1750 mMediaPlayer.setOnCompletionListener(listener); 1751 mMediaPlayer.setOnErrorListener(errorListener); 1752 Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); 1753 i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); 1754 i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); 1755 sendBroadcast(i); 1756 mIsInitialized = true; 1757 } 1758 1759 public boolean isInitialized() { 1760 return mIsInitialized; 1761 } 1762 1763 public void start() { 1764 MusicUtils.debugLog(new Exception("MultiPlayer.start called")); 1765 mMediaPlayer.start(); 1766 } 1767 1768 public void stop() { 1769 mMediaPlayer.reset(); 1770 mIsInitialized = false; 1771 } 1772 1773 /** 1774 * You CANNOT use this player anymore after calling release() 1775 */ 1776 public void release() { 1777 stop(); 1778 mMediaPlayer.release(); 1779 } 1780 1781 public void pause() { 1782 mMediaPlayer.pause(); 1783 } 1784 1785 public void setHandler(Handler handler) { 1786 mHandler = handler; 1787 } 1788 1789 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() { 1790 public void onCompletion(MediaPlayer mp) { 1791 // Acquire a temporary wakelock, since when we return from 1792 // this callback the MediaPlayer will release its wakelock 1793 // and allow the device to go to sleep. 1794 // This temporary wakelock is released when the RELEASE_WAKELOCK 1795 // message is processed, but just in case, put a timeout on it. 1796 mWakeLock.acquire(30000); 1797 mHandler.sendEmptyMessage(TRACK_ENDED); 1798 mHandler.sendEmptyMessage(RELEASE_WAKELOCK); 1799 } 1800 }; 1801 1802 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() { 1803 public boolean onError(MediaPlayer mp, int what, int extra) { 1804 switch (what) { 1805 case MediaPlayer.MEDIA_ERROR_SERVER_DIED: 1806 mIsInitialized = false; 1807 mMediaPlayer.release(); 1808 // Creating a new MediaPlayer and settings its wakemode does not 1809 // require the media service, so it's OK to do this now, while the 1810 // service is still being restarted 1811 mMediaPlayer = new MediaPlayer(); 1812 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1813 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000); 1814 return true; 1815 default: 1816 Log.d("MultiPlayer", "Error: " + what + "," + extra); 1817 break; 1818 } 1819 return false; 1820 } 1821 }; 1822 1823 public long duration() { 1824 return mMediaPlayer.getDuration(); 1825 } 1826 1827 public long position() { 1828 return mMediaPlayer.getCurrentPosition(); 1829 } 1830 1831 public long seek(long whereto) { 1832 mMediaPlayer.seekTo((int) whereto); 1833 return whereto; 1834 } 1835 1836 public void setVolume(float vol) { 1837 mMediaPlayer.setVolume(vol, vol); 1838 } 1839 1840 public void setAudioSessionId(int sessionId) { 1841 mMediaPlayer.setAudioSessionId(sessionId); 1842 } 1843 1844 public int getAudioSessionId() { 1845 return mMediaPlayer.getAudioSessionId(); 1846 } 1847 } 1848 1849 /* 1850 * By making this a static class with a WeakReference to the Service, we 1851 * ensure that the Service can be GCd even when the system process still 1852 * has a remote reference to the stub. 1853 */ 1854 static class ServiceStub extends IMediaPlaybackService.Stub { 1855 WeakReference<MediaPlaybackService> mService; 1856 1857 ServiceStub(MediaPlaybackService service) { 1858 mService = new WeakReference<MediaPlaybackService>(service); 1859 } 1860 1861 public void openFile(String path) 1862 { 1863 mService.get().open(path); 1864 } 1865 public void open(long [] list, int position) { 1866 mService.get().open(list, position); 1867 } 1868 public int getQueuePosition() { 1869 return mService.get().getQueuePosition(); 1870 } 1871 public void setQueuePosition(int index) { 1872 mService.get().setQueuePosition(index); 1873 } 1874 public boolean isPlaying() { 1875 return mService.get().isPlaying(); 1876 } 1877 public void stop() { 1878 mService.get().stop(); 1879 } 1880 public void pause() { 1881 mService.get().pause(); 1882 } 1883 public void play() { 1884 mService.get().play(); 1885 } 1886 public void prev() { 1887 mService.get().prev(); 1888 } 1889 public void next() { 1890 mService.get().next(true); 1891 } 1892 public String getTrackName() { 1893 return mService.get().getTrackName(); 1894 } 1895 public String getAlbumName() { 1896 return mService.get().getAlbumName(); 1897 } 1898 public long getAlbumId() { 1899 return mService.get().getAlbumId(); 1900 } 1901 public String getArtistName() { 1902 return mService.get().getArtistName(); 1903 } 1904 public long getArtistId() { 1905 return mService.get().getArtistId(); 1906 } 1907 public void enqueue(long [] list , int action) { 1908 mService.get().enqueue(list, action); 1909 } 1910 public long [] getQueue() { 1911 return mService.get().getQueue(); 1912 } 1913 public void moveQueueItem(int from, int to) { 1914 mService.get().moveQueueItem(from, to); 1915 } 1916 public String getPath() { 1917 return mService.get().getPath(); 1918 } 1919 public long getAudioId() { 1920 return mService.get().getAudioId(); 1921 } 1922 public long position() { 1923 return mService.get().position(); 1924 } 1925 public long duration() { 1926 return mService.get().duration(); 1927 } 1928 public long seek(long pos) { 1929 return mService.get().seek(pos); 1930 } 1931 public void setShuffleMode(int shufflemode) { 1932 mService.get().setShuffleMode(shufflemode); 1933 } 1934 public int getShuffleMode() { 1935 return mService.get().getShuffleMode(); 1936 } 1937 public int removeTracks(int first, int last) { 1938 return mService.get().removeTracks(first, last); 1939 } 1940 public int removeTrack(long id) { 1941 return mService.get().removeTrack(id); 1942 } 1943 public void setRepeatMode(int repeatmode) { 1944 mService.get().setRepeatMode(repeatmode); 1945 } 1946 public int getRepeatMode() { 1947 return mService.get().getRepeatMode(); 1948 } 1949 public int getMediaMountedCount() { 1950 return mService.get().getMediaMountedCount(); 1951 } 1952 public int getAudioSessionId() { 1953 return mService.get().getAudioSessionId(); 1954 } 1955 } 1956 1957 @Override 1958 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 1959 writer.println("" + mPlayListLen + " items in queue, currently at index " + mPlayPos); 1960 writer.println("Currently loaded:"); 1961 writer.println(getArtistName()); 1962 writer.println(getAlbumName()); 1963 writer.println(getTrackName()); 1964 writer.println(getPath()); 1965 writer.println("playing: " + mIsSupposedToBePlaying); 1966 writer.println("actual: " + mPlayer.mMediaPlayer.isPlaying()); 1967 writer.println("shuffle mode: " + mShuffleMode); 1968 MusicUtils.debugDump(writer); 1969 } 1970 1971 private final IBinder mBinder = new ServiceStub(this); 1972 } 1973