OSDN Git Service

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