OSDN Git Service

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