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