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