2 * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
3 * (the "License"); you may not use this file except in compliance with the
4 * License. You may obtain a copy of the License at
5 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6 * or agreed to in writing, software distributed under the License is
7 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 * KIND, either express or implied. See the License for the specific language
9 * governing permissions and limitations under the License.
12 package com.andrew.apollo;
14 import android.annotation.SuppressLint;
15 import android.app.AlarmManager;
16 import android.app.PendingIntent;
17 import android.app.Service;
18 import android.appwidget.AppWidgetManager;
19 import android.content.BroadcastReceiver;
20 import android.content.ComponentName;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.SharedPreferences;
26 import android.database.Cursor;
27 import android.graphics.Bitmap;
28 import android.media.AudioManager;
29 import android.media.AudioManager.OnAudioFocusChangeListener;
30 import android.media.MediaMetadataRetriever;
31 import android.media.MediaPlayer;
32 import android.media.MediaPlayer.OnCompletionListener;
33 import android.media.RemoteControlClient;
34 import android.media.audiofx.AudioEffect;
35 import android.net.Uri;
36 import android.os.Handler;
37 import android.os.HandlerThread;
38 import android.os.IBinder;
39 import android.os.Looper;
40 import android.os.Message;
41 import android.os.PowerManager;
42 import android.os.PowerManager.WakeLock;
43 import android.os.RemoteException;
44 import android.os.SystemClock;
45 import android.provider.MediaStore;
46 import android.provider.MediaStore.Audio.AudioColumns;
48 import com.andrew.apollo.appwidgets.AppWidgetLarge;
49 import com.andrew.apollo.appwidgets.AppWidgetLargeAlternate;
50 import com.andrew.apollo.appwidgets.AppWidgetSmall;
51 import com.andrew.apollo.appwidgets.RecentWidgetProvider;
52 import com.andrew.apollo.cache.ImageCache;
53 import com.andrew.apollo.cache.ImageFetcher;
54 import com.andrew.apollo.provider.FavoritesStore;
55 import com.andrew.apollo.provider.RecentStore;
56 import com.andrew.apollo.utils.ApolloUtils;
57 import com.andrew.apollo.utils.Lists;
58 import com.andrew.apollo.utils.MusicUtils;
59 import com.andrew.apollo.utils.PreferenceUtils;
61 import java.io.IOException;
62 import java.lang.ref.WeakReference;
63 import java.util.LinkedList;
64 import java.util.Random;
65 import java.util.TreeSet;
68 * A backbround {@link Service} used to keep music playing between activities
69 * and when the user moves Apollo into the background.
71 @SuppressLint("NewApi")
72 public class MusicPlaybackService extends Service {
74 * Indicates that the music has paused or resumed
76 public static final String PLAYSTATE_CHANGED = "com.andrew.apollo.playstatechanged";
79 * Indicates the meta data has changed in some way, like a track change
81 public static final String META_CHANGED = "com.andrew.apollo.metachanged";
84 * Indicates the queue has been updated
86 public static final String QUEUE_CHANGED = "com.andrew.apollo.queuechanged";
89 * Indicates the repeat mode chaned
91 public static final String REPEATMODE_CHANGED = "com.andrew.apollo.repeatmodechanged";
94 * Indicates the shuffle mode chaned
96 public static final String SHUFFLEMODE_CHANGED = "com.andrew.apollo.shufflemodechanged";
99 * For backwards compatibility reasons, also provide sticky
100 * broadcasts under the music package
102 public static final String APOLLO_PACKAGE_NAME = "com.andrew.apollo";
103 public static final String MUSIC_PACKAGE_NAME = "com.android.music";
106 * Called to indicate a general service commmand. Used in
107 * {@link MediaButtonIntentReceiver}
109 public static final String SERVICECMD = "com.andrew.apollo.musicservicecommand";
112 * Called to go toggle between pausing and playing the music
114 public static final String TOGGLEPAUSE_ACTION = "com.andrew.apollo.togglepause";
117 * Called to go to pause the playback
119 public static final String PAUSE_ACTION = "com.andrew.apollo.pause";
122 * Called to go to stop the playback
124 public static final String STOP_ACTION = "com.andrew.apollo.stop";
127 * Called to go to the previous track
129 public static final String PREVIOUS_ACTION = "com.andrew.apollo.previous";
132 * Called to go to the next track
134 public static final String NEXT_ACTION = "com.andrew.apollo.next";
137 * Called to change the repeat mode
139 public static final String REPEAT_ACTION = "com.andrew.apollo.repeat";
142 * Called to change the shuffle mode
144 public static final String SHUFFLE_ACTION = "com.andrew.apollo.shuffle";
147 * Called to update the service about the foreground state of Apollo's activities
149 public static final String FOREGROUND_STATE_CHANGED = "com.andrew.apollo.fgstatechanged";
151 public static final String NOW_IN_FOREGROUND = "nowinforeground";
154 * Used to easily notify a list that it should refresh. i.e. A playlist
157 public static final String REFRESH = "com.andrew.apollo.refresh";
160 * Used by the alarm intent to remove the notification
162 private static final String KILL_NOTIFICATION = "com.andrew.apollo.killnotification";
165 * Called to update the remote control client
167 public static final String UPDATE_LOCKSCREEN = "com.andrew.apollo.updatelockscreen";
169 public static final String CMDNAME = "command";
171 public static final String CMDTOGGLEPAUSE = "togglepause";
173 public static final String CMDSTOP = "stop";
175 public static final String CMDPAUSE = "pause";
177 public static final String CMDPLAY = "play";
179 public static final String CMDPREVIOUS = "previous";
181 public static final String CMDNEXT = "next";
183 public static final String CMDNOTIF = "buttonId";
185 private static final int IDCOLIDX = 0;
188 * Moves a list to the front of the queue
190 public static final int NOW = 1;
193 * Moves a list to the next position in the queue
195 public static final int NEXT = 2;
198 * Moves a list to the last position in the queue
200 public static final int LAST = 3;
203 * Shuffles no songs, turns shuffling off
205 public static final int SHUFFLE_NONE = 0;
210 public static final int SHUFFLE_NORMAL = 1;
215 public static final int SHUFFLE_AUTO = 2;
220 public static final int REPEAT_NONE = 0;
223 * Repeats the current track in a list
225 public static final int REPEAT_CURRENT = 1;
228 * Repeats all the tracks in a list
230 public static final int REPEAT_ALL = 2;
233 * Indicates when the track ends
235 private static final int TRACK_ENDED = 1;
238 * Indicates that the current track was changed the next track
240 private static final int TRACK_WENT_TO_NEXT = 2;
243 * Indicates when the release the wake lock
245 private static final int RELEASE_WAKELOCK = 3;
248 * Indicates the player died
250 private static final int SERVER_DIED = 4;
253 * Indicates some sort of focus change, maybe a phone call
255 private static final int FOCUSCHANGE = 5;
258 * Indicates to fade the volume down
260 private static final int FADEDOWN = 6;
263 * Indicates to fade the volume back up
265 private static final int FADEUP = 7;
268 * Idle time before stopping the foreground notfication (1 minute)
270 private static final int IDLE_DELAY = 60000;
273 * The max size allowed for the track history
275 private static final int MAX_HISTORY_SIZE = 100;
278 * The columns used to retrieve any info from the current track
280 private static final String[] PROJECTION = new String[] {
281 "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
282 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
283 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
284 MediaStore.Audio.Media.ARTIST_ID
288 * Keeps a mapping of the track history
290 private static final LinkedList<Integer> mHistory = Lists.newLinkedList();
293 * Used to shuffle the tracks
295 private static final Shuffler mShuffler = new Shuffler();
298 * Used to save the queue as reverse hexadecimal numbers, which we can
299 * generate faster than normal decimal or hexadecimal numbers, which in
300 * turn allows us to save the playlist more often without worrying too
301 * much about performance
303 private static final char HEX_DIGITS[] = new char[] {
304 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
310 private final IBinder mBinder = new ServiceStub(this);
315 private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
320 private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
323 * 4x2 alternate widget
325 private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
329 * Recently listened widget
331 private final RecentWidgetProvider mRecentWidgetProvider = RecentWidgetProvider.getInstance();
336 private MultiPlayer mPlayer;
339 * The path of the current file to play
341 private String mFileToPlay;
344 * Keeps the service running when the screen is off
346 private WakeLock mWakeLock;
349 * Alarm intent for removing the notification when nothing is playing
352 private AlarmManager mAlarmManager;
353 private PendingIntent mKillNotificationIntent;
356 * The cursor used to retrieve info on the current track and run the
357 * necessary queries to play audio files
359 private Cursor mCursor;
362 * Monitors the audio state
364 private AudioManager mAudioManager;
367 * Settings used to save and retrieve the queue and history
369 private SharedPreferences mPreferences;
372 * Used to know when the service is active
374 private boolean mServiceInUse = false;
377 * Used to know if something should be playing or not
379 private boolean mIsSupposedToBePlaying = false;
382 * Used to indicate if the queue can be saved
384 private boolean mQueueIsSaveable = true;
387 * Used to track what type of audio focus loss caused the playback to pause
389 private boolean mPausedByTransientLossOfFocus = false;
392 * Used to track whether any of Apollo's activities is in the foreground
394 private boolean mAnyActivityInForeground = false;
397 * Lock screen controls
399 private RemoteControlClient mRemoteControlClient;
401 private ComponentName mMediaButtonReceiverComponent;
403 // We use this to distinguish between different cards when saving/restoring
407 private int mPlayListLen = 0;
409 private int mPlayPos = -1;
411 private int mNextPlayPos = -1;
413 private int mOpenFailedCounter = 0;
415 private int mMediaMountedCount = 0;
417 private int mShuffleMode = SHUFFLE_NONE;
419 private int mRepeatMode = REPEAT_NONE;
421 private int mServiceStartId = -1;
423 private long[] mPlayList = null;
425 private long[] mAutoShuffleList = null;
427 private MusicPlayerHandler mPlayerHandler;
429 private DelayedHandler mDelayedStopHandler;
431 private BroadcastReceiver mUnmountReceiver = null;
436 private ImageFetcher mImageFetcher;
439 * Used to build the notification
441 private NotificationHelper mNotificationHelper;
444 * Recently listened database
446 private RecentStore mRecentsCache;
451 private FavoritesStore mFavoritesCache;
457 public IBinder onBind(final Intent intent) {
458 mDelayedStopHandler.removeCallbacksAndMessages(null);
459 mServiceInUse = true;
467 public boolean onUnbind(final Intent intent) {
468 mServiceInUse = false;
471 if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
472 // Something is currently playing, or will be playing once
473 // an in-progress action requesting audio focus ends, so don't stop
477 // If there is a playlist but playback is paused, then wait a while
478 // before stopping the service, so that pause/resume isn't slow.
479 // Also delay stopping the service if we're transitioning between
481 } else if (mPlayListLen > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
482 final Message msg = mDelayedStopHandler.obtainMessage();
483 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
486 stopSelf(mServiceStartId);
494 public void onRebind(final Intent intent) {
495 mDelayedStopHandler.removeCallbacksAndMessages(null);
496 mServiceInUse = true;
503 public void onCreate() {
506 // Initialize the favorites and recents databases
507 mRecentsCache = RecentStore.getInstance(this);
508 mFavoritesCache = FavoritesStore.getInstance(this);
510 // Initialize the notification helper
511 mNotificationHelper = new NotificationHelper(this);
513 // Initialize the image fetcher
514 mImageFetcher = ImageFetcher.getInstance(this);
515 // Initialize the image cache
516 mImageFetcher.setImageCache(ImageCache.getInstance(this));
518 // Start up the thread running the service. Note that we create a
519 // separate thread because the service normally runs in the process's
520 // main thread, which we don't want to block. We also make it
521 // background priority so CPU-intensive work will not disrupt the UI.
522 final HandlerThread thread = new HandlerThread("MusicPlayerHandler",
523 android.os.Process.THREAD_PRIORITY_BACKGROUND);
526 // Initialize the handlers
527 mPlayerHandler = new MusicPlayerHandler(this, thread.getLooper());
528 mDelayedStopHandler = new DelayedHandler(this);
530 // Initialize the audio manager and register any headset controls for
532 mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
533 mMediaButtonReceiverComponent = new ComponentName(getPackageName(),
534 MediaButtonIntentReceiver.class.getName());
535 mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);
537 // Use the remote control APIs (if available) to set the playback state
538 setUpRemoteControlClient();
540 // Initialize the preferences
541 mPreferences = getSharedPreferences("Service", 0);
542 mCardId = getCardId();
544 registerExternalStorageListener();
546 // Initialize the media player
547 mPlayer = new MultiPlayer(this);
548 mPlayer.setHandler(mPlayerHandler);
550 // Initialize the intent filter and each action
551 final IntentFilter filter = new IntentFilter();
552 filter.addAction(SERVICECMD);
553 filter.addAction(TOGGLEPAUSE_ACTION);
554 filter.addAction(PAUSE_ACTION);
555 filter.addAction(STOP_ACTION);
556 filter.addAction(NEXT_ACTION);
557 filter.addAction(PREVIOUS_ACTION);
558 filter.addAction(REPEAT_ACTION);
559 filter.addAction(SHUFFLE_ACTION);
560 // Attach the broadcast listener
561 registerReceiver(mIntentReceiver, filter);
563 // Initialize the wake lock
564 final PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
565 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
566 mWakeLock.setReferenceCounted(false);
568 // Initialize the notification removal intent
569 final Intent killIntent = new Intent(this, MusicPlaybackService.class);
570 killIntent.setAction(KILL_NOTIFICATION);
572 mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
573 mKillNotificationIntent = PendingIntent.getService(this, 0, killIntent, 0);
575 // Bring the queue back
577 notifyChange(QUEUE_CHANGED);
578 notifyChange(META_CHANGED);
580 // Listen for the idle state
581 final Message message = mDelayedStopHandler.obtainMessage();
582 mDelayedStopHandler.sendMessageDelayed(message, IDLE_DELAY);
586 * Initializes the remote control client
588 private void setUpRemoteControlClient() {
589 final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
590 mediaButtonIntent.setComponent(mMediaButtonReceiverComponent);
591 mRemoteControlClient = new RemoteControlClient(
592 PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent,
593 PendingIntent.FLAG_UPDATE_CURRENT));
594 mAudioManager.registerRemoteControlClient(mRemoteControlClient);
596 // Flags for the media transport control that this client supports.
597 final int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
598 | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
599 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY
600 | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
601 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
602 | RemoteControlClient.FLAG_KEY_MEDIA_STOP;
603 mRemoteControlClient.setTransportControlFlags(flags);
610 public void onDestroy() {
612 // Remove any sound effects
613 final Intent audioEffectsIntent = new Intent(
614 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
615 audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
616 audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
617 sendBroadcast(audioEffectsIntent);
619 // remove any pending alarms
620 mAlarmManager.cancel(mKillNotificationIntent);
622 // Release the player
626 // Remove the audio focus listener and lock screen controls
627 mAudioManager.abandonAudioFocus(mAudioFocusListener);
628 mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
630 // Remove any callbacks from the handlers
631 mDelayedStopHandler.removeCallbacksAndMessages(null);
632 mPlayerHandler.removeCallbacksAndMessages(null);
635 if (mCursor != null) {
640 // Unregister the mount listener
641 unregisterReceiver(mIntentReceiver);
642 if (mUnmountReceiver != null) {
643 unregisterReceiver(mUnmountReceiver);
644 mUnmountReceiver = null;
647 // Release the wake lock
655 public int onStartCommand(final Intent intent, final int flags, final int startId) {
656 mServiceStartId = startId;
658 if (intent != null) {
659 final String action = intent.getAction();
661 if (intent.hasExtra(NOW_IN_FOREGROUND)) {
662 mAnyActivityInForeground = intent.getBooleanExtra(NOW_IN_FOREGROUND, false);
663 updateNotification();
667 handleCommandIntent(intent);
669 if (UPDATE_LOCKSCREEN.equals(action)) {
670 mEnableLockscreenControls = intent.getBooleanExtra(UPDATE_LOCKSCREEN, true);
671 if (mEnableLockscreenControls) {
672 setUpRemoteControlClient();
673 // Update the controls according to the current playback
674 notifyChange(PLAYSTATE_CHANGED);
675 notifyChange(META_CHANGED);
677 // Remove then unregister the controls
679 .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
680 mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
682 } else if (KILL_NOTIFICATION.equals(action)) {
683 mNotificationHelper.killNotification();
685 handleCommandIntent(intent);
687 >>>>>>> aeb807a... Make sure notification is shown when playback is started via media
690 // Make sure the service will shut down on its own if it was
691 // just started but not bound to and nothing is playing
692 mDelayedStopHandler.removeCallbacksAndMessages(null);
693 final Message msg = mDelayedStopHandler.obtainMessage();
694 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
698 private void handleCommandIntent(Intent intent) {
699 final String action = intent.getAction();
700 final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
702 if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
704 } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) {
705 if (position() < 2000) {
711 } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
714 mPausedByTransientLossOfFocus = false;
718 } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
720 mPausedByTransientLossOfFocus = false;
721 } else if (CMDPLAY.equals(command)) {
723 } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
725 mPausedByTransientLossOfFocus = false;
727 mNotificationHelper.killNotification();
728 } else if (REPEAT_ACTION.equals(action)) {
730 } else if (SHUFFLE_ACTION.equals(action)) {
736 * Updates the notification, considering the current play and activity state
738 private void updateNotification() {
739 if (!mAnyActivityInForeground && isPlaying()) {
740 mAlarmManager.cancel(mKillNotificationIntent);
741 mNotificationHelper.buildNotification(getAlbumName(), getArtistName(),
742 getTrackName(), getAlbumId(), getAlbumArt(), isPlaying());
743 } else if (mAnyActivityInForeground) {
744 mNotificationHelper.killNotification();
749 * @return A card ID used to save and restore playlists, i.e., the queue.
751 private int getCardId() {
752 final ContentResolver resolver = getContentResolver();
753 Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
756 if (cursor != null && cursor.moveToFirst()) {
757 mCardId = cursor.getInt(0);
765 * Called when we receive a ACTION_MEDIA_EJECT notification.
767 * @param storagePath The path to mount point for the removed media
769 public void closeExternalStorageFiles(final String storagePath) {
771 notifyChange(QUEUE_CHANGED);
772 notifyChange(META_CHANGED);
776 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
777 * intent will call closeExternalStorageFiles() if the external media is
778 * going to be ejected, so applications can clean up any files they have
781 public void registerExternalStorageListener() {
782 if (mUnmountReceiver == null) {
783 mUnmountReceiver = new BroadcastReceiver() {
789 public void onReceive(final Context context, final Intent intent) {
790 final String action = intent.getAction();
791 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
793 mQueueIsSaveable = false;
794 closeExternalStorageFiles(intent.getData().getPath());
795 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
796 mMediaMountedCount++;
797 mCardId = getCardId();
799 mQueueIsSaveable = true;
800 notifyChange(QUEUE_CHANGED);
801 notifyChange(META_CHANGED);
805 final IntentFilter filter = new IntentFilter();
806 filter.addAction(Intent.ACTION_MEDIA_EJECT);
807 filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
808 filter.addDataScheme("file");
809 registerReceiver(mUnmountReceiver, filter);
814 * Changes the notification buttons to a paused state and beging the
815 * countdown to calling {@code #stopForeground(true)}
817 private void gotoIdleState() {
818 mDelayedStopHandler.removeCallbacksAndMessages(null);
819 final Message msg = mDelayedStopHandler.obtainMessage();
820 mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
821 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
822 SystemClock.elapsedRealtime() + IDLE_DELAY, mKillNotificationIntent);
828 * @param goToIdle True to go to the idle state, false otherwise
830 private void stop(final boolean goToIdle) {
831 if (mPlayer.isInitialized()) {
835 if (mCursor != null) {
841 mIsSupposedToBePlaying = false;
843 stopForeground(false);
848 * Removes the range of tracks specified from the play list. If a file
849 * within the range is the file currently being played, playback will move
850 * to the next file after the range.
852 * @param first The first file to be removed
853 * @param last The last file to be removed
854 * @return the number of tracks deleted
856 private int removeTracksInternal(int first, int last) {
857 synchronized (this) {
860 } else if (first < 0) {
862 } else if (last >= mPlayListLen) {
863 last = mPlayListLen - 1;
866 boolean gotonext = false;
867 if (first <= mPlayPos && mPlayPos <= last) {
870 } else if (mPlayPos > last) {
871 mPlayPos -= last - first + 1;
873 final int num = mPlayListLen - last - 1;
874 for (int i = 0; i < num; i++) {
875 mPlayList[first + i] = mPlayList[last + 1 + i];
877 mPlayListLen -= last - first + 1;
880 if (mPlayListLen == 0) {
883 if (mCursor != null) {
888 if (mPlayPos >= mPlayListLen) {
891 final boolean wasPlaying = isPlaying();
893 openCurrentAndNext();
898 notifyChange(META_CHANGED);
900 return last - first + 1;
905 * Adds a list to the playlist
907 * @param list The list to add
908 * @param position The position to place the tracks
910 private void addToPlayList(final long[] list, int position) {
911 final int addlen = list.length;
916 ensurePlayListCapacity(mPlayListLen + addlen);
917 if (position > mPlayListLen) {
918 position = mPlayListLen;
921 final int tailsize = mPlayListLen - position;
922 for (int i = tailsize; i > 0; i--) {
923 mPlayList[position + i] = mPlayList[position + i - addlen];
926 for (int i = 0; i < addlen; i++) {
927 mPlayList[position + i] = list[i];
929 mPlayListLen += addlen;
930 if (mPlayListLen == 0) {
933 notifyChange(META_CHANGED);
938 * @param lid The list ID
939 * @return The cursor used for a specific ID
941 private Cursor getCursorForId(final long id) {
942 final Cursor c = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
943 PROJECTION, "_id=" + id, null, null);
951 * Called to open a new file as the current track and prepare the next for
954 private void openCurrentAndNext() {
955 openCurrentAndMaybeNext(true);
959 * Called to open a new file as the current track and prepare the next for
962 * @param openNext True to prepare the next track for playback, false
965 private void openCurrentAndMaybeNext(final boolean openNext) {
966 synchronized (this) {
967 if (mCursor != null) {
972 if (mPlayListLen == 0) {
977 mCursor = getCursorForId(mPlayList[mPlayPos]);
980 && mCursor.getCount() != 0
981 && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
982 + mCursor.getLong(IDCOLIDX))) {
985 // if we get here then opening the file failed. We can close the
986 // cursor now, because
987 // we're either going to create a new one next, or stop trying
988 if (mCursor != null) {
992 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
993 final int pos = getNextPosition(false);
996 if (mIsSupposedToBePlaying) {
997 mIsSupposedToBePlaying = false;
998 notifyChange(PLAYSTATE_CHANGED);
1005 mCursor = getCursorForId(mPlayList[mPlayPos]);
1007 mOpenFailedCounter = 0;
1009 if (mIsSupposedToBePlaying) {
1010 mIsSupposedToBePlaying = false;
1011 notifyChange(PLAYSTATE_CHANGED);
1023 * @param force True to force the player onto the track next, false
1025 * @return The next position to play.
1027 private int getNextPosition(final boolean force) {
1028 if (!force && mRepeatMode == REPEAT_CURRENT) {
1033 } else if (mShuffleMode == SHUFFLE_NORMAL) {
1034 if (mPlayPos >= 0) {
1035 mHistory.add(mPlayPos);
1037 if (mHistory.size() > MAX_HISTORY_SIZE) {
1040 final int numTracks = mPlayListLen;
1041 final int[] tracks = new int[numTracks];
1042 for (int i = 0; i < numTracks; i++) {
1046 final int numHistory = mHistory.size();
1047 int numUnplayed = numTracks;
1048 for (int i = 0; i < numHistory; i++) {
1049 final int idx = mHistory.get(i).intValue();
1050 if (idx < numTracks && tracks[idx] >= 0) {
1055 if (numUnplayed <= 0) {
1056 if (mRepeatMode == REPEAT_ALL || force) {
1057 numUnplayed = numTracks;
1058 for (int i = 0; i < numTracks; i++) {
1066 if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
1067 skip = mShuffler.nextInt(numUnplayed);
1071 while (tracks[++cnt] < 0) {
1080 } else if (mShuffleMode == SHUFFLE_AUTO) {
1081 doAutoShuffleUpdate();
1082 return mPlayPos + 1;
1084 if (mPlayPos >= mPlayListLen - 1) {
1085 if (mRepeatMode == REPEAT_NONE && !force) {
1087 } else if (mRepeatMode == REPEAT_ALL || force) {
1092 return mPlayPos + 1;
1098 * Sets the track track to be played
1100 private void setNextTrack() {
1101 mNextPlayPos = getNextPosition(false);
1102 if (mNextPlayPos >= 0 && mPlayList != null) {
1103 final long id = mPlayList[mNextPlayPos];
1104 mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
1109 * Creates a shuffled playlist used for party mode
1111 private boolean makeAutoShuffleList() {
1112 Cursor cursor = null;
1114 cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1116 MediaStore.Audio.Media._ID
1117 }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
1118 if (cursor == null || cursor.getCount() == 0) {
1121 final int len = cursor.getCount();
1122 final long[] list = new long[len];
1123 for (int i = 0; i < len; i++) {
1124 cursor.moveToNext();
1125 list[i] = cursor.getLong(0);
1127 mAutoShuffleList = list;
1129 } catch (final RuntimeException e) {
1131 if (cursor != null) {
1140 * Creates the party shuffle playlist
1142 private void doAutoShuffleUpdate() {
1143 boolean notify = false;
1144 if (mPlayPos > 10) {
1145 removeTracks(0, mPlayPos - 9);
1148 final int toAdd = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
1149 for (int i = 0; i < toAdd; i++) {
1150 int lookback = mHistory.size();
1153 idx = mShuffler.nextInt(mAutoShuffleList.length);
1154 if (!wasRecentlyUsed(idx, lookback)) {
1160 if (mHistory.size() > MAX_HISTORY_SIZE) {
1163 ensurePlayListCapacity(mPlayListLen + 1);
1164 mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
1168 notifyChange(QUEUE_CHANGED);
1173 private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
1174 if (lookbacksize == 0) {
1177 final int histsize = mHistory.size();
1178 if (histsize < lookbacksize) {
1179 lookbacksize = histsize;
1181 final int maxidx = histsize - 1;
1182 for (int i = 0; i < lookbacksize; i++) {
1183 final long entry = mHistory.get(maxidx - i);
1192 * Makes sure the playlist has enough space to hold all of the songs
1194 * @param size The size of the playlist
1196 private void ensurePlayListCapacity(final int size) {
1197 if (mPlayList == null || size > mPlayList.length) {
1198 // reallocate at 2x requested size so we don't
1199 // need to grow and copy the array for every
1201 final long[] newlist = new long[size * 2];
1202 final int len = mPlayList != null ? mPlayList.length : mPlayListLen;
1203 for (int i = 0; i < len; i++) {
1204 newlist[i] = mPlayList[i];
1206 mPlayList = newlist;
1208 // FIXME: shrink the array when the needed size is much smaller
1209 // than the allocated size
1213 * Notify the change-receivers that something has changed.
1215 private void notifyChange(final String what) {
1216 final Intent intent = new Intent(what);
1217 intent.putExtra("id", getAudioId());
1218 intent.putExtra("artist", getArtistName());
1219 intent.putExtra("album", getAlbumName());
1220 intent.putExtra("track", getTrackName());
1221 intent.putExtra("playing", isPlaying());
1222 intent.putExtra("isfavorite", isFavorite());
1223 sendStickyBroadcast(intent);
1225 final Intent musicIntent = new Intent(intent);
1226 musicIntent.setAction(what.replace(APOLLO_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
1227 sendStickyBroadcast(musicIntent);
1229 // Update the lockscreen controls
1230 updateRemoteControlClient(what);
1232 if (what.equals(META_CHANGED)) {
1233 // Increase the play count for favorite songs.
1234 if (mFavoritesCache.getSongId(getAudioId()) != null) {
1235 mFavoritesCache.addSongId(getAudioId(), getTrackName(), getAlbumName(),
1238 // Add the track to the recently played list.
1239 mRecentsCache.addAlbumId(getAlbumId(), getAlbumName(), getArtistName(),
1240 MusicUtils.getSongCountForAlbum(this, getAlbumName()),
1241 MusicUtils.getReleaseDateForAlbum(this, getAlbumName()));
1242 } else if (what.equals(QUEUE_CHANGED)) {
1248 if (what.equals(PLAYSTATE_CHANGED)) {
1249 mNotificationHelper.updatePlayState(isPlaying());
1252 // Update the app-widgets
1253 mAppWidgetSmall.notifyChange(this, what);
1254 mAppWidgetLarge.notifyChange(this, what);
1255 mAppWidgetLargeAlternate.notifyChange(this, what);
1256 mRecentWidgetProvider.notifyChange(this, what);
1260 * Updates the lockscreen controls.
1262 * @param what The broadcast
1264 private void updateRemoteControlClient(final String what) {
1265 if (what.equals(PLAYSTATE_CHANGED)) {
1266 mRemoteControlClient.setPlaybackState(mIsSupposedToBePlaying
1267 ? RemoteControlClient.PLAYSTATE_PLAYING
1268 : RemoteControlClient.PLAYSTATE_PAUSED);
1269 } else if (what.equals(META_CHANGED)) {
1270 Bitmap albumArt = getAlbumArt();
1271 if (albumArt != null) {
1272 // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
1273 // to make sure not to hand out our cache copy
1274 Bitmap.Config config = albumArt.getConfig();
1275 if (config == null) {
1276 config = Bitmap.Config.ARGB_8888;
1278 albumArt = albumArt.copy(config, false);
1280 mRemoteControlClient
1282 .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getArtistName())
1283 .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getAlbumName())
1284 .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getTrackName())
1285 .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration())
1286 .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, albumArt)
1294 * @param full True if the queue is full
1296 private void saveQueue(final boolean full) {
1297 if (!mQueueIsSaveable) {
1301 final SharedPreferences.Editor editor = mPreferences.edit();
1303 final StringBuilder q = new StringBuilder();
1304 int len = mPlayListLen;
1305 for (int i = 0; i < len; i++) {
1306 long n = mPlayList[i];
1309 } else if (n == 0) {
1313 final int digit = (int)(n & 0xf);
1315 q.append(HEX_DIGITS[digit]);
1320 editor.putString("queue", q.toString());
1321 editor.putInt("cardid", mCardId);
1322 if (mShuffleMode != SHUFFLE_NONE) {
1323 len = mHistory.size();
1325 for (int i = 0; i < len; i++) {
1326 int n = mHistory.get(i);
1331 final int digit = n & 0xf;
1333 q.append(HEX_DIGITS[digit]);
1338 editor.putString("history", q.toString());
1341 editor.putInt("curpos", mPlayPos);
1342 if (mPlayer.isInitialized()) {
1343 editor.putLong("seekpos", mPlayer.position());
1345 editor.putInt("repeatmode", mRepeatMode);
1346 editor.putInt("shufflemode", mShuffleMode);
1351 * Reloads the queue as the user left it the last time they stopped using
1354 private void reloadQueue() {
1357 if (mPreferences.contains("cardid")) {
1358 id = mPreferences.getInt("cardid", ~mCardId);
1360 if (id == mCardId) {
1361 q = mPreferences.getString("queue", "");
1363 int qlen = q != null ? q.length() : 0;
1368 for (int i = 0; i < qlen; i++) {
1369 final char c = q.charAt(i);
1371 ensurePlayListCapacity(plen + 1);
1372 mPlayList[plen] = n;
1377 if (c >= '0' && c <= '9') {
1378 n += c - '0' << shift;
1379 } else if (c >= 'a' && c <= 'f') {
1380 n += 10 + c - 'a' << shift;
1388 mPlayListLen = plen;
1389 final int pos = mPreferences.getInt("curpos", 0);
1390 if (pos < 0 || pos >= mPlayListLen) {
1395 Cursor mCursor = getContentResolver().query(
1396 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[] {
1398 }, "_id=" + mPlayList[mPlayPos], null, null);
1399 if (mCursor == null || mCursor.getCount() == 0) {
1400 SystemClock.sleep(3000);
1401 mCursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1402 PROJECTION, "_id=" + mPlayList[mPlayPos], null, null);
1404 if (mCursor != null) {
1408 mOpenFailedCounter = 20;
1409 openCurrentAndNext();
1410 if (!mPlayer.isInitialized()) {
1415 final long seekpos = mPreferences.getLong("seekpos", 0);
1416 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
1418 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
1419 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
1420 repmode = REPEAT_NONE;
1422 mRepeatMode = repmode;
1424 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
1425 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
1426 shufmode = SHUFFLE_NONE;
1428 if (shufmode != SHUFFLE_NONE) {
1429 q = mPreferences.getString("history", "");
1430 qlen = q != null ? q.length() : 0;
1436 for (int i = 0; i < qlen; i++) {
1437 final char c = q.charAt(i);
1439 if (n >= mPlayListLen) {
1447 if (c >= '0' && c <= '9') {
1448 n += c - '0' << shift;
1449 } else if (c >= 'a' && c <= 'f') {
1450 n += 10 + c - 'a' << shift;
1460 if (shufmode == SHUFFLE_AUTO) {
1461 if (!makeAutoShuffleList()) {
1462 shufmode = SHUFFLE_NONE;
1465 mShuffleMode = shufmode;
1470 * Opens a file and prepares it for playback
1472 * @param path The path of the file to open
1474 public boolean openFile(final String path) {
1475 synchronized (this) {
1480 // If mCursor is null, try to associate path with a database cursor
1481 if (mCursor == null) {
1482 final ContentResolver resolver = getContentResolver();
1485 String selectionArgs[];
1486 if (path.startsWith("content://media/")) {
1487 uri = Uri.parse(path);
1489 selectionArgs = null;
1491 uri = MediaStore.Audio.Media.getContentUriForPath(path);
1492 where = MediaStore.Audio.Media.DATA + "=?";
1493 selectionArgs = new String[] {
1498 mCursor = resolver.query(uri, PROJECTION, where, selectionArgs, null);
1499 if (mCursor != null) {
1500 if (mCursor.getCount() == 0) {
1504 mCursor.moveToNext();
1505 ensurePlayListCapacity(1);
1507 mPlayList[0] = mCursor.getLong(IDCOLIDX);
1511 } catch (final UnsupportedOperationException ex) {
1515 mPlayer.setDataSource(mFileToPlay);
1516 if (mPlayer.isInitialized()) {
1517 mOpenFailedCounter = 0;
1526 * Returns the audio session ID
1528 * @return The current media player audio session ID
1530 public int getAudioSessionId() {
1531 synchronized (this) {
1532 return mPlayer.getAudioSessionId();
1537 * Indicates if the media storeage device has been mounted or not
1539 * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
1541 public int getMediaMountedCount() {
1542 return mMediaMountedCount;
1546 * Returns the shuffle mode
1548 * @return The current shuffle mode (all, party, none)
1550 public int getShuffleMode() {
1551 return mShuffleMode;
1555 * Returns the repeat mode
1557 * @return The current repeat mode (all, one, none)
1559 public int getRepeatMode() {
1564 * Removes all instances of the track with the given ID from the playlist.
1566 * @param id The id to be removed
1567 * @return how many instances of the track were removed
1569 public int removeTrack(final long id) {
1571 synchronized (this) {
1572 for (int i = 0; i < mPlayListLen; i++) {
1573 if (mPlayList[i] == id) {
1574 numremoved += removeTracksInternal(i, i);
1579 if (numremoved > 0) {
1580 notifyChange(QUEUE_CHANGED);
1586 * Removes the range of tracks specified from the play list. If a file
1587 * within the range is the file currently being played, playback will move
1588 * to the next file after the range.
1590 * @param first The first file to be removed
1591 * @param last The last file to be removed
1592 * @return the number of tracks deleted
1594 public int removeTracks(final int first, final int last) {
1595 final int numremoved = removeTracksInternal(first, last);
1596 if (numremoved > 0) {
1597 notifyChange(QUEUE_CHANGED);
1603 * Returns the position in the queue
1605 * @return the current position in the queue
1607 public int getQueuePosition() {
1608 synchronized (this) {
1614 * Returns the path to current song
1616 * @return The path to the current song
1618 public String getPath() {
1619 synchronized (this) {
1620 if (mCursor == null) {
1623 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
1628 * Returns the album name
1630 * @return The current song album Name
1632 public String getAlbumName() {
1633 synchronized (this) {
1634 if (mCursor == null) {
1637 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
1642 * Returns the song name
1644 * @return The current song name
1646 public String getTrackName() {
1647 synchronized (this) {
1648 if (mCursor == null) {
1651 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
1656 * Returns the artist name
1658 * @return The current song artist name
1660 public String getArtistName() {
1661 synchronized (this) {
1662 if (mCursor == null) {
1665 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
1670 * Returns the album ID
1672 * @return The current song album ID
1674 public long getAlbumId() {
1675 synchronized (this) {
1676 if (mCursor == null) {
1679 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
1684 * Returns the artist ID
1686 * @return The current song artist ID
1688 public long getArtistId() {
1689 synchronized (this) {
1690 if (mCursor == null) {
1693 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
1698 * Returns the current audio ID
1700 * @return The current track ID
1702 public long getAudioId() {
1703 synchronized (this) {
1704 if (mPlayPos >= 0 && mPlayer.isInitialized()) {
1705 return mPlayList[mPlayPos];
1712 * Seeks the current track to a specific time
1714 * @param position The time to seek to
1715 * @return The time to play the track at
1717 public long seek(long position) {
1718 if (mPlayer.isInitialized()) {
1721 } else if (position > mPlayer.duration()) {
1722 position = mPlayer.duration();
1724 return mPlayer.seek(position);
1730 * Returns the current position in time of the currenttrack
1732 * @return The current playback position in miliseconds
1734 public long position() {
1735 if (mPlayer.isInitialized()) {
1736 return mPlayer.position();
1742 * Returns the full duration of the current track
1744 * @return The duration of the current track in miliseconds
1746 public long duration() {
1747 if (mPlayer.isInitialized()) {
1748 return mPlayer.duration();
1756 * @return The queue as a long[]
1758 public long[] getQueue() {
1759 synchronized (this) {
1760 final int len = mPlayListLen;
1761 final long[] list = new long[len];
1762 for (int i = 0; i < len; i++) {
1763 list[i] = mPlayList[i];
1770 * @return True if music is playing, false otherwise
1772 public boolean isPlaying() {
1773 return mIsSupposedToBePlaying;
1777 * True if the current track is a "favorite", false otherwise
1779 public boolean isFavorite() {
1780 if (mFavoritesCache != null) {
1781 synchronized (this) {
1782 final Long id = mFavoritesCache.getSongId(getAudioId());
1783 return id != null ? true : false;
1790 * Opens a list for playback
1792 * @param list The list of tracks to open
1793 * @param position The position to start playback at
1795 public void open(final long[] list, final int position) {
1796 synchronized (this) {
1797 if (mShuffleMode == SHUFFLE_AUTO) {
1798 mShuffleMode = SHUFFLE_NORMAL;
1800 final long oldId = getAudioId();
1801 final int listlength = list.length;
1802 boolean newlist = true;
1803 if (mPlayListLen == listlength) {
1805 for (int i = 0; i < listlength; i++) {
1806 if (list[i] != mPlayList[i]) {
1813 addToPlayList(list, -1);
1814 notifyChange(QUEUE_CHANGED);
1816 if (position >= 0) {
1817 mPlayPos = position;
1819 mPlayPos = mShuffler.nextInt(mPlayListLen);
1822 openCurrentAndNext();
1823 if (oldId != getAudioId()) {
1824 notifyChange(META_CHANGED);
1832 public void stop() {
1837 * Resumes or starts playback.
1839 public void play() {
1840 int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
1841 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
1842 if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
1846 mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
1847 MediaButtonIntentReceiver.class.getName()));
1849 if (mPlayer.isInitialized()) {
1850 final long duration = mPlayer.duration();
1851 if (mRepeatMode != REPEAT_CURRENT && duration > 2000
1852 && mPlayer.position() >= duration - 2000) {
1857 mPlayerHandler.removeMessages(FADEDOWN);
1858 mPlayerHandler.sendEmptyMessage(FADEUP);
1860 if (!mIsSupposedToBePlaying) {
1861 mIsSupposedToBePlaying = true;
1862 notifyChange(PLAYSTATE_CHANGED);
1865 updateNotification();
1866 } else if (mPlayListLen <= 0) {
1867 setShuffleMode(SHUFFLE_AUTO);
1872 * Temporarily pauses playback.
1874 public void pause() {
1875 synchronized (this) {
1876 mPlayerHandler.removeMessages(FADEUP);
1877 if (mIsSupposedToBePlaying) {
1880 mIsSupposedToBePlaying = false;
1881 notifyChange(PLAYSTATE_CHANGED);
1887 * Changes from the current track to the next track
1889 public void gotoNext(final boolean force) {
1890 synchronized (this) {
1891 if (mPlayListLen <= 0) {
1894 final int pos = getNextPosition(force);
1897 if (mIsSupposedToBePlaying) {
1898 mIsSupposedToBePlaying = false;
1899 notifyChange(PLAYSTATE_CHANGED);
1906 openCurrentAndNext();
1908 notifyChange(META_CHANGED);
1913 * Changes from the current track to the previous played track
1915 public void prev() {
1916 synchronized (this) {
1917 if (mShuffleMode == SHUFFLE_NORMAL) {
1918 // Go to previously-played track and remove it from the history
1919 final int histsize = mHistory.size();
1920 if (histsize == 0) {
1923 final Integer pos = mHistory.remove(histsize - 1);
1924 mPlayPos = pos.intValue();
1929 mPlayPos = mPlayListLen - 1;
1935 notifyChange(META_CHANGED);
1940 * We don't want to open the current and next track when the user is using
1941 * the {@code #prev()} method because they won't be able to travel back to
1942 * the previously listened track if they're shuffling.
1944 private void openCurrent() {
1945 openCurrentAndMaybeNext(false);
1949 * Toggles the current song as a favorite.
1951 public void toggleFavorite() {
1952 if (mFavoritesCache != null) {
1953 synchronized (this) {
1954 mFavoritesCache.toggleSong(getAudioId(), getTrackName(), getAlbumName(),
1961 * Moves an item in the queue from one position to another
1963 * @param from The position the item is currently at
1964 * @param to The position the item is being moved to
1966 public void moveQueueItem(int index1, int index2) {
1967 synchronized (this) {
1968 if (index1 >= mPlayListLen) {
1969 index1 = mPlayListLen - 1;
1971 if (index2 >= mPlayListLen) {
1972 index2 = mPlayListLen - 1;
1974 if (index1 < index2) {
1975 final long tmp = mPlayList[index1];
1976 for (int i = index1; i < index2; i++) {
1977 mPlayList[i] = mPlayList[i + 1];
1979 mPlayList[index2] = tmp;
1980 if (mPlayPos == index1) {
1982 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
1985 } else if (index2 < index1) {
1986 final long tmp = mPlayList[index1];
1987 for (int i = index1; i > index2; i--) {
1988 mPlayList[i] = mPlayList[i - 1];
1990 mPlayList[index2] = tmp;
1991 if (mPlayPos == index1) {
1993 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
1997 notifyChange(QUEUE_CHANGED);
2002 * Sets the repeat mode
2004 * @param repeatmode The repeat mode to use
2006 public void setRepeatMode(final int repeatmode) {
2007 synchronized (this) {
2008 mRepeatMode = repeatmode;
2011 notifyChange(REPEATMODE_CHANGED);
2016 * Sets the shuffle mode
2018 * @param shufflemode The shuffle mode to use
2020 public void setShuffleMode(final int shufflemode) {
2021 synchronized (this) {
2022 if (mShuffleMode == shufflemode && mPlayListLen > 0) {
2025 mShuffleMode = shufflemode;
2026 if (mShuffleMode == SHUFFLE_AUTO) {
2027 if (makeAutoShuffleList()) {
2029 doAutoShuffleUpdate();
2031 openCurrentAndNext();
2033 notifyChange(META_CHANGED);
2036 mShuffleMode = SHUFFLE_NONE;
2040 notifyChange(SHUFFLEMODE_CHANGED);
2045 * Sets the position of a track in the queue
2047 * @param index The position to place the track
2049 public void setQueuePosition(final int index) {
2050 synchronized (this) {
2053 openCurrentAndNext();
2055 notifyChange(META_CHANGED);
2056 if (mShuffleMode == SHUFFLE_AUTO) {
2057 doAutoShuffleUpdate();
2063 * Queues a new list for playback
2065 * @param list The list to queue
2066 * @param action The action to take
2068 public void enqueue(final long[] list, final int action) {
2069 synchronized (this) {
2070 if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
2071 addToPlayList(list, mPlayPos + 1);
2072 notifyChange(QUEUE_CHANGED);
2074 addToPlayList(list, Integer.MAX_VALUE);
2075 notifyChange(QUEUE_CHANGED);
2076 if (action == NOW) {
2077 mPlayPos = mPlayListLen - list.length;
2078 openCurrentAndNext();
2080 notifyChange(META_CHANGED);
2086 openCurrentAndNext();
2088 notifyChange(META_CHANGED);
2094 * Cycles through the different repeat modes
2096 private void cycleRepeat() {
2097 if (mRepeatMode == REPEAT_NONE) {
2098 setRepeatMode(REPEAT_ALL);
2099 } else if (mRepeatMode == REPEAT_ALL) {
2100 setRepeatMode(REPEAT_CURRENT);
2101 if (mShuffleMode != SHUFFLE_NONE) {
2102 setShuffleMode(SHUFFLE_NONE);
2105 setRepeatMode(REPEAT_NONE);
2110 * Cycles through the different shuffle modes
2112 private void cycleShuffle() {
2113 if (mShuffleMode == SHUFFLE_NONE) {
2114 setShuffleMode(SHUFFLE_NORMAL);
2115 if (mRepeatMode == REPEAT_CURRENT) {
2116 setRepeatMode(REPEAT_ALL);
2118 } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
2119 setShuffleMode(SHUFFLE_NONE);
2124 * @return The album art for the current album.
2126 public Bitmap getAlbumArt() {
2127 // Return the cached artwork
2128 final Bitmap bitmap = mImageFetcher.getArtwork(getAlbumName(),
2129 getAlbumId(), getArtistName());
2134 * Called when one of the lists should refresh or requery.
2136 public void refresh() {
2137 notifyChange(REFRESH);
2140 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
2145 public void onReceive(final Context context, final Intent intent) {
2146 final String command = intent.getStringExtra(CMDNAME);
2148 if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
2149 final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2150 mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
2151 } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
2152 final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2153 mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
2154 } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
2155 final int[] largeAlt = intent
2156 .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2157 mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
2158 } else if (RecentWidgetProvider.CMDAPPWIDGETUPDATE.equals(command)) {
2159 final int[] recent = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2160 mRecentWidgetProvider.performUpdate(MusicPlaybackService.this, recent);
2162 handleCommandIntent(intent);
2167 private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
2172 public void onAudioFocusChange(final int focusChange) {
2173 mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
2177 private static final class DelayedHandler extends Handler {
2178 private final WeakReference<MusicPlaybackService> mService;
2181 * Constructor of <code>DelayedHandler</code>
2183 * @param service The service to use.
2185 public DelayedHandler(final MusicPlaybackService service) {
2186 mService = new WeakReference<MusicPlaybackService>(service);
2193 public void handleMessage(final Message msg) {
2194 final MusicPlaybackService service = mService.get();
2195 if (service == null) {
2198 if (service.isPlaying()
2199 || service.mPausedByTransientLossOfFocus
2200 || service.mServiceInUse
2201 || service.mPlayerHandler.hasMessages(TRACK_ENDED)) {
2204 service.saveQueue(true);
2205 service.stopSelf(service.mServiceStartId);
2209 private static final class MusicPlayerHandler extends Handler {
2210 private final WeakReference<MusicPlaybackService> mService;
2211 private float mCurrentVolume = 1.0f;
2214 * Constructor of <code>MusicPlayerHandler</code>
2216 * @param service The service to use.
2217 * @param looper The thread to run on.
2219 public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
2221 mService = new WeakReference<MusicPlaybackService>(service);
2228 public void handleMessage(final Message msg) {
2229 final MusicPlaybackService service = mService.get();
2230 if (service == null) {
2236 mCurrentVolume -= .05f;
2237 if (mCurrentVolume > .2f) {
2238 sendEmptyMessageDelayed(FADEDOWN, 10);
2240 mCurrentVolume = .2f;
2242 service.mPlayer.setVolume(mCurrentVolume);
2245 mCurrentVolume += .01f;
2246 if (mCurrentVolume < 1.0f) {
2247 sendEmptyMessageDelayed(FADEUP, 10);
2249 mCurrentVolume = 1.0f;
2251 service.mPlayer.setVolume(mCurrentVolume);
2254 if (service.isPlaying()) {
2255 service.gotoNext(true);
2257 service.openCurrentAndNext();
2260 case TRACK_WENT_TO_NEXT:
2261 service.mPlayPos = service.mNextPlayPos;
2262 if (service.mCursor != null) {
2263 service.mCursor.close();
2265 service.mCursor = service.getCursorForId(service.mPlayList[service.mPlayPos]);
2266 service.notifyChange(META_CHANGED);
2267 service.updateNotification();
2268 service.setNextTrack();
2271 if (service.mRepeatMode == REPEAT_CURRENT) {
2275 service.gotoNext(false);
2278 case RELEASE_WAKELOCK:
2279 service.mWakeLock.release();
2283 case AudioManager.AUDIOFOCUS_LOSS:
2284 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
2285 if (service.isPlaying()) {
2286 service.mPausedByTransientLossOfFocus =
2287 msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
2291 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
2292 removeMessages(FADEUP);
2293 sendEmptyMessage(FADEDOWN);
2295 case AudioManager.AUDIOFOCUS_GAIN:
2296 if (!service.isPlaying()
2297 && service.mPausedByTransientLossOfFocus) {
2298 service.mPausedByTransientLossOfFocus = false;
2299 mCurrentVolume = 0f;
2300 service.mPlayer.setVolume(mCurrentVolume);
2303 removeMessages(FADEDOWN);
2304 sendEmptyMessage(FADEUP);
2316 private static final class Shuffler {
2318 private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<Integer>();
2320 private final TreeSet<Integer> mPreviousNumbers = new TreeSet<Integer>();
2322 private final Random mRandom = new Random();
2324 private int mPrevious;
2327 * Constructor of <code>Shuffler</code>
2334 * @param interval The length the queue
2335 * @return The position of the next track to play
2337 public int nextInt(final int interval) {
2340 next = mRandom.nextInt(interval);
2341 } while (next == mPrevious && interval > 1
2342 && !mPreviousNumbers.contains(Integer.valueOf(next)));
2344 mHistoryOfNumbers.add(mPrevious);
2345 mPreviousNumbers.add(mPrevious);
2351 * Removes old tracks and cleans up the history preparing for new tracks
2352 * to be added to the mapping
2354 private void cleanUpHistory() {
2355 if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
2356 for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
2357 mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
2363 private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
2364 MediaPlayer.OnCompletionListener {
2366 private final WeakReference<MusicPlaybackService> mService;
2368 private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
2370 private MediaPlayer mNextMediaPlayer;
2372 private Handler mHandler;
2374 private boolean mIsInitialized = false;
2377 * Constructor of <code>MultiPlayer</code>
2379 public MultiPlayer(final MusicPlaybackService service) {
2380 mService = new WeakReference<MusicPlaybackService>(service);
2381 mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
2385 * @param path The path of the file, or the http/rtsp URL of the stream
2388 public void setDataSource(final String path) {
2389 mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
2390 if (mIsInitialized) {
2391 setNextDataSource(null);
2396 * @param player The {@link MediaPlayer} to use
2397 * @param path The path of the file, or the http/rtsp URL of the stream
2399 * @return True if the <code>player</code> has been prepared and is
2400 * ready to play, false otherwise
2402 private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
2405 player.setOnPreparedListener(null);
2406 if (path.startsWith("content://")) {
2407 player.setDataSource(mService.get(), Uri.parse(path));
2409 player.setDataSource(path);
2411 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
2413 } catch (final IOException todo) {
2414 // TODO: notify the user why the file couldn't be opened
2416 } catch (final IllegalArgumentException todo) {
2417 // TODO: notify the user why the file couldn't be opened
2420 player.setOnCompletionListener(this);
2421 player.setOnErrorListener(this);
2422 final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
2423 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2424 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, mService.get().getPackageName());
2425 mService.get().sendBroadcast(intent);
2430 * Set the MediaPlayer to start when this MediaPlayer finishes playback.
2432 * @param path The path of the file, or the http/rtsp URL of the stream
2435 public void setNextDataSource(final String path) {
2436 mCurrentMediaPlayer.setNextMediaPlayer(null);
2437 if (mNextMediaPlayer != null) {
2438 mNextMediaPlayer.release();
2439 mNextMediaPlayer = null;
2444 mNextMediaPlayer = new MediaPlayer();
2445 mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
2446 mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
2447 if (setDataSourceImpl(mNextMediaPlayer, path)) {
2448 mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
2450 if (mNextMediaPlayer != null) {
2451 mNextMediaPlayer.release();
2452 mNextMediaPlayer = null;
2460 * @param handler The handler to use
2462 public void setHandler(final Handler handler) {
2467 * @return True if the player is ready to go, false otherwise
2469 public boolean isInitialized() {
2470 return mIsInitialized;
2474 * Starts or resumes playback.
2476 public void start() {
2477 mCurrentMediaPlayer.start();
2481 * Resets the MediaPlayer to its uninitialized state.
2483 public void stop() {
2484 mCurrentMediaPlayer.reset();
2485 mIsInitialized = false;
2489 * Releases resources associated with this MediaPlayer object.
2491 public void release() {
2493 mCurrentMediaPlayer.release();
2497 * Pauses playback. Call start() to resume.
2499 public void pause() {
2500 mCurrentMediaPlayer.pause();
2504 * Gets the duration of the file.
2506 * @return The duration in milliseconds
2508 public long duration() {
2509 return mCurrentMediaPlayer.getDuration();
2513 * Gets the current playback position.
2515 * @return The current position in milliseconds
2517 public long position() {
2518 return mCurrentMediaPlayer.getCurrentPosition();
2522 * Gets the current playback position.
2524 * @param whereto The offset in milliseconds from the start to seek to
2525 * @return The offset in milliseconds from the start to seek to
2527 public long seek(final long whereto) {
2528 mCurrentMediaPlayer.seekTo((int)whereto);
2533 * Sets the volume on this player.
2535 * @param vol Left and right volume scalar
2537 public void setVolume(final float vol) {
2538 mCurrentMediaPlayer.setVolume(vol, vol);
2542 * Sets the audio session ID.
2544 * @param sessionId The audio session ID
2546 public void setAudioSessionId(final int sessionId) {
2547 mCurrentMediaPlayer.setAudioSessionId(sessionId);
2551 * Returns the audio session ID.
2553 * @return The current audio session ID.
2555 public int getAudioSessionId() {
2556 return mCurrentMediaPlayer.getAudioSessionId();
2563 public boolean onError(final MediaPlayer mp, final int what, final int extra) {
2565 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
2566 mIsInitialized = false;
2567 mCurrentMediaPlayer.release();
2568 mCurrentMediaPlayer = new MediaPlayer();
2569 mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
2570 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
2582 public void onCompletion(final MediaPlayer mp) {
2583 if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
2584 mCurrentMediaPlayer.release();
2585 mCurrentMediaPlayer = mNextMediaPlayer;
2586 mNextMediaPlayer = null;
2587 mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
2589 mService.get().mWakeLock.acquire(30000);
2590 mHandler.sendEmptyMessage(TRACK_ENDED);
2591 mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
2596 private static final class ServiceStub extends IApolloService.Stub {
2598 private final WeakReference<MusicPlaybackService> mService;
2600 private ServiceStub(final MusicPlaybackService service) {
2601 mService = new WeakReference<MusicPlaybackService>(service);
2608 public void openFile(final String path) throws RemoteException {
2609 mService.get().openFile(path);
2616 public void open(final long[] list, final int position) throws RemoteException {
2617 mService.get().open(list, position);
2624 public void stop() throws RemoteException {
2625 mService.get().stop();
2632 public void pause() throws RemoteException {
2633 mService.get().pause();
2640 public void play() throws RemoteException {
2641 mService.get().play();
2648 public void prev() throws RemoteException {
2649 mService.get().prev();
2656 public void next() throws RemoteException {
2657 mService.get().gotoNext(true);
2664 public void enqueue(final long[] list, final int action) throws RemoteException {
2665 mService.get().enqueue(list, action);
2672 public void setQueuePosition(final int index) throws RemoteException {
2673 mService.get().setQueuePosition(index);
2680 public void setShuffleMode(final int shufflemode) throws RemoteException {
2681 mService.get().setShuffleMode(shufflemode);
2688 public void setRepeatMode(final int repeatmode) throws RemoteException {
2689 mService.get().setRepeatMode(repeatmode);
2696 public void moveQueueItem(final int from, final int to) throws RemoteException {
2697 mService.get().moveQueueItem(from, to);
2704 public void toggleFavorite() throws RemoteException {
2705 mService.get().toggleFavorite();
2712 public void refresh() throws RemoteException {
2713 mService.get().refresh();
2720 public boolean isFavorite() throws RemoteException {
2721 return mService.get().isFavorite();
2728 public boolean isPlaying() throws RemoteException {
2729 return mService.get().isPlaying();
2736 public long[] getQueue() throws RemoteException {
2737 return mService.get().getQueue();
2744 public long duration() throws RemoteException {
2745 return mService.get().duration();
2752 public long position() throws RemoteException {
2753 return mService.get().position();
2760 public long seek(final long position) throws RemoteException {
2761 return mService.get().seek(position);
2768 public long getAudioId() throws RemoteException {
2769 return mService.get().getAudioId();
2776 public long getArtistId() throws RemoteException {
2777 return mService.get().getArtistId();
2784 public long getAlbumId() throws RemoteException {
2785 return mService.get().getAlbumId();
2792 public String getArtistName() throws RemoteException {
2793 return mService.get().getArtistName();
2800 public String getTrackName() throws RemoteException {
2801 return mService.get().getTrackName();
2808 public String getAlbumName() throws RemoteException {
2809 return mService.get().getAlbumName();
2816 public String getPath() throws RemoteException {
2817 return mService.get().getPath();
2824 public int getQueuePosition() throws RemoteException {
2825 return mService.get().getQueuePosition();
2832 public int getShuffleMode() throws RemoteException {
2833 return mService.get().getShuffleMode();
2840 public int getRepeatMode() throws RemoteException {
2841 return mService.get().getRepeatMode();
2848 public int removeTracks(final int first, final int last) throws RemoteException {
2849 return mService.get().removeTracks(first, last);
2856 public int removeTrack(final long id) throws RemoteException {
2857 return mService.get().removeTrack(id);
2864 public int getMediaMountedCount() throws RemoteException {
2865 return mService.get().getMediaMountedCount();
2872 public int getAudioSessionId() throws RemoteException {
2873 return mService.get().getAudioSessionId();