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