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