MediaPlaybackService.java revision b6e7bf7ddcc861e81219f9869b5dddc101341458
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 something is currently playing
1072     *
1073     * @return true if something is playing (or will be playing shortly, in case
1074     * we're currently transitioning between tracks), false if not.
1075     */
1076    public boolean isPlaying() {
1077        return mIsSupposedToBePlaying;
1078    }
1079
1080    /*
1081      Desired behavior for prev/next/shuffle:
1082
1083      - NEXT will move to the next track in the list when not shuffling, and to
1084        a track randomly picked from the not-yet-played tracks when shuffling.
1085        If all tracks have already been played, pick from the full set, but
1086        avoid picking the previously played track if possible.
1087      - when shuffling, PREV will go to the previously played track. Hitting PREV
1088        again will go to the track played before that, etc. When the start of the
1089        history has been reached, PREV is a no-op.
1090        When not shuffling, PREV will go to the sequentially previous track (the
1091        difference with the shuffle-case is mainly that when not shuffling, the
1092        user can back up to tracks that are not in the history).
1093
1094        Example:
1095        When playing an album with 10 tracks from the start, and enabling shuffle
1096        while playing track 5, the remaining tracks (6-10) will be shuffled, e.g.
1097        the final play order might be 1-2-3-4-5-8-10-6-9-7.
1098        When hitting 'prev' 8 times while playing track 7 in this example, the
1099        user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next',
1100        a random track will be picked again. If at any time user disables shuffling
1101        the next/previous track will be picked in sequential order again.
1102     */
1103
1104    public void prev() {
1105        synchronized (this) {
1106            if (mOneShot) {
1107                // we were playing a specific file not part of a playlist, so there is no 'previous'
1108                seek(0);
1109                play();
1110                return;
1111            }
1112            if (mShuffleMode == SHUFFLE_NORMAL) {
1113                // go to previously-played track and remove it from the history
1114                int histsize = mHistory.size();
1115                if (histsize == 0) {
1116                    // prev is a no-op
1117                    return;
1118                }
1119                Integer pos = mHistory.remove(histsize - 1);
1120                mPlayPos = pos.intValue();
1121            } else {
1122                if (mPlayPos > 0) {
1123                    mPlayPos--;
1124                } else {
1125                    mPlayPos = mPlayListLen - 1;
1126                }
1127            }
1128            saveBookmarkIfNeeded();
1129            stop(false);
1130            openCurrent();
1131            play();
1132            notifyChange(META_CHANGED);
1133        }
1134    }
1135
1136    public void next(boolean force) {
1137        synchronized (this) {
1138            if (mOneShot) {
1139                // we were playing a specific file not part of a playlist, so there is no 'next'
1140                seek(0);
1141                play();
1142                return;
1143            }
1144
1145            // Store the current file in the history, but keep the history at a
1146            // reasonable size
1147            if (mPlayPos >= 0) {
1148                mHistory.add(Integer.valueOf(mPlayPos));
1149            }
1150            if (mHistory.size() > MAX_HISTORY_SIZE) {
1151                mHistory.removeElementAt(0);
1152            }
1153
1154            if (mShuffleMode == SHUFFLE_NORMAL) {
1155                // Pick random next track from the not-yet-played ones
1156                // TODO: make it work right after adding/removing items in the queue.
1157
1158                int numTracks = mPlayListLen;
1159                int[] tracks = new int[numTracks];
1160                for (int i=0;i < numTracks; i++) {
1161                    tracks[i] = i;
1162                }
1163
1164                int numHistory = mHistory.size();
1165                int numUnplayed = numTracks;
1166                for (int i=0;i < numHistory; i++) {
1167                    int idx = mHistory.get(i).intValue();
1168                    if (idx < numTracks && tracks[idx] >= 0) {
1169                        numUnplayed--;
1170                        tracks[idx] = -1;
1171                    }
1172                }
1173
1174                // 'numUnplayed' now indicates how many tracks have not yet
1175                // been played, and 'tracks' contains the indices of those
1176                // tracks.
1177                if (numUnplayed <=0) {
1178                    // everything's already been played
1179                    if (mRepeatMode == REPEAT_ALL || force) {
1180                        //pick from full set
1181                        numUnplayed = numTracks;
1182                        for (int i=0;i < numTracks; i++) {
1183                            tracks[i] = i;
1184                        }
1185                    } else {
1186                        // all done
1187                        gotoIdleState();
1188                        return;
1189                    }
1190                }
1191                int skip = mRand.nextInt(numUnplayed);
1192                int cnt = -1;
1193                while (true) {
1194                    while (tracks[++cnt] < 0)
1195                        ;
1196                    skip--;
1197                    if (skip < 0) {
1198                        break;
1199                    }
1200                }
1201                mPlayPos = cnt;
1202            } else if (mShuffleMode == SHUFFLE_AUTO) {
1203                doAutoShuffleUpdate();
1204                mPlayPos++;
1205            } else {
1206                if (mPlayPos >= mPlayListLen - 1) {
1207                    // we're at the end of the list
1208                    if (mRepeatMode == REPEAT_NONE && !force) {
1209                        // all done
1210                        gotoIdleState();
1211                        notifyChange(PLAYBACK_COMPLETE);
1212                        mIsSupposedToBePlaying = false;
1213                        return;
1214                    } else if (mRepeatMode == REPEAT_ALL || force) {
1215                        mPlayPos = 0;
1216                    }
1217                } else {
1218                    mPlayPos++;
1219                }
1220            }
1221            saveBookmarkIfNeeded();
1222            stop(false);
1223            openCurrent();
1224            play();
1225            notifyChange(META_CHANGED);
1226        }
1227    }
1228
1229    private void gotoIdleState() {
1230        NotificationManager nm =
1231            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
1232        nm.cancel(PLAYBACKSERVICE_STATUS);
1233        mDelayedStopHandler.removeCallbacksAndMessages(null);
1234        Message msg = mDelayedStopHandler.obtainMessage();
1235        mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
1236    }
1237
1238    private void saveBookmarkIfNeeded() {
1239        try {
1240            if (isPodcast()) {
1241                long pos = position();
1242                long bookmark = getBookmark();
1243                long duration = duration();
1244                if ((pos < bookmark && (pos + 10000) > bookmark) ||
1245                        (pos > bookmark && (pos - 10000) < bookmark)) {
1246                    // The existing bookmark is close to the current
1247                    // position, so don't update it.
1248                    return;
1249                }
1250                if (pos < 15000 || (pos + 10000) > duration) {
1251                    // if we're near the start or end, clear the bookmark
1252                    pos = 0;
1253                }
1254
1255                // write 'pos' to the bookmark field
1256                ContentValues values = new ContentValues();
1257                values.put(MediaStore.Audio.Media.BOOKMARK, pos);
1258                Uri uri = ContentUris.withAppendedId(
1259                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX));
1260                getContentResolver().update(uri, values, null, null);
1261            }
1262        } catch (SQLiteException ex) {
1263        }
1264    }
1265
1266    // Make sure there are at least 5 items after the currently playing item
1267    // and no more than 10 items before.
1268    private void doAutoShuffleUpdate() {
1269        boolean notify = false;
1270        // remove old entries
1271        if (mPlayPos > 10) {
1272            removeTracks(0, mPlayPos - 9);
1273            notify = true;
1274        }
1275        // add new entries if needed
1276        int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
1277        for (int i = 0; i < to_add; i++) {
1278            // pick something at random from the list
1279            int idx = mRand.nextInt(mAutoShuffleList.length);
1280            Integer which = mAutoShuffleList[idx];
1281            ensurePlayListCapacity(mPlayListLen + 1);
1282            mPlayList[mPlayListLen++] = which;
1283            notify = true;
1284        }
1285        if (notify) {
1286            notifyChange(QUEUE_CHANGED);
1287        }
1288    }
1289
1290    // A simple variation of Random that makes sure that the
1291    // value it returns is not equal to the value it returned
1292    // previously, unless the interval is 1.
1293    private class Shuffler {
1294        private int mPrevious;
1295        private Random mRandom = new Random();
1296        public int nextInt(int interval) {
1297            int ret;
1298            do {
1299                ret = mRandom.nextInt(interval);
1300            } while (ret == mPrevious && interval > 1);
1301            mPrevious = ret;
1302            return ret;
1303        }
1304    };
1305
1306    private boolean makeAutoShuffleList() {
1307        ContentResolver res = getContentResolver();
1308        Cursor c = null;
1309        try {
1310            c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1311                    new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
1312                    null, null);
1313            if (c == null || c.getCount() == 0) {
1314                return false;
1315            }
1316            int len = c.getCount();
1317            int[] list = new int[len];
1318            for (int i = 0; i < len; i++) {
1319                c.moveToNext();
1320                list[i] = c.getInt(0);
1321            }
1322            mAutoShuffleList = list;
1323            return true;
1324        } catch (RuntimeException ex) {
1325        } finally {
1326            if (c != null) {
1327                c.close();
1328            }
1329        }
1330        return false;
1331    }
1332
1333    /**
1334     * Removes the range of tracks specified from the play list. If a file within the range is
1335     * the file currently being played, playback will move to the next file after the
1336     * range.
1337     * @param first The first file to be removed
1338     * @param last The last file to be removed
1339     * @return the number of tracks deleted
1340     */
1341    public int removeTracks(int first, int last) {
1342        int numremoved = removeTracksInternal(first, last);
1343        if (numremoved > 0) {
1344            notifyChange(QUEUE_CHANGED);
1345        }
1346        return numremoved;
1347    }
1348
1349    private int removeTracksInternal(int first, int last) {
1350        synchronized (this) {
1351            if (last < first) return 0;
1352            if (first < 0) first = 0;
1353            if (last >= mPlayListLen) last = mPlayListLen - 1;
1354
1355            boolean gotonext = false;
1356            if (first <= mPlayPos && mPlayPos <= last) {
1357                mPlayPos = first;
1358                gotonext = true;
1359            } else if (mPlayPos > last) {
1360                mPlayPos -= (last - first + 1);
1361            }
1362            int num = mPlayListLen - last - 1;
1363            for (int i = 0; i < num; i++) {
1364                mPlayList[first + i] = mPlayList[last + 1 + i];
1365            }
1366            mPlayListLen -= last - first + 1;
1367
1368            if (gotonext) {
1369                if (mPlayListLen == 0) {
1370                    stop(true);
1371                    mPlayPos = -1;
1372                } else {
1373                    if (mPlayPos >= mPlayListLen) {
1374                        mPlayPos = 0;
1375                    }
1376                    boolean wasPlaying = isPlaying();
1377                    stop(false);
1378                    openCurrent();
1379                    if (wasPlaying) {
1380                        play();
1381                    }
1382                }
1383            }
1384            return last - first + 1;
1385        }
1386    }
1387
1388    /**
1389     * Removes all instances of the track with the given id
1390     * from the playlist.
1391     * @param id The id to be removed
1392     * @return how many instances of the track were removed
1393     */
1394    public int removeTrack(int id) {
1395        int numremoved = 0;
1396        synchronized (this) {
1397            for (int i = 0; i < mPlayListLen; i++) {
1398                if (mPlayList[i] == id) {
1399                    numremoved += removeTracksInternal(i, i);
1400                    i--;
1401                }
1402            }
1403        }
1404        if (numremoved > 0) {
1405            notifyChange(QUEUE_CHANGED);
1406        }
1407        return numremoved;
1408    }
1409
1410    public void setShuffleMode(int shufflemode) {
1411        synchronized(this) {
1412            if (mShuffleMode == shufflemode && mPlayListLen > 0) {
1413                return;
1414            }
1415            mShuffleMode = shufflemode;
1416            if (mShuffleMode == SHUFFLE_AUTO) {
1417                if (makeAutoShuffleList()) {
1418                    mPlayListLen = 0;
1419                    doAutoShuffleUpdate();
1420                    mPlayPos = 0;
1421                    openCurrent();
1422                    play();
1423                    notifyChange(META_CHANGED);
1424                    return;
1425                } else {
1426                    // failed to build a list of files to shuffle
1427                    mShuffleMode = SHUFFLE_NONE;
1428                }
1429            }
1430            saveQueue(false);
1431        }
1432    }
1433    public int getShuffleMode() {
1434        return mShuffleMode;
1435    }
1436
1437    public void setRepeatMode(int repeatmode) {
1438        synchronized(this) {
1439            mRepeatMode = repeatmode;
1440            saveQueue(false);
1441        }
1442    }
1443    public int getRepeatMode() {
1444        return mRepeatMode;
1445    }
1446
1447    public int getMediaMountedCount() {
1448        return mMediaMountedCount;
1449    }
1450
1451    /**
1452     * Returns the path of the currently playing file, or null if
1453     * no file is currently playing.
1454     */
1455    public String getPath() {
1456        return mFileToPlay;
1457    }
1458
1459    /**
1460     * Returns the rowid of the currently playing file, or -1 if
1461     * no file is currently playing.
1462     */
1463    public int getAudioId() {
1464        synchronized (this) {
1465            if (mPlayPos >= 0 && mPlayer.isInitialized()) {
1466                return mPlayList[mPlayPos];
1467            }
1468        }
1469        return -1;
1470    }
1471
1472    /**
1473     * Returns the position in the queue
1474     * @return the position in the queue
1475     */
1476    public int getQueuePosition() {
1477        synchronized(this) {
1478            return mPlayPos;
1479        }
1480    }
1481
1482    /**
1483     * Starts playing the track at the given position in the queue.
1484     * @param pos The position in the queue of the track that will be played.
1485     */
1486    public void setQueuePosition(int pos) {
1487        synchronized(this) {
1488            stop(false);
1489            mPlayPos = pos;
1490            openCurrent();
1491            play();
1492            notifyChange(META_CHANGED);
1493        }
1494    }
1495
1496    public String getArtistName() {
1497        synchronized(this) {
1498            if (mCursor == null) {
1499                return null;
1500            }
1501            return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
1502        }
1503    }
1504
1505    public int getArtistId() {
1506        synchronized (this) {
1507            if (mCursor == null) {
1508                return -1;
1509            }
1510            return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));
1511        }
1512    }
1513
1514    public String getAlbumName() {
1515        synchronized (this) {
1516            if (mCursor == null) {
1517                return null;
1518            }
1519            return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
1520        }
1521    }
1522
1523    public int getAlbumId() {
1524        synchronized (this) {
1525            if (mCursor == null) {
1526                return -1;
1527            }
1528            return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));
1529        }
1530    }
1531
1532    public String getTrackName() {
1533        synchronized (this) {
1534            if (mCursor == null) {
1535                return null;
1536            }
1537            return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
1538        }
1539    }
1540
1541    private boolean isPodcast() {
1542        synchronized (this) {
1543            if (mCursor == null) {
1544                return false;
1545            }
1546            return (mCursor.getInt(PODCASTCOLIDX) > 0);
1547        }
1548    }
1549
1550    private long getBookmark() {
1551        synchronized (this) {
1552            if (mCursor == null) {
1553                return 0;
1554            }
1555            return mCursor.getLong(BOOKMARKCOLIDX);
1556        }
1557    }
1558
1559    /**
1560     * Returns the duration of the file in milliseconds.
1561     * Currently this method returns -1 for the duration of MIDI files.
1562     */
1563    public long duration() {
1564        if (mPlayer.isInitialized()) {
1565            return mPlayer.duration();
1566        }
1567        return -1;
1568    }
1569
1570    /**
1571     * Returns the current playback position in milliseconds
1572     */
1573    public long position() {
1574        if (mPlayer.isInitialized()) {
1575            return mPlayer.position();
1576        }
1577        return -1;
1578    }
1579
1580    /**
1581     * Seeks to the position specified.
1582     *
1583     * @param pos The position to seek to, in milliseconds
1584     */
1585    public long seek(long pos) {
1586        if (mPlayer.isInitialized()) {
1587            if (pos < 0) pos = 0;
1588            if (pos > mPlayer.duration()) pos = mPlayer.duration();
1589            return mPlayer.seek(pos);
1590        }
1591        return -1;
1592    }
1593
1594    /**
1595     * Provides a unified interface for dealing with midi files and
1596     * other media files.
1597     */
1598    private class MultiPlayer {
1599        private MediaPlayer mMediaPlayer = new MediaPlayer();
1600        private Handler mHandler;
1601        private boolean mIsInitialized = false;
1602
1603        public MultiPlayer() {
1604            mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1605        }
1606
1607        public void setDataSourceAsync(String path) {
1608            try {
1609                mMediaPlayer.reset();
1610                mMediaPlayer.setDataSource(path);
1611                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1612                mMediaPlayer.setOnPreparedListener(preparedlistener);
1613                mMediaPlayer.prepareAsync();
1614            } catch (IOException ex) {
1615                // TODO: notify the user why the file couldn't be opened
1616                mIsInitialized = false;
1617                return;
1618            } catch (IllegalArgumentException ex) {
1619                // TODO: notify the user why the file couldn't be opened
1620                mIsInitialized = false;
1621                return;
1622            }
1623            mMediaPlayer.setOnCompletionListener(listener);
1624            mMediaPlayer.setOnErrorListener(errorListener);
1625
1626            mIsInitialized = true;
1627        }
1628
1629        public void setDataSource(String path) {
1630            try {
1631                mMediaPlayer.reset();
1632                mMediaPlayer.setOnPreparedListener(null);
1633                if (path.startsWith("content://")) {
1634                    mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path));
1635                } else {
1636                    mMediaPlayer.setDataSource(path);
1637                }
1638                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1639                mMediaPlayer.prepare();
1640            } catch (IOException ex) {
1641                // TODO: notify the user why the file couldn't be opened
1642                mIsInitialized = false;
1643                return;
1644            } catch (IllegalArgumentException ex) {
1645                // TODO: notify the user why the file couldn't be opened
1646                mIsInitialized = false;
1647                return;
1648            }
1649            mMediaPlayer.setOnCompletionListener(listener);
1650            mMediaPlayer.setOnErrorListener(errorListener);
1651
1652            mIsInitialized = true;
1653        }
1654
1655        public boolean isInitialized() {
1656            return mIsInitialized;
1657        }
1658
1659        public void start() {
1660            mMediaPlayer.start();
1661        }
1662
1663        public void stop() {
1664            mMediaPlayer.reset();
1665            mIsInitialized = false;
1666        }
1667
1668        public void pause() {
1669            mMediaPlayer.pause();
1670        }
1671
1672        public void setHandler(Handler handler) {
1673            mHandler = handler;
1674        }
1675
1676        MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {
1677            public void onCompletion(MediaPlayer mp) {
1678                // Acquire a temporary wakelock, since when we return from
1679                // this callback the MediaPlayer will release its wakelock
1680                // and allow the device to go to sleep.
1681                // This temporary wakelock is released when the RELEASE_WAKELOCK
1682                // message is processed, but just in case, put a timeout on it.
1683                mWakeLock.acquire(30000);
1684                mHandler.sendEmptyMessage(TRACK_ENDED);
1685                mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
1686            }
1687        };
1688
1689        MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() {
1690            public void onPrepared(MediaPlayer mp) {
1691                notifyChange(ASYNC_OPEN_COMPLETE);
1692            }
1693        };
1694
1695        MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {
1696            public boolean onError(MediaPlayer mp, int what, int extra) {
1697                switch (what) {
1698                case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
1699                    mIsInitialized = false;
1700                    mMediaPlayer.release();
1701                    // Creating a new MediaPlayer and settings its wakemode does not
1702                    // require the media service, so it's OK to do this now, while the
1703                    // service is still being restarted
1704                    mMediaPlayer = new MediaPlayer();
1705                    mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1706                    mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
1707                    return true;
1708                default:
1709                    break;
1710                }
1711                return false;
1712           }
1713        };
1714
1715        public long duration() {
1716            return mMediaPlayer.getDuration();
1717        }
1718
1719        public long position() {
1720            return mMediaPlayer.getCurrentPosition();
1721        }
1722
1723        public long seek(long whereto) {
1724            mMediaPlayer.seekTo((int) whereto);
1725            return whereto;
1726        }
1727
1728        public void setVolume(float vol) {
1729            mMediaPlayer.setVolume(vol, vol);
1730        }
1731    }
1732
1733    private final IMediaPlaybackService.Stub mBinder = new IMediaPlaybackService.Stub()
1734    {
1735        public void openFileAsync(String path)
1736        {
1737            MediaPlaybackService.this.openAsync(path);
1738        }
1739        public void openFile(String path, boolean oneShot)
1740        {
1741            MediaPlaybackService.this.open(path, oneShot);
1742        }
1743        public void open(int [] list, int position) {
1744            MediaPlaybackService.this.open(list, position);
1745        }
1746        public int getQueuePosition() {
1747            return MediaPlaybackService.this.getQueuePosition();
1748        }
1749        public void setQueuePosition(int index) {
1750            MediaPlaybackService.this.setQueuePosition(index);
1751        }
1752        public boolean isPlaying() {
1753            return MediaPlaybackService.this.isPlaying();
1754        }
1755        public void stop() {
1756            MediaPlaybackService.this.stop();
1757        }
1758        public void pause() {
1759            MediaPlaybackService.this.pause();
1760        }
1761        public void play() {
1762            MediaPlaybackService.this.play();
1763        }
1764        public void prev() {
1765            MediaPlaybackService.this.prev();
1766        }
1767        public void next() {
1768            MediaPlaybackService.this.next(true);
1769        }
1770        public String getTrackName() {
1771            return MediaPlaybackService.this.getTrackName();
1772        }
1773        public String getAlbumName() {
1774            return MediaPlaybackService.this.getAlbumName();
1775        }
1776        public int getAlbumId() {
1777            return MediaPlaybackService.this.getAlbumId();
1778        }
1779        public String getArtistName() {
1780            return MediaPlaybackService.this.getArtistName();
1781        }
1782        public int getArtistId() {
1783            return MediaPlaybackService.this.getArtistId();
1784        }
1785        public void enqueue(int [] list , int action) {
1786            MediaPlaybackService.this.enqueue(list, action);
1787        }
1788        public int [] getQueue() {
1789            return MediaPlaybackService.this.getQueue();
1790        }
1791        public void moveQueueItem(int from, int to) {
1792            MediaPlaybackService.this.moveQueueItem(from, to);
1793        }
1794        public String getPath() {
1795            return MediaPlaybackService.this.getPath();
1796        }
1797        public int getAudioId() {
1798            return MediaPlaybackService.this.getAudioId();
1799        }
1800        public long position() {
1801            return MediaPlaybackService.this.position();
1802        }
1803        public long duration() {
1804            return MediaPlaybackService.this.duration();
1805        }
1806        public long seek(long pos) {
1807            return MediaPlaybackService.this.seek(pos);
1808        }
1809        public void setShuffleMode(int shufflemode) {
1810            MediaPlaybackService.this.setShuffleMode(shufflemode);
1811        }
1812        public int getShuffleMode() {
1813            return MediaPlaybackService.this.getShuffleMode();
1814        }
1815        public int removeTracks(int first, int last) {
1816            return MediaPlaybackService.this.removeTracks(first, last);
1817        }
1818        public int removeTrack(int id) {
1819            return MediaPlaybackService.this.removeTrack(id);
1820        }
1821        public void setRepeatMode(int repeatmode) {
1822            MediaPlaybackService.this.setRepeatMode(repeatmode);
1823        }
1824        public int getRepeatMode() {
1825            return MediaPlaybackService.this.getRepeatMode();
1826        }
1827        public int getMediaMountedCount() {
1828            return MediaPlaybackService.this.getMediaMountedCount();
1829        }
1830    };
1831}
1832