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