2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.music;
19 import android.app.Notification;
20 import android.app.PendingIntent;
21 import android.app.Service;
22 import android.appwidget.AppWidgetManager;
23 import android.content.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.BroadcastReceiver;
30 import android.content.SharedPreferences;
31 import android.content.SharedPreferences.Editor;
32 import android.database.Cursor;
33 import android.database.sqlite.SQLiteException;
34 import android.media.AudioManager;
35 import android.media.MediaPlayer;
36 import android.net.Uri;
37 import android.os.Environment;
38 import android.os.FileUtils;
39 import android.os.Handler;
40 import android.os.IBinder;
41 import android.os.Message;
42 import android.os.PowerManager;
43 import android.os.SystemClock;
44 import android.os.PowerManager.WakeLock;
45 import android.provider.MediaStore;
46 import android.telephony.PhoneStateListener;
47 import android.telephony.TelephonyManager;
48 import android.util.Log;
49 import android.widget.RemoteViews;
50 import android.widget.Toast;
52 import java.io.IOException;
53 import java.lang.ref.WeakReference;
54 import java.util.Random;
55 import java.util.Vector;
58 * Provides "background" audio playback capabilities, allowing the
59 * user to switch between activities without stopping playback.
61 public 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
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;
71 public static final int SHUFFLE_NONE = 0;
72 public static final int SHUFFLE_NORMAL = 1;
73 public static final int SHUFFLE_AUTO = 2;
75 public static final int REPEAT_NONE = 0;
76 public static final int REPEAT_CURRENT = 1;
77 public static final int REPEAT_ALL = 2;
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";
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";
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";
98 private static final int TRACK_ENDED = 1;
99 private static final int RELEASE_WAKELOCK = 2;
100 private static final int SERVER_DIED = 3;
101 private static final int FADEIN = 4;
102 private static final int MAX_HISTORY_SIZE = 100;
104 private MultiPlayer mPlayer;
105 private String mFileToPlay;
106 private int mShuffleMode = SHUFFLE_NONE;
107 private int mRepeatMode = REPEAT_NONE;
108 private int mMediaMountedCount = 0;
109 private long [] mAutoShuffleList = null;
110 private boolean mOneShot;
111 private long [] mPlayList = null;
112 private int mPlayListLen = 0;
113 private Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE);
114 private Cursor mCursor;
115 private int mPlayPos = -1;
116 private static final String LOGTAG = "MediaPlaybackService";
117 private final Shuffler mRand = new Shuffler();
118 private int mOpenFailedCounter = 0;
119 String[] mCursorCols = new String[] {
120 "audio._id AS _id", // index must match IDCOLIDX below
121 MediaStore.Audio.Media.ARTIST,
122 MediaStore.Audio.Media.ALBUM,
123 MediaStore.Audio.Media.TITLE,
124 MediaStore.Audio.Media.DATA,
125 MediaStore.Audio.Media.MIME_TYPE,
126 MediaStore.Audio.Media.ALBUM_ID,
127 MediaStore.Audio.Media.ARTIST_ID,
128 MediaStore.Audio.Media.IS_PODCAST, // index must match PODCASTCOLIDX below
129 MediaStore.Audio.Media.BOOKMARK // index must match BOOKMARKCOLIDX below
131 private final static int IDCOLIDX = 0;
132 private final static int PODCASTCOLIDX = 8;
133 private final static int BOOKMARKCOLIDX = 9;
134 private BroadcastReceiver mUnmountReceiver = null;
135 private WakeLock mWakeLock;
136 private int mServiceStartId = -1;
137 private boolean mServiceInUse = false;
138 private boolean mResumeAfterCall = false;
139 private boolean mIsSupposedToBePlaying = false;
140 private boolean mQuietMode = false;
142 private SharedPreferences mPreferences;
143 // We use this to distinguish between different cards when saving/restoring playlists.
144 // This will have to change if we want to support multiple simultaneous cards.
147 private MediaAppWidgetProvider mAppWidgetProvider = MediaAppWidgetProvider.getInstance();
149 // interval after which we stop the service when idle
150 private static final int IDLE_DELAY = 60000;
152 private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
154 public void onCallStateChanged(int state, String incomingNumber) {
155 if (state == TelephonyManager.CALL_STATE_RINGING) {
156 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
157 int ringvolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
158 if (ringvolume > 0) {
159 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
162 } else if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
163 // pause the music while a conversation is in progress
164 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
166 } else if (state == TelephonyManager.CALL_STATE_IDLE) {
167 // start playing again
168 if (mResumeAfterCall) {
169 // resume playback only if music was playing
170 // when the call was answered
172 mResumeAfterCall = false;
178 private void startAndFadeIn() {
179 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
182 private Handler mMediaplayerHandler = new Handler() {
183 float mCurrentVolume = 1.0f;
185 public void handleMessage(Message msg) {
190 mPlayer.setVolume(mCurrentVolume);
192 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
194 mCurrentVolume += 0.01f;
195 if (mCurrentVolume < 1.0f) {
196 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
198 mCurrentVolume = 1.0f;
200 mPlayer.setVolume(mCurrentVolume);
204 if (mIsSupposedToBePlaying) {
207 // the server died when we were idle, so just
208 // reopen the same song (it will start again
209 // from the beginning though when the user
215 if (mRepeatMode == REPEAT_CURRENT) {
218 } else if (!mOneShot) {
221 notifyChange(PLAYBACK_COMPLETE);
222 mIsSupposedToBePlaying = false;
225 case RELEASE_WAKELOCK:
234 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
236 public void onReceive(Context context, Intent intent) {
237 String action = intent.getAction();
238 String cmd = intent.getStringExtra("command");
239 if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
241 } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
243 } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
249 } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
251 } else if (CMDSTOP.equals(cmd)) {
254 } else if (MediaAppWidgetProvider.CMDAPPWIDGETUPDATE.equals(cmd)) {
255 // Someone asked us to refresh a set of specific widgets, probably
256 // because they were just added.
257 int[] appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
258 mAppWidgetProvider.performUpdate(MediaPlaybackService.this, appWidgetIds);
263 public MediaPlaybackService() {
267 public void onCreate() {
270 mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
271 mCardId = FileUtils.getFatVolumeId(Environment.getExternalStorageDirectory().getPath());
273 registerExternalStorageListener();
275 // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes.
276 mPlayer = new MultiPlayer();
277 mPlayer.setHandler(mMediaplayerHandler);
281 IntentFilter commandFilter = new IntentFilter();
282 commandFilter.addAction(SERVICECMD);
283 commandFilter.addAction(TOGGLEPAUSE_ACTION);
284 commandFilter.addAction(PAUSE_ACTION);
285 commandFilter.addAction(NEXT_ACTION);
286 commandFilter.addAction(PREVIOUS_ACTION);
287 registerReceiver(mIntentReceiver, commandFilter);
289 TelephonyManager tmgr = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
290 tmgr.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
291 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
292 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
293 mWakeLock.setReferenceCounted(false);
295 // If the service was idle, but got killed before it stopped itself, the
296 // system will relaunch it. Make sure it gets stopped again in that case.
297 Message msg = mDelayedStopHandler.obtainMessage();
298 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
302 public void onDestroy() {
303 // Check that we're not being destroyed while something is still playing.
305 Log.e(LOGTAG, "Service being destroyed while still playing.");
307 // release all MediaPlayer resources, including the native player and wakelocks
311 // make sure there aren't any other messages coming
312 mDelayedStopHandler.removeCallbacksAndMessages(null);
313 mMediaplayerHandler.removeCallbacksAndMessages(null);
315 if (mCursor != null) {
320 TelephonyManager tmgr = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
321 tmgr.listen(mPhoneStateListener, 0);
323 unregisterReceiver(mIntentReceiver);
324 if (mUnmountReceiver != null) {
325 unregisterReceiver(mUnmountReceiver);
326 mUnmountReceiver = null;
332 private final char hexdigits [] = new char [] {
339 private void saveQueue(boolean full) {
343 Editor ed = mPreferences.edit();
344 //long start = System.currentTimeMillis();
346 StringBuilder q = new StringBuilder();
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
354 int len = mPlayListLen;
355 for (int i = 0; i < len; i++) {
356 long n = mPlayList[i];
361 int digit = (int)(n & 0xf);
363 q.append(hexdigits[digit]);
368 //Log.i("@@@@ service", "created queue string in " + (System.currentTimeMillis() - start) + " ms");
369 ed.putString("queue", q.toString());
370 ed.putInt("cardid", mCardId);
371 if (mShuffleMode != SHUFFLE_NONE) {
372 // In shuffle mode we need to save the history too
373 len = mHistory.size();
375 for (int i = 0; i < len; i++) {
376 int n = mHistory.get(i);
381 int digit = (n & 0xf);
383 q.append(hexdigits[digit]);
388 ed.putString("history", q.toString());
391 ed.putInt("curpos", mPlayPos);
392 if (mPlayer.isInitialized()) {
393 ed.putLong("seekpos", mPlayer.position());
395 ed.putInt("repeatmode", mRepeatMode);
396 ed.putInt("shufflemode", mShuffleMode);
399 //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms");
402 private void reloadQueue() {
405 boolean newstyle = false;
407 if (mPreferences.contains("cardid")) {
409 id = mPreferences.getInt("cardid", ~mCardId);
412 // Only restore the saved playlist if the card is still
413 // the same one as when the playlist was saved
414 q = mPreferences.getString("queue", "");
416 int qlen = q != null ? q.length() : 0;
418 //Log.i("@@@@ service", "loaded queue: " + q);
422 for (int i = 0; i < qlen; i++) {
423 char c = q.charAt(i);
425 ensurePlayListCapacity(plen + 1);
431 if (c >= '0' && c <= '9') {
432 n += ((c - '0') << shift);
433 } else if (c >= 'a' && c <= 'f') {
434 n += ((10 + c - 'a') << shift);
436 // bogus playlist data
445 int pos = mPreferences.getInt("curpos", 0);
446 if (pos < 0 || pos >= mPlayListLen) {
447 // The saved playlist is bogus, discard it
453 // When reloadQueue is called in response to a card-insertion,
454 // we might not be able to query the media provider right away.
455 // To deal with this, try querying for the current file, and if
456 // that fails, wait a while and try again. If that too fails,
457 // assume there is a problem and don't restore the state.
458 Cursor crsr = MusicUtils.query(this,
459 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
460 new String [] {"_id"}, "_id=" + mPlayList[mPlayPos] , null, null);
461 if (crsr == null || crsr.getCount() == 0) {
462 // wait a bit and try again
463 SystemClock.sleep(3000);
464 crsr = getContentResolver().query(
465 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
466 mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null);
472 // Make sure we don't auto-skip to the next song, since that
473 // also starts playback. What could happen in that case is:
475 // - go to UMS and delete some files, including the currently playing one
476 // - come back from UMS
478 // - music app is killed for some reason (out of memory)
479 // - music service is restarted, service restores state, doesn't find
480 // the "current" file, goes to the next and: playback starts on its
481 // own, potentially at some random inconvenient time.
482 mOpenFailedCounter = 20;
486 if (!mPlayer.isInitialized()) {
487 // couldn't restore the saved state
492 long seekpos = mPreferences.getLong("seekpos", 0);
493 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
494 Log.d(LOGTAG, "restored queue, currently at position "
495 + position() + "/" + duration()
496 + " (requested " + seekpos + ")");
498 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
499 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
500 repmode = REPEAT_NONE;
502 mRepeatMode = repmode;
504 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
505 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
506 shufmode = SHUFFLE_NONE;
508 if (shufmode != SHUFFLE_NONE) {
509 // in shuffle mode we need to restore the history too
510 q = mPreferences.getString("history", "");
511 qlen = q != null ? q.length() : 0;
517 for (int i = 0; i < qlen; i++) {
518 char c = q.charAt(i);
520 if (n >= mPlayListLen) {
521 // bogus history data
529 if (c >= '0' && c <= '9') {
530 n += ((c - '0') << shift);
531 } else if (c >= 'a' && c <= 'f') {
532 n += ((10 + c - 'a') << shift);
534 // bogus history data
543 if (shufmode == SHUFFLE_AUTO) {
544 if (! makeAutoShuffleList()) {
545 shufmode = SHUFFLE_NONE;
548 mShuffleMode = shufmode;
553 public IBinder onBind(Intent intent) {
554 mDelayedStopHandler.removeCallbacksAndMessages(null);
555 mServiceInUse = true;
560 public void onRebind(Intent intent) {
561 mDelayedStopHandler.removeCallbacksAndMessages(null);
562 mServiceInUse = true;
566 public int onStartCommand(Intent intent, int flags, int startId) {
567 mServiceStartId = startId;
568 mDelayedStopHandler.removeCallbacksAndMessages(null);
570 if (intent != null) {
571 String action = intent.getAction();
572 String cmd = intent.getStringExtra("command");
574 if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
576 } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
577 if (position() < 2000) {
583 } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
589 } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
591 } else if (CMDSTOP.equals(cmd)) {
597 // make sure the service will shut down on its own if it was
598 // just started but not bound to and nothing is playing
599 mDelayedStopHandler.removeCallbacksAndMessages(null);
600 Message msg = mDelayedStopHandler.obtainMessage();
601 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
606 public boolean onUnbind(Intent intent) {
607 mServiceInUse = false;
609 // Take a snapshot of the current playlist
612 if (isPlaying() || mResumeAfterCall) {
613 // something is currently playing, or will be playing once
614 // an in-progress call ends, so don't stop the service now.
618 // If there is a playlist but playback is paused, then wait a while
619 // before stopping the service, so that pause/resume isn't slow.
620 // Also delay stopping the service if we're transitioning between tracks.
621 if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
622 Message msg = mDelayedStopHandler.obtainMessage();
623 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
627 // No active playlist, OK to stop the service right now
628 stopSelf(mServiceStartId);
632 private Handler mDelayedStopHandler = new Handler() {
634 public void handleMessage(Message msg) {
635 // Check again to make sure nothing is playing right now
636 if (isPlaying() || mResumeAfterCall || mServiceInUse
637 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
640 // save the queue again, because it might have changed
641 // since the user exited the music app (because of
642 // party-shuffle or because the play-position changed)
644 stopSelf(mServiceStartId);
649 * Called when we receive a ACTION_MEDIA_EJECT notification.
651 * @param storagePath path to mount point for the removed media
653 public void closeExternalStorageFiles(String storagePath) {
654 // stop playback and clean up if the SD card is going to be unmounted.
656 notifyChange(QUEUE_CHANGED);
657 notifyChange(META_CHANGED);
661 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications.
662 * The intent will call closeExternalStorageFiles() if the external media
663 * is going to be ejected, so applications can clean up any files they have open.
665 public void registerExternalStorageListener() {
666 if (mUnmountReceiver == null) {
667 mUnmountReceiver = new BroadcastReceiver() {
669 public void onReceive(Context context, Intent intent) {
670 String action = intent.getAction();
671 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
673 mOneShot = true; // This makes us not save the state again later,
674 // which would be wrong because the song ids and
675 // card id might not match.
676 closeExternalStorageFiles(intent.getData().getPath());
677 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
678 mMediaMountedCount++;
679 mCardId = FileUtils.getFatVolumeId(intent.getData().getPath());
681 notifyChange(QUEUE_CHANGED);
682 notifyChange(META_CHANGED);
686 IntentFilter iFilter = new IntentFilter();
687 iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
688 iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
689 iFilter.addDataScheme("file");
690 registerReceiver(mUnmountReceiver, iFilter);
695 * Notify the change-receivers that something has changed.
696 * The intent that is sent contains the following data
697 * for the currently playing track:
698 * "id" - Integer: the database row ID
699 * "artist" - String: the name of the artist
700 * "album" - String: the name of the album
701 * "track" - String: the name of the track
702 * The intent has an action that is one of
703 * "com.android.music.metachanged"
704 * "com.android.music.queuechanged",
705 * "com.android.music.playbackcomplete"
706 * "com.android.music.playstatechanged"
707 * respectively indicating that a new track has
708 * started playing, that the playback queue has
709 * changed, that playback has stopped because
710 * the last file in the list has been played,
711 * or that the play-state changed (paused/resumed).
713 private void notifyChange(String what) {
715 Intent i = new Intent(what);
716 i.putExtra("id", Long.valueOf(getAudioId()));
717 i.putExtra("artist", getArtistName());
718 i.putExtra("album",getAlbumName());
719 i.putExtra("track", getTrackName());
722 if (what.equals(QUEUE_CHANGED)) {
728 // Share this notification directly with our widgets
729 mAppWidgetProvider.notifyChange(this, what);
732 private void ensurePlayListCapacity(int size) {
733 if (mPlayList == null || size > mPlayList.length) {
734 // reallocate at 2x requested size so we don't
735 // need to grow and copy the array for every
737 long [] newlist = new long[size * 2];
738 int len = mPlayList != null ? mPlayList.length : mPlayListLen;
739 for (int i = 0; i < len; i++) {
740 newlist[i] = mPlayList[i];
744 // FIXME: shrink the array when the needed size is much smaller
745 // than the allocated size
748 // insert the list of songs at the specified position in the playlist
749 private void addToPlayList(long [] list, int position) {
750 int addlen = list.length;
751 if (position < 0) { // overwrite
755 ensurePlayListCapacity(mPlayListLen + addlen);
756 if (position > mPlayListLen) {
757 position = mPlayListLen;
760 // move part of list after insertion point
761 int tailsize = mPlayListLen - position;
762 for (int i = tailsize ; i > 0 ; i--) {
763 mPlayList[position + i] = mPlayList[position + i - addlen];
766 // copy list into playlist
767 for (int i = 0; i < addlen; i++) {
768 mPlayList[position + i] = list[i];
770 mPlayListLen += addlen;
774 * Appends a list of tracks to the current playlist.
775 * If nothing is playing currently, playback will be started at
777 * If the action is NOW, playback will switch to the first of
778 * the new tracks immediately.
779 * @param list The list of tracks to append.
780 * @param action NOW, NEXT or LAST
782 public void enqueue(long [] list, int action) {
784 if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
785 addToPlayList(list, mPlayPos + 1);
786 notifyChange(QUEUE_CHANGED);
788 // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen
789 addToPlayList(list, Integer.MAX_VALUE);
790 notifyChange(QUEUE_CHANGED);
792 mPlayPos = mPlayListLen - list.length;
795 notifyChange(META_CHANGED);
803 notifyChange(META_CHANGED);
809 * Replaces the current playlist with a new list,
810 * and prepares for starting playback at the specified
811 * position in the list, or a random position if the
812 * specified position is 0.
813 * @param list The new list of tracks.
815 public void open(long [] list, int position) {
816 synchronized (this) {
817 if (mShuffleMode == SHUFFLE_AUTO) {
818 mShuffleMode = SHUFFLE_NORMAL;
820 long oldId = getAudioId();
821 int listlength = list.length;
822 boolean newlist = true;
823 if (mPlayListLen == listlength) {
824 // possible fast path: list might be the same
826 for (int i = 0; i < listlength; i++) {
827 if (list[i] != mPlayList[i]) {
834 addToPlayList(list, -1);
835 notifyChange(QUEUE_CHANGED);
837 int oldpos = mPlayPos;
841 mPlayPos = mRand.nextInt(mPlayListLen);
845 saveBookmarkIfNeeded();
847 if (oldId != getAudioId()) {
848 notifyChange(META_CHANGED);
854 * Moves the item at index1 to index2.
858 public void moveQueueItem(int index1, int index2) {
859 synchronized (this) {
860 if (index1 >= mPlayListLen) {
861 index1 = mPlayListLen - 1;
863 if (index2 >= mPlayListLen) {
864 index2 = mPlayListLen - 1;
866 if (index1 < index2) {
867 long tmp = mPlayList[index1];
868 for (int i = index1; i < index2; i++) {
869 mPlayList[i] = mPlayList[i+1];
871 mPlayList[index2] = tmp;
872 if (mPlayPos == index1) {
874 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
877 } else if (index2 < index1) {
878 long tmp = mPlayList[index1];
879 for (int i = index1; i > index2; i--) {
880 mPlayList[i] = mPlayList[i-1];
882 mPlayList[index2] = tmp;
883 if (mPlayPos == index1) {
885 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
889 notifyChange(QUEUE_CHANGED);
894 * Returns the current play list
895 * @return An array of integers containing the IDs of the tracks in the play list
897 public long [] getQueue() {
898 synchronized (this) {
899 int len = mPlayListLen;
900 long [] list = new long[len];
901 for (int i = 0; i < len; i++) {
902 list[i] = mPlayList[i];
908 private void openCurrent() {
909 synchronized (this) {
910 if (mCursor != null) {
914 if (mPlayListLen == 0) {
919 String id = String.valueOf(mPlayList[mPlayPos]);
921 mCursor = getContentResolver().query(
922 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
923 mCursorCols, "_id=" + id , null, null);
924 if (mCursor != null) {
925 mCursor.moveToFirst();
926 open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id, false);
927 // go to bookmark if needed
929 long bookmark = getBookmark();
930 // Start playing a little bit before the bookmark,
931 // so it's easier to get back in to the narrative.
932 seek(bookmark - 5000);
938 public void openAsync(String path) {
939 synchronized (this) {
944 mRepeatMode = REPEAT_NONE;
945 ensurePlayListCapacity(1);
951 mPlayer.setDataSourceAsync(mFileToPlay);
957 * Opens the specified file and readies it for playback.
959 * @param path The full path of the file to be opened.
960 * @param oneshot when set to true, playback will stop after this file completes, instead
961 * of moving on to the next track in the list
963 public void open(String path, boolean oneshot) {
964 synchronized (this) {
970 mRepeatMode = REPEAT_NONE;
971 ensurePlayListCapacity(1);
976 // if mCursor is null, try to associate path with a database cursor
977 if (mCursor == null) {
979 ContentResolver resolver = getContentResolver();
982 String selectionArgs[];
983 if (path.startsWith("content://media/")) {
984 uri = Uri.parse(path);
986 selectionArgs = null;
988 uri = MediaStore.Audio.Media.getContentUriForPath(path);
989 where = MediaStore.Audio.Media.DATA + "=?";
990 selectionArgs = new String[] { path };
994 mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);
995 if (mCursor != null) {
996 if (mCursor.getCount() == 0) {
1000 mCursor.moveToNext();
1001 ensurePlayListCapacity(1);
1003 mPlayList[0] = mCursor.getLong(IDCOLIDX);
1007 } catch (UnsupportedOperationException ex) {
1011 mPlayer.setDataSource(mFileToPlay);
1013 if (! mPlayer.isInitialized()) {
1015 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
1016 // beware: this ends up being recursive because next() calls open() again.
1019 if (! mPlayer.isInitialized() && mOpenFailedCounter != 0) {
1020 // need to make sure we only shows this once
1021 mOpenFailedCounter = 0;
1023 Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
1025 Log.d(LOGTAG, "Failed to open file for playback");
1028 mOpenFailedCounter = 0;
1034 * Starts playback of a previously opened file.
1036 public void play() {
1037 if (mPlayer.isInitialized()) {
1038 // if we are at the end of the song, go to the next song first
1039 long duration = mPlayer.duration();
1040 if (mRepeatMode != REPEAT_CURRENT && duration > 2000 &&
1041 mPlayer.position() >= duration - 2000) {
1047 RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar);
1048 views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer);
1049 if (getAudioId() < 0) {
1051 views.setTextViewText(R.id.trackname, getPath());
1052 views.setTextViewText(R.id.artistalbum, null);
1054 String artist = getArtistName();
1055 views.setTextViewText(R.id.trackname, getTrackName());
1056 if (artist == null || artist.equals(MediaStore.UNKNOWN_STRING)) {
1057 artist = getString(R.string.unknown_artist_name);
1059 String album = getAlbumName();
1060 if (album == null || album.equals(MediaStore.UNKNOWN_STRING)) {
1061 album = getString(R.string.unknown_album_name);
1064 views.setTextViewText(R.id.artistalbum,
1065 getString(R.string.notification_artist_album, artist, album)
1069 Notification status = new Notification();
1070 status.contentView = views;
1071 status.flags |= Notification.FLAG_ONGOING_EVENT;
1072 status.icon = R.drawable.stat_notify_musicplayer;
1073 status.contentIntent = PendingIntent.getActivity(this, 0,
1074 new Intent("com.android.music.PLAYBACK_VIEWER")
1075 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0);
1076 startForeground(PLAYBACKSERVICE_STATUS, status);
1077 if (!mIsSupposedToBePlaying) {
1078 mIsSupposedToBePlaying = true;
1079 notifyChange(PLAYSTATE_CHANGED);
1082 } else if (mPlayListLen <= 0) {
1083 // This is mostly so that if you press 'play' on a bluetooth headset
1084 // without every having played anything before, it will still play
1086 setShuffleMode(SHUFFLE_AUTO);
1090 private void stop(boolean remove_status_icon) {
1091 if (mPlayer.isInitialized()) {
1095 if (mCursor != null) {
1099 if (remove_status_icon) {
1102 stopForeground(false);
1104 if (remove_status_icon) {
1105 mIsSupposedToBePlaying = false;
1112 public void stop() {
1117 * Pauses playback (call play() to resume)
1119 public void pause() {
1120 synchronized(this) {
1124 mIsSupposedToBePlaying = false;
1125 notifyChange(PLAYSTATE_CHANGED);
1126 saveBookmarkIfNeeded();
1131 /** Returns whether something is currently playing
1133 * @return true if something is playing (or will be playing shortly, in case
1134 * we're currently transitioning between tracks), false if not.
1136 public boolean isPlaying() {
1137 return mIsSupposedToBePlaying;
1141 Desired behavior for prev/next/shuffle:
1143 - NEXT will move to the next track in the list when not shuffling, and to
1144 a track randomly picked from the not-yet-played tracks when shuffling.
1145 If all tracks have already been played, pick from the full set, but
1146 avoid picking the previously played track if possible.
1147 - when shuffling, PREV will go to the previously played track. Hitting PREV
1148 again will go to the track played before that, etc. When the start of the
1149 history has been reached, PREV is a no-op.
1150 When not shuffling, PREV will go to the sequentially previous track (the
1151 difference with the shuffle-case is mainly that when not shuffling, the
1152 user can back up to tracks that are not in the history).
1155 When playing an album with 10 tracks from the start, and enabling shuffle
1156 while playing track 5, the remaining tracks (6-10) will be shuffled, e.g.
1157 the final play order might be 1-2-3-4-5-8-10-6-9-7.
1158 When hitting 'prev' 8 times while playing track 7 in this example, the
1159 user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next',
1160 a random track will be picked again. If at any time user disables shuffling
1161 the next/previous track will be picked in sequential order again.
1164 public void prev() {
1165 synchronized (this) {
1167 // we were playing a specific file not part of a playlist, so there is no 'previous'
1172 if (mShuffleMode == SHUFFLE_NORMAL) {
1173 // go to previously-played track and remove it from the history
1174 int histsize = mHistory.size();
1175 if (histsize == 0) {
1179 Integer pos = mHistory.remove(histsize - 1);
1180 mPlayPos = pos.intValue();
1185 mPlayPos = mPlayListLen - 1;
1188 saveBookmarkIfNeeded();
1192 notifyChange(META_CHANGED);
1196 public void next(boolean force) {
1197 synchronized (this) {
1199 // we were playing a specific file not part of a playlist, so there is no 'next'
1205 if (mPlayListLen <= 0) {
1206 Log.d(LOGTAG, "No play queue");
1210 // Store the current file in the history, but keep the history at a
1212 if (mPlayPos >= 0) {
1213 mHistory.add(Integer.valueOf(mPlayPos));
1215 if (mHistory.size() > MAX_HISTORY_SIZE) {
1216 mHistory.removeElementAt(0);
1219 if (mShuffleMode == SHUFFLE_NORMAL) {
1220 // Pick random next track from the not-yet-played ones
1221 // TODO: make it work right after adding/removing items in the queue.
1223 int numTracks = mPlayListLen;
1224 int[] tracks = new int[numTracks];
1225 for (int i=0;i < numTracks; i++) {
1229 int numHistory = mHistory.size();
1230 int numUnplayed = numTracks;
1231 for (int i=0;i < numHistory; i++) {
1232 int idx = mHistory.get(i).intValue();
1233 if (idx < numTracks && tracks[idx] >= 0) {
1239 // 'numUnplayed' now indicates how many tracks have not yet
1240 // been played, and 'tracks' contains the indices of those
1242 if (numUnplayed <=0) {
1243 // everything's already been played
1244 if (mRepeatMode == REPEAT_ALL || force) {
1245 //pick from full set
1246 numUnplayed = numTracks;
1247 for (int i=0;i < numTracks; i++) {
1256 int skip = mRand.nextInt(numUnplayed);
1259 while (tracks[++cnt] < 0)
1267 } else if (mShuffleMode == SHUFFLE_AUTO) {
1268 doAutoShuffleUpdate();
1271 if (mPlayPos >= mPlayListLen - 1) {
1272 // we're at the end of the list
1273 if (mRepeatMode == REPEAT_NONE && !force) {
1276 notifyChange(PLAYBACK_COMPLETE);
1277 mIsSupposedToBePlaying = false;
1279 } else if (mRepeatMode == REPEAT_ALL || force) {
1286 saveBookmarkIfNeeded();
1290 notifyChange(META_CHANGED);
1294 private void gotoIdleState() {
1295 mDelayedStopHandler.removeCallbacksAndMessages(null);
1296 Message msg = mDelayedStopHandler.obtainMessage();
1297 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
1298 stopForeground(true);
1301 private void saveBookmarkIfNeeded() {
1304 long pos = position();
1305 long bookmark = getBookmark();
1306 long duration = duration();
1307 if ((pos < bookmark && (pos + 10000) > bookmark) ||
1308 (pos > bookmark && (pos - 10000) < bookmark)) {
1309 // The existing bookmark is close to the current
1310 // position, so don't update it.
1313 if (pos < 15000 || (pos + 10000) > duration) {
1314 // if we're near the start or end, clear the bookmark
1318 // write 'pos' to the bookmark field
1319 ContentValues values = new ContentValues();
1320 values.put(MediaStore.Audio.Media.BOOKMARK, pos);
1321 Uri uri = ContentUris.withAppendedId(
1322 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX));
1323 getContentResolver().update(uri, values, null, null);
1325 } catch (SQLiteException ex) {
1329 // Make sure there are at least 5 items after the currently playing item
1330 // and no more than 10 items before.
1331 private void doAutoShuffleUpdate() {
1332 boolean notify = false;
1333 // remove old entries
1334 if (mPlayPos > 10) {
1335 removeTracks(0, mPlayPos - 9);
1338 // add new entries if needed
1339 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
1340 for (int i = 0; i < to_add; i++) {
1341 // pick something at random from the list
1342 int idx = mRand.nextInt(mAutoShuffleList.length);
1343 long which = mAutoShuffleList[idx];
1344 ensurePlayListCapacity(mPlayListLen + 1);
1345 mPlayList[mPlayListLen++] = which;
1349 notifyChange(QUEUE_CHANGED);
1353 // A simple variation of Random that makes sure that the
1354 // value it returns is not equal to the value it returned
1355 // previously, unless the interval is 1.
1356 private static class Shuffler {
1357 private int mPrevious;
1358 private Random mRandom = new Random();
1359 public int nextInt(int interval) {
1362 ret = mRandom.nextInt(interval);
1363 } while (ret == mPrevious && interval > 1);
1369 private boolean makeAutoShuffleList() {
1370 ContentResolver res = getContentResolver();
1373 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1374 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
1376 if (c == null || c.getCount() == 0) {
1379 int len = c.getCount();
1380 long [] list = new long[len];
1381 for (int i = 0; i < len; i++) {
1383 list[i] = c.getLong(0);
1385 mAutoShuffleList = list;
1387 } catch (RuntimeException ex) {
1397 * Removes the range of tracks specified from the play list. If a file within the range is
1398 * the file currently being played, playback will move to the next file after the
1400 * @param first The first file to be removed
1401 * @param last The last file to be removed
1402 * @return the number of tracks deleted
1404 public int removeTracks(int first, int last) {
1405 int numremoved = removeTracksInternal(first, last);
1406 if (numremoved > 0) {
1407 notifyChange(QUEUE_CHANGED);
1412 private int removeTracksInternal(int first, int last) {
1413 synchronized (this) {
1414 if (last < first) return 0;
1415 if (first < 0) first = 0;
1416 if (last >= mPlayListLen) last = mPlayListLen - 1;
1418 boolean gotonext = false;
1419 if (first <= mPlayPos && mPlayPos <= last) {
1422 } else if (mPlayPos > last) {
1423 mPlayPos -= (last - first + 1);
1425 int num = mPlayListLen - last - 1;
1426 for (int i = 0; i < num; i++) {
1427 mPlayList[first + i] = mPlayList[last + 1 + i];
1429 mPlayListLen -= last - first + 1;
1432 if (mPlayListLen == 0) {
1436 if (mPlayPos >= mPlayListLen) {
1439 boolean wasPlaying = isPlaying();
1447 return last - first + 1;
1452 * Removes all instances of the track with the given id
1453 * from the playlist.
1454 * @param id The id to be removed
1455 * @return how many instances of the track were removed
1457 public int removeTrack(long id) {
1459 synchronized (this) {
1460 for (int i = 0; i < mPlayListLen; i++) {
1461 if (mPlayList[i] == id) {
1462 numremoved += removeTracksInternal(i, i);
1467 if (numremoved > 0) {
1468 notifyChange(QUEUE_CHANGED);
1473 public void setShuffleMode(int shufflemode) {
1474 synchronized(this) {
1475 if (mShuffleMode == shufflemode && mPlayListLen > 0) {
1478 mShuffleMode = shufflemode;
1479 if (mShuffleMode == SHUFFLE_AUTO) {
1480 if (makeAutoShuffleList()) {
1482 doAutoShuffleUpdate();
1486 notifyChange(META_CHANGED);
1489 // failed to build a list of files to shuffle
1490 mShuffleMode = SHUFFLE_NONE;
1496 public int getShuffleMode() {
1497 return mShuffleMode;
1500 public void setRepeatMode(int repeatmode) {
1501 synchronized(this) {
1502 mRepeatMode = repeatmode;
1506 public int getRepeatMode() {
1510 public int getMediaMountedCount() {
1511 return mMediaMountedCount;
1515 * Returns the path of the currently playing file, or null if
1516 * no file is currently playing.
1518 public String getPath() {
1523 * Returns the rowid of the currently playing file, or -1 if
1524 * no file is currently playing.
1526 public long getAudioId() {
1527 synchronized (this) {
1528 if (mPlayPos >= 0 && mPlayer.isInitialized()) {
1529 return mPlayList[mPlayPos];
1536 * Returns the position in the queue
1537 * @return the position in the queue
1539 public int getQueuePosition() {
1540 synchronized(this) {
1546 * Starts playing the track at the given position in the queue.
1547 * @param pos The position in the queue of the track that will be played.
1549 public void setQueuePosition(int pos) {
1550 synchronized(this) {
1555 notifyChange(META_CHANGED);
1556 if (mShuffleMode == SHUFFLE_AUTO) {
1557 doAutoShuffleUpdate();
1562 public String getArtistName() {
1563 synchronized(this) {
1564 if (mCursor == null) {
1567 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
1571 public long getArtistId() {
1572 synchronized (this) {
1573 if (mCursor == null) {
1576 return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));
1580 public String getAlbumName() {
1581 synchronized (this) {
1582 if (mCursor == null) {
1585 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
1589 public long getAlbumId() {
1590 synchronized (this) {
1591 if (mCursor == null) {
1594 return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));
1598 public String getTrackName() {
1599 synchronized (this) {
1600 if (mCursor == null) {
1603 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
1607 private boolean isPodcast() {
1608 synchronized (this) {
1609 if (mCursor == null) {
1612 return (mCursor.getInt(PODCASTCOLIDX) > 0);
1616 private long getBookmark() {
1617 synchronized (this) {
1618 if (mCursor == null) {
1621 return mCursor.getLong(BOOKMARKCOLIDX);
1626 * Returns the duration of the file in milliseconds.
1627 * Currently this method returns -1 for the duration of MIDI files.
1629 public long duration() {
1630 if (mPlayer.isInitialized()) {
1631 return mPlayer.duration();
1637 * Returns the current playback position in milliseconds
1639 public long position() {
1640 if (mPlayer.isInitialized()) {
1641 return mPlayer.position();
1647 * Seeks to the position specified.
1649 * @param pos The position to seek to, in milliseconds
1651 public long seek(long pos) {
1652 if (mPlayer.isInitialized()) {
1653 if (pos < 0) pos = 0;
1654 if (pos > mPlayer.duration()) pos = mPlayer.duration();
1655 return mPlayer.seek(pos);
1661 * Provides a unified interface for dealing with midi files and
1662 * other media files.
1664 private class MultiPlayer {
1665 private MediaPlayer mMediaPlayer = new MediaPlayer();
1666 private Handler mHandler;
1667 private boolean mIsInitialized = false;
1669 public MultiPlayer() {
1670 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1673 public void setDataSourceAsync(String path) {
1675 mMediaPlayer.reset();
1676 mMediaPlayer.setDataSource(path);
1677 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1678 mMediaPlayer.setOnPreparedListener(preparedlistener);
1679 mMediaPlayer.prepareAsync();
1680 } catch (IOException ex) {
1681 // TODO: notify the user why the file couldn't be opened
1682 mIsInitialized = false;
1684 } catch (IllegalArgumentException ex) {
1685 // TODO: notify the user why the file couldn't be opened
1686 mIsInitialized = false;
1689 mMediaPlayer.setOnCompletionListener(listener);
1690 mMediaPlayer.setOnErrorListener(errorListener);
1692 mIsInitialized = true;
1695 public void setDataSource(String path) {
1697 mMediaPlayer.reset();
1698 mMediaPlayer.setOnPreparedListener(null);
1699 if (path.startsWith("content://")) {
1700 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path));
1702 mMediaPlayer.setDataSource(path);
1704 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1705 mMediaPlayer.prepare();
1706 } catch (IOException ex) {
1707 // TODO: notify the user why the file couldn't be opened
1708 mIsInitialized = false;
1710 } catch (IllegalArgumentException ex) {
1711 // TODO: notify the user why the file couldn't be opened
1712 mIsInitialized = false;
1715 mMediaPlayer.setOnCompletionListener(listener);
1716 mMediaPlayer.setOnErrorListener(errorListener);
1718 mIsInitialized = true;
1721 public boolean isInitialized() {
1722 return mIsInitialized;
1725 public void start() {
1726 mMediaPlayer.start();
1729 public void stop() {
1730 mMediaPlayer.reset();
1731 mIsInitialized = false;
1735 * You CANNOT use this player anymore after calling release()
1737 public void release() {
1739 mMediaPlayer.release();
1742 public void pause() {
1743 mMediaPlayer.pause();
1746 public void setHandler(Handler handler) {
1750 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {
1751 public void onCompletion(MediaPlayer mp) {
1752 // Acquire a temporary wakelock, since when we return from
1753 // this callback the MediaPlayer will release its wakelock
1754 // and allow the device to go to sleep.
1755 // This temporary wakelock is released when the RELEASE_WAKELOCK
1756 // message is processed, but just in case, put a timeout on it.
1757 mWakeLock.acquire(30000);
1758 mHandler.sendEmptyMessage(TRACK_ENDED);
1759 mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
1763 MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() {
1764 public void onPrepared(MediaPlayer mp) {
1765 notifyChange(ASYNC_OPEN_COMPLETE);
1769 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {
1770 public boolean onError(MediaPlayer mp, int what, int extra) {
1772 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
1773 mIsInitialized = false;
1774 mMediaPlayer.release();
1775 // Creating a new MediaPlayer and settings its wakemode does not
1776 // require the media service, so it's OK to do this now, while the
1777 // service is still being restarted
1778 mMediaPlayer = new MediaPlayer();
1779 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1780 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
1783 Log.d("MultiPlayer", "Error: " + what + "," + extra);
1790 public long duration() {
1791 return mMediaPlayer.getDuration();
1794 public long position() {
1795 return mMediaPlayer.getCurrentPosition();
1798 public long seek(long whereto) {
1799 mMediaPlayer.seekTo((int) whereto);
1803 public void setVolume(float vol) {
1804 mMediaPlayer.setVolume(vol, vol);
1809 * By making this a static class with a WeakReference to the Service, we
1810 * ensure that the Service can be GCd even when the system process still
1811 * has a remote reference to the stub.
1813 static class ServiceStub extends IMediaPlaybackService.Stub {
1814 WeakReference<MediaPlaybackService> mService;
1816 ServiceStub(MediaPlaybackService service) {
1817 mService = new WeakReference<MediaPlaybackService>(service);
1820 public void openFileAsync(String path)
1822 mService.get().openAsync(path);
1824 public void openFile(String path, boolean oneShot)
1826 mService.get().open(path, oneShot);
1828 public void open(long [] list, int position) {
1829 mService.get().open(list, position);
1831 public int getQueuePosition() {
1832 return mService.get().getQueuePosition();
1834 public void setQueuePosition(int index) {
1835 mService.get().setQueuePosition(index);
1837 public boolean isPlaying() {
1838 return mService.get().isPlaying();
1840 public void stop() {
1841 mService.get().stop();
1843 public void pause() {
1844 mService.get().pause();
1846 public void play() {
1847 mService.get().play();
1849 public void prev() {
1850 mService.get().prev();
1852 public void next() {
1853 mService.get().next(true);
1855 public String getTrackName() {
1856 return mService.get().getTrackName();
1858 public String getAlbumName() {
1859 return mService.get().getAlbumName();
1861 public long getAlbumId() {
1862 return mService.get().getAlbumId();
1864 public String getArtistName() {
1865 return mService.get().getArtistName();
1867 public long getArtistId() {
1868 return mService.get().getArtistId();
1870 public void enqueue(long [] list , int action) {
1871 mService.get().enqueue(list, action);
1873 public long [] getQueue() {
1874 return mService.get().getQueue();
1876 public void moveQueueItem(int from, int to) {
1877 mService.get().moveQueueItem(from, to);
1879 public String getPath() {
1880 return mService.get().getPath();
1882 public long getAudioId() {
1883 return mService.get().getAudioId();
1885 public long position() {
1886 return mService.get().position();
1888 public long duration() {
1889 return mService.get().duration();
1891 public long seek(long pos) {
1892 return mService.get().seek(pos);
1894 public void setShuffleMode(int shufflemode) {
1895 mService.get().setShuffleMode(shufflemode);
1897 public int getShuffleMode() {
1898 return mService.get().getShuffleMode();
1900 public int removeTracks(int first, int last) {
1901 return mService.get().removeTracks(first, last);
1903 public int removeTrack(long id) {
1904 return mService.get().removeTrack(id);
1906 public void setRepeatMode(int repeatmode) {
1907 mService.get().setRepeatMode(repeatmode);
1909 public int getRepeatMode() {
1910 return mService.get().getRepeatMode();
1912 public int getMediaMountedCount() {
1913 return mService.get().getMediaMountedCount();
1918 private final IBinder mBinder = new ServiceStub(this);