OSDN Git Service

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