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