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