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