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