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