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