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