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