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