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