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