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