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