OSDN Git Service

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