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