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