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