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