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