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.AlbumColumns;
47 import android.provider.MediaStore.Audio.AudioColumns;
48 import android.util.Log;
50 import com.andrew.apollo.appwidgets.AppWidgetLarge;
51 import com.andrew.apollo.appwidgets.AppWidgetLargeAlternate;
52 import com.andrew.apollo.appwidgets.AppWidgetSmall;
53 import com.andrew.apollo.appwidgets.RecentWidgetProvider;
54 import com.andrew.apollo.cache.ImageCache;
55 import com.andrew.apollo.cache.ImageFetcher;
56 import com.andrew.apollo.provider.FavoritesStore;
57 import com.andrew.apollo.provider.RecentStore;
58 import com.andrew.apollo.utils.ApolloUtils;
59 import com.andrew.apollo.utils.Lists;
60 import com.andrew.apollo.utils.MusicUtils;
61 import com.andrew.apollo.utils.PreferenceUtils;
63 import java.io.IOException;
64 import java.lang.ref.WeakReference;
65 import java.util.LinkedList;
66 import java.util.Random;
67 import java.util.TreeSet;
70 * A backbround {@link Service} used to keep music playing between activities
71 * and when the user moves Apollo into the background.
73 @SuppressLint("NewApi")
74 public class MusicPlaybackService extends Service {
75 private static final String TAG = "MusicPlaybackService";
76 private static final boolean D = false;
79 * Indicates that the music has paused or resumed
81 public static final String PLAYSTATE_CHANGED = "com.andrew.apollo.playstatechanged";
84 * Indicates that music playback position within
87 public static final String POSITION_CHANGED = "com.android.apollo.positionchanged";
90 * Indicates the meta data has changed in some way, like a track change
92 public static final String META_CHANGED = "com.andrew.apollo.metachanged";
95 * Indicates the queue has been updated
97 public static final String QUEUE_CHANGED = "com.andrew.apollo.queuechanged";
100 * Indicates the repeat mode chaned
102 public static final String REPEATMODE_CHANGED = "com.andrew.apollo.repeatmodechanged";
105 * Indicates the shuffle mode chaned
107 public static final String SHUFFLEMODE_CHANGED = "com.andrew.apollo.shufflemodechanged";
110 * For backwards compatibility reasons, also provide sticky
111 * broadcasts under the music package
113 public static final String APOLLO_PACKAGE_NAME = "com.andrew.apollo";
114 public static final String MUSIC_PACKAGE_NAME = "com.android.music";
117 * Called to indicate a general service commmand. Used in
118 * {@link MediaButtonIntentReceiver}
120 public static final String SERVICECMD = "com.andrew.apollo.musicservicecommand";
123 * Called to go toggle between pausing and playing the music
125 public static final String TOGGLEPAUSE_ACTION = "com.andrew.apollo.togglepause";
128 * Called to go to pause the playback
130 public static final String PAUSE_ACTION = "com.andrew.apollo.pause";
133 * Called to go to stop the playback
135 public static final String STOP_ACTION = "com.andrew.apollo.stop";
138 * Called to go to the previous track
140 public static final String PREVIOUS_ACTION = "com.andrew.apollo.previous";
143 * Called to go to the next track
145 public static final String NEXT_ACTION = "com.andrew.apollo.next";
148 * Called to change the repeat mode
150 public static final String REPEAT_ACTION = "com.andrew.apollo.repeat";
153 * Called to change the shuffle mode
155 public static final String SHUFFLE_ACTION = "com.andrew.apollo.shuffle";
158 * Called to update the service about the foreground state of Apollo's activities
160 public static final String FOREGROUND_STATE_CHANGED = "com.andrew.apollo.fgstatechanged";
162 public static final String NOW_IN_FOREGROUND = "nowinforeground";
164 public static final String FROM_MEDIA_BUTTON = "frommediabutton";
167 * Used to easily notify a list that it should refresh. i.e. A playlist
170 public static final String REFRESH = "com.andrew.apollo.refresh";
173 * Used by the alarm intent to shutdown the service after being idle
175 private static final String SHUTDOWN = "com.andrew.apollo.shutdown";
178 * Called to update the remote control client
180 public static final String UPDATE_LOCKSCREEN = "com.andrew.apollo.updatelockscreen";
182 public static final String CMDNAME = "command";
184 public static final String CMDTOGGLEPAUSE = "togglepause";
186 public static final String CMDSTOP = "stop";
188 public static final String CMDPAUSE = "pause";
190 public static final String CMDPLAY = "play";
192 public static final String CMDPREVIOUS = "previous";
194 public static final String CMDNEXT = "next";
196 public static final String CMDNOTIF = "buttonId";
198 private static final int IDCOLIDX = 0;
201 * Moves a list to the front of the queue
203 public static final int NOW = 1;
206 * Moves a list to the next position in the queue
208 public static final int NEXT = 2;
211 * Moves a list to the last position in the queue
213 public static final int LAST = 3;
216 * Shuffles no songs, turns shuffling off
218 public static final int SHUFFLE_NONE = 0;
223 public static final int SHUFFLE_NORMAL = 1;
228 public static final int SHUFFLE_AUTO = 2;
233 public static final int REPEAT_NONE = 0;
236 * Repeats the current track in a list
238 public static final int REPEAT_CURRENT = 1;
241 * Repeats all the tracks in a list
243 public static final int REPEAT_ALL = 2;
246 * Indicates when the track ends
248 private static final int TRACK_ENDED = 1;
251 * Indicates that the current track was changed the next track
253 private static final int TRACK_WENT_TO_NEXT = 2;
256 * Indicates when the release the wake lock
258 private static final int RELEASE_WAKELOCK = 3;
261 * Indicates the player died
263 private static final int SERVER_DIED = 4;
266 * Indicates some sort of focus change, maybe a phone call
268 private static final int FOCUSCHANGE = 5;
271 * Indicates to fade the volume down
273 private static final int FADEDOWN = 6;
276 * Indicates to fade the volume back up
278 private static final int FADEUP = 7;
281 * Idle time before stopping the foreground notfication (1 minute)
283 private static final int IDLE_DELAY = 60000;
286 * Song play time used as threshold for rewinding to the beginning of the
287 * track instead of skipping to the previous track when getting the PREVIOUS
290 private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
293 * The max size allowed for the track history
295 private static final int MAX_HISTORY_SIZE = 100;
298 * The columns used to retrieve any info from the current track
300 private static final String[] PROJECTION = new String[] {
301 "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
302 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
303 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
304 MediaStore.Audio.Media.ARTIST_ID
308 * The columns used to retrieve any info from the current album
310 private static final String[] ALBUM_PROJECTION = new String[] {
311 MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
312 MediaStore.Audio.Albums.LAST_YEAR
316 * Keeps a mapping of the track history
318 private static final LinkedList<Integer> mHistory = Lists.newLinkedList();
321 * Used to shuffle the tracks
323 private static final Shuffler mShuffler = new Shuffler();
326 * Used to save the queue as reverse hexadecimal numbers, which we can
327 * generate faster than normal decimal or hexadecimal numbers, which in
328 * turn allows us to save the playlist more often without worrying too
329 * much about performance
331 private static final char HEX_DIGITS[] = new char[] {
332 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
338 private final IBinder mBinder = new ServiceStub(this);
343 private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
348 private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
351 * 4x2 alternate widget
353 private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
357 * Recently listened widget
359 private final RecentWidgetProvider mRecentWidgetProvider = RecentWidgetProvider.getInstance();
364 private MultiPlayer mPlayer;
367 * The path of the current file to play
369 private String mFileToPlay;
372 * Keeps the service running when the screen is off
374 private WakeLock mWakeLock;
377 * Alarm intent for removing the notification when nothing is playing
380 private AlarmManager mAlarmManager;
381 private PendingIntent mShutdownIntent;
382 private boolean mShutdownScheduled;
385 * The cursor used to retrieve info on the current track and run the
386 * necessary queries to play audio files
388 private Cursor mCursor;
391 * The cursor used to retrieve info on the album the current track is
394 private Cursor mAlbumCursor;
397 * Monitors the audio state
399 private AudioManager mAudioManager;
402 * Settings used to save and retrieve the queue and history
404 private SharedPreferences mPreferences;
407 * Used to know when the service is active
409 private boolean mServiceInUse = false;
412 * Used to know if something should be playing or not
414 private boolean mIsSupposedToBePlaying = false;
417 * Used to indicate if the queue can be saved
419 private boolean mQueueIsSaveable = true;
422 * Used to track what type of audio focus loss caused the playback to pause
424 private boolean mPausedByTransientLossOfFocus = false;
427 * Used to track whether any of Apollo's activities is in the foreground
429 private boolean mAnyActivityInForeground = false;
432 * Lock screen controls
434 private RemoteControlClient mRemoteControlClient;
436 private ComponentName mMediaButtonReceiverComponent;
438 // We use this to distinguish between different cards when saving/restoring
442 private int mPlayListLen = 0;
444 private int mPlayPos = -1;
446 private int mNextPlayPos = -1;
448 private int mOpenFailedCounter = 0;
450 private int mMediaMountedCount = 0;
452 private int mShuffleMode = SHUFFLE_NONE;
454 private int mRepeatMode = REPEAT_NONE;
456 private int mServiceStartId = -1;
458 private long[] mPlayList = null;
460 private long[] mAutoShuffleList = null;
462 private MusicPlayerHandler mPlayerHandler;
464 private BroadcastReceiver mUnmountReceiver = null;
469 private ImageFetcher mImageFetcher;
472 * Used to build the notification
474 private NotificationHelper mNotificationHelper;
477 * Recently listened database
479 private RecentStore mRecentsCache;
484 private FavoritesStore mFavoritesCache;
490 public IBinder onBind(final Intent intent) {
491 if (D) Log.d(TAG, "Service bound, intent = " + intent);
493 mServiceInUse = true;
501 public boolean onUnbind(final Intent intent) {
502 if (D) Log.d(TAG, "Service unbound");
503 mServiceInUse = false;
506 if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
507 // Something is currently playing, or will be playing once
508 // an in-progress action requesting audio focus ends, so don't stop
512 // If there is a playlist but playback is paused, then wait a while
513 // before stopping the service, so that pause/resume isn't slow.
514 // Also delay stopping the service if we're transitioning between
516 } else if (mPlayListLen > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
517 scheduleDelayedShutdown();
520 stopSelf(mServiceStartId);
528 public void onRebind(final Intent intent) {
530 mServiceInUse = true;
537 public void onCreate() {
538 if (D) Log.d(TAG, "Creating service");
541 // Initialize the favorites and recents databases
542 mRecentsCache = RecentStore.getInstance(this);
543 mFavoritesCache = FavoritesStore.getInstance(this);
545 // Initialize the notification helper
546 mNotificationHelper = new NotificationHelper(this);
548 // Initialize the image fetcher
549 mImageFetcher = ImageFetcher.getInstance(this);
550 // Initialize the image cache
551 mImageFetcher.setImageCache(ImageCache.getInstance(this));
553 // Start up the thread running the service. Note that we create a
554 // separate thread because the service normally runs in the process's
555 // main thread, which we don't want to block. We also make it
556 // background priority so CPU-intensive work will not disrupt the UI.
557 final HandlerThread thread = new HandlerThread("MusicPlayerHandler",
558 android.os.Process.THREAD_PRIORITY_BACKGROUND);
561 // Initialize the handler
562 mPlayerHandler = new MusicPlayerHandler(this, thread.getLooper());
564 // Initialize the audio manager and register any headset controls for
566 mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
567 mMediaButtonReceiverComponent = new ComponentName(getPackageName(),
568 MediaButtonIntentReceiver.class.getName());
569 mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);
571 // Use the remote control APIs to set the playback state
572 setUpRemoteControlClient();
574 // Initialize the preferences
575 mPreferences = getSharedPreferences("Service", 0);
576 mCardId = getCardId();
578 registerExternalStorageListener();
580 // Initialize the media player
581 mPlayer = new MultiPlayer(this);
582 mPlayer.setHandler(mPlayerHandler);
584 // Initialize the intent filter and each action
585 final IntentFilter filter = new IntentFilter();
586 filter.addAction(SERVICECMD);
587 filter.addAction(TOGGLEPAUSE_ACTION);
588 filter.addAction(PAUSE_ACTION);
589 filter.addAction(STOP_ACTION);
590 filter.addAction(NEXT_ACTION);
591 filter.addAction(PREVIOUS_ACTION);
592 filter.addAction(REPEAT_ACTION);
593 filter.addAction(SHUFFLE_ACTION);
594 // Attach the broadcast listener
595 registerReceiver(mIntentReceiver, filter);
597 // Initialize the wake lock
598 final PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
599 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
600 mWakeLock.setReferenceCounted(false);
602 // Initialize the delayed shutdown intent
603 final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
604 shutdownIntent.setAction(SHUTDOWN);
606 mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
607 mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
609 // Listen for the idle state
610 scheduleDelayedShutdown();
612 // Bring the queue back
614 notifyChange(QUEUE_CHANGED);
615 notifyChange(META_CHANGED);
619 * Initializes the remote control client
621 private void setUpRemoteControlClient() {
622 final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
623 mediaButtonIntent.setComponent(mMediaButtonReceiverComponent);
624 mRemoteControlClient = new RemoteControlClient(
625 PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent,
626 PendingIntent.FLAG_UPDATE_CURRENT));
627 mAudioManager.registerRemoteControlClient(mRemoteControlClient);
629 // Flags for the media transport control that this client supports.
630 int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
631 | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
632 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY
633 | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
634 | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
635 | RemoteControlClient.FLAG_KEY_MEDIA_STOP;
637 if (ApolloUtils.hasJellyBeanMR2()) {
638 flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
640 mRemoteControlClient.setOnGetPlaybackPositionListener(
641 new RemoteControlClient.OnGetPlaybackPositionListener() {
643 public long onGetPlaybackPosition() {
647 mRemoteControlClient.setPlaybackPositionUpdateListener(
648 new RemoteControlClient.OnPlaybackPositionUpdateListener() {
650 public void onPlaybackPositionUpdate(long newPositionMs) {
656 mRemoteControlClient.setTransportControlFlags(flags);
663 public void onDestroy() {
664 if (D) Log.d(TAG, "Destroying service");
666 // Remove any sound effects
667 final Intent audioEffectsIntent = new Intent(
668 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
669 audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
670 audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
671 sendBroadcast(audioEffectsIntent);
673 // remove any pending alarms
674 mAlarmManager.cancel(mShutdownIntent);
676 // Release the player
680 // Remove the audio focus listener and lock screen controls
681 mAudioManager.abandonAudioFocus(mAudioFocusListener);
682 mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
684 // Remove any callbacks from the handler
685 mPlayerHandler.removeCallbacksAndMessages(null);
690 // Unregister the mount listener
691 unregisterReceiver(mIntentReceiver);
692 if (mUnmountReceiver != null) {
693 unregisterReceiver(mUnmountReceiver);
694 mUnmountReceiver = null;
697 // Release the wake lock
705 public int onStartCommand(final Intent intent, final int flags, final int startId) {
706 if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
707 mServiceStartId = startId;
709 if (intent != null) {
710 final String action = intent.getAction();
712 if (intent.hasExtra(NOW_IN_FOREGROUND)) {
713 mAnyActivityInForeground = intent.getBooleanExtra(NOW_IN_FOREGROUND, false);
714 updateNotification();
717 if (SHUTDOWN.equals(action)) {
718 mShutdownScheduled = false;
719 releaseServiceUiAndStop();
720 return START_NOT_STICKY;
723 handleCommandIntent(intent);
726 // Make sure the service will shut down on its own if it was
727 // just started but not bound to and nothing is playing
728 scheduleDelayedShutdown();
730 if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
731 MediaButtonIntentReceiver.completeWakefulIntent(intent);
737 private void releaseServiceUiAndStop() {
739 || mPausedByTransientLossOfFocus
740 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
744 if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
745 mNotificationHelper.killNotification();
746 mAudioManager.abandonAudioFocus(mAudioFocusListener);
748 if (!mServiceInUse) {
750 stopSelf(mServiceStartId);
754 private void handleCommandIntent(Intent intent) {
755 final String action = intent.getAction();
756 final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
758 if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
760 if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
762 } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) {
763 if (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD) {
769 } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
772 mPausedByTransientLossOfFocus = false;
776 } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
778 mPausedByTransientLossOfFocus = false;
779 } else if (CMDPLAY.equals(command)) {
781 } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
783 mPausedByTransientLossOfFocus = false;
785 releaseServiceUiAndStop();
786 } else if (REPEAT_ACTION.equals(action)) {
788 } else if (SHUFFLE_ACTION.equals(action)) {
794 * Updates the notification, considering the current play and activity state
796 private void updateNotification() {
797 if (!mAnyActivityInForeground && isPlaying()) {
798 mNotificationHelper.buildNotification(getAlbumName(), getArtistName(),
799 getTrackName(), getAlbumId(), getAlbumArt(), isPlaying());
800 } else if (mAnyActivityInForeground) {
801 mNotificationHelper.killNotification();
806 * @return A card ID used to save and restore playlists, i.e., the queue.
808 private int getCardId() {
809 final ContentResolver resolver = getContentResolver();
810 Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
813 if (cursor != null && cursor.moveToFirst()) {
814 mCardId = cursor.getInt(0);
822 * Called when we receive a ACTION_MEDIA_EJECT notification.
824 * @param storagePath The path to mount point for the removed media
826 public void closeExternalStorageFiles(final String storagePath) {
828 notifyChange(QUEUE_CHANGED);
829 notifyChange(META_CHANGED);
833 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
834 * intent will call closeExternalStorageFiles() if the external media is
835 * going to be ejected, so applications can clean up any files they have
838 public void registerExternalStorageListener() {
839 if (mUnmountReceiver == null) {
840 mUnmountReceiver = new BroadcastReceiver() {
846 public void onReceive(final Context context, final Intent intent) {
847 final String action = intent.getAction();
848 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
850 mQueueIsSaveable = false;
851 closeExternalStorageFiles(intent.getData().getPath());
852 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
853 mMediaMountedCount++;
854 mCardId = getCardId();
856 mQueueIsSaveable = true;
857 notifyChange(QUEUE_CHANGED);
858 notifyChange(META_CHANGED);
862 final IntentFilter filter = new IntentFilter();
863 filter.addAction(Intent.ACTION_MEDIA_EJECT);
864 filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
865 filter.addDataScheme("file");
866 registerReceiver(mUnmountReceiver, filter);
870 private void scheduleDelayedShutdown() {
871 if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
872 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
873 SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
874 mShutdownScheduled = true;
877 private void cancelShutdown() {
878 if (D) Log.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled);
879 if (mShutdownScheduled) {
880 mAlarmManager.cancel(mShutdownIntent);
881 mShutdownScheduled = false;
888 * @param goToIdle True to go to the idle state, false otherwise
890 private void stop(final boolean goToIdle) {
891 if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
892 if (mPlayer.isInitialized()) {
898 scheduleDelayedShutdown();
899 mIsSupposedToBePlaying = false;
901 stopForeground(false);
906 * Removes the range of tracks specified from the play list. If a file
907 * within the range is the file currently being played, playback will move
908 * to the next file after the range.
910 * @param first The first file to be removed
911 * @param last The last file to be removed
912 * @return the number of tracks deleted
914 private int removeTracksInternal(int first, int last) {
915 synchronized (this) {
918 } else if (first < 0) {
920 } else if (last >= mPlayListLen) {
921 last = mPlayListLen - 1;
924 boolean gotonext = false;
925 if (first <= mPlayPos && mPlayPos <= last) {
928 } else if (mPlayPos > last) {
929 mPlayPos -= last - first + 1;
931 final int num = mPlayListLen - last - 1;
932 for (int i = 0; i < num; i++) {
933 mPlayList[first + i] = mPlayList[last + 1 + i];
935 mPlayListLen -= last - first + 1;
938 if (mPlayListLen == 0) {
943 if (mShuffleMode != SHUFFLE_NONE) {
944 mPlayPos = getNextPosition(true);
945 } else if (mPlayPos >= mPlayListLen) {
948 final boolean wasPlaying = isPlaying();
950 openCurrentAndNext();
955 notifyChange(META_CHANGED);
957 return last - first + 1;
962 * Adds a list to the playlist
964 * @param list The list to add
965 * @param position The position to place the tracks
967 private void addToPlayList(final long[] list, int position) {
968 final int addlen = list.length;
973 ensurePlayListCapacity(mPlayListLen + addlen);
974 if (position > mPlayListLen) {
975 position = mPlayListLen;
978 final int tailsize = mPlayListLen - position;
979 for (int i = tailsize; i > 0; i--) {
980 mPlayList[position + i] = mPlayList[position + i - addlen];
983 for (int i = 0; i < addlen; i++) {
984 mPlayList[position + i] = list[i];
986 mPlayListLen += addlen;
987 if (mPlayListLen == 0) {
989 notifyChange(META_CHANGED);
994 * @param trackId The track ID
996 private void updateCursor(final long trackId) {
997 updateCursor("_id=" + trackId, null);
1000 private void updateCursor(final String selection, final String[] selectionArgs) {
1001 synchronized (this) {
1003 mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1004 PROJECTION, selection, selectionArgs);
1006 updateAlbumCursor();
1009 private void updateCursor(final Uri uri) {
1010 synchronized (this) {
1012 mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
1014 updateAlbumCursor();
1017 private void updateAlbumCursor() {
1018 long albumId = getAlbumId();
1020 mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
1021 ALBUM_PROJECTION, "_id=" + albumId, null);
1023 mAlbumCursor = null;
1027 private Cursor openCursorAndGoToFirst(Uri uri, String[] projection,
1028 String selection, String[] selectionArgs) {
1029 Cursor c = getContentResolver().query(uri, projection,
1030 selection, selectionArgs, null, null);
1034 if (!c.moveToFirst()) {
1041 private void closeCursor() {
1042 if (mCursor != null) {
1046 if (mAlbumCursor != null) {
1047 mAlbumCursor.close();
1048 mAlbumCursor = null;
1053 * Called to open a new file as the current track and prepare the next for
1056 private void openCurrentAndNext() {
1057 openCurrentAndMaybeNext(true);
1061 * Called to open a new file as the current track and prepare the next for
1064 * @param openNext True to prepare the next track for playback, false
1067 private void openCurrentAndMaybeNext(final boolean openNext) {
1068 synchronized (this) {
1071 if (mPlayListLen == 0) {
1076 updateCursor(mPlayList[mPlayPos]);
1079 && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
1080 + mCursor.getLong(IDCOLIDX))) {
1083 // if we get here then opening the file failed. We can close the
1084 // cursor now, because
1085 // we're either going to create a new one next, or stop trying
1087 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
1088 final int pos = getNextPosition(false);
1090 scheduleDelayedShutdown();
1091 if (mIsSupposedToBePlaying) {
1092 mIsSupposedToBePlaying = false;
1093 notifyChange(PLAYSTATE_CHANGED);
1100 updateCursor(mPlayList[mPlayPos]);
1102 mOpenFailedCounter = 0;
1103 Log.w(TAG, "Failed to open file for playback");
1104 scheduleDelayedShutdown();
1105 if (mIsSupposedToBePlaying) {
1106 mIsSupposedToBePlaying = false;
1107 notifyChange(PLAYSTATE_CHANGED);
1119 * @param force True to force the player onto the track next, false
1121 * @return The next position to play.
1123 private int getNextPosition(final boolean force) {
1124 if (!force && mRepeatMode == REPEAT_CURRENT) {
1129 } else if (mShuffleMode == SHUFFLE_NORMAL) {
1130 if (mPlayPos >= 0) {
1131 mHistory.add(mPlayPos);
1133 if (mHistory.size() > MAX_HISTORY_SIZE) {
1136 final int numTracks = mPlayListLen;
1137 final int[] tracks = new int[numTracks];
1138 for (int i = 0; i < numTracks; i++) {
1142 final int numHistory = mHistory.size();
1143 int numUnplayed = numTracks;
1144 for (int i = 0; i < numHistory; i++) {
1145 final int idx = mHistory.get(i).intValue();
1146 if (idx < numTracks && tracks[idx] >= 0) {
1151 if (numUnplayed <= 0) {
1152 if (mRepeatMode == REPEAT_ALL || force) {
1153 numUnplayed = numTracks;
1154 for (int i = 0; i < numTracks; i++) {
1162 if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
1163 skip = mShuffler.nextInt(numUnplayed);
1167 while (tracks[++cnt] < 0) {
1176 } else if (mShuffleMode == SHUFFLE_AUTO) {
1177 doAutoShuffleUpdate();
1178 return mPlayPos + 1;
1180 if (mPlayPos >= mPlayListLen - 1) {
1181 if (mRepeatMode == REPEAT_NONE && !force) {
1183 } else if (mRepeatMode == REPEAT_ALL || force) {
1188 return mPlayPos + 1;
1194 * Sets the track track to be played
1196 private void setNextTrack() {
1197 mNextPlayPos = getNextPosition(false);
1198 if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
1199 if (mNextPlayPos >= 0 && mPlayList != null) {
1200 final long id = mPlayList[mNextPlayPos];
1201 mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
1203 mPlayer.setNextDataSource(null);
1208 * Creates a shuffled playlist used for party mode
1210 private boolean makeAutoShuffleList() {
1211 Cursor cursor = null;
1213 cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1215 MediaStore.Audio.Media._ID
1216 }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
1217 if (cursor == null || cursor.getCount() == 0) {
1220 final int len = cursor.getCount();
1221 final long[] list = new long[len];
1222 for (int i = 0; i < len; i++) {
1223 cursor.moveToNext();
1224 list[i] = cursor.getLong(0);
1226 mAutoShuffleList = list;
1228 } catch (final RuntimeException e) {
1230 if (cursor != null) {
1239 * Creates the party shuffle playlist
1241 private void doAutoShuffleUpdate() {
1242 boolean notify = false;
1243 if (mPlayPos > 10) {
1244 removeTracks(0, mPlayPos - 9);
1247 final int toAdd = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
1248 for (int i = 0; i < toAdd; i++) {
1249 int lookback = mHistory.size();
1252 idx = mShuffler.nextInt(mAutoShuffleList.length);
1253 if (!wasRecentlyUsed(idx, lookback)) {
1259 if (mHistory.size() > MAX_HISTORY_SIZE) {
1262 ensurePlayListCapacity(mPlayListLen + 1);
1263 mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
1267 notifyChange(QUEUE_CHANGED);
1272 private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
1273 if (lookbacksize == 0) {
1276 final int histsize = mHistory.size();
1277 if (histsize < lookbacksize) {
1278 lookbacksize = histsize;
1280 final int maxidx = histsize - 1;
1281 for (int i = 0; i < lookbacksize; i++) {
1282 final long entry = mHistory.get(maxidx - i);
1291 * Makes sure the playlist has enough space to hold all of the songs
1293 * @param size The size of the playlist
1295 private void ensurePlayListCapacity(final int size) {
1296 if (mPlayList == null || size > mPlayList.length) {
1297 // reallocate at 2x requested size so we don't
1298 // need to grow and copy the array for every
1300 final long[] newlist = new long[size * 2];
1301 final int len = mPlayList != null ? mPlayList.length : mPlayListLen;
1302 for (int i = 0; i < len; i++) {
1303 newlist[i] = mPlayList[i];
1305 mPlayList = newlist;
1307 // FIXME: shrink the array when the needed size is much smaller
1308 // than the allocated size
1312 * Notify the change-receivers that something has changed.
1314 private void notifyChange(final String what) {
1315 if (D) Log.d(TAG, "notifyChange: what = " + what);
1317 // Update the lockscreen controls
1318 updateRemoteControlClient(what);
1320 if (what.equals(POSITION_CHANGED)) {
1324 final Intent intent = new Intent(what);
1325 intent.putExtra("id", getAudioId());
1326 intent.putExtra("artist", getArtistName());
1327 intent.putExtra("album", getAlbumName());
1328 intent.putExtra("track", getTrackName());
1329 intent.putExtra("playing", isPlaying());
1330 intent.putExtra("isfavorite", isFavorite());
1331 sendStickyBroadcast(intent);
1333 final Intent musicIntent = new Intent(intent);
1334 musicIntent.setAction(what.replace(APOLLO_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
1335 sendStickyBroadcast(musicIntent);
1337 if (what.equals(META_CHANGED)) {
1338 // Increase the play count for favorite songs.
1339 if (mFavoritesCache.getSongId(getAudioId()) != null) {
1340 mFavoritesCache.addSongId(getAudioId(), getTrackName(), getAlbumName(),
1343 // Add the track to the recently played list.
1344 mRecentsCache.addAlbumId(getAlbumId(), getAlbumName(), getArtistName(),
1345 MusicUtils.getSongCountForAlbum(this, getAlbumId()),
1346 MusicUtils.getReleaseDateForAlbum(this, getAlbumId()));
1347 } else if (what.equals(QUEUE_CHANGED)) {
1356 if (what.equals(PLAYSTATE_CHANGED)) {
1357 mNotificationHelper.updatePlayState(isPlaying());
1360 // Update the app-widgets
1361 mAppWidgetSmall.notifyChange(this, what);
1362 mAppWidgetLarge.notifyChange(this, what);
1363 mAppWidgetLargeAlternate.notifyChange(this, what);
1364 mRecentWidgetProvider.notifyChange(this, what);
1368 * Updates the lockscreen controls.
1370 * @param what The broadcast
1372 private void updateRemoteControlClient(final String what) {
1373 int playState = mIsSupposedToBePlaying
1374 ? RemoteControlClient.PLAYSTATE_PLAYING
1375 : RemoteControlClient.PLAYSTATE_PAUSED;
1377 if (ApolloUtils.hasJellyBeanMR2()
1378 && (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED))) {
1379 mRemoteControlClient.setPlaybackState(playState, position(), 1.0f);
1380 } else if (what.equals(PLAYSTATE_CHANGED)) {
1381 mRemoteControlClient.setPlaybackState(playState);
1382 } else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
1383 Bitmap albumArt = getAlbumArt();
1384 if (albumArt != null) {
1385 // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
1386 // to make sure not to hand out our cache copy
1387 Bitmap.Config config = albumArt.getConfig();
1388 if (config == null) {
1389 config = Bitmap.Config.ARGB_8888;
1391 albumArt = albumArt.copy(config, false);
1393 mRemoteControlClient
1395 .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getArtistName())
1396 .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
1397 getAlbumArtistName())
1398 .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getAlbumName())
1399 .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getTrackName())
1400 .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration())
1401 .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, albumArt)
1404 if (ApolloUtils.hasJellyBeanMR2()) {
1405 mRemoteControlClient.setPlaybackState(playState, position(), 1.0f);
1413 * @param full True if the queue is full
1415 private void saveQueue(final boolean full) {
1416 if (!mQueueIsSaveable) {
1420 final SharedPreferences.Editor editor = mPreferences.edit();
1422 final StringBuilder q = new StringBuilder();
1423 int len = mPlayListLen;
1424 for (int i = 0; i < len; i++) {
1425 long n = mPlayList[i];
1428 } else if (n == 0) {
1432 final int digit = (int)(n & 0xf);
1434 q.append(HEX_DIGITS[digit]);
1439 editor.putString("queue", q.toString());
1440 editor.putInt("cardid", mCardId);
1441 if (mShuffleMode != SHUFFLE_NONE) {
1442 len = mHistory.size();
1444 for (int i = 0; i < len; i++) {
1445 int n = mHistory.get(i);
1450 final int digit = n & 0xf;
1452 q.append(HEX_DIGITS[digit]);
1457 editor.putString("history", q.toString());
1460 editor.putInt("curpos", mPlayPos);
1461 if (mPlayer.isInitialized()) {
1462 editor.putLong("seekpos", mPlayer.position());
1464 editor.putInt("repeatmode", mRepeatMode);
1465 editor.putInt("shufflemode", mShuffleMode);
1470 * Reloads the queue as the user left it the last time they stopped using
1473 private void reloadQueue() {
1476 if (mPreferences.contains("cardid")) {
1477 id = mPreferences.getInt("cardid", ~mCardId);
1479 if (id == mCardId) {
1480 q = mPreferences.getString("queue", "");
1482 int qlen = q != null ? q.length() : 0;
1487 for (int i = 0; i < qlen; i++) {
1488 final char c = q.charAt(i);
1490 ensurePlayListCapacity(plen + 1);
1491 mPlayList[plen] = n;
1496 if (c >= '0' && c <= '9') {
1497 n += c - '0' << shift;
1498 } else if (c >= 'a' && c <= 'f') {
1499 n += 10 + c - 'a' << shift;
1507 mPlayListLen = plen;
1508 final int pos = mPreferences.getInt("curpos", 0);
1509 if (pos < 0 || pos >= mPlayListLen) {
1514 updateCursor(mPlayList[mPlayPos]);
1515 if (mCursor == null) {
1516 SystemClock.sleep(3000);
1517 updateCursor(mPlayList[mPlayPos]);
1519 synchronized (this) {
1521 mOpenFailedCounter = 20;
1522 openCurrentAndNext();
1524 if (!mPlayer.isInitialized()) {
1529 final long seekpos = mPreferences.getLong("seekpos", 0);
1530 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
1533 Log.d(TAG, "restored queue, currently at position "
1534 + position() + "/" + duration()
1535 + " (requested " + seekpos + ")");
1538 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
1539 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
1540 repmode = REPEAT_NONE;
1542 mRepeatMode = repmode;
1544 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
1545 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
1546 shufmode = SHUFFLE_NONE;
1548 if (shufmode != SHUFFLE_NONE) {
1549 q = mPreferences.getString("history", "");
1550 qlen = q != null ? q.length() : 0;
1556 for (int i = 0; i < qlen; i++) {
1557 final char c = q.charAt(i);
1559 if (n >= mPlayListLen) {
1567 if (c >= '0' && c <= '9') {
1568 n += c - '0' << shift;
1569 } else if (c >= 'a' && c <= 'f') {
1570 n += 10 + c - 'a' << shift;
1580 if (shufmode == SHUFFLE_AUTO) {
1581 if (!makeAutoShuffleList()) {
1582 shufmode = SHUFFLE_NONE;
1585 mShuffleMode = shufmode;
1590 * Opens a file and prepares it for playback
1592 * @param path The path of the file to open
1594 public boolean openFile(final String path) {
1595 if (D) Log.d(TAG, "openFile: path = " + path);
1596 synchronized (this) {
1601 // If mCursor is null, try to associate path with a database cursor
1602 if (mCursor == null) {
1603 Uri uri = Uri.parse(path);
1606 id = Long.valueOf(uri.getLastPathSegment());
1607 } catch (NumberFormatException ex) {
1611 if (id != -1 && path.startsWith(MediaStore.Audio.Media.
1612 EXTERNAL_CONTENT_URI.toString())) {
1615 } else if (id != -1 && path.startsWith(MediaStore.Files.getContentUri(
1616 "external").toString())) {
1620 String where = MediaStore.Audio.Media.DATA + "=?";
1621 String[] selectionArgs = new String[] {path};
1622 updateCursor(where, selectionArgs);
1625 if (mCursor != null) {
1626 ensurePlayListCapacity(1);
1628 mPlayList[0] = mCursor.getLong(IDCOLIDX);
1631 } catch (final UnsupportedOperationException ex) {
1635 mPlayer.setDataSource(mFileToPlay);
1636 if (mPlayer.isInitialized()) {
1637 mOpenFailedCounter = 0;
1646 * Returns the audio session ID
1648 * @return The current media player audio session ID
1650 public int getAudioSessionId() {
1651 synchronized (this) {
1652 return mPlayer.getAudioSessionId();
1657 * Indicates if the media storeage device has been mounted or not
1659 * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
1661 public int getMediaMountedCount() {
1662 return mMediaMountedCount;
1666 * Returns the shuffle mode
1668 * @return The current shuffle mode (all, party, none)
1670 public int getShuffleMode() {
1671 return mShuffleMode;
1675 * Returns the repeat mode
1677 * @return The current repeat mode (all, one, none)
1679 public int getRepeatMode() {
1684 * Removes all instances of the track with the given ID from the playlist.
1686 * @param id The id to be removed
1687 * @return how many instances of the track were removed
1689 public int removeTrack(final long id) {
1691 synchronized (this) {
1692 for (int i = 0; i < mPlayListLen; i++) {
1693 if (mPlayList[i] == id) {
1694 numremoved += removeTracksInternal(i, i);
1699 if (numremoved > 0) {
1700 notifyChange(QUEUE_CHANGED);
1706 * Removes the range of tracks specified from the play list. If a file
1707 * within the range is the file currently being played, playback will move
1708 * to the next file after the range.
1710 * @param first The first file to be removed
1711 * @param last The last file to be removed
1712 * @return the number of tracks deleted
1714 public int removeTracks(final int first, final int last) {
1715 final int numremoved = removeTracksInternal(first, last);
1716 if (numremoved > 0) {
1717 notifyChange(QUEUE_CHANGED);
1723 * Returns the position in the queue
1725 * @return the current position in the queue
1727 public int getQueuePosition() {
1728 synchronized (this) {
1734 * Returns the path to current song
1736 * @return The path to the current song
1738 public String getPath() {
1739 synchronized (this) {
1740 if (mCursor == null) {
1743 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
1748 * Returns the album name
1750 * @return The current song album Name
1752 public String getAlbumName() {
1753 synchronized (this) {
1754 if (mCursor == null) {
1757 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
1762 * Returns the song name
1764 * @return The current song name
1766 public String getTrackName() {
1767 synchronized (this) {
1768 if (mCursor == null) {
1771 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
1776 * Returns the artist name
1778 * @return The current song artist name
1780 public String getArtistName() {
1781 synchronized (this) {
1782 if (mCursor == null) {
1785 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
1790 * Returns the artist name
1792 * @return The current song artist name
1794 public String getAlbumArtistName() {
1795 synchronized (this) {
1796 if (mAlbumCursor == null) {
1799 return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(AlbumColumns.ARTIST));
1804 * Returns the album ID
1806 * @return The current song album ID
1808 public long getAlbumId() {
1809 synchronized (this) {
1810 if (mCursor == null) {
1813 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
1818 * Returns the artist ID
1820 * @return The current song artist ID
1822 public long getArtistId() {
1823 synchronized (this) {
1824 if (mCursor == null) {
1827 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
1832 * Returns the current audio ID
1834 * @return The current track ID
1836 public long getAudioId() {
1837 synchronized (this) {
1838 if (mPlayPos >= 0 && mPlayer.isInitialized()) {
1839 return mPlayList[mPlayPos];
1846 * Seeks the current track to a specific time
1848 * @param position The time to seek to
1849 * @return The time to play the track at
1851 public long seek(long position) {
1852 if (mPlayer.isInitialized()) {
1855 } else if (position > mPlayer.duration()) {
1856 position = mPlayer.duration();
1858 long result = mPlayer.seek(position);
1859 notifyChange(POSITION_CHANGED);
1866 * Returns the current position in time of the currenttrack
1868 * @return The current playback position in miliseconds
1870 public long position() {
1871 if (mPlayer.isInitialized()) {
1872 return mPlayer.position();
1878 * Returns the full duration of the current track
1880 * @return The duration of the current track in miliseconds
1882 public long duration() {
1883 if (mPlayer.isInitialized()) {
1884 return mPlayer.duration();
1892 * @return The queue as a long[]
1894 public long[] getQueue() {
1895 synchronized (this) {
1896 final int len = mPlayListLen;
1897 final long[] list = new long[len];
1898 for (int i = 0; i < len; i++) {
1899 list[i] = mPlayList[i];
1906 * @return True if music is playing, false otherwise
1908 public boolean isPlaying() {
1909 return mIsSupposedToBePlaying;
1913 * True if the current track is a "favorite", false otherwise
1915 public boolean isFavorite() {
1916 if (mFavoritesCache != null) {
1917 synchronized (this) {
1918 final Long id = mFavoritesCache.getSongId(getAudioId());
1919 return id != null ? true : false;
1926 * Opens a list for playback
1928 * @param list The list of tracks to open
1929 * @param position The position to start playback at
1931 public void open(final long[] list, final int position) {
1932 synchronized (this) {
1933 if (mShuffleMode == SHUFFLE_AUTO) {
1934 mShuffleMode = SHUFFLE_NORMAL;
1936 final long oldId = getAudioId();
1937 final int listlength = list.length;
1938 boolean newlist = true;
1939 if (mPlayListLen == listlength) {
1941 for (int i = 0; i < listlength; i++) {
1942 if (list[i] != mPlayList[i]) {
1949 addToPlayList(list, -1);
1950 notifyChange(QUEUE_CHANGED);
1952 if (position >= 0) {
1953 mPlayPos = position;
1955 mPlayPos = mShuffler.nextInt(mPlayListLen);
1958 openCurrentAndNext();
1959 if (oldId != getAudioId()) {
1960 notifyChange(META_CHANGED);
1968 public void stop() {
1973 * Resumes or starts playback.
1975 public void play() {
1976 int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
1977 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
1979 if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
1981 if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
1985 mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
1986 MediaButtonIntentReceiver.class.getName()));
1990 if (mPlayer.isInitialized()) {
1991 final long duration = mPlayer.duration();
1992 if (mRepeatMode != REPEAT_CURRENT && duration > 2000
1993 && mPlayer.position() >= duration - 2000) {
1998 mPlayerHandler.removeMessages(FADEDOWN);
1999 mPlayerHandler.sendEmptyMessage(FADEUP);
2001 if (!mIsSupposedToBePlaying) {
2002 mIsSupposedToBePlaying = true;
2003 notifyChange(PLAYSTATE_CHANGED);
2007 updateNotification();
2008 } else if (mPlayListLen <= 0) {
2009 setShuffleMode(SHUFFLE_AUTO);
2014 * Temporarily pauses playback.
2016 public void pause() {
2017 if (D) Log.d(TAG, "Pausing playback");
2018 synchronized (this) {
2019 mPlayerHandler.removeMessages(FADEUP);
2020 if (mIsSupposedToBePlaying) {
2022 scheduleDelayedShutdown();
2023 mIsSupposedToBePlaying = false;
2024 notifyChange(PLAYSTATE_CHANGED);
2030 * Changes from the current track to the next track
2032 public void gotoNext(final boolean force) {
2033 if (D) Log.d(TAG, "Going to next track");
2034 synchronized (this) {
2035 if (mPlayListLen <= 0) {
2036 if (D) Log.d(TAG, "No play queue");
2037 scheduleDelayedShutdown();
2040 final int pos = getNextPosition(force);
2042 scheduleDelayedShutdown();
2043 if (mIsSupposedToBePlaying) {
2044 mIsSupposedToBePlaying = false;
2045 notifyChange(PLAYSTATE_CHANGED);
2052 openCurrentAndNext();
2054 notifyChange(META_CHANGED);
2059 * Changes from the current track to the previous played track
2061 public void prev() {
2062 if (D) Log.d(TAG, "Going to previous track");
2063 synchronized (this) {
2064 if (mShuffleMode == SHUFFLE_NORMAL) {
2065 // Go to previously-played track and remove it from the history
2066 final int histsize = mHistory.size();
2067 if (histsize == 0) {
2070 final Integer pos = mHistory.remove(histsize - 1);
2071 mPlayPos = pos.intValue();
2076 mPlayPos = mPlayListLen - 1;
2082 notifyChange(META_CHANGED);
2087 * We don't want to open the current and next track when the user is using
2088 * the {@code #prev()} method because they won't be able to travel back to
2089 * the previously listened track if they're shuffling.
2091 private void openCurrent() {
2092 openCurrentAndMaybeNext(false);
2096 * Toggles the current song as a favorite.
2098 public void toggleFavorite() {
2099 if (mFavoritesCache != null) {
2100 synchronized (this) {
2101 mFavoritesCache.toggleSong(getAudioId(), getTrackName(), getAlbumName(),
2108 * Moves an item in the queue from one position to another
2110 * @param from The position the item is currently at
2111 * @param to The position the item is being moved to
2113 public void moveQueueItem(int index1, int index2) {
2114 synchronized (this) {
2115 if (index1 >= mPlayListLen) {
2116 index1 = mPlayListLen - 1;
2118 if (index2 >= mPlayListLen) {
2119 index2 = mPlayListLen - 1;
2121 if (index1 < index2) {
2122 final long tmp = mPlayList[index1];
2123 for (int i = index1; i < index2; i++) {
2124 mPlayList[i] = mPlayList[i + 1];
2126 mPlayList[index2] = tmp;
2127 if (mPlayPos == index1) {
2129 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
2132 } else if (index2 < index1) {
2133 final long tmp = mPlayList[index1];
2134 for (int i = index1; i > index2; i--) {
2135 mPlayList[i] = mPlayList[i - 1];
2137 mPlayList[index2] = tmp;
2138 if (mPlayPos == index1) {
2140 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
2144 notifyChange(QUEUE_CHANGED);
2149 * Sets the repeat mode
2151 * @param repeatmode The repeat mode to use
2153 public void setRepeatMode(final int repeatmode) {
2154 synchronized (this) {
2155 mRepeatMode = repeatmode;
2158 notifyChange(REPEATMODE_CHANGED);
2163 * Sets the shuffle mode
2165 * @param shufflemode The shuffle mode to use
2167 public void setShuffleMode(final int shufflemode) {
2168 synchronized (this) {
2169 if (mShuffleMode == shufflemode && mPlayListLen > 0) {
2172 mShuffleMode = shufflemode;
2173 if (mShuffleMode == SHUFFLE_AUTO) {
2174 if (makeAutoShuffleList()) {
2176 doAutoShuffleUpdate();
2178 openCurrentAndNext();
2180 notifyChange(META_CHANGED);
2183 mShuffleMode = SHUFFLE_NONE;
2187 notifyChange(SHUFFLEMODE_CHANGED);
2192 * Sets the position of a track in the queue
2194 * @param index The position to place the track
2196 public void setQueuePosition(final int index) {
2197 synchronized (this) {
2200 openCurrentAndNext();
2202 notifyChange(META_CHANGED);
2203 if (mShuffleMode == SHUFFLE_AUTO) {
2204 doAutoShuffleUpdate();
2210 * Queues a new list for playback
2212 * @param list The list to queue
2213 * @param action The action to take
2215 public void enqueue(final long[] list, final int action) {
2216 synchronized (this) {
2217 if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
2218 addToPlayList(list, mPlayPos + 1);
2219 notifyChange(QUEUE_CHANGED);
2221 addToPlayList(list, Integer.MAX_VALUE);
2222 notifyChange(QUEUE_CHANGED);
2223 if (action == NOW) {
2224 mPlayPos = mPlayListLen - list.length;
2225 openCurrentAndNext();
2227 notifyChange(META_CHANGED);
2233 openCurrentAndNext();
2235 notifyChange(META_CHANGED);
2241 * Cycles through the different repeat modes
2243 private void cycleRepeat() {
2244 if (mRepeatMode == REPEAT_NONE) {
2245 setRepeatMode(REPEAT_ALL);
2246 } else if (mRepeatMode == REPEAT_ALL) {
2247 setRepeatMode(REPEAT_CURRENT);
2248 if (mShuffleMode != SHUFFLE_NONE) {
2249 setShuffleMode(SHUFFLE_NONE);
2252 setRepeatMode(REPEAT_NONE);
2257 * Cycles through the different shuffle modes
2259 private void cycleShuffle() {
2260 if (mShuffleMode == SHUFFLE_NONE) {
2261 setShuffleMode(SHUFFLE_NORMAL);
2262 if (mRepeatMode == REPEAT_CURRENT) {
2263 setRepeatMode(REPEAT_ALL);
2265 } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
2266 setShuffleMode(SHUFFLE_NONE);
2271 * @return The album art for the current album.
2273 public Bitmap getAlbumArt() {
2274 // Return the cached artwork
2275 final Bitmap bitmap = mImageFetcher.getArtwork(getAlbumName(),
2276 getAlbumId(), getArtistName());
2281 * Called when one of the lists should refresh or requery.
2283 public void refresh() {
2284 notifyChange(REFRESH);
2287 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
2292 public void onReceive(final Context context, final Intent intent) {
2293 final String command = intent.getStringExtra(CMDNAME);
2295 if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
2296 final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2297 mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
2298 } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
2299 final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2300 mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
2301 } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
2302 final int[] largeAlt = intent
2303 .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2304 mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
2305 } else if (RecentWidgetProvider.CMDAPPWIDGETUPDATE.equals(command)) {
2306 final int[] recent = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2307 mRecentWidgetProvider.performUpdate(MusicPlaybackService.this, recent);
2309 handleCommandIntent(intent);
2314 private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
2319 public void onAudioFocusChange(final int focusChange) {
2320 mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
2324 private static final class MusicPlayerHandler extends Handler {
2325 private final WeakReference<MusicPlaybackService> mService;
2326 private float mCurrentVolume = 1.0f;
2329 * Constructor of <code>MusicPlayerHandler</code>
2331 * @param service The service to use.
2332 * @param looper The thread to run on.
2334 public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
2336 mService = new WeakReference<MusicPlaybackService>(service);
2343 public void handleMessage(final Message msg) {
2344 final MusicPlaybackService service = mService.get();
2345 if (service == null) {
2351 mCurrentVolume -= .05f;
2352 if (mCurrentVolume > .2f) {
2353 sendEmptyMessageDelayed(FADEDOWN, 10);
2355 mCurrentVolume = .2f;
2357 service.mPlayer.setVolume(mCurrentVolume);
2360 mCurrentVolume += .01f;
2361 if (mCurrentVolume < 1.0f) {
2362 sendEmptyMessageDelayed(FADEUP, 10);
2364 mCurrentVolume = 1.0f;
2366 service.mPlayer.setVolume(mCurrentVolume);
2369 if (service.isPlaying()) {
2370 service.gotoNext(true);
2372 service.openCurrentAndNext();
2375 case TRACK_WENT_TO_NEXT:
2376 service.mPlayPos = service.mNextPlayPos;
2377 if (service.mCursor != null) {
2378 service.mCursor.close();
2380 service.updateCursor(service.mPlayList[service.mPlayPos]);
2381 service.notifyChange(META_CHANGED);
2382 service.updateNotification();
2383 service.setNextTrack();
2386 if (service.mRepeatMode == REPEAT_CURRENT) {
2390 service.gotoNext(false);
2393 case RELEASE_WAKELOCK:
2394 service.mWakeLock.release();
2397 if (D) Log.d(TAG, "Received audio focus change event " + msg.arg1);
2399 case AudioManager.AUDIOFOCUS_LOSS:
2400 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
2401 if (service.isPlaying()) {
2402 service.mPausedByTransientLossOfFocus =
2403 msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
2407 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
2408 removeMessages(FADEUP);
2409 sendEmptyMessage(FADEDOWN);
2411 case AudioManager.AUDIOFOCUS_GAIN:
2412 if (!service.isPlaying()
2413 && service.mPausedByTransientLossOfFocus) {
2414 service.mPausedByTransientLossOfFocus = false;
2415 mCurrentVolume = 0f;
2416 service.mPlayer.setVolume(mCurrentVolume);
2419 removeMessages(FADEDOWN);
2420 sendEmptyMessage(FADEUP);
2432 private static final class Shuffler {
2434 private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<Integer>();
2436 private final TreeSet<Integer> mPreviousNumbers = new TreeSet<Integer>();
2438 private final Random mRandom = new Random();
2440 private int mPrevious;
2443 * Constructor of <code>Shuffler</code>
2450 * @param interval The length the queue
2451 * @return The position of the next track to play
2453 public int nextInt(final int interval) {
2456 next = mRandom.nextInt(interval);
2457 } while (next == mPrevious && interval > 1
2458 && !mPreviousNumbers.contains(Integer.valueOf(next)));
2460 mHistoryOfNumbers.add(mPrevious);
2461 mPreviousNumbers.add(mPrevious);
2467 * Removes old tracks and cleans up the history preparing for new tracks
2468 * to be added to the mapping
2470 private void cleanUpHistory() {
2471 if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
2472 for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
2473 mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
2479 private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
2480 MediaPlayer.OnCompletionListener {
2482 private final WeakReference<MusicPlaybackService> mService;
2484 private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
2486 private MediaPlayer mNextMediaPlayer;
2488 private Handler mHandler;
2490 private boolean mIsInitialized = false;
2493 * Constructor of <code>MultiPlayer</code>
2495 public MultiPlayer(final MusicPlaybackService service) {
2496 mService = new WeakReference<MusicPlaybackService>(service);
2497 mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
2501 * @param path The path of the file, or the http/rtsp URL of the stream
2504 public void setDataSource(final String path) {
2505 mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
2506 if (mIsInitialized) {
2507 setNextDataSource(null);
2512 * @param player The {@link MediaPlayer} to use
2513 * @param path The path of the file, or the http/rtsp URL of the stream
2515 * @return True if the <code>player</code> has been prepared and is
2516 * ready to play, false otherwise
2518 private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
2521 player.setOnPreparedListener(null);
2522 if (path.startsWith("content://")) {
2523 player.setDataSource(mService.get(), Uri.parse(path));
2525 player.setDataSource(path);
2527 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
2529 } catch (final IOException todo) {
2530 // TODO: notify the user why the file couldn't be opened
2532 } catch (final IllegalArgumentException todo) {
2533 // TODO: notify the user why the file couldn't be opened
2536 player.setOnCompletionListener(this);
2537 player.setOnErrorListener(this);
2538 final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
2539 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2540 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, mService.get().getPackageName());
2541 mService.get().sendBroadcast(intent);
2546 * Set the MediaPlayer to start when this MediaPlayer finishes playback.
2548 * @param path The path of the file, or the http/rtsp URL of the stream
2551 public void setNextDataSource(final String path) {
2553 mCurrentMediaPlayer.setNextMediaPlayer(null);
2554 } catch (IllegalArgumentException e) {
2555 Log.i(TAG, "Next media player is current one, continuing");
2556 } catch (IllegalStateException e) {
2557 Log.e(TAG, "Media player not initialized!");
2560 if (mNextMediaPlayer != null) {
2561 mNextMediaPlayer.release();
2562 mNextMediaPlayer = null;
2567 mNextMediaPlayer = new MediaPlayer();
2568 mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
2569 mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
2570 if (setDataSourceImpl(mNextMediaPlayer, path)) {
2571 mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
2573 if (mNextMediaPlayer != null) {
2574 mNextMediaPlayer.release();
2575 mNextMediaPlayer = null;
2583 * @param handler The handler to use
2585 public void setHandler(final Handler handler) {
2590 * @return True if the player is ready to go, false otherwise
2592 public boolean isInitialized() {
2593 return mIsInitialized;
2597 * Starts or resumes playback.
2599 public void start() {
2600 mCurrentMediaPlayer.start();
2604 * Resets the MediaPlayer to its uninitialized state.
2606 public void stop() {
2607 mCurrentMediaPlayer.reset();
2608 mIsInitialized = false;
2612 * Releases resources associated with this MediaPlayer object.
2614 public void release() {
2616 mCurrentMediaPlayer.release();
2620 * Pauses playback. Call start() to resume.
2622 public void pause() {
2623 mCurrentMediaPlayer.pause();
2627 * Gets the duration of the file.
2629 * @return The duration in milliseconds
2631 public long duration() {
2632 return mCurrentMediaPlayer.getDuration();
2636 * Gets the current playback position.
2638 * @return The current position in milliseconds
2640 public long position() {
2641 return mCurrentMediaPlayer.getCurrentPosition();
2645 * Gets the current playback position.
2647 * @param whereto The offset in milliseconds from the start to seek to
2648 * @return The offset in milliseconds from the start to seek to
2650 public long seek(final long whereto) {
2651 mCurrentMediaPlayer.seekTo((int)whereto);
2656 * Sets the volume on this player.
2658 * @param vol Left and right volume scalar
2660 public void setVolume(final float vol) {
2661 mCurrentMediaPlayer.setVolume(vol, vol);
2665 * Sets the audio session ID.
2667 * @param sessionId The audio session ID
2669 public void setAudioSessionId(final int sessionId) {
2670 mCurrentMediaPlayer.setAudioSessionId(sessionId);
2674 * Returns the audio session ID.
2676 * @return The current audio session ID.
2678 public int getAudioSessionId() {
2679 return mCurrentMediaPlayer.getAudioSessionId();
2686 public boolean onError(final MediaPlayer mp, final int what, final int extra) {
2688 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
2689 mIsInitialized = false;
2690 mCurrentMediaPlayer.release();
2691 mCurrentMediaPlayer = new MediaPlayer();
2692 mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
2693 mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
2705 public void onCompletion(final MediaPlayer mp) {
2706 if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
2707 mCurrentMediaPlayer.release();
2708 mCurrentMediaPlayer = mNextMediaPlayer;
2709 mNextMediaPlayer = null;
2710 mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
2712 mService.get().mWakeLock.acquire(30000);
2713 mHandler.sendEmptyMessage(TRACK_ENDED);
2714 mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
2719 private static final class ServiceStub extends IApolloService.Stub {
2721 private final WeakReference<MusicPlaybackService> mService;
2723 private ServiceStub(final MusicPlaybackService service) {
2724 mService = new WeakReference<MusicPlaybackService>(service);
2731 public void openFile(final String path) throws RemoteException {
2732 mService.get().openFile(path);
2739 public void open(final long[] list, final int position) throws RemoteException {
2740 mService.get().open(list, position);
2747 public void stop() throws RemoteException {
2748 mService.get().stop();
2755 public void pause() throws RemoteException {
2756 mService.get().pause();
2763 public void play() throws RemoteException {
2764 mService.get().play();
2771 public void prev() throws RemoteException {
2772 mService.get().prev();
2779 public void next() throws RemoteException {
2780 mService.get().gotoNext(true);
2787 public void enqueue(final long[] list, final int action) throws RemoteException {
2788 mService.get().enqueue(list, action);
2795 public void setQueuePosition(final int index) throws RemoteException {
2796 mService.get().setQueuePosition(index);
2803 public void setShuffleMode(final int shufflemode) throws RemoteException {
2804 mService.get().setShuffleMode(shufflemode);
2811 public void setRepeatMode(final int repeatmode) throws RemoteException {
2812 mService.get().setRepeatMode(repeatmode);
2819 public void moveQueueItem(final int from, final int to) throws RemoteException {
2820 mService.get().moveQueueItem(from, to);
2827 public void toggleFavorite() throws RemoteException {
2828 mService.get().toggleFavorite();
2835 public void refresh() throws RemoteException {
2836 mService.get().refresh();
2843 public boolean isFavorite() throws RemoteException {
2844 return mService.get().isFavorite();
2851 public boolean isPlaying() throws RemoteException {
2852 return mService.get().isPlaying();
2859 public long[] getQueue() throws RemoteException {
2860 return mService.get().getQueue();
2867 public long duration() throws RemoteException {
2868 return mService.get().duration();
2875 public long position() throws RemoteException {
2876 return mService.get().position();
2883 public long seek(final long position) throws RemoteException {
2884 return mService.get().seek(position);
2891 public long getAudioId() throws RemoteException {
2892 return mService.get().getAudioId();
2899 public long getArtistId() throws RemoteException {
2900 return mService.get().getArtistId();
2907 public long getAlbumId() throws RemoteException {
2908 return mService.get().getAlbumId();
2915 public String getArtistName() throws RemoteException {
2916 return mService.get().getArtistName();
2923 public String getTrackName() throws RemoteException {
2924 return mService.get().getTrackName();
2931 public String getAlbumName() throws RemoteException {
2932 return mService.get().getAlbumName();
2939 public String getPath() throws RemoteException {
2940 return mService.get().getPath();
2947 public int getQueuePosition() throws RemoteException {
2948 return mService.get().getQueuePosition();
2955 public int getShuffleMode() throws RemoteException {
2956 return mService.get().getShuffleMode();
2963 public int getRepeatMode() throws RemoteException {
2964 return mService.get().getRepeatMode();
2971 public int removeTracks(final int first, final int last) throws RemoteException {
2972 return mService.get().removeTracks(first, last);
2979 public int removeTrack(final long id) throws RemoteException {
2980 return mService.get().removeTrack(id);
2987 public int getMediaMountedCount() throws RemoteException {
2988 return mService.get().getMediaMountedCount();
2995 public int getAudioSessionId() throws RemoteException {
2996 return mService.get().getAudioSessionId();