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 CMDPREVIOUS = "previous";
88 public static final String CMDNEXT = "next";
90 private static final int PHONE_CHANGED = 1;
91 private static final int TRACK_ENDED = 1;
92 private static final int RELEASE_WAKELOCK = 2;
93 private static final int SERVER_DIED = 3;
94 private static final int FADEIN = 4;
95 private static final int MAX_HISTORY_SIZE = 10;
97 private MultiPlayer mPlayer;
98 private String mFileToPlay;
99 private PhoneStateIntentReceiver mPsir;
100 private int mShuffleMode = SHUFFLE_NONE;
101 private int mRepeatMode = REPEAT_NONE;
102 private int mMediaMountedCount = 0;
103 private int [] mAutoShuffleList = null;
104 private boolean mOneShot;
105 private int [] mPlayList = null;
106 private int mPlayListLen = 0;
107 private Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE);
108 private Cursor mCursor;
109 private int mPlayPos = -1;
110 private static final String LOGTAG = "MediaPlaybackService";
111 private final Shuffler mRand = new Shuffler();
112 private int mOpenFailedCounter = 0;
113 String[] mCursorCols = new String[] {
115 MediaStore.Audio.Media.ARTIST,
116 MediaStore.Audio.Media.ALBUM,
117 MediaStore.Audio.Media.TITLE,
118 MediaStore.Audio.Media.DATA,
119 MediaStore.Audio.Media.MIME_TYPE,
120 MediaStore.Audio.Media.ALBUM_ID,
121 MediaStore.Audio.Media.ARTIST_ID
123 private BroadcastReceiver mUnmountReceiver = null;
124 private WakeLock mWakeLock;
125 private int mServiceStartId = -1;
126 private boolean mServiceInUse = false;
127 private boolean mResumeAfterCall = false;
128 private boolean mWasPlaying = false;
129 private boolean mQuietMode = false;
131 private SharedPreferences mPreferences;
132 // We use this to distinguish between different cards when saving/restoring playlists.
133 // This will have to change if we want to support multiple simultaneous cards.
136 // interval after which we stop the service when idle
137 private static final int IDLE_DELAY = 60000;
139 private Handler mPhoneHandler = new Handler() {
141 public void handleMessage(Message msg) {
144 Phone.State state = mPsir.getPhoneState();
145 if (state == Phone.State.RINGING) {
146 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
147 int ringvolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
148 if (ringvolume > 0) {
149 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
152 } else if (state == Phone.State.OFFHOOK) {
153 // pause the music while a conversation is in progress
154 mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
156 } else if (state == Phone.State.IDLE) {
157 // start playing again
158 if (mResumeAfterCall) {
159 // resume playback only if music was playing
160 // when the call was answered
162 mResumeAfterCall = false;
172 private void startAndFadeIn() {
173 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
176 private Handler mMediaplayerHandler = new Handler() {
177 float mCurrentVolume = 1.0f;
179 public void handleMessage(Message msg) {
184 mPlayer.setVolume(mCurrentVolume);
186 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
188 mCurrentVolume += 0.01f;
189 if (mCurrentVolume < 1.0f) {
190 mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
192 mCurrentVolume = 1.0f;
194 mPlayer.setVolume(mCurrentVolume);
201 // the server died when we were idle, so just
202 // reopen the same song (it will start again
203 // from the beginning though when the user
209 if (mRepeatMode == REPEAT_CURRENT) {
212 } else if (!mOneShot) {
215 notifyChange(PLAYBACK_COMPLETE);
218 case RELEASE_WAKELOCK:
227 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
229 public void onReceive(Context context, Intent intent) {
230 String cmd = intent.getStringExtra("command");
231 if (CMDNEXT.equals(cmd)) {
233 } else if (CMDPREVIOUS.equals(cmd)) {
235 } else if (CMDTOGGLEPAUSE.equals(cmd)) {
241 } else if (CMDPAUSE.equals(cmd)) {
247 public MediaPlaybackService() {
248 mPsir = new PhoneStateIntentReceiver(this, mPhoneHandler);
249 mPsir.notifyPhoneCallState(PHONE_CHANGED);
253 public void onCreate() {
256 mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
257 mCardId = FileUtils.getFatVolumeId(Environment.getExternalStorageDirectory().getPath());
259 registerExternalStorageListener();
261 // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes.
262 mPlayer = new MultiPlayer();
263 mPlayer.setHandler(mMediaplayerHandler);
265 // Clear leftover notification in case this service previously got killed while playing
266 NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
267 nm.cancel(PLAYBACKSERVICE_STATUS);
271 registerReceiver(mIntentReceiver, new IntentFilter(SERVICECMD));
272 mPsir.registerIntent();
273 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
274 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
275 mWakeLock.setReferenceCounted(false);
279 public void onDestroy() {
280 if (mCursor != null) {
285 unregisterReceiver(mIntentReceiver);
286 if (mUnmountReceiver != null) {
287 unregisterReceiver(mUnmountReceiver);
288 mUnmountReceiver = null;
290 mPsir.unregisterIntent();
295 private final char hexdigits [] = new char [] {
302 private void saveQueue(boolean full) {
306 Editor ed = mPreferences.edit();
307 //long start = System.currentTimeMillis();
309 StringBuilder q = new StringBuilder();
311 // The current playlist is saved as a list of "reverse hexadecimal"
312 // numbers, which we can generate faster than normal decimal or
313 // hexadecimal numbers, which in turn allows us to save the playlist
314 // more often without worrying too much about performance.
315 // (saving the full state takes about 40 ms under no-load conditions
317 int len = mPlayListLen;
318 for (int i = 0; i < len; i++) {
319 int n = mPlayList[i];
326 q.append(hexdigits[digit]);
331 //Log.i("@@@@ service", "created queue string in " + (System.currentTimeMillis() - start) + " ms");
332 ed.putString("queue", q.toString());
333 ed.putInt("cardid", mCardId);
335 ed.putInt("curpos", mPlayPos);
336 if (mPlayer.isInitialized()) {
337 ed.putLong("seekpos", mPlayer.position());
339 ed.putInt("repeatmode", mRepeatMode);
340 ed.putInt("shufflemode", mShuffleMode);
343 //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms");
346 private void reloadQueue() {
349 boolean newstyle = false;
351 if (mPreferences.contains("cardid")) {
353 id = mPreferences.getInt("cardid", ~mCardId);
356 // Only restore the saved playlist if the card is still
357 // the same one as when the playlist was saved
358 q = mPreferences.getString("queue", "");
360 if (q != null && q.length() > 1) {
361 //Log.i("@@@@ service", "loaded queue: " + q);
362 String [] entries = q.split(";");
363 int len = entries.length;
364 ensurePlayListCapacity(len);
365 for (int i = 0; i < len; i++) {
367 String revhex = entries[i];
369 for (int j = revhex.length() - 1; j >= 0 ; j--) {
371 char c = revhex.charAt(j);
372 if (c >= '0' && c <= '9') {
374 } else if (c >= 'a' && c <= 'f') {
377 // bogus playlist data
384 mPlayList[i] = Integer.parseInt(entries[i]);
389 int pos = mPreferences.getInt("curpos", 0);
390 if (pos < 0 || pos >= len) {
391 // The saved playlist is bogus, discard it
397 // When reloadQueue is called in response to a card-insertion,
398 // we might not be able to query the media provider right away.
399 // To deal with this, try querying for the current file, and if
400 // that fails, wait a while and try again. If that too fails,
401 // assume there is a problem and don't restore the state.
402 Cursor c = MusicUtils.query(this,
403 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
404 new String [] {"_id"}, "_id=" + mPlayList[mPlayPos] , null, null);
405 if (c == null || c.getCount() == 0) {
406 // wait a bit and try again
407 SystemClock.sleep(3000);
408 c = getContentResolver().query(
409 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
410 mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null);
416 // Make sure we don't auto-skip to the next song, since that
417 // also starts playback. What could happen in that case is:
419 // - go to UMS and delete some files, including the currently playing one
420 // - come back from UMS
422 // - music app is killed for some reason (out of memory)
423 // - music service is restarted, service restores state, doesn't find
424 // the "current" file, goes to the next and: playback starts on its
425 // own, potentially at some random inconvenient time.
426 mOpenFailedCounter = 20;
430 if (!mPlayer.isInitialized()) {
431 // couldn't restore the saved state
436 long seekpos = mPreferences.getLong("seekpos", 0);
437 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
439 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
440 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
441 repmode = REPEAT_NONE;
443 mRepeatMode = repmode;
445 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
446 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
447 shufmode = SHUFFLE_NONE;
449 if (shufmode == SHUFFLE_AUTO) {
450 if (! makeAutoShuffleList()) {
451 shufmode = SHUFFLE_NONE;
454 mShuffleMode = shufmode;
459 public IBinder onBind(Intent intent) {
460 mDelayedStopHandler.removeCallbacksAndMessages(null);
461 mServiceInUse = true;
466 public void onRebind(Intent intent) {
467 mDelayedStopHandler.removeCallbacksAndMessages(null);
468 mServiceInUse = true;
472 public void onStart(Intent intent, int startId) {
473 mServiceStartId = startId;
474 mDelayedStopHandler.removeCallbacksAndMessages(null);
475 String cmd = intent.getStringExtra("command");
476 if (CMDNEXT.equals(cmd)) {
478 } else if (CMDPREVIOUS.equals(cmd)) {
480 } else if (CMDTOGGLEPAUSE.equals(cmd)) {
486 } else if (CMDPAUSE.equals(cmd)) {
489 // make sure the service will shut down on its own if it was
490 // just started but not bound to and nothing is playing
491 mDelayedStopHandler.removeCallbacksAndMessages(null);
492 Message msg = mDelayedStopHandler.obtainMessage();
493 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
497 public boolean onUnbind(Intent intent) {
498 mServiceInUse = false;
500 // Take a snapshot of the current playlist
503 if (isPlaying() || mResumeAfterCall) {
504 // something is currently playing, or will be playing once
505 // an in-progress call ends, so don't stop the service now.
509 // If there is a playlist but playback is paused, then wait a while
510 // before stopping the service, so that pause/resume isn't slow.
511 // Also delay stopping the service if we're transitioning between tracks.
512 if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
513 Message msg = mDelayedStopHandler.obtainMessage();
514 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
518 // No active playlist, OK to stop the service right now
519 stopSelf(mServiceStartId);
523 private Handler mDelayedStopHandler = new Handler() {
525 public void handleMessage(Message msg) {
526 // Check again to make sure nothing is playing right now
527 if (isPlaying() || mResumeAfterCall || mServiceInUse
528 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
531 // save the queue again, because it might have change
532 // since the user exited the music app (because of
533 // party-shuffle or because the play-position changed)
535 stopSelf(mServiceStartId);
540 * Called when we receive a ACTION_MEDIA_EJECT notification.
542 * @param storagePath path to mount point for the removed media
544 public void closeExternalStorageFiles(String storagePath) {
545 // stop playback and clean up if the SD card is going to be unmounted.
547 notifyChange(QUEUE_CHANGED);
548 notifyChange(META_CHANGED);
552 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications.
553 * The intent will call closeExternalStorageFiles() if the external media
554 * is going to be ejected, so applications can clean up any files they have open.
556 public void registerExternalStorageListener() {
557 if (mUnmountReceiver == null) {
558 mUnmountReceiver = new BroadcastReceiver() {
560 public void onReceive(Context context, Intent intent) {
561 String action = intent.getAction();
562 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
564 mOneShot = true; // This makes us not save the state again later,
565 // which would be wrong because the song ids and
566 // card id might not match.
567 closeExternalStorageFiles(intent.getData().getPath());
568 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
569 mMediaMountedCount++;
570 mCardId = FileUtils.getFatVolumeId(intent.getData().getPath());
572 notifyChange(QUEUE_CHANGED);
573 notifyChange(META_CHANGED);
577 IntentFilter iFilter = new IntentFilter();
578 iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
579 iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
580 iFilter.addDataScheme("file");
581 registerReceiver(mUnmountReceiver, iFilter);
586 * Notify the change-receivers that something has changed.
587 * The intent that is sent contains the following data
588 * for the currently playing track:
589 * "id" - Integer: the database row ID
590 * "artist" - String: the name of the artist
591 * "album" - String: the name of the album
592 * "track" - String: the name of the track
593 * The intent has an action that is one of
594 * "com.android.music.metachanged"
595 * "com.android.music.queuechanged",
596 * "com.android.music.playbackcomplete"
597 * "com.android.music.playstatechanged"
598 * respectively indicating that a new track has
599 * started playing, that the playback queue has
600 * changed, that playback has stopped because
601 * the last file in the list has been played,
602 * or that the play-state changed (paused/resumed).
604 private void notifyChange(String what) {
606 Intent i = new Intent(what);
607 i.putExtra("id", Integer.valueOf(getAudioId()));
608 i.putExtra("artist", getArtistName());
609 i.putExtra("album",getAlbumName());
610 i.putExtra("track", getTrackName());
613 if (what.equals(QUEUE_CHANGED)) {
620 private void ensurePlayListCapacity(int size) {
621 if (mPlayList == null || size > mPlayList.length) {
622 // reallocate at 2x requested size so we don't
623 // need to grow and copy the array for every
625 int [] newlist = new int[size * 2];
626 int len = mPlayListLen;
627 for (int i = 0; i < len; i++) {
628 newlist[i] = mPlayList[i];
632 // FIXME: shrink the array when the needed size is much smaller
633 // than the allocated size
636 private void addToPlayList(int id) {
638 ensurePlayListCapacity(mPlayListLen + 1);
639 mPlayList[mPlayListLen++] = id;
643 private void addToPlayList(int [] list, int position) {
644 int addlen = list.length;
645 if (position < 0) { // overwrite
649 ensurePlayListCapacity(mPlayListLen + addlen);
650 if (position > mPlayListLen) {
651 position = mPlayListLen;
654 // move part of list after insertion point
655 int tailsize = mPlayListLen - position;
656 for (int i = tailsize ; i > 0 ; i--) {
657 mPlayList[position + i] = mPlayList[position + i - addlen];
660 // copy list into playlist
661 for (int i = 0; i < addlen; i++) {
662 mPlayList[position + i] = list[i];
664 mPlayListLen += addlen;
668 * Appends a list of tracks to the current playlist.
669 * If nothing is playing currently, playback will be started at
671 * If the action is NOW, playback will switch to the first of
672 * the new tracks immediately.
673 * @param list The list of tracks to append.
674 * @param action NOW, NEXT or LAST
676 public void enqueue(int [] list, int action) {
678 if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
679 addToPlayList(list, mPlayPos + 1);
680 notifyChange(QUEUE_CHANGED);
682 // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen
683 addToPlayList(list, Integer.MAX_VALUE);
684 notifyChange(QUEUE_CHANGED);
686 mPlayPos = mPlayListLen - list.length;
689 notifyChange(META_CHANGED);
697 notifyChange(META_CHANGED);
703 * Replaces the current playlist with a new list,
704 * and prepares for starting playback at the specified
705 * position in the list.
706 * @param list The new list of tracks.
708 public void open(int [] list, int position) {
709 synchronized (this) {
710 if (mShuffleMode == SHUFFLE_AUTO) {
711 mShuffleMode = SHUFFLE_NORMAL;
713 addToPlayList(list, -1);
722 * Moves the item at index1 to index2.
726 public void moveQueueItem(int index1, int index2) {
727 synchronized (this) {
728 if (index1 >= mPlayListLen) {
729 index1 = mPlayListLen - 1;
731 if (index2 >= mPlayListLen) {
732 index2 = mPlayListLen - 1;
734 if (index1 < index2) {
735 int tmp = mPlayList[index1];
736 for (int i = index1; i < index2; i++) {
737 mPlayList[i] = mPlayList[i+1];
739 mPlayList[index2] = tmp;
740 if (mPlayPos == index1) {
742 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
745 } else if (index2 < index1) {
746 int tmp = mPlayList[index1];
747 for (int i = index1; i > index2; i--) {
748 mPlayList[i] = mPlayList[i-1];
750 mPlayList[index2] = tmp;
751 if (mPlayPos == index1) {
753 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
757 notifyChange(QUEUE_CHANGED);
762 * Returns the current play list
763 * @return An array of integers containing the IDs of the tracks in the play list
765 public int [] getQueue() {
766 synchronized (this) {
767 int len = mPlayListLen;
768 int [] list = new int[len];
769 for (int i = 0; i < len; i++) {
770 list[i] = mPlayList[i];
776 private void openCurrent() {
777 synchronized (this) {
778 if (mCursor != null) {
782 if (mPlayListLen == 0) {
787 String id = String.valueOf(mPlayList[mPlayPos]);
789 mCursor = getContentResolver().query(
790 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
791 mCursorCols, "_id=" + id , null, null);
792 if (mCursor != null) {
793 mCursor.moveToFirst();
794 open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id, false);
799 public void openAsync(String path) {
800 synchronized (this) {
805 mRepeatMode = REPEAT_NONE;
806 ensurePlayListCapacity(1);
812 mPlayer.setDataSourceAsync(mFileToPlay);
818 * Opens the specified file and readies it for playback.
820 * @param path The full path of the file to be opened.
821 * @param oneshot when set to true, playback will stop after this file completes, instead
822 * of moving on to the next track in the list
824 public void open(String path, boolean oneshot) {
825 synchronized (this) {
831 mRepeatMode = REPEAT_NONE;
832 ensurePlayListCapacity(1);
837 // if mCursor is null, try to associate path with a database cursor
838 if (mCursor == null) {
840 ContentResolver resolver = getContentResolver();
843 String selectionArgs[];
844 if (path.startsWith("content://media/")) {
845 uri = Uri.parse(path);
847 selectionArgs = null;
849 uri = MediaStore.Audio.Media.getContentUriForPath(path);
850 where = MediaStore.Audio.Media.DATA + "=?";
851 selectionArgs = new String[] { path };
855 mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);
856 if (mCursor != null) {
857 if (mCursor.getCount() == 0) {
861 mCursor.moveToNext();
862 ensurePlayListCapacity(1);
864 mPlayList[0] = mCursor.getInt(0);
868 } catch (UnsupportedOperationException ex) {
872 mPlayer.setDataSource(mFileToPlay);
874 if (! mPlayer.isInitialized()) {
876 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
877 // beware: this ends up being recursive because next() calls open() again.
880 if (! mPlayer.isInitialized() && mOpenFailedCounter != 0) {
881 // need to make sure we only shows this once
882 mOpenFailedCounter = 0;
884 Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
888 mOpenFailedCounter = 0;
894 * Starts playback of a previously opened file.
897 if (mPlayer.isInitialized()) {
902 NotificationManager nm = (NotificationManager)
903 getSystemService(Context.NOTIFICATION_SERVICE);
905 RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar);
906 views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer);
907 if (getAudioId() < 0) {
909 views.setTextViewText(R.id.trackname, getPath());
910 views.setTextViewText(R.id.artistalbum, null);
912 String artist = getArtistName();
913 views.setTextViewText(R.id.trackname, getTrackName());
914 if (artist == null || artist.equals(MediaFile.UNKNOWN_STRING)) {
915 artist = getString(R.string.unknown_artist_name);
917 String album = getAlbumName();
918 if (album == null || album.equals(MediaFile.UNKNOWN_STRING)) {
919 album = getString(R.string.unknown_album_name);
922 views.setTextViewText(R.id.artistalbum,
923 getString(R.string.notification_artist_album, artist, album)
927 Intent statusintent = new Intent("com.android.music.PLAYBACK_VIEWER");
928 statusintent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
929 Notification status = new Notification();
930 status.contentView = views;
931 status.flags |= Notification.FLAG_ONGOING_EVENT;
932 status.icon = R.drawable.stat_notify_musicplayer;
933 status.contentIntent = PendingIntent.getActivity(this, 0,
934 new Intent("com.android.music.PLAYBACK_VIEWER"), 0);
935 nm.notify(PLAYBACKSERVICE_STATUS, status);
936 notifyChange(PLAYSTATE_CHANGED);
940 private void stop(boolean remove_status_icon) {
941 if (mPlayer.isInitialized()) {
945 if (mCursor != null) {
949 if (remove_status_icon) {
952 setForeground(false);
964 * Pauses playback (call play() to resume)
966 public void pause() {
970 setForeground(false);
972 notifyChange(PLAYSTATE_CHANGED);
976 /** Returns whether playback is currently paused
978 * @return true if playback is paused, false if not
980 public boolean isPlaying() {
981 if (mPlayer.isInitialized()) {
982 return mPlayer.isPlaying();
988 Desired behavior for prev/next/shuffle:
990 - NEXT will move to the next track in the list when not shuffling, and to
991 a track randomly picked from the not-yet-played tracks when shuffling.
992 If all tracks have already been played, pick from the full set, but
993 avoid picking the previously played track if possible.
994 - when shuffling, PREV will go to the previously played track. Hitting PREV
995 again will go to the track played before that, etc. When the start of the
996 history has been reached, PREV is a no-op.
997 When not shuffling, PREV will go to the sequentially previous track (the
998 difference with the shuffle-case is mainly that when not shuffling, the
999 user can back up to tracks that are not in the history).
1002 When playing an album with 10 tracks from the start, and enabling shuffle
1003 while playing track 5, the remaining tracks (6-10) will be shuffled, e.g.
1004 the final play order might be 1-2-3-4-5-8-10-6-9-7.
1005 When hitting 'prev' 8 times while playing track 7 in this example, the
1006 user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next',
1007 a random track will be picked again. If at any time user disables shuffling
1008 the next/previous track will be picked in sequential order again.
1011 public void prev() {
1012 synchronized (this) {
1014 // we were playing a specific file not part of a playlist, so there is no 'previous'
1019 if (mShuffleMode == SHUFFLE_NORMAL) {
1020 // go to previously-played track and remove it from the history
1021 int histsize = mHistory.size();
1022 if (histsize == 0) {
1026 Integer pos = mHistory.remove(histsize - 1);
1027 mPlayPos = pos.intValue();
1032 mPlayPos = mPlayListLen - 1;
1038 notifyChange(META_CHANGED);
1042 public void next(boolean force) {
1043 synchronized (this) {
1045 // we were playing a specific file not part of a playlist, so there is no 'next'
1051 // Store the current file in the history, but keep the history at a
1053 mHistory.add(Integer.valueOf(mPlayPos));
1054 if (mHistory.size() > MAX_HISTORY_SIZE) {
1055 mHistory.removeElementAt(0);
1058 if (mShuffleMode == SHUFFLE_NORMAL) {
1059 // Pick random next track from the not-yet-played ones
1060 // TODO: make it work right after adding/removing items in the queue.
1062 int numTracks = mPlayListLen;
1063 int[] tracks = new int[numTracks];
1064 for (int i=0;i < numTracks; i++) {
1068 int numHistory = mHistory.size();
1069 int numUnplayed = numTracks;
1070 for (int i=0;i < numHistory; i++) {
1071 int idx = mHistory.get(i).intValue();
1072 if (idx < numTracks && tracks[idx] >= 0) {
1078 // 'numUnplayed' now indicates how many tracks have not yet
1079 // been played, and 'tracks' contains the indices of those
1081 if (numUnplayed <=0) {
1082 // everything's already been played
1083 if (mRepeatMode == REPEAT_ALL || force) {
1084 //pick from full set
1085 numUnplayed = numTracks;
1086 for (int i=0;i < numTracks; i++) {
1095 int skip = mRand.nextInt(numUnplayed);
1098 while (tracks[++cnt] < 0)
1106 } else if (mShuffleMode == SHUFFLE_AUTO) {
1107 doAutoShuffleUpdate();
1110 if (mPlayPos >= mPlayListLen - 1) {
1111 // we're at the end of the list
1112 if (mRepeatMode == REPEAT_NONE && !force) {
1115 notifyChange(PLAYBACK_COMPLETE);
1117 } else if (mRepeatMode == REPEAT_ALL || force) {
1127 notifyChange(META_CHANGED);
1131 private void gotoIdleState() {
1132 NotificationManager nm =
1133 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
1134 nm.cancel(PLAYBACKSERVICE_STATUS);
1135 mDelayedStopHandler.removeCallbacksAndMessages(null);
1136 Message msg = mDelayedStopHandler.obtainMessage();
1137 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
1140 // Make sure there are at least 5 items after the currently playing item
1141 // and no more than 10 items before.
1142 private void doAutoShuffleUpdate() {
1143 // remove old entries
1144 if (mPlayPos > 10) {
1145 removeTracks(0, mPlayPos - 9);
1147 // add new entries if needed
1148 int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
1150 for (int i = 0; i < to_add; i++) {
1151 // pick something at random from the list
1152 int idx = mRand.nextInt(mAutoShuffleList.length);
1153 Integer which = mAutoShuffleList[idx];
1154 addToPlayList(which);
1156 notifyChange(QUEUE_CHANGED);
1160 // A simple variation of Random that makes sure that the
1161 // value it returns is not equal to the value it returned
1162 // previously, unless the interval is 1.
1163 private class Shuffler {
1164 private int mPrevious;
1165 private Random mRandom = new Random();
1166 public int nextInt(int interval) {
1169 ret = mRandom.nextInt(interval);
1170 } while (ret == mPrevious && interval > 1);
1176 private boolean makeAutoShuffleList() {
1177 ContentResolver res = getContentResolver();
1180 c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1181 new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
1183 if (c == null || c.getCount() == 0) {
1186 int len = c.getCount();
1187 int[] list = new int[len];
1188 for (int i = 0; i < len; i++) {
1190 list[i] = c.getInt(0);
1192 mAutoShuffleList = list;
1194 } catch (RuntimeException ex) {
1204 * Removes the range of tracks specified from the play list. If a file within the range is
1205 * the file currently being played, playback will move to the next file after the
1207 * @param first The first file to be removed
1208 * @param last The last file to be removed
1209 * @return the number of tracks deleted
1211 public int removeTracks(int first, int last) {
1212 synchronized (this) {
1213 if (last < first) return 0;
1214 if (first < 0) first = 0;
1215 if (last >= mPlayListLen) last = mPlayListLen - 1;
1217 boolean gotonext = false;
1218 if (first <= mPlayPos && mPlayPos <= last) {
1221 } else if (mPlayPos > last) {
1222 mPlayPos -= (last - first + 1);
1224 int num = mPlayListLen - last - 1;
1225 for (int i = 0; i < num; i++) {
1226 mPlayList[first + i] = mPlayList[last + 1 + i];
1228 mPlayListLen -= last - first + 1;
1231 if (mPlayListLen == 0) {
1235 if (mPlayPos >= mPlayListLen) {
1243 notifyChange(QUEUE_CHANGED);
1244 return last - first + 1;
1249 * Removes all instances of the track with the given id
1250 * from the playlist.
1251 * @param id The id to be removed
1252 * @return how many instances of the track were removed
1254 public int removeTrack(int id) {
1256 synchronized (this) {
1257 for (int i = 0; i < mPlayListLen; i++) {
1258 if (mPlayList[i] == id) {
1259 numremoved += removeTracks(i, i);
1267 public void setShuffleMode(int shufflemode) {
1268 synchronized(this) {
1269 if (mShuffleMode == shufflemode) {
1272 mShuffleMode = shufflemode;
1273 if (mShuffleMode == SHUFFLE_AUTO) {
1274 if (makeAutoShuffleList()) {
1276 doAutoShuffleUpdate();
1280 notifyChange(META_CHANGED);
1282 // failed to build a list of files to shuffle
1283 mShuffleMode = SHUFFLE_NONE;
1288 public int getShuffleMode() {
1289 return mShuffleMode;
1292 public void setRepeatMode(int repeatmode) {
1293 synchronized(this) {
1294 mRepeatMode = repeatmode;
1297 public int getRepeatMode() {
1301 public int getMediaMountedCount() {
1302 return mMediaMountedCount;
1306 * Returns the path of the currently playing file, or null if
1307 * no file is currently playing.
1309 public String getPath() {
1314 * Returns the rowid of the currently playing file, or -1 if
1315 * no file is currently playing.
1317 public int getAudioId() {
1318 synchronized (this) {
1319 if (mPlayPos >= 0 && mPlayer.isInitialized()) {
1320 return mPlayList[mPlayPos];
1327 * Returns the position in the queue
1328 * @return the position in the queue
1330 public int getQueuePosition() {
1331 synchronized(this) {
1337 * Starts playing the track at the given position in the queue.
1338 * @param pos The position in the queue of the track that will be played.
1340 public void setQueuePosition(int pos) {
1341 synchronized(this) {
1346 notifyChange(META_CHANGED);
1350 public String getArtistName() {
1351 if (mCursor == null) {
1354 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
1357 public int getArtistId() {
1358 if (mCursor == null) {
1361 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));
1364 public String getAlbumName() {
1365 if (mCursor == null) {
1368 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
1371 public int getAlbumId() {
1372 if (mCursor == null) {
1375 return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));
1378 public String getTrackName() {
1379 if (mCursor == null) {
1382 return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
1388 * Returns the duration of the file in milliseconds.
1389 * Currently this method returns -1 for the duration of MIDI files.
1391 public long duration() {
1392 if (mPlayer.isInitialized()) {
1393 return mPlayer.duration();
1395 // TODO: when the MIDI engine supports it, return MIDI duration.
1400 * Returns the current playback position in milliseconds
1402 public long position() {
1403 if (mPlayer.isInitialized()) {
1404 return mPlayer.position();
1410 * Seeks to the position specified.
1412 * @param pos The position to seek to, in milliseconds
1414 public long seek(long pos) {
1415 if (mPlayer.isInitialized()) {
1416 if (pos < 0) pos = 0;
1417 if (pos > mPlayer.duration()) pos = mPlayer.duration();
1418 return mPlayer.seek(pos);
1424 * Provides a unified interface for dealing with midi files and
1425 * other media files.
1427 private class MultiPlayer {
1428 private MediaPlayer mMediaPlayer = new MediaPlayer();
1429 private Handler mHandler;
1430 private boolean mIsInitialized = false;
1432 public MultiPlayer() {
1433 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1436 public void setDataSourceAsync(String path) {
1438 mMediaPlayer.reset();
1439 mMediaPlayer.setDataSource(path);
1440 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1441 mMediaPlayer.setOnPreparedListener(preparedlistener);
1442 mMediaPlayer.prepareAsync();
1443 } catch (IOException ex) {
1444 // TODO: notify the user why the file couldn't be opened
1445 mIsInitialized = false;
1447 } catch (IllegalArgumentException ex) {
1448 // TODO: notify the user why the file couldn't be opened
1449 mIsInitialized = false;
1452 mMediaPlayer.setOnCompletionListener(listener);
1453 mMediaPlayer.setOnErrorListener(errorListener);
1455 mIsInitialized = true;
1458 public void setDataSource(String path) {
1460 mMediaPlayer.reset();
1461 mMediaPlayer.setOnPreparedListener(null);
1462 if (path.startsWith("content://")) {
1463 mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path));
1465 mMediaPlayer.setDataSource(path);
1467 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
1468 mMediaPlayer.prepare();
1469 } catch (IOException ex) {
1470 // TODO: notify the user why the file couldn't be opened
1471 mIsInitialized = false;
1473 } catch (IllegalArgumentException ex) {
1474 // TODO: notify the user why the file couldn't be opened
1475 mIsInitialized = false;
1478 mMediaPlayer.setOnCompletionListener(listener);
1479 mMediaPlayer.setOnErrorListener(errorListener);
1481 mIsInitialized = true;
1484 public boolean isInitialized() {
1485 return mIsInitialized;
1488 public void start() {
1489 mMediaPlayer.start();
1492 public void stop() {
1493 mMediaPlayer.reset();
1494 mIsInitialized = false;
1497 public void pause() {
1498 mMediaPlayer.pause();
1501 public boolean isPlaying() {
1502 return mMediaPlayer.isPlaying();
1505 public void setHandler(Handler handler) {
1509 MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {
1510 public void onCompletion(MediaPlayer mp) {
1511 // Acquire a temporary wakelock, since when we return from
1512 // this callback the MediaPlayer will release its wakelock
1513 // and allow the device to go to sleep.
1514 // This temporary wakelock is released when the RELEASE_WAKELOCK
1515 // message is processed, but just in case, put a timeout on it.
1516 mWakeLock.acquire(30000);
1517 mHandler.sendEmptyMessage(TRACK_ENDED);
1518 mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
1522 MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() {
1523 public void onPrepared(MediaPlayer mp) {
1524 notifyChange(ASYNC_OPEN_COMPLETE);
1528 MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {
1529 public boolean onError(MediaPlayer mp, int what, int extra) {
1531 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
1532 mIsInitialized = false;
1533 mMediaPlayer.release();
1534 // Creating a new MediaPlayer and settings its wakemode does not
1535 // require the media service, so it's OK to do this now, while the
1536 // service is still being restarted
1537 mMediaPlayer = new MediaPlayer();
1538 mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
1539 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
1548 public long duration() {
1549 return mMediaPlayer.getDuration();
1552 public long position() {
1553 return mMediaPlayer.getCurrentPosition();
1556 public long seek(long whereto) {
1557 mMediaPlayer.seekTo((int) whereto);
1561 public void setVolume(float vol) {
1562 mMediaPlayer.setVolume(vol, vol);
1566 private final IMediaPlaybackService.Stub mBinder = new IMediaPlaybackService.Stub()
1568 public void openfileAsync(String path)
1570 MediaPlaybackService.this.openAsync(path);
1572 public void openfile(String path)
1574 MediaPlaybackService.this.open(path, true);
1576 public void open(int [] list, int position) {
1577 MediaPlaybackService.this.open(list, position);
1579 public int getQueuePosition() {
1580 return MediaPlaybackService.this.getQueuePosition();
1582 public void setQueuePosition(int index) {
1583 MediaPlaybackService.this.setQueuePosition(index);
1585 public boolean isPlaying() {
1586 return MediaPlaybackService.this.isPlaying();
1588 public void stop() {
1589 MediaPlaybackService.this.stop();
1591 public void pause() {
1592 MediaPlaybackService.this.pause();
1594 public void play() {
1595 MediaPlaybackService.this.play();
1597 public void prev() {
1598 MediaPlaybackService.this.prev();
1600 public void next() {
1601 MediaPlaybackService.this.next(true);
1603 public String getTrackName() {
1604 return MediaPlaybackService.this.getTrackName();
1606 public String getAlbumName() {
1607 return MediaPlaybackService.this.getAlbumName();
1609 public int getAlbumId() {
1610 return MediaPlaybackService.this.getAlbumId();
1612 public String getArtistName() {
1613 return MediaPlaybackService.this.getArtistName();
1615 public int getArtistId() {
1616 return MediaPlaybackService.this.getArtistId();
1618 public void enqueue(int [] list , int action) {
1619 MediaPlaybackService.this.enqueue(list, action);
1621 public int [] getQueue() {
1622 return MediaPlaybackService.this.getQueue();
1624 public void moveQueueItem(int from, int to) {
1625 MediaPlaybackService.this.moveQueueItem(from, to);
1627 public String getPath() {
1628 return MediaPlaybackService.this.getPath();
1630 public int getAudioId() {
1631 return MediaPlaybackService.this.getAudioId();
1633 public long position() {
1634 return MediaPlaybackService.this.position();
1636 public long duration() {
1637 return MediaPlaybackService.this.duration();
1639 public long seek(long pos) {
1640 return MediaPlaybackService.this.seek(pos);
1642 public void setShuffleMode(int shufflemode) {
1643 MediaPlaybackService.this.setShuffleMode(shufflemode);
1645 public int getShuffleMode() {
1646 return MediaPlaybackService.this.getShuffleMode();
1648 public int removeTracks(int first, int last) {
1649 return MediaPlaybackService.this.removeTracks(first, last);
1651 public int removeTrack(int id) {
1652 return MediaPlaybackService.this.removeTrack(id);
1654 public void setRepeatMode(int repeatmode) {
1655 MediaPlaybackService.this.setRepeatMode(repeatmode);
1657 public int getRepeatMode() {
1658 return MediaPlaybackService.this.getRepeatMode();
1660 public int getMediaMountedCount() {
1661 return MediaPlaybackService.this.getMediaMountedCount();