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