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