2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.music;
19 import android.app.Notification;
20 import android.app.NotificationManager;
21 import android.app.PendingIntent;
22 import android.app.Service;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.BroadcastReceiver;
28 import android.content.SharedPreferences;
29 import android.content.SharedPreferences.Editor;
30 import android.database.Cursor;
31 import android.media.AudioManager;
32 import android.media.MediaFile;
33 import android.media.MediaPlayer;
34 import android.net.Uri;
35 import android.os.Environment;
36 import android.os.FileUtils;
37 import android.os.Handler;
38 import android.os.IBinder;
39 import android.os.Message;
40 import android.os.PowerManager;
41 import android.os.SystemClock;
42 import android.os.PowerManager.WakeLock;
43 import android.provider.MediaStore;
44 import android.provider.Settings;
45 import com.android.internal.telephony.Phone;
46 import com.android.internal.telephony.PhoneStateIntentReceiver;
47 import android.util.Log;
48 import android.widget.RemoteViews;
49 import android.widget.Toast;
51 import java.io.IOException;
52 import java.util.Random;
53 import java.util.Vector;
56 * Provides "background" audio playback capabilities, allowing the
57 * user to switch between activities without stopping playback.
59 public class MediaPlaybackService extends Service {
60 /** used to specify whether enqueue() should start playing
61 * the new list of files right away, next or once all the currently
62 * queued files have been played
64 public static final int NOW = 1;
65 public static final int NEXT = 2;
66 public static final int LAST = 3;
67 public static final int PLAYBACKSERVICE_STATUS = 1;
69 public static final int SHUFFLE_NONE = 0;
70 public static final int SHUFFLE_NORMAL = 1;
71 public static final int SHUFFLE_AUTO = 2;
73 public static final int REPEAT_NONE = 0;
74 public static final int REPEAT_CURRENT = 1;
75 public static final int REPEAT_ALL = 2;
77 public static final String PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
78 public static final String META_CHANGED = "com.android.music.metachanged";
79 public static final String QUEUE_CHANGED = "com.android.music.queuechanged";
80 public static final String PLAYBACK_COMPLETE = "com.android.music.playbackcomplete";
81 public static final String ASYNC_OPEN_COMPLETE = "com.android.music.asyncopencomplete";
83 public static final String SERVICECMD = "com.android.music.musicservicecommand";
84 public static final String CMDNAME = "command";
85 public static final String CMDTOGGLEPAUSE = "togglepause";
86 public static final String CMDPAUSE = "pause";
87 public static final String CMDNEXT = "next";
89 private static final int PHONE_CHANGED = 1;
90 private static final int TRACK_ENDED = 1;
91 private static final int RELEASE_WAKELOCK = 2;
92 private static final int SERVER_DIED = 3;
93 private static final int MAX_HISTORY_SIZE = 10;
95 private MultiPlayer mPlayer;
96 private String mFileToPlay;
97 private PhoneStateIntentReceiver mPsir;
98 private int mShuffleMode = SHUFFLE_NONE;
99 private int mRepeatMode = REPEAT_NONE;
100 private int mMediaMountedCount = 0;
101 private int [] mAutoShuffleList = null;
102 private boolean mOneShot;
103 private int [] mPlayList = null;
104 private int mPlayListLen = 0;
105 private Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE);
106 private Cursor mCursor;
107 private int mPlayPos = -1;
108 private static final String LOGTAG = "MediaPlaybackService";
109 private final Shuffler mRand = new Shuffler();
110 private int mOpenFailedCounter = 0;
111 String[] mCursorCols = new String[] {
113 MediaStore.Audio.Media.ARTIST,
114 MediaStore.Audio.Media.ALBUM,
115 MediaStore.Audio.Media.TITLE,
116 MediaStore.Audio.Media.DATA,
117 MediaStore.Audio.Media.MIME_TYPE,
118 MediaStore.Audio.Media.ALBUM_ID,
119 MediaStore.Audio.Media.ARTIST_ID
121 private BroadcastReceiver mUnmountReceiver = null;
122 private WakeLock mWakeLock;
123 private int mServiceStartId = -1;
124 private boolean mServiceInUse = false;
125 private boolean mResumeAfterCall = false;
126 private boolean mWasPlaying = false;
128 private SharedPreferences mPreferences;
129 // We use this to distinguish between different cards when saving/restoring playlists.
130 // This will have to change if we want to support multiple simultaneous cards.
133 // interval after which we stop the service when idle
134 private static final int IDLE_DELAY = 60000;
136 private Handler mPhoneHandler = new Handler() {
138 public void handleMessage(Message msg) {
141 Phone.State state = mPsir.getPhoneState();
142 if (state == Phone.State.RINGING) {
143 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
144 int ringvolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
145 if (ringvolume > 0) {
146 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
149 } else if (state == Phone.State.OFFHOOK) {
150 // pause the music while a conversation is in progress
151 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
153 } else if (state == Phone.State.IDLE) {
154 // start playing again
155 if (mResumeAfterCall) {
156 // resume playback only if music was playing
157 // when the call was answered
159 mResumeAfterCall = false;
169 private Handler mMediaplayerHandler = new Handler() {
171 public void handleMessage(Message msg) {
177 // the server died when we were idle, so just
178 // reopen the same song (it will start again
179 // from the beginning though when the user
185 if (mRepeatMode == REPEAT_CURRENT) {
188 } else if (!mOneShot) {
191 notifyChange(PLAYBACK_COMPLETE);
194 case RELEASE_WAKELOCK:
203 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
205 public void onReceive(Context context, Intent intent) {
206 String cmd = intent.getStringExtra("command");
207 if (CMDNEXT.equals(cmd)) {
209 } else if (CMDTOGGLEPAUSE.equals(cmd)) {
215 } else if (CMDPAUSE.equals(cmd)) {
221 public MediaPlaybackService() {
222 mPsir = new PhoneStateIntentReceiver(this, mPhoneHandler);
223 mPsir.notifyPhoneCallState(PHONE_CHANGED);
227 public void onCreate() {
230 mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
231 mCardId = FileUtils.getFatVolumeId(Environment.getExternalStorageDirectory().getPath());
233 registerExternalStorageListener();
235 // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes.
236 mPlayer = new MultiPlayer();
237 mPlayer.setHandler(mMediaplayerHandler);
239 // Clear leftover notification in case this service previously got killed while playing
240 NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
241 nm.cancel(PLAYBACKSERVICE_STATUS);
245 registerReceiver(mIntentReceiver, new IntentFilter(SERVICECMD));
246 mPsir.registerIntent();
247 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
248 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
249 mWakeLock.setReferenceCounted(false);
253 public void onDestroy() {
254 unregisterReceiver(mIntentReceiver);
255 if (mUnmountReceiver != null) {
256 unregisterReceiver(mUnmountReceiver);
257 mUnmountReceiver = null;
259 mPsir.unregisterIntent();
264 private final char hexdigits [] = new char [] {
271 private void saveQueue(boolean full) {
275 Editor ed = mPreferences.edit();
276 //long start = System.currentTimeMillis();
278 StringBuilder q = new StringBuilder();
280 // The current playlist is saved as a list of "reverse hexadecimal"
281 // numbers, which we can generate faster than normal decimal or
282 // hexadecimal numbers, which in turn allows us to save the playlist
283 // more often without worrying too much about performance.
284 // (saving the full state takes about 40 ms under no-load conditions
286 int len = mPlayListLen;
287 for (int i = 0; i < len; i++) {
288 int n = mPlayList[i];
295 q.append(hexdigits[digit]);
300 //Log.i("@@@@ service", "created queue string in " + (System.currentTimeMillis() - start) + " ms");
301 ed.putString("queue", q.toString());
302 ed.putInt("cardid", mCardId);
304 ed.putInt("curpos", mPlayPos);
305 if (mPlayer.isInitialized()) {
306 ed.putLong("seekpos", mPlayer.position());
308 ed.putInt("repeatmode", mRepeatMode);
309 ed.putInt("shufflemode", mShuffleMode);
312 //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms");
315 private void reloadQueue() {
318 boolean newstyle = false;
320 if (mPreferences.contains("cardid")) {
322 id = mPreferences.getInt("cardid", ~mCardId);
325 // Only restore the saved playlist if the card is still
326 // the same one as when the playlist was saved
327 q = mPreferences.getString("queue", "");
329 if (q != null && q.length() > 1) {
330 //Log.i("@@@@ service", "loaded queue: " + q);
331 String [] entries = q.split(";");
332 int len = entries.length;
333 ensurePlayListCapacity(len);
334 for (int i = 0; i < len; i++) {
336 String revhex = entries[i];
338 for (int j = revhex.length() - 1; j >= 0 ; j--) {
340 char c = revhex.charAt(j);
341 if (c >= '0' && c <= '9') {
343 } else if (c >= 'a' && c <= 'f') {
346 // bogus playlist data
353 mPlayList[i] = Integer.parseInt(entries[i]);
358 int pos = mPreferences.getInt("curpos", 0);
359 if (pos < 0 || pos >= len) {
360 // The saved playlist is bogus, discard it
366 // When reloadQueue is called in response to a card-insertion,
367 // we might not be able to query the media provider right away.
368 // To deal with this, try querying for the current file, and if
369 // that fails, wait a while and try again. If that too fails,
370 // assume there is a problem and don't restore the state.
371 Cursor c = MusicUtils.query(this,
372 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
373 new String [] {"_id"}, "_id=" + mPlayList[mPlayPos] , null, null);
374 if (c == null || c.getCount() == 0) {
375 // wait a bit and try again
376 SystemClock.sleep(3000);
377 c = getContentResolver().query(
378 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
379 mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null);
385 // Make sure we don't auto-skip to the next song, since that
386 // also starts playback. What could happen in that case is:
388 // - go to UMS and delete some files, including the currently playing one
389 // - come back from UMS
391 // - music app is killed for some reason (out of memory)
392 // - music service is restarted, service restores state, doesn't find
393 // the "current" file, goes to the next and: playback starts on its
394 // own, potentially at some random inconvenient time.
395 mOpenFailedCounter = 20;
397 if (!mPlayer.isInitialized()) {
398 // couldn't restore the saved state
403 long seekpos = mPreferences.getLong("seekpos", 0);
404 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
406 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
407 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
408 repmode = REPEAT_NONE;
410 mRepeatMode = repmode;
412 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
413 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
414 shufmode = SHUFFLE_NONE;
416 if (shufmode == SHUFFLE_AUTO) {
417 if (! makeAutoShuffleList()) {
418 shufmode = SHUFFLE_NONE;
421 mShuffleMode = shufmode;
426 public IBinder onBind(Intent intent) {
427 mDelayedStopHandler.removeCallbacksAndMessages(null);
428 mServiceInUse = true;
433 public void onRebind(Intent intent) {
434 mDelayedStopHandler.removeCallbacksAndMessages(null);
435 mServiceInUse = true;
439 public void onStart(Intent intent, int startId) {
440 mServiceStartId = startId;
441 mDelayedStopHandler.removeCallbacksAndMessages(null);
442 String cmd = intent.getStringExtra("command");
443 if (CMDNEXT.equals(cmd)) {
445 } else if (CMDTOGGLEPAUSE.equals(cmd)) {
451 } else if (CMDPAUSE.equals(cmd)) {
454 // make sure the service will shut down on its own if it was
455 // just started but not bound to and nothing is playing
456 mDelayedStopHandler.removeCallbacksAndMessages(null);
457 Message msg = mDelayedStopHandler.obtainMessage();
458 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
462 public boolean onUnbind(Intent intent) {
463 mServiceInUse = false;
465 // Take a snapshot of the current playlist
468 if (isPlaying() || mResumeAfterCall) {
469 // something is currently playing, or will be playing once
470 // an in-progress call ends, so don't stop the service now.
474 // If there is a playlist but playback is paused, then wait a while
475 // before stopping the service, so that pause/resume isn't slow.
476 // Also delay stopping the service if we're transitioning between tracks.
477 if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
478 Message msg = mDelayedStopHandler.obtainMessage();
479 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
483 // No active playlist, OK to stop the service right now
484 stopSelf(mServiceStartId);
488 private Handler mDelayedStopHandler = new Handler() {
490 public void handleMessage(Message msg) {
491 // Check again to make sure nothing is playing right now
492 if (isPlaying() || mResumeAfterCall || mServiceInUse
493 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
496 // save the queue again, because it might have change
497 // since the user exited the music app (because of
498 // party-shuffle or because the play-position changed)
500 stopSelf(mServiceStartId);
505 * Called when we receive a ACTION_MEDIA_EJECT notification.
507 * @param storagePath path to mount point for the removed media
509 public void closeExternalStorageFiles(String storagePath) {
510 // stop playback and clean up if the SD card is going to be unmounted.
512 notifyChange(QUEUE_CHANGED);
513 notifyChange(META_CHANGED);
517 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications.
518 * The intent will call closeExternalStorageFiles() if the external media
519 * is going to be ejected, so applications can clean up any files they have open.
521 public void registerExternalStorageListener() {
522 if (mUnmountReceiver == null) {
523 mUnmountReceiver = new BroadcastReceiver() {
525 public void onReceive(Context context, Intent intent) {
526 String action = intent.getAction();
527 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
529 mOneShot = true; // This makes us not save the state again later,
530 // which would be wrong because the song ids and
531 // card id might not match.
532 closeExternalStorageFiles(intent.getData().getPath());
533 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
534 mMediaMountedCount++;
535 mCardId = FileUtils.getFatVolumeId(intent.getData().getPath());
537 notifyChange(QUEUE_CHANGED);
538 notifyChange(META_CHANGED);
542 IntentFilter iFilter = new IntentFilter();
543 iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
544 iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
545 iFilter.addDataScheme("file");
546 registerReceiver(mUnmountReceiver, iFilter);
551 * Notify the change-receivers that something has changed.
552 * The intent that is sent contains the following data
553 * for the currently playing track:
554 * "id" - Integer: the database row ID
555 * "artist" - String: the name of the artist
556 * "album" - String: the name of the album
557 * "track" - String: the name of the track
558 * The intent has an action that is one of
559 * "com.android.music.metachanged"
560 * "com.android.music.queuechanged",
561 * "com.android.music.playbackcomplete"
562 * "com.android.music.playstatechanged"
563 * respectively indicating that a new track has
564 * started playing, that the playback queue has
565 * changed, that playback has stopped because
566 * the last file in the list has been played,
567 * or that the play-state changed (paused/resumed).
569 private void notifyChange(String what) {
571 Intent i = new Intent(what);
572 i.putExtra("id", Integer.valueOf(getAudioId()));
573 i.putExtra("artist", getArtistName());
574 i.putExtra("album",getAlbumName());
575 i.putExtra("track", getTrackName());
578 if (what.equals(QUEUE_CHANGED)) {
585 private void ensurePlayListCapacity(int size) {
586 if (mPlayList == null || size > mPlayList.length) {
587 // reallocate at 2x requested size so we don't
588 // need to grow and copy the array for every
590 int [] newlist = new int[size * 2];
591 int len = mPlayListLen;
592 for (int i = 0; i < len; i++) {
593 newlist[i] = mPlayList[i];
597 // FIXME: shrink the array when the needed size is much smaller
598 // than the allocated size
601 private void addToPlayList(int id) {
603 ensurePlayListCapacity(mPlayListLen + 1);
604 mPlayList[mPlayListLen++] = id;
608 private void addToPlayList(int [] list, int position) {
609 int addlen = list.length;
610 if (position < 0) { // overwrite
614 ensurePlayListCapacity(mPlayListLen + addlen);
615 if (position > mPlayListLen) {
616 position = mPlayListLen;
619 // move part of list after insertion point
620 int tailsize = mPlayListLen - position;
621 for (int i = tailsize ; i > 0 ; i--) {
622 mPlayList[position + i] = mPlayList[position + i - addlen];
625 // copy list into playlist
626 for (int i = 0; i < addlen; i++) {
627 mPlayList[position + i] = list[i];
629 mPlayListLen += addlen;
633 * Appends a list of tracks to the current playlist.
634 * If nothing is playing currently, playback will be started at
636 * If the action is NOW, playback will switch to the first of
637 * the new tracks immediately.
638 * @param list The list of tracks to append.
639 * @param action NOW, NEXT or LAST
641 public void enqueue(int [] list, int action) {
643 if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
644 addToPlayList(list, mPlayPos + 1);
645 notifyChange(QUEUE_CHANGED);
647 // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen
648 addToPlayList(list, Integer.MAX_VALUE);
649 notifyChange(QUEUE_CHANGED);
651 mPlayPos = mPlayListLen - list.length;
654 notifyChange(META_CHANGED);
662 notifyChange(META_CHANGED);
668 * Replaces the current playlist with a new list,
669 * and prepares for starting playback at the specified
670 * position in the list.
671 * @param list The new list of tracks.
673 public void open(int [] list, int position) {
674 synchronized (this) {
675 if (mShuffleMode == SHUFFLE_AUTO) {
676 mShuffleMode = SHUFFLE_NORMAL;
678 addToPlayList(list, -1);
687 * Moves the item at index1 to index2.
691 public void moveQueueItem(int index1, int index2) {
692 synchronized (this) {
693 if (index1 >= mPlayListLen) {
694 index1 = mPlayListLen - 1;
696 if (index2 >= mPlayListLen) {
697 index2 = mPlayListLen - 1;
699 if (index1 < index2) {
700 int tmp = mPlayList[index1];
701 for (int i = index1; i < index2; i++) {
702 mPlayList[i] = mPlayList[i+1];
704 mPlayList[index2] = tmp;
705 if (mPlayPos == index1) {
707 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
710 } else if (index2 < index1) {
711 int tmp = mPlayList[index1];
712 for (int i = index1; i > index2; i--) {
713 mPlayList[i] = mPlayList[i-1];
715 mPlayList[index2] = tmp;
716 if (mPlayPos == index1) {
718 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
722 notifyChange(QUEUE_CHANGED);
727 * Returns the current play list
728 * @return An array of integers containing the IDs of the tracks in the play list
730 public int [] getQueue() {
731 synchronized (this) {
732 int len = mPlayListLen;
733 int [] list = new int[len];
734 for (int i = 0; i < len; i++) {
735 list[i] = mPlayList[i];
741 private void openCurrent() {
742 synchronized (this) {
743 if (mCursor != null) {
747 if (mPlayListLen == 0) {
752 String id = String.valueOf(mPlayList[mPlayPos]);
754 mCursor = getContentResolver().query(
755 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
756 mCursorCols, "_id=" + id , null, null);
757 if (mCursor != null) {
758 mCursor.moveToFirst();
759 open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id, false);
764 public void openAsync(String path) {
765 synchronized (this) {
770 mRepeatMode = REPEAT_NONE;
771 ensurePlayListCapacity(1);
777 mPlayer.setDataSourceAsync(mFileToPlay);
783 * Opens the specified file and readies it for playback.
785 * @param path The full path of the file to be opened.
786 * @param oneshot when set to true, playback will stop after this file completes, instead
787 * of moving on to the next track in the list
789 public void open(String path, boolean oneshot) {
790 synchronized (this) {
796 mRepeatMode = REPEAT_NONE;
797 ensurePlayListCapacity(1);
802 // if mCursor is null, try to associate path with a database cursor
803 if (mCursor == null) {
805 ContentResolver resolver = getContentResolver();
808 String selectionArgs[];
809 if (path.startsWith("content://media/")) {
810 uri = Uri.parse(path);
812 selectionArgs = null;
814 uri = MediaStore.Audio.Media.getContentUriForPath(path);
815 where = MediaStore.Audio.Media.DATA + "=?";
816 selectionArgs = new String[] { path };
820 mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);
821 if (mCursor != null) {
822 if (mCursor.getCount() == 0) {
826 mCursor.moveToNext();
827 ensurePlayListCapacity(1);
829 mPlayList[0] = mCursor.getInt(0);
833 } catch (UnsupportedOperationException ex) {
837 mPlayer.setDataSource(mFileToPlay);
839 if (! mPlayer.isInitialized()) {
841 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
842 // beware: this ends up being recursive because next() calls open() again.
845 if (! mPlayer.isInitialized() && mOpenFailedCounter != 0) {
846 // need to make sure we only shows this once
847 mOpenFailedCounter = 0;
848 Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
851 mOpenFailedCounter = 0;
857 * Starts playback of a previously opened file.
860 if (mPlayer.isInitialized()) {
865 NotificationManager nm = (NotificationManager)
866 getSystemService(Context.NOTIFICATION_SERVICE);
868 RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar);
869 views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer);
870 views.setTextViewText(R.id.trackname, getTrackName());
871 String artist = getArtistName();
872 if (artist == null || artist.equals(MediaFile.UNKNOWN_STRING)) {
873 artist = getString(R.string.unknown_artist_name);
875 String album = getAlbumName();
876 if (album == null || album.equals(MediaFile.UNKNOWN_STRING)) {
877 album = getString(R.string.unknown_album_name);
880 views.setTextViewText(R.id.artistalbum,
881 getString(R.string.notification_artist_album, artist, album)
884 Intent statusintent = new Intent("com.android.music.PLAYBACK_VIEWER");
885 statusintent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
886 Notification status = new Notification();
887 status.contentView = views;
888 status.flags |= Notification.FLAG_ONGOING_EVENT;
889 status.icon = R.drawable.stat_notify_musicplayer;
890 status.contentIntent = PendingIntent.getActivity(this, 0,
891 new Intent("com.android.music.PLAYBACK_VIEWER"), 0);
892 nm.notify(PLAYBACKSERVICE_STATUS, status);
893 notifyChange(PLAYSTATE_CHANGED);
897 private void stop(boolean remove_status_icon) {
898 if (mPlayer.isInitialized()) {
902 if (mCursor != null) {
906 if (remove_status_icon) {
909 setForeground(false);
921 * Pauses playback (call play() to resume)
923 public void pause() {
927 setForeground(false);
929 notifyChange(PLAYSTATE_CHANGED);
933 /** Returns whether playback is currently paused
935 * @return true if playback is paused, false if not
937 public boolean isPlaying() {
938 if (mPlayer.isInitialized()) {
939 return mPlayer.isPlaying();
945 Desired behavior for prev/next/shuffle:
947 - NEXT will move to the next track in the list when not shuffling, and to
948 a track randomly picked from the not-yet-played tracks when shuffling.
949 If all tracks have already been played, pick from the full set, but
950 avoid picking the previously played track if possible.
951 - when shuffling, PREV will go to the previously played track. Hitting PREV
952 again will go to the track played before that, etc. When the start of the
953 history has been reached, PREV is a no-op.
954 When not shuffling, PREV will go to the sequentially previous track (the
955 difference with the shuffle-case is mainly that when not shuffling, the
956 user can back up to tracks that are not in the history).
959 When playing an album with 10 tracks from the start, and enabling shuffle
960 while playing track 5, the remaining tracks (6-10) will be shuffled, e.g.
961 the final play order might be 1-2-3-4-5-8-10-6-9-7.
962 When hitting 'prev' 8 times while playing track 7 in this example, the
963 user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next',
964 a random track will be picked again. If at any time user disables shuffling
965 the next/previous track will be picked in sequential order again.
969 synchronized (this) {
971 // we were playing a specific file not part of a playlist, so there is no 'previous'
976 if (mShuffleMode == SHUFFLE_NORMAL) {
977 // go to previously-played track and remove it from the history
978 int histsize = mHistory.size();
983 Integer pos = mHistory.remove(histsize - 1);
984 mPlayPos = pos.intValue();
989 mPlayPos = mPlayListLen - 1;
995 notifyChange(META_CHANGED);
999 public void next(boolean force) {
1000 synchronized (this) {
1002 // we were playing a specific file not part of a playlist, so there is no 'next'
1008 // Store the current file in the history, but keep the history at a
1010 mHistory.add(Integer.valueOf(mPlayPos));
1011 if (mHistory.size() > MAX_HISTORY_SIZE) {
1012 mHistory.removeElementAt(0);
1015 if (mShuffleMode == SHUFFLE_NORMAL) {
1016 // Pick random next track from the not-yet-played ones
1017 // TODO: make it work right after adding/removing items in the queue.
1019 int numTracks = mPlayListLen;
1020 int[] tracks = new int[numTracks];
1021 for (int i=0;i < numTracks; i++) {
1025 int numHistory = mHistory.size();
1026 int numUnplayed = numTracks;
1027 for (int i=0;i < numHistory; i++) {
1028 int idx = mHistory.get(i).intValue();
1029 if (idx < numTracks && tracks[idx] >= 0) {
1035 // 'numUnplayed' now indicates how many tracks have not yet
1036 // been played, and 'tracks' contains the indices of those
1038 if (numUnplayed <=0) {
1039 // everything's already been played
1040 if (mRepeatMode == REPEAT_ALL || force) {
1041 //pick from full set
1042 numUnplayed = numTracks;
1043 for (int i=0;i < numTracks; i++) {
1052 int skip = mRand.nextInt(numUnplayed);
1055 while (tracks[++cnt] < 0)
1063 } else if (mShuffleMode == SHUFFLE_AUTO) {
1064 doAutoShuffleUpdate();
1067 if (mPlayPos >= mPlayListLen - 1) {
1068 // we're at the end of the list
1069 if (mRepeatMode == REPEAT_NONE && !force) {
1072 notifyChange(PLAYBACK_COMPLETE);
1074 } else if (mRepeatMode == REPEAT_ALL || force) {
1084 notifyChange(META_CHANGED);
1088 private void gotoIdleState() {
1089 NotificationManager nm =
1090 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
1091 nm.cancel(PLAYBACKSERVICE_STATUS);
1092 mDelayedStopHandler.removeCallbacksAndMessages(null);
1093 Message msg = mDelayedStopHandler.obtainMessage();
1094 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
1097 // Make sure there are at least 5 items after the currently playing item
1098 // and no more than 10 items before.
1099 private void doAutoShuffleUpdate() {
1100 // remove old entries
1101 if (mPlayPos > 10) {
1102 removeTracks(0, mPlayPos - 9);
1104 // add new entries if needed
1105 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
1107 for (int i = 0; i < to_add; i++) {
1108 // pick something at random from the list
1109 int idx = mRand.nextInt(mAutoShuffleList.length);
1110 Integer which = mAutoShuffleList[idx];
1111 addToPlayList(which);
1113 notifyChange(QUEUE_CHANGED);
1117 // A simple variation of Random that makes sure that the
1118 // value it returns is not equal to the value it returned
1119 // previously, unless the interval is 1.
1120 private class Shuffler {
1121 private int mPrevious;
1122 private Random mRandom = new Random();
1123 public int nextInt(int interval) {
1126 ret = mRandom.nextInt(interval);
1127 } while (ret == mPrevious && interval > 1);
1133 private boolean makeAutoShuffleList() {
1134 ContentResolver res = getContentResolver();
1137 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1138 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
1140 if (c == null || c.getCount() == 0) {
1143 int len = c.getCount();
1144 int[] list = new int[len];
1145 for (int i = 0; i < len; i++) {
1147 list[i] = c.getInt(0);
1149 mAutoShuffleList = list;
1151 } catch (RuntimeException ex) {
1161 * Removes the range of tracks specified from the play list. If a file within the range is
1162 * the file currently being played, playback will move to the next file after the
1164 * @param first The first file to be removed
1165 * @param last The last file to be removed
1166 * @return the number of tracks deleted
1168 public int removeTracks(int first, int last) {
1169 synchronized (this) {
1170 if (last < first) return 0;
1171 if (first < 0) first = 0;
1172 if (last >= mPlayListLen) last = mPlayListLen - 1;
1174 boolean gotonext = false;
1175 if (first <= mPlayPos && mPlayPos <= last) {
1178 } else if (mPlayPos > last) {
1179 mPlayPos -= (last - first + 1);
1181 int num = mPlayListLen - last - 1;
1182 for (int i = 0; i < num; i++) {
1183 mPlayList[first + i] = mPlayList[last + 1 + i];
1185 mPlayListLen -= last - first + 1;
1188 if (mPlayListLen == 0) {
1192 if (mPlayPos >= mPlayListLen) {
1200 notifyChange(QUEUE_CHANGED);
1201 return last - first + 1;
1206 * Removes all instances of the track with the given id
1207 * from the playlist.
1208 * @param id The id to be removed
1209 * @return how many instances of the track were removed
1211 public int removeTrack(int id) {
1213 synchronized (this) {
1214 for (int i = 0; i < mPlayListLen; i++) {
1215 if (mPlayList[i] == id) {
1216 numremoved += removeTracks(i, i);
1224 public void setShuffleMode(int shufflemode) {
1225 synchronized(this) {
1226 if (mShuffleMode == shufflemode) {
1229 mShuffleMode = shufflemode;
1230 if (mShuffleMode == SHUFFLE_AUTO) {
1231 if (makeAutoShuffleList()) {
1233 doAutoShuffleUpdate();
1237 notifyChange(META_CHANGED);
1239 // failed to build a list of files to shuffle
1240 mShuffleMode = SHUFFLE_NONE;
1245 public int getShuffleMode() {
1246 return mShuffleMode;
1249 public void setRepeatMode(int repeatmode) {
1250 synchronized(this) {
1251 mRepeatMode = repeatmode;
1254 public int getRepeatMode() {
1258 public int getMediaMountedCount() {
1259 return mMediaMountedCount;
1263 * Returns the path of the currently playing file, or null if
1264 * no file is currently playing.
1266 public String getPath() {
1271 * Returns the rowid of the currently playing file, or -1 if
1272 * no file is currently playing.
1274 public int getAudioId() {
1275 synchronized (this) {
1276 if (mPlayPos >= 0 && mPlayer.isInitialized()) {
1277 return mPlayList[mPlayPos];
1284 * Returns the position in the queue
1285 * @return the position in the queue
1287 public int getQueuePosition() {
1288 synchronized(this) {
1294 * Starts playing the track at the given position in the queue.
1295 * @param pos The position in the queue of the track that will be played.
1297 public void setQueuePosition(int pos) {
1298 synchronized(this) {
1303 notifyChange(META_CHANGED);
1307 public String getArtistName() {
1308 if (mCursor == null) {
1311 return mCursor.getString(mCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
1314 public int getArtistId() {
1315 if (mCursor == null) {
1318 return mCursor.getInt(mCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST_ID));
1321 public String getAlbumName() {
1322 if (mCursor == null) {
1325 return mCursor.getString(mCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
1328 public int getAlbumId() {
1329 if (mCursor == null) {
1332 return mCursor.getInt(mCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
1335 public String getTrackName() {
1336 if (mCursor == null) {
1339 return mCursor.getString(mCursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
1345 * Returns the duration of the file in milliseconds.
1346 * Currently this method returns -1 for the duration of MIDI files.
1348 public long duration() {
1349 if (mPlayer.isInitialized()) {
1350 return mPlayer.duration();
1352 // TODO: when the MIDI engine supports it, return MIDI duration.
1357 * Returns the current playback position in milliseconds
1359 public long position() {
1360 if (mPlayer.isInitialized()) {
1361 return mPlayer.position();
1367 * Seeks to the position specified.
1369 * @param pos The position to seek to, in milliseconds
1371 public long seek(long pos) {
1372 if (mPlayer.isInitialized()) {
1373 if (pos < 0) pos = 0;
1374 if (pos > mPlayer.duration()) pos = mPlayer.duration();
1375 return mPlayer.seek(pos);
1381 * Provides a unified interface for dealing with midi files and
1382 * other media files.
1384 private class MultiPlayer {
1385 private MediaPlayer mMediaPlayer = new MediaPlayer();
1386 private Handler mHandler;
1387 private boolean mIsInitialized = false;
1389 public MultiPlayer() {
1390 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1393 public void setDataSourceAsync(String path) {
1395 mMediaPlayer.reset();
1396 mMediaPlayer.setDataSource(path);
1397 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1398 mMediaPlayer.setOnPreparedListener(preparedlistener);
1399 mMediaPlayer.prepareAsync();
1400 } catch (IOException ex) {
1401 // TODO: notify the user why the file couldn't be opened
1402 mIsInitialized = false;
1404 } catch (IllegalArgumentException ex) {
1405 // TODO: notify the user why the file couldn't be opened
1406 mIsInitialized = false;
1409 mMediaPlayer.setOnCompletionListener(listener);
1410 mMediaPlayer.setOnErrorListener(errorListener);
1412 mIsInitialized = true;
1415 public void setDataSource(String path) {
1417 mMediaPlayer.reset();
1418 mMediaPlayer.setOnPreparedListener(null);
1419 if (path.startsWith("content://")) {
1420 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path));
1422 mMediaPlayer.setDataSource(path);
1424 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1425 mMediaPlayer.prepare();
1426 } catch (IOException ex) {
1427 // TODO: notify the user why the file couldn't be opened
1428 mIsInitialized = false;
1430 } catch (IllegalArgumentException ex) {
1431 // TODO: notify the user why the file couldn't be opened
1432 mIsInitialized = false;
1435 mMediaPlayer.setOnCompletionListener(listener);
1436 mMediaPlayer.setOnErrorListener(errorListener);
1438 mIsInitialized = true;
1441 public boolean isInitialized() {
1442 return mIsInitialized;
1445 public void start() {
1446 mMediaPlayer.start();
1449 public void stop() {
1450 mMediaPlayer.reset();
1451 mIsInitialized = false;
1454 public void pause() {
1455 mMediaPlayer.pause();
1458 public boolean isPlaying() {
1459 return mMediaPlayer.isPlaying();
1462 public void setHandler(Handler handler) {
1466 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {
1467 public void onCompletion(MediaPlayer mp) {
1468 // Acquire a temporary wakelock, since when we return from
1469 // this callback the MediaPlayer will release its wakelock
1470 // and allow the device to go to sleep.
1471 // This temporary wakelock is released when the RELEASE_WAKELOCK
1472 // message is processed, but just in case, put a timeout on it.
1473 mWakeLock.acquire(30000);
1474 mHandler.sendEmptyMessage(TRACK_ENDED);
1475 mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
1479 MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() {
1480 public void onPrepared(MediaPlayer mp) {
1481 notifyChange(ASYNC_OPEN_COMPLETE);
1485 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {
1486 public boolean onError(MediaPlayer mp, int what, int extra) {
1488 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
1489 mIsInitialized = false;
1490 mMediaPlayer.release();
1491 // Creating a new MediaPlayer and settings its wakemode does not
1492 // require the media service, so it's OK to do this now, while the
1493 // service is still being restarted
1494 mMediaPlayer = new MediaPlayer();
1495 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1496 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
1505 public long duration() {
1506 return mMediaPlayer.getDuration();
1509 public long position() {
1510 return mMediaPlayer.getCurrentPosition();
1513 public long seek(long whereto) {
1514 mMediaPlayer.seekTo((int) whereto);
1519 private final IMediaPlaybackService.Stub mBinder = new IMediaPlaybackService.Stub()
1521 public void openfileAsync(String path)
1523 MediaPlaybackService.this.openAsync(path);
1525 public void openfile(String path)
1527 MediaPlaybackService.this.open(path, true);
1529 public void open(int [] list, int position) {
1530 MediaPlaybackService.this.open(list, position);
1532 public int getQueuePosition() {
1533 return MediaPlaybackService.this.getQueuePosition();
1535 public void setQueuePosition(int index) {
1536 MediaPlaybackService.this.setQueuePosition(index);
1538 public boolean isPlaying() {
1539 return MediaPlaybackService.this.isPlaying();
1541 public void stop() {
1542 MediaPlaybackService.this.stop();
1544 public void pause() {
1545 MediaPlaybackService.this.pause();
1547 public void play() {
1548 MediaPlaybackService.this.play();
1550 public void prev() {
1551 MediaPlaybackService.this.prev();
1553 public void next() {
1554 MediaPlaybackService.this.next(true);
1556 public String getTrackName() {
1557 return MediaPlaybackService.this.getTrackName();
1559 public String getAlbumName() {
1560 return MediaPlaybackService.this.getAlbumName();
1562 public int getAlbumId() {
1563 return MediaPlaybackService.this.getAlbumId();
1565 public String getArtistName() {
1566 return MediaPlaybackService.this.getArtistName();
1568 public int getArtistId() {
1569 return MediaPlaybackService.this.getArtistId();
1571 public void enqueue(int [] list , int action) {
1572 MediaPlaybackService.this.enqueue(list, action);
1574 public int [] getQueue() {
1575 return MediaPlaybackService.this.getQueue();
1577 public void moveQueueItem(int from, int to) {
1578 MediaPlaybackService.this.moveQueueItem(from, to);
1580 public String getPath() {
1581 return MediaPlaybackService.this.getPath();
1583 public int getAudioId() {
1584 return MediaPlaybackService.this.getAudioId();
1586 public long position() {
1587 return MediaPlaybackService.this.position();
1589 public long duration() {
1590 return MediaPlaybackService.this.duration();
1592 public long seek(long pos) {
1593 return MediaPlaybackService.this.seek(pos);
1595 public void setShuffleMode(int shufflemode) {
1596 MediaPlaybackService.this.setShuffleMode(shufflemode);
1598 public int getShuffleMode() {
1599 return MediaPlaybackService.this.getShuffleMode();
1601 public int removeTracks(int first, int last) {
1602 return MediaPlaybackService.this.removeTracks(first, last);
1604 public int removeTrack(int id) {
1605 return MediaPlaybackService.this.removeTrack(id);
1607 public void setRepeatMode(int repeatmode) {
1608 MediaPlaybackService.this.setRepeatMode(repeatmode);
1610 public int getRepeatMode() {
1611 return MediaPlaybackService.this.getRepeatMode();
1613 public int getMediaMountedCount() {
1614 return MediaPlaybackService.this.getMediaMountedCount();