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