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