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