MediaPlaybackService.java revision 173c561b6ee64fe8ce37c7402d9d715eebd7f959
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 private MediaGadgetProvider mGadgetProvider = MediaGadgetProvider.getInstance(); 150 151 // interval after which we stop the service when idle 152 private static final int IDLE_DELAY = 60000; 153 154 private Handler mPhoneHandler = new Handler() { 155 @Override 156 public void handleMessage(Message msg) { 157 switch (msg.what) { 158 case PHONE_CHANGED: 159 Phone.State state = mPsir.getPhoneState(); 160 if (state == Phone.State.RINGING) { 161 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 162 int ringvolume = audioManager.getStreamVolume(AudioManager.STREAM_RING); 163 if (ringvolume > 0) { 164 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0); 165 pause(); 166 } 167 } else if (state == Phone.State.OFFHOOK) { 168 // pause the music while a conversation is in progress 169 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0); 170 pause(); 171 } else if (state == Phone.State.IDLE) { 172 // start playing again 173 if (mResumeAfterCall) { 174 // resume playback only if music was playing 175 // when the call was answered 176 startAndFadeIn(); 177 mResumeAfterCall = false; 178 } 179 } 180 break; 181 default: 182 break; 183 } 184 } 185 }; 186 187 private void startAndFadeIn() { 188 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10); 189 } 190 191 private Handler mMediaplayerHandler = new Handler() { 192 float mCurrentVolume = 1.0f; 193 @Override 194 public void handleMessage(Message msg) { 195 switch (msg.what) { 196 case FADEIN: 197 if (!isPlaying()) { 198 mCurrentVolume = 0f; 199 mPlayer.setVolume(mCurrentVolume); 200 play(); 201 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10); 202 } else { 203 mCurrentVolume += 0.01f; 204 if (mCurrentVolume < 1.0f) { 205 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10); 206 } else { 207 mCurrentVolume = 1.0f; 208 } 209 mPlayer.setVolume(mCurrentVolume); 210 } 211 break; 212 case SERVER_DIED: 213 if (mWasPlaying) { 214 next(true); 215 } else { 216 // the server died when we were idle, so just 217 // reopen the same song (it will start again 218 // from the beginning though when the user 219 // restarts) 220 openCurrent(); 221 } 222 break; 223 case TRACK_ENDED: 224 if (mRepeatMode == REPEAT_CURRENT) { 225 seek(0); 226 play(); 227 } else if (!mOneShot) { 228 next(false); 229 } else { 230 notifyChange(PLAYBACK_COMPLETE); 231 } 232 break; 233 case RELEASE_WAKELOCK: 234 mWakeLock.release(); 235 break; 236 default: 237 break; 238 } 239 } 240 }; 241 242 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 243 @Override 244 public void onReceive(Context context, Intent intent) { 245 String action = intent.getAction(); 246 String cmd = intent.getStringExtra("command"); 247 if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) { 248 next(true); 249 } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) { 250 prev(); 251 } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) { 252 if (isPlaying()) { 253 pause(); 254 } else { 255 play(); 256 } 257 } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) { 258 pause(); 259 } else if (CMDSTOP.equals(cmd)) { 260 pause(); 261 seek(0); 262 } else if (MediaGadgetProvider.CMDGADGETUPDATE.equals(cmd)) { 263 // Someone asked us to refresh a set of specific gadgets, probably 264 // because they were just added. 265 int[] gadgetIds = intent.getIntArrayExtra(GadgetManager.EXTRA_GADGET_IDS); 266 mGadgetProvider.performUpdate(MediaPlaybackService.this, gadgetIds); 267 } 268 } 269 }; 270 271 public MediaPlaybackService() { 272 mPsir = new PhoneStateIntentReceiver(this, mPhoneHandler); 273 mPsir.notifyPhoneCallState(PHONE_CHANGED); 274 } 275 276 @Override 277 public void onCreate() { 278 super.onCreate(); 279 280 mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE); 281 mCardId = FileUtils.getFatVolumeId(Environment.getExternalStorageDirectory().getPath()); 282 283 registerExternalStorageListener(); 284 285 // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes. 286 mPlayer = new MultiPlayer(); 287 mPlayer.setHandler(mMediaplayerHandler); 288 289 // Clear leftover notification in case this service previously got killed while playing 290 NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 291 nm.cancel(PLAYBACKSERVICE_STATUS); 292 293 reloadQueue(); 294 295 IntentFilter commandFilter = new IntentFilter(); 296 commandFilter.addAction(SERVICECMD); 297 commandFilter.addAction(TOGGLEPAUSE_ACTION); 298 commandFilter.addAction(PAUSE_ACTION); 299 commandFilter.addAction(NEXT_ACTION); 300 commandFilter.addAction(PREVIOUS_ACTION); 301 registerReceiver(mIntentReceiver, commandFilter); 302 303 mPsir.registerIntent(); 304 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 305 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); 306 mWakeLock.setReferenceCounted(false); 307 308 // If the service was idle, but got killed before it stopped itself, the 309 // system will relaunch it. Make sure it gets stopped again in that case. 310 Message msg = mDelayedStopHandler.obtainMessage(); 311 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 312 } 313 314 @Override 315 public void onDestroy() { 316 if (mCursor != null) { 317 mCursor.close(); 318 mCursor = null; 319 } 320 321 unregisterReceiver(mIntentReceiver); 322 if (mUnmountReceiver != null) { 323 unregisterReceiver(mUnmountReceiver); 324 mUnmountReceiver = null; 325 } 326 mPsir.unregisterIntent(); 327 mWakeLock.release(); 328 super.onDestroy(); 329 } 330 331 private final char hexdigits [] = new char [] { 332 '0', '1', '2', '3', 333 '4', '5', '6', '7', 334 '8', '9', 'a', 'b', 335 'c', 'd', 'e', 'f' 336 }; 337 338 private void saveQueue(boolean full) { 339 if (mOneShot) { 340 return; 341 } 342 Editor ed = mPreferences.edit(); 343 //long start = System.currentTimeMillis(); 344 if (full) { 345 StringBuilder q = new StringBuilder(); 346 347 // The current playlist is saved as a list of "reverse hexadecimal" 348 // numbers, which we can generate faster than normal decimal or 349 // hexadecimal numbers, which in turn allows us to save the playlist 350 // more often without worrying too much about performance. 351 // (saving the full state takes about 40 ms under no-load conditions 352 // on the phone) 353 int len = mPlayListLen; 354 for (int i = 0; i < len; i++) { 355 int n = mPlayList[i]; 356 if (n == 0) { 357 q.append("0;"); 358 } else { 359 while (n != 0) { 360 int digit = n & 0xf; 361 n >>= 4; 362 q.append(hexdigits[digit]); 363 } 364 q.append(";"); 365 } 366 } 367 //Log.i("@@@@ service", "created queue string in " + (System.currentTimeMillis() - start) + " ms"); 368 ed.putString("queue", q.toString()); 369 ed.putInt("cardid", mCardId); 370 } 371 ed.putInt("curpos", mPlayPos); 372 if (mPlayer.isInitialized()) { 373 ed.putLong("seekpos", mPlayer.position()); 374 } 375 ed.putInt("repeatmode", mRepeatMode); 376 ed.putInt("shufflemode", mShuffleMode); 377 ed.commit(); 378 379 //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms"); 380 } 381 382 private void reloadQueue() { 383 String q = null; 384 385 boolean newstyle = false; 386 int id = mCardId; 387 if (mPreferences.contains("cardid")) { 388 newstyle = true; 389 id = mPreferences.getInt("cardid", ~mCardId); 390 } 391 if (id == mCardId) { 392 // Only restore the saved playlist if the card is still 393 // the same one as when the playlist was saved 394 q = mPreferences.getString("queue", ""); 395 } 396 if (q != null && q.length() > 1) { 397 //Log.i("@@@@ service", "loaded queue: " + q); 398 String [] entries = q.split(";"); 399 int len = entries.length; 400 ensurePlayListCapacity(len); 401 for (int i = 0; i < len; i++) { 402 if (newstyle) { 403 String revhex = entries[i]; 404 int n = 0; 405 for (int j = revhex.length() - 1; j >= 0 ; j--) { 406 n <<= 4; 407 char c = revhex.charAt(j); 408 if (c >= '0' && c <= '9') { 409 n += (c - '0'); 410 } else if (c >= 'a' && c <= 'f') { 411 n += (10 + c - 'a'); 412 } else { 413 // bogus playlist data 414 len = 0; 415 break; 416 } 417 } 418 mPlayList[i] = n; 419 } else { 420 mPlayList[i] = Integer.parseInt(entries[i]); 421 } 422 } 423 mPlayListLen = len; 424 425 int pos = mPreferences.getInt("curpos", 0); 426 if (pos < 0 || pos >= len) { 427 // The saved playlist is bogus, discard it 428 mPlayListLen = 0; 429 return; 430 } 431 mPlayPos = pos; 432 433 // When reloadQueue is called in response to a card-insertion, 434 // we might not be able to query the media provider right away. 435 // To deal with this, try querying for the current file, and if 436 // that fails, wait a while and try again. If that too fails, 437 // assume there is a problem and don't restore the state. 438 Cursor c = MusicUtils.query(this, 439 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 440 new String [] {"_id"}, "_id=" + mPlayList[mPlayPos] , null, null); 441 if (c == null || c.getCount() == 0) { 442 // wait a bit and try again 443 SystemClock.sleep(3000); 444 c = getContentResolver().query( 445 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 446 mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null); 447 } 448 if (c != null) { 449 c.close(); 450 } 451 452 // Make sure we don't auto-skip to the next song, since that 453 // also starts playback. What could happen in that case is: 454 // - music is paused 455 // - go to UMS and delete some files, including the currently playing one 456 // - come back from UMS 457 // (time passes) 458 // - music app is killed for some reason (out of memory) 459 // - music service is restarted, service restores state, doesn't find 460 // the "current" file, goes to the next and: playback starts on its 461 // own, potentially at some random inconvenient time. 462 mOpenFailedCounter = 20; 463 mQuietMode = true; 464 openCurrent(); 465 mQuietMode = false; 466 if (!mPlayer.isInitialized()) { 467 // couldn't restore the saved state 468 mPlayListLen = 0; 469 return; 470 } 471 472 long seekpos = mPreferences.getLong("seekpos", 0); 473 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0); 474 475 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE); 476 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) { 477 repmode = REPEAT_NONE; 478 } 479 mRepeatMode = repmode; 480 481 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE); 482 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) { 483 shufmode = SHUFFLE_NONE; 484 } 485 if (shufmode == SHUFFLE_AUTO) { 486 if (! makeAutoShuffleList()) { 487 shufmode = SHUFFLE_NONE; 488 } 489 } 490 mShuffleMode = shufmode; 491 } 492 } 493 494 @Override 495 public IBinder onBind(Intent intent) { 496 mDelayedStopHandler.removeCallbacksAndMessages(null); 497 mServiceInUse = true; 498 return mBinder; 499 } 500 501 @Override 502 public void onRebind(Intent intent) { 503 mDelayedStopHandler.removeCallbacksAndMessages(null); 504 mServiceInUse = true; 505 } 506 507 @Override 508 public void onStart(Intent intent, int startId) { 509 mServiceStartId = startId; 510 mDelayedStopHandler.removeCallbacksAndMessages(null); 511 512 String action = intent.getAction(); 513 String cmd = intent.getStringExtra("command"); 514 515 if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) { 516 next(true); 517 } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) { 518 prev(); 519 } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) { 520 if (isPlaying()) { 521 pause(); 522 } else { 523 play(); 524 } 525 } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) { 526 pause(); 527 } else if (CMDSTOP.equals(cmd)) { 528 pause(); 529 seek(0); 530 } 531 532 // make sure the service will shut down on its own if it was 533 // just started but not bound to and nothing is playing 534 mDelayedStopHandler.removeCallbacksAndMessages(null); 535 Message msg = mDelayedStopHandler.obtainMessage(); 536 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 537 } 538 539 @Override 540 public boolean onUnbind(Intent intent) { 541 mServiceInUse = false; 542 543 // Take a snapshot of the current playlist 544 saveQueue(true); 545 546 if (isPlaying() || mResumeAfterCall) { 547 // something is currently playing, or will be playing once 548 // an in-progress call ends, so don't stop the service now. 549 return true; 550 } 551 552 // If there is a playlist but playback is paused, then wait a while 553 // before stopping the service, so that pause/resume isn't slow. 554 // Also delay stopping the service if we're transitioning between tracks. 555 if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) { 556 Message msg = mDelayedStopHandler.obtainMessage(); 557 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 558 return true; 559 } 560 561 // No active playlist, OK to stop the service right now 562 stopSelf(mServiceStartId); 563 return true; 564 } 565 566 private Handler mDelayedStopHandler = new Handler() { 567 @Override 568 public void handleMessage(Message msg) { 569 // Check again to make sure nothing is playing right now 570 if (isPlaying() || mResumeAfterCall || mServiceInUse 571 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) { 572 return; 573 } 574 // save the queue again, because it might have changed 575 // since the user exited the music app (because of 576 // party-shuffle or because the play-position changed) 577 saveQueue(true); 578 stopSelf(mServiceStartId); 579 } 580 }; 581 582 /** 583 * Called when we receive a ACTION_MEDIA_EJECT notification. 584 * 585 * @param storagePath path to mount point for the removed media 586 */ 587 public void closeExternalStorageFiles(String storagePath) { 588 // stop playback and clean up if the SD card is going to be unmounted. 589 stop(true); 590 notifyChange(QUEUE_CHANGED); 591 notifyChange(META_CHANGED); 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 } 618 } 619 }; 620 IntentFilter iFilter = new IntentFilter(); 621 iFilter.addAction(Intent.ACTION_MEDIA_EJECT); 622 iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); 623 iFilter.addDataScheme("file"); 624 registerReceiver(mUnmountReceiver, iFilter); 625 } 626 } 627 628 /** 629 * Notify the change-receivers that something has changed. 630 * The intent that is sent contains the following data 631 * for the currently playing track: 632 * "id" - Integer: the database row ID 633 * "artist" - String: the name of the artist 634 * "album" - String: the name of the album 635 * "track" - String: the name of the track 636 * The intent has an action that is one of 637 * "com.android.music.metachanged" 638 * "com.android.music.queuechanged", 639 * "com.android.music.playbackcomplete" 640 * "com.android.music.playstatechanged" 641 * respectively indicating that a new track has 642 * started playing, that the playback queue has 643 * changed, that playback has stopped because 644 * the last file in the list has been played, 645 * or that the play-state changed (paused/resumed). 646 */ 647 private void notifyChange(String what) { 648 649 Intent i = new Intent(what); 650 i.putExtra("id", Integer.valueOf(getAudioId())); 651 i.putExtra("artist", getArtistName()); 652 i.putExtra("album",getAlbumName()); 653 i.putExtra("track", getTrackName()); 654 sendBroadcast(i); 655 656 if (what.equals(QUEUE_CHANGED)) { 657 saveQueue(true); 658 } else { 659 saveQueue(false); 660 } 661 662 // Share this notification directly with our gadgets 663 mGadgetProvider.notifyChange(this, what); 664 } 665 666 private void ensurePlayListCapacity(int size) { 667 if (mPlayList == null || size > mPlayList.length) { 668 // reallocate at 2x requested size so we don't 669 // need to grow and copy the array for every 670 // insert 671 int [] newlist = new int[size * 2]; 672 int len = mPlayListLen; 673 for (int i = 0; i < len; i++) { 674 newlist[i] = mPlayList[i]; 675 } 676 mPlayList = newlist; 677 } 678 // FIXME: shrink the array when the needed size is much smaller 679 // than the allocated size 680 } 681 682 // insert the list of songs at the specified position in the playlist 683 private void addToPlayList(int [] list, int position) { 684 int addlen = list.length; 685 if (position < 0) { // overwrite 686 mPlayListLen = 0; 687 position = 0; 688 } 689 ensurePlayListCapacity(mPlayListLen + addlen); 690 if (position > mPlayListLen) { 691 position = mPlayListLen; 692 } 693 694 // move part of list after insertion point 695 int tailsize = mPlayListLen - position; 696 for (int i = tailsize ; i > 0 ; i--) { 697 mPlayList[position + i] = mPlayList[position + i - addlen]; 698 } 699 700 // copy list into playlist 701 for (int i = 0; i < addlen; i++) { 702 mPlayList[position + i] = list[i]; 703 } 704 mPlayListLen += addlen; 705 } 706 707 /** 708 * Appends a list of tracks to the current playlist. 709 * If nothing is playing currently, playback will be started at 710 * the first track. 711 * If the action is NOW, playback will switch to the first of 712 * the new tracks immediately. 713 * @param list The list of tracks to append. 714 * @param action NOW, NEXT or LAST 715 */ 716 public void enqueue(int [] list, int action) { 717 synchronized(this) { 718 if (action == NEXT && mPlayPos + 1 < mPlayListLen) { 719 addToPlayList(list, mPlayPos + 1); 720 notifyChange(QUEUE_CHANGED); 721 } else { 722 // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen 723 addToPlayList(list, Integer.MAX_VALUE); 724 notifyChange(QUEUE_CHANGED); 725 if (action == NOW) { 726 mPlayPos = mPlayListLen - list.length; 727 openCurrent(); 728 play(); 729 notifyChange(META_CHANGED); 730 return; 731 } 732 } 733 if (mPlayPos < 0) { 734 mPlayPos = 0; 735 openCurrent(); 736 play(); 737 notifyChange(META_CHANGED); 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() { 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 } else if (mPlayListLen <= 0) { 1013 // This is mostly so that if you press 'play' on a bluetooth headset 1014 // without every having played anything before, it will still play 1015 // something. 1016 setShuffleMode(SHUFFLE_AUTO); 1017 } 1018 } 1019 1020 private void stop(boolean remove_status_icon) { 1021 if (mPlayer.isInitialized()) { 1022 mPlayer.stop(); 1023 } 1024 mFileToPlay = null; 1025 if (mCursor != null) { 1026 mCursor.close(); 1027 mCursor = null; 1028 } 1029 if (remove_status_icon) { 1030 gotoIdleState(); 1031 } 1032 setForeground(false); 1033 if (remove_status_icon) { 1034 mWasPlaying = false; 1035 } 1036 } 1037 1038 /** 1039 * Stops playback. 1040 */ 1041 public void stop() { 1042 stop(true); 1043 } 1044 1045 /** 1046 * Pauses playback (call play() to resume) 1047 */ 1048 public void pause() { 1049 if (isPlaying()) { 1050 mPlayer.pause(); 1051 gotoIdleState(); 1052 setForeground(false); 1053 mWasPlaying = false; 1054 notifyChange(PLAYSTATE_CHANGED); 1055 saveBookmarkIfNeeded(); 1056 } 1057 } 1058 1059 /** Returns whether playback is currently paused 1060 * 1061 * @return true if playback is paused, false if not 1062 */ 1063 public boolean isPlaying() { 1064 if (mPlayer.isInitialized()) { 1065 return mPlayer.isPlaying(); 1066 } 1067 return false; 1068 } 1069 1070 /* 1071 Desired behavior for prev/next/shuffle: 1072 1073 - NEXT will move to the next track in the list when not shuffling, and to 1074 a track randomly picked from the not-yet-played tracks when shuffling. 1075 If all tracks have already been played, pick from the full set, but 1076 avoid picking the previously played track if possible. 1077 - when shuffling, PREV will go to the previously played track. Hitting PREV 1078 again will go to the track played before that, etc. When the start of the 1079 history has been reached, PREV is a no-op. 1080 When not shuffling, PREV will go to the sequentially previous track (the 1081 difference with the shuffle-case is mainly that when not shuffling, the 1082 user can back up to tracks that are not in the history). 1083 1084 Example: 1085 When playing an album with 10 tracks from the start, and enabling shuffle 1086 while playing track 5, the remaining tracks (6-10) will be shuffled, e.g. 1087 the final play order might be 1-2-3-4-5-8-10-6-9-7. 1088 When hitting 'prev' 8 times while playing track 7 in this example, the 1089 user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next', 1090 a random track will be picked again. If at any time user disables shuffling 1091 the next/previous track will be picked in sequential order again. 1092 */ 1093 1094 public void prev() { 1095 synchronized (this) { 1096 if (mOneShot) { 1097 // we were playing a specific file not part of a playlist, so there is no 'previous' 1098 seek(0); 1099 play(); 1100 return; 1101 } 1102 if (mShuffleMode == SHUFFLE_NORMAL) { 1103 // go to previously-played track and remove it from the history 1104 int histsize = mHistory.size(); 1105 if (histsize == 0) { 1106 // prev is a no-op 1107 return; 1108 } 1109 Integer pos = mHistory.remove(histsize - 1); 1110 mPlayPos = pos.intValue(); 1111 } else { 1112 if (mPlayPos > 0) { 1113 mPlayPos--; 1114 } else { 1115 mPlayPos = mPlayListLen - 1; 1116 } 1117 } 1118 saveBookmarkIfNeeded(); 1119 stop(false); 1120 openCurrent(); 1121 play(); 1122 notifyChange(META_CHANGED); 1123 } 1124 } 1125 1126 public void next(boolean force) { 1127 synchronized (this) { 1128 if (mOneShot) { 1129 // we were playing a specific file not part of a playlist, so there is no 'next' 1130 seek(0); 1131 play(); 1132 return; 1133 } 1134 1135 // Store the current file in the history, but keep the history at a 1136 // reasonable size 1137 if (mPlayPos >= 0) { 1138 mHistory.add(Integer.valueOf(mPlayPos)); 1139 } 1140 if (mHistory.size() > MAX_HISTORY_SIZE) { 1141 mHistory.removeElementAt(0); 1142 } 1143 1144 if (mShuffleMode == SHUFFLE_NORMAL) { 1145 // Pick random next track from the not-yet-played ones 1146 // TODO: make it work right after adding/removing items in the queue. 1147 1148 int numTracks = mPlayListLen; 1149 int[] tracks = new int[numTracks]; 1150 for (int i=0;i < numTracks; i++) { 1151 tracks[i] = i; 1152 } 1153 1154 int numHistory = mHistory.size(); 1155 int numUnplayed = numTracks; 1156 for (int i=0;i < numHistory; i++) { 1157 int idx = mHistory.get(i).intValue(); 1158 if (idx < numTracks && tracks[idx] >= 0) { 1159 numUnplayed--; 1160 tracks[idx] = -1; 1161 } 1162 } 1163 1164 // 'numUnplayed' now indicates how many tracks have not yet 1165 // been played, and 'tracks' contains the indices of those 1166 // tracks. 1167 if (numUnplayed <=0) { 1168 // everything's already been played 1169 if (mRepeatMode == REPEAT_ALL || force) { 1170 //pick from full set 1171 numUnplayed = numTracks; 1172 for (int i=0;i < numTracks; i++) { 1173 tracks[i] = i; 1174 } 1175 } else { 1176 // all done 1177 gotoIdleState(); 1178 return; 1179 } 1180 } 1181 int skip = mRand.nextInt(numUnplayed); 1182 int cnt = -1; 1183 while (true) { 1184 while (tracks[++cnt] < 0) 1185 ; 1186 skip--; 1187 if (skip < 0) { 1188 break; 1189 } 1190 } 1191 mPlayPos = cnt; 1192 } else if (mShuffleMode == SHUFFLE_AUTO) { 1193 doAutoShuffleUpdate(); 1194 mPlayPos++; 1195 } else { 1196 if (mPlayPos >= mPlayListLen - 1) { 1197 // we're at the end of the list 1198 if (mRepeatMode == REPEAT_NONE && !force) { 1199 // all done 1200 gotoIdleState(); 1201 notifyChange(PLAYBACK_COMPLETE); 1202 return; 1203 } else if (mRepeatMode == REPEAT_ALL || force) { 1204 mPlayPos = 0; 1205 } 1206 } else { 1207 mPlayPos++; 1208 } 1209 } 1210 saveBookmarkIfNeeded(); 1211 stop(false); 1212 openCurrent(); 1213 play(); 1214 notifyChange(META_CHANGED); 1215 } 1216 } 1217 1218 private void gotoIdleState() { 1219 NotificationManager nm = 1220 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 1221 nm.cancel(PLAYBACKSERVICE_STATUS); 1222 mDelayedStopHandler.removeCallbacksAndMessages(null); 1223 Message msg = mDelayedStopHandler.obtainMessage(); 1224 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); 1225 } 1226 1227 private void saveBookmarkIfNeeded() { 1228 try { 1229 if (isPodcast()) { 1230 long pos = position(); 1231 long bookmark = getBookmark(); 1232 long duration = duration(); 1233 if ((pos < bookmark && (pos + 10000) > bookmark) || 1234 (pos > bookmark && (pos - 10000) < bookmark)) { 1235 // The existing bookmark is close to the current 1236 // position, so don't update it. 1237 return; 1238 } 1239 if (pos < 15000 || (pos + 10000) > duration) { 1240 // if we're near the start or end, clear the bookmark 1241 pos = 0; 1242 } 1243 1244 // write 'pos' to the bookmark field 1245 ContentValues values = new ContentValues(); 1246 values.put(MediaStore.Audio.Media.BOOKMARK, pos); 1247 Uri uri = ContentUris.withAppendedId( 1248 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX)); 1249 getContentResolver().update(uri, values, null, null); 1250 } 1251 } catch (SQLiteException ex) { 1252 } 1253 } 1254 1255 // Make sure there are at least 5 items after the currently playing item 1256 // and no more than 10 items before. 1257 private void doAutoShuffleUpdate() { 1258 boolean notify = false; 1259 // remove old entries 1260 if (mPlayPos > 10) { 1261 removeTracks(0, mPlayPos - 9); 1262 notify = true; 1263 } 1264 // add new entries if needed 1265 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos)); 1266 for (int i = 0; i < to_add; i++) { 1267 // pick something at random from the list 1268 int idx = mRand.nextInt(mAutoShuffleList.length); 1269 Integer which = mAutoShuffleList[idx]; 1270 ensurePlayListCapacity(mPlayListLen + 1); 1271 mPlayList[mPlayListLen++] = which; 1272 notify = true; 1273 } 1274 if (notify) { 1275 notifyChange(QUEUE_CHANGED); 1276 } 1277 } 1278 1279 // A simple variation of Random that makes sure that the 1280 // value it returns is not equal to the value it returned 1281 // previously, unless the interval is 1. 1282 private class Shuffler { 1283 private int mPrevious; 1284 private Random mRandom = new Random(); 1285 public int nextInt(int interval) { 1286 int ret; 1287 do { 1288 ret = mRandom.nextInt(interval); 1289 } while (ret == mPrevious && interval > 1); 1290 mPrevious = ret; 1291 return ret; 1292 } 1293 }; 1294 1295 private boolean makeAutoShuffleList() { 1296 ContentResolver res = getContentResolver(); 1297 Cursor c = null; 1298 try { 1299 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1300 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1", 1301 null, null); 1302 if (c == null || c.getCount() == 0) { 1303 return false; 1304 } 1305 int len = c.getCount(); 1306 int[] list = new int[len]; 1307 for (int i = 0; i < len; i++) { 1308 c.moveToNext(); 1309 list[i] = c.getInt(0); 1310 } 1311 mAutoShuffleList = list; 1312 return true; 1313 } catch (RuntimeException ex) { 1314 } finally { 1315 if (c != null) { 1316 c.close(); 1317 } 1318 } 1319 return false; 1320 } 1321 1322 /** 1323 * Removes the range of tracks specified from the play list. If a file within the range is 1324 * the file currently being played, playback will move to the next file after the 1325 * range. 1326 * @param first The first file to be removed 1327 * @param last The last file to be removed 1328 * @return the number of tracks deleted 1329 */ 1330 public int removeTracks(int first, int last) { 1331 int numremoved = removeTracksInternal(first, last); 1332 if (numremoved > 0) { 1333 notifyChange(QUEUE_CHANGED); 1334 } 1335 return numremoved; 1336 } 1337 1338 private int removeTracksInternal(int first, int last) { 1339 synchronized (this) { 1340 if (last < first) return 0; 1341 if (first < 0) first = 0; 1342 if (last >= mPlayListLen) last = mPlayListLen - 1; 1343 1344 boolean gotonext = false; 1345 if (first <= mPlayPos && mPlayPos <= last) { 1346 mPlayPos = first; 1347 gotonext = true; 1348 } else if (mPlayPos > last) { 1349 mPlayPos -= (last - first + 1); 1350 } 1351 int num = mPlayListLen - last - 1; 1352 for (int i = 0; i < num; i++) { 1353 mPlayList[first + i] = mPlayList[last + 1 + i]; 1354 } 1355 mPlayListLen -= last - first + 1; 1356 1357 if (gotonext) { 1358 if (mPlayListLen == 0) { 1359 stop(true); 1360 mPlayPos = -1; 1361 } else { 1362 if (mPlayPos >= mPlayListLen) { 1363 mPlayPos = 0; 1364 } 1365 boolean wasPlaying = isPlaying(); 1366 stop(false); 1367 openCurrent(); 1368 if (wasPlaying) { 1369 play(); 1370 } 1371 } 1372 } 1373 return last - first + 1; 1374 } 1375 } 1376 1377 /** 1378 * Removes all instances of the track with the given id 1379 * from the playlist. 1380 * @param id The id to be removed 1381 * @return how many instances of the track were removed 1382 */ 1383 public int removeTrack(int id) { 1384 int numremoved = 0; 1385 synchronized (this) { 1386 for (int i = 0; i < mPlayListLen; i++) { 1387 if (mPlayList[i] == id) { 1388 numremoved += removeTracksInternal(i, i); 1389 i--; 1390 } 1391 } 1392 } 1393 if (numremoved > 0) { 1394 notifyChange(QUEUE_CHANGED); 1395 } 1396 return numremoved; 1397 } 1398 1399 public void setShuffleMode(int shufflemode) { 1400 synchronized(this) { 1401 if (mShuffleMode == shufflemode && mPlayListLen > 0) { 1402 return; 1403 } 1404 mShuffleMode = shufflemode; 1405 if (mShuffleMode == SHUFFLE_AUTO) { 1406 if (makeAutoShuffleList()) { 1407 mPlayListLen = 0; 1408 doAutoShuffleUpdate(); 1409 mPlayPos = 0; 1410 openCurrent(); 1411 play(); 1412 notifyChange(META_CHANGED); 1413 return; 1414 } else { 1415 // failed to build a list of files to shuffle 1416 mShuffleMode = SHUFFLE_NONE; 1417 } 1418 } 1419 saveQueue(false); 1420 } 1421 } 1422 public int getShuffleMode() { 1423 return mShuffleMode; 1424 } 1425 1426 public void setRepeatMode(int repeatmode) { 1427 synchronized(this) { 1428 mRepeatMode = repeatmode; 1429 saveQueue(false); 1430 } 1431 } 1432 public int getRepeatMode() { 1433 return mRepeatMode; 1434 } 1435 1436 public int getMediaMountedCount() { 1437 return mMediaMountedCount; 1438 } 1439 1440 /** 1441 * Returns the path of the currently playing file, or null if 1442 * no file is currently playing. 1443 */ 1444 public String getPath() { 1445 return mFileToPlay; 1446 } 1447 1448 /** 1449 * Returns the rowid of the currently playing file, or -1 if 1450 * no file is currently playing. 1451 */ 1452 public int getAudioId() { 1453 synchronized (this) { 1454 if (mPlayPos >= 0 && mPlayer.isInitialized()) { 1455 return mPlayList[mPlayPos]; 1456 } 1457 } 1458 return -1; 1459 } 1460 1461 /** 1462 * Returns the position in the queue 1463 * @return the position in the queue 1464 */ 1465 public int getQueuePosition() { 1466 synchronized(this) { 1467 return mPlayPos; 1468 } 1469 } 1470 1471 /** 1472 * Starts playing the track at the given position in the queue. 1473 * @param pos The position in the queue of the track that will be played. 1474 */ 1475 public void setQueuePosition(int pos) { 1476 synchronized(this) { 1477 stop(false); 1478 mPlayPos = pos; 1479 openCurrent(); 1480 play(); 1481 notifyChange(META_CHANGED); 1482 } 1483 } 1484 1485 public String getArtistName() { 1486 synchronized(this) { 1487 if (mCursor == null) { 1488 return null; 1489 } 1490 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); 1491 } 1492 } 1493 1494 public int getArtistId() { 1495 synchronized (this) { 1496 if (mCursor == null) { 1497 return -1; 1498 } 1499 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID)); 1500 } 1501 } 1502 1503 public String getAlbumName() { 1504 synchronized (this) { 1505 if (mCursor == null) { 1506 return null; 1507 } 1508 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)); 1509 } 1510 } 1511 1512 public int getAlbumId() { 1513 synchronized (this) { 1514 if (mCursor == null) { 1515 return -1; 1516 } 1517 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)); 1518 } 1519 } 1520 1521 public String getTrackName() { 1522 synchronized (this) { 1523 if (mCursor == null) { 1524 return null; 1525 } 1526 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); 1527 } 1528 } 1529 1530 private boolean isPodcast() { 1531 synchronized (this) { 1532 if (mCursor == null) { 1533 return false; 1534 } 1535 return (mCursor.getInt(PODCASTCOLIDX) > 0); 1536 } 1537 } 1538 1539 private long getBookmark() { 1540 synchronized (this) { 1541 if (mCursor == null) { 1542 return 0; 1543 } 1544 return mCursor.getLong(BOOKMARKCOLIDX); 1545 } 1546 } 1547 1548 /** 1549 * Returns the duration of the file in milliseconds. 1550 * Currently this method returns -1 for the duration of MIDI files. 1551 */ 1552 public long duration() { 1553 if (mPlayer.isInitialized()) { 1554 return mPlayer.duration(); 1555 } 1556 return -1; 1557 } 1558 1559 /** 1560 * Returns the current playback position in milliseconds 1561 */ 1562 public long position() { 1563 if (mPlayer.isInitialized()) { 1564 return mPlayer.position(); 1565 } 1566 return -1; 1567 } 1568 1569 /** 1570 * Seeks to the position specified. 1571 * 1572 * @param pos The position to seek to, in milliseconds 1573 */ 1574 public long seek(long pos) { 1575 if (mPlayer.isInitialized()) { 1576 if (pos < 0) pos = 0; 1577 if (pos > mPlayer.duration()) pos = mPlayer.duration(); 1578 return mPlayer.seek(pos); 1579 } 1580 return -1; 1581 } 1582 1583 /** 1584 * Provides a unified interface for dealing with midi files and 1585 * other media files. 1586 */ 1587 private class MultiPlayer { 1588 private MediaPlayer mMediaPlayer = new MediaPlayer(); 1589 private Handler mHandler; 1590 private boolean mIsInitialized = false; 1591 1592 public MultiPlayer() { 1593 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1594 } 1595 1596 public void setDataSourceAsync(String path) { 1597 try { 1598 mMediaPlayer.reset(); 1599 mMediaPlayer.setDataSource(path); 1600 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1601 mMediaPlayer.setOnPreparedListener(preparedlistener); 1602 mMediaPlayer.prepareAsync(); 1603 } catch (IOException ex) { 1604 // TODO: notify the user why the file couldn't be opened 1605 mIsInitialized = false; 1606 return; 1607 } catch (IllegalArgumentException ex) { 1608 // TODO: notify the user why the file couldn't be opened 1609 mIsInitialized = false; 1610 return; 1611 } 1612 mMediaPlayer.setOnCompletionListener(listener); 1613 mMediaPlayer.setOnErrorListener(errorListener); 1614 1615 mIsInitialized = true; 1616 } 1617 1618 public void setDataSource(String path) { 1619 try { 1620 mMediaPlayer.reset(); 1621 mMediaPlayer.setOnPreparedListener(null); 1622 if (path.startsWith("content://")) { 1623 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path)); 1624 } else { 1625 mMediaPlayer.setDataSource(path); 1626 } 1627 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 1628 mMediaPlayer.prepare(); 1629 } catch (IOException ex) { 1630 // TODO: notify the user why the file couldn't be opened 1631 mIsInitialized = false; 1632 return; 1633 } catch (IllegalArgumentException ex) { 1634 // TODO: notify the user why the file couldn't be opened 1635 mIsInitialized = false; 1636 return; 1637 } 1638 mMediaPlayer.setOnCompletionListener(listener); 1639 mMediaPlayer.setOnErrorListener(errorListener); 1640 1641 mIsInitialized = true; 1642 } 1643 1644 public boolean isInitialized() { 1645 return mIsInitialized; 1646 } 1647 1648 public void start() { 1649 mMediaPlayer.start(); 1650 } 1651 1652 public void stop() { 1653 mMediaPlayer.reset(); 1654 mIsInitialized = false; 1655 } 1656 1657 public void pause() { 1658 mMediaPlayer.pause(); 1659 } 1660 1661 public boolean isPlaying() { 1662 return mMediaPlayer.isPlaying(); 1663 } 1664 1665 public void setHandler(Handler handler) { 1666 mHandler = handler; 1667 } 1668 1669 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() { 1670 public void onCompletion(MediaPlayer mp) { 1671 // Acquire a temporary wakelock, since when we return from 1672 // this callback the MediaPlayer will release its wakelock 1673 // and allow the device to go to sleep. 1674 // This temporary wakelock is released when the RELEASE_WAKELOCK 1675 // message is processed, but just in case, put a timeout on it. 1676 mWakeLock.acquire(30000); 1677 mHandler.sendEmptyMessage(TRACK_ENDED); 1678 mHandler.sendEmptyMessage(RELEASE_WAKELOCK); 1679 } 1680 }; 1681 1682 MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() { 1683 public void onPrepared(MediaPlayer mp) { 1684 notifyChange(ASYNC_OPEN_COMPLETE); 1685 } 1686 }; 1687 1688 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() { 1689 public boolean onError(MediaPlayer mp, int what, int extra) { 1690 switch (what) { 1691 case MediaPlayer.MEDIA_ERROR_SERVER_DIED: 1692 mIsInitialized = false; 1693 mMediaPlayer.release(); 1694 // Creating a new MediaPlayer and settings its wakemode does not 1695 // require the media service, so it's OK to do this now, while the 1696 // service is still being restarted 1697 mMediaPlayer = new MediaPlayer(); 1698 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); 1699 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000); 1700 return true; 1701 default: 1702 break; 1703 } 1704 return false; 1705 } 1706 }; 1707 1708 public long duration() { 1709 return mMediaPlayer.getDuration(); 1710 } 1711 1712 public long position() { 1713 return mMediaPlayer.getCurrentPosition(); 1714 } 1715 1716 public long seek(long whereto) { 1717 mMediaPlayer.seekTo((int) whereto); 1718 return whereto; 1719 } 1720 1721 public void setVolume(float vol) { 1722 mMediaPlayer.setVolume(vol, vol); 1723 } 1724 } 1725 1726 private final IMediaPlaybackService.Stub mBinder = new IMediaPlaybackService.Stub() 1727 { 1728 public void openfileAsync(String path) 1729 { 1730 MediaPlaybackService.this.openAsync(path); 1731 } 1732 public void openfile(String path) 1733 { 1734 MediaPlaybackService.this.open(path, true); 1735 } 1736 public void open(int [] list, int position) { 1737 MediaPlaybackService.this.open(list, position); 1738 } 1739 public int getQueuePosition() { 1740 return MediaPlaybackService.this.getQueuePosition(); 1741 } 1742 public void setQueuePosition(int index) { 1743 MediaPlaybackService.this.setQueuePosition(index); 1744 } 1745 public boolean isPlaying() { 1746 return MediaPlaybackService.this.isPlaying(); 1747 } 1748 public void stop() { 1749 MediaPlaybackService.this.stop(); 1750 } 1751 public void pause() { 1752 MediaPlaybackService.this.pause(); 1753 } 1754 public void play() { 1755 MediaPlaybackService.this.play(); 1756 } 1757 public void prev() { 1758 MediaPlaybackService.this.prev(); 1759 } 1760 public void next() { 1761 MediaPlaybackService.this.next(true); 1762 } 1763 public String getTrackName() { 1764 return MediaPlaybackService.this.getTrackName(); 1765 } 1766 public String getAlbumName() { 1767 return MediaPlaybackService.this.getAlbumName(); 1768 } 1769 public int getAlbumId() { 1770 return MediaPlaybackService.this.getAlbumId(); 1771 } 1772 public String getArtistName() { 1773 return MediaPlaybackService.this.getArtistName(); 1774 } 1775 public int getArtistId() { 1776 return MediaPlaybackService.this.getArtistId(); 1777 } 1778 public void enqueue(int [] list , int action) { 1779 MediaPlaybackService.this.enqueue(list, action); 1780 } 1781 public int [] getQueue() { 1782 return MediaPlaybackService.this.getQueue(); 1783 } 1784 public void moveQueueItem(int from, int to) { 1785 MediaPlaybackService.this.moveQueueItem(from, to); 1786 } 1787 public String getPath() { 1788 return MediaPlaybackService.this.getPath(); 1789 } 1790 public int getAudioId() { 1791 return MediaPlaybackService.this.getAudioId(); 1792 } 1793 public long position() { 1794 return MediaPlaybackService.this.position(); 1795 } 1796 public long duration() { 1797 return MediaPlaybackService.this.duration(); 1798 } 1799 public long seek(long pos) { 1800 return MediaPlaybackService.this.seek(pos); 1801 } 1802 public void setShuffleMode(int shufflemode) { 1803 MediaPlaybackService.this.setShuffleMode(shufflemode); 1804 } 1805 public int getShuffleMode() { 1806 return MediaPlaybackService.this.getShuffleMode(); 1807 } 1808 public int removeTracks(int first, int last) { 1809 return MediaPlaybackService.this.removeTracks(first, last); 1810 } 1811 public int removeTrack(int id) { 1812 return MediaPlaybackService.this.removeTrack(id); 1813 } 1814 public void setRepeatMode(int repeatmode) { 1815 MediaPlaybackService.this.setRepeatMode(repeatmode); 1816 } 1817 public int getRepeatMode() { 1818 return MediaPlaybackService.this.getRepeatMode(); 1819 } 1820 public int getMediaMountedCount() { 1821 return MediaPlaybackService.this.getMediaMountedCount(); 1822 } 1823 }; 1824} 1825