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