OSDN Git Service

Automatic translation import
[android-x86/packages-apps-Eleven.git] / src / com / andrew / apollo / MusicPlaybackService.java
1 /*
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.
10  */
11
12 package com.andrew.apollo;
13
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;
49
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;
62
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;
68
69 /**
70  * A backbround {@link Service} used to keep music playing between activities
71  * and when the user moves Apollo into the background.
72  */
73 @SuppressLint("NewApi")
74 public class MusicPlaybackService extends Service {
75     private static final String TAG = "MusicPlaybackService";
76     private static final boolean D = false;
77
78     /**
79      * Indicates that the music has paused or resumed
80      */
81     public static final String PLAYSTATE_CHANGED = "com.andrew.apollo.playstatechanged";
82
83     /**
84      * Indicates that music playback position within
85      * a title was changed
86      */
87     public static final String POSITION_CHANGED = "com.android.apollo.positionchanged";
88
89     /**
90      * Indicates the meta data has changed in some way, like a track change
91      */
92     public static final String META_CHANGED = "com.andrew.apollo.metachanged";
93
94     /**
95      * Indicates the queue has been updated
96      */
97     public static final String QUEUE_CHANGED = "com.andrew.apollo.queuechanged";
98
99     /**
100      * Indicates the repeat mode chaned
101      */
102     public static final String REPEATMODE_CHANGED = "com.andrew.apollo.repeatmodechanged";
103
104     /**
105      * Indicates the shuffle mode chaned
106      */
107     public static final String SHUFFLEMODE_CHANGED = "com.andrew.apollo.shufflemodechanged";
108
109     /**
110      * For backwards compatibility reasons, also provide sticky
111      * broadcasts under the music package
112      */
113     public static final String APOLLO_PACKAGE_NAME = "com.andrew.apollo";
114     public static final String MUSIC_PACKAGE_NAME = "com.android.music";
115
116     /**
117      * Called to indicate a general service commmand. Used in
118      * {@link MediaButtonIntentReceiver}
119      */
120     public static final String SERVICECMD = "com.andrew.apollo.musicservicecommand";
121
122     /**
123      * Called to go toggle between pausing and playing the music
124      */
125     public static final String TOGGLEPAUSE_ACTION = "com.andrew.apollo.togglepause";
126
127     /**
128      * Called to go to pause the playback
129      */
130     public static final String PAUSE_ACTION = "com.andrew.apollo.pause";
131
132     /**
133      * Called to go to stop the playback
134      */
135     public static final String STOP_ACTION = "com.andrew.apollo.stop";
136
137     /**
138      * Called to go to the previous track
139      */
140     public static final String PREVIOUS_ACTION = "com.andrew.apollo.previous";
141
142     /**
143      * Called to go to the next track
144      */
145     public static final String NEXT_ACTION = "com.andrew.apollo.next";
146
147     /**
148      * Called to change the repeat mode
149      */
150     public static final String REPEAT_ACTION = "com.andrew.apollo.repeat";
151
152     /**
153      * Called to change the shuffle mode
154      */
155     public static final String SHUFFLE_ACTION = "com.andrew.apollo.shuffle";
156
157     /**
158      * Called to update the service about the foreground state of Apollo's activities
159      */
160     public static final String FOREGROUND_STATE_CHANGED = "com.andrew.apollo.fgstatechanged";
161
162     public static final String NOW_IN_FOREGROUND = "nowinforeground";
163
164     public static final String FROM_MEDIA_BUTTON = "frommediabutton";
165
166     /**
167      * Used to easily notify a list that it should refresh. i.e. A playlist
168      * changes
169      */
170     public static final String REFRESH = "com.andrew.apollo.refresh";
171
172     /**
173      * Used by the alarm intent to shutdown the service after being idle
174      */
175     private static final String SHUTDOWN = "com.andrew.apollo.shutdown";
176
177     /**
178      * Called to update the remote control client
179      */
180     public static final String UPDATE_LOCKSCREEN = "com.andrew.apollo.updatelockscreen";
181
182     public static final String CMDNAME = "command";
183
184     public static final String CMDTOGGLEPAUSE = "togglepause";
185
186     public static final String CMDSTOP = "stop";
187
188     public static final String CMDPAUSE = "pause";
189
190     public static final String CMDPLAY = "play";
191
192     public static final String CMDPREVIOUS = "previous";
193
194     public static final String CMDNEXT = "next";
195
196     public static final String CMDNOTIF = "buttonId";
197
198     private static final int IDCOLIDX = 0;
199
200     /**
201      * Moves a list to the front of the queue
202      */
203     public static final int NOW = 1;
204
205     /**
206      * Moves a list to the next position in the queue
207      */
208     public static final int NEXT = 2;
209
210     /**
211      * Moves a list to the last position in the queue
212      */
213     public static final int LAST = 3;
214
215     /**
216      * Shuffles no songs, turns shuffling off
217      */
218     public static final int SHUFFLE_NONE = 0;
219
220     /**
221      * Shuffles all songs
222      */
223     public static final int SHUFFLE_NORMAL = 1;
224
225     /**
226      * Party shuffle
227      */
228     public static final int SHUFFLE_AUTO = 2;
229
230     /**
231      * Turns repeat off
232      */
233     public static final int REPEAT_NONE = 0;
234
235     /**
236      * Repeats the current track in a list
237      */
238     public static final int REPEAT_CURRENT = 1;
239
240     /**
241      * Repeats all the tracks in a list
242      */
243     public static final int REPEAT_ALL = 2;
244
245     /**
246      * Indicates when the track ends
247      */
248     private static final int TRACK_ENDED = 1;
249
250     /**
251      * Indicates that the current track was changed the next track
252      */
253     private static final int TRACK_WENT_TO_NEXT = 2;
254
255     /**
256      * Indicates when the release the wake lock
257      */
258     private static final int RELEASE_WAKELOCK = 3;
259
260     /**
261      * Indicates the player died
262      */
263     private static final int SERVER_DIED = 4;
264
265     /**
266      * Indicates some sort of focus change, maybe a phone call
267      */
268     private static final int FOCUSCHANGE = 5;
269
270     /**
271      * Indicates to fade the volume down
272      */
273     private static final int FADEDOWN = 6;
274
275     /**
276      * Indicates to fade the volume back up
277      */
278     private static final int FADEUP = 7;
279
280     /**
281      * Idle time before stopping the foreground notfication (1 minute)
282      */
283     private static final int IDLE_DELAY = 60000;
284
285     /**
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
288      * command
289      */
290     private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
291
292     /**
293      * The max size allowed for the track history
294      */
295     private static final int MAX_HISTORY_SIZE = 100;
296
297     /**
298      * The columns used to retrieve any info from the current track
299      */
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
305     };
306
307     /**
308      * The columns used to retrieve any info from the current album
309      */
310     private static final String[] ALBUM_PROJECTION = new String[] {
311             MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
312             MediaStore.Audio.Albums.LAST_YEAR
313     };
314
315     /**
316      * Keeps a mapping of the track history
317      */
318     private static final LinkedList<Integer> mHistory = Lists.newLinkedList();
319
320     /**
321      * Used to shuffle the tracks
322      */
323     private static final Shuffler mShuffler = new Shuffler();
324
325     /**
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
330      */
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'
333     };
334
335     /**
336      * Service stub
337      */
338     private final IBinder mBinder = new ServiceStub(this);
339
340     /**
341      * 4x1 widget
342      */
343     private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
344
345     /**
346      * 4x2 widget
347      */
348     private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
349
350     /**
351      * 4x2 alternate widget
352      */
353     private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
354             .getInstance();
355
356     /**
357      * Recently listened widget
358      */
359     private final RecentWidgetProvider mRecentWidgetProvider = RecentWidgetProvider.getInstance();
360
361     /**
362      * The media player
363      */
364     private MultiPlayer mPlayer;
365
366     /**
367      * The path of the current file to play
368      */
369     private String mFileToPlay;
370
371     /**
372      * Keeps the service running when the screen is off
373      */
374     private WakeLock mWakeLock;
375
376     /**
377      * Alarm intent for removing the notification when nothing is playing
378      * for some time
379      */
380     private AlarmManager mAlarmManager;
381     private PendingIntent mShutdownIntent;
382     private boolean mShutdownScheduled;
383
384     /**
385      * The cursor used to retrieve info on the current track and run the
386      * necessary queries to play audio files
387      */
388     private Cursor mCursor;
389
390     /**
391      * The cursor used to retrieve info on the album the current track is
392      * part of, if any.
393      */
394     private Cursor mAlbumCursor;
395
396     /**
397      * Monitors the audio state
398      */
399     private AudioManager mAudioManager;
400
401     /**
402      * Settings used to save and retrieve the queue and history
403      */
404     private SharedPreferences mPreferences;
405
406     /**
407      * Used to know when the service is active
408      */
409     private boolean mServiceInUse = false;
410
411     /**
412      * Used to know if something should be playing or not
413      */
414     private boolean mIsSupposedToBePlaying = false;
415
416     /**
417      * Used to indicate if the queue can be saved
418      */
419     private boolean mQueueIsSaveable = true;
420
421     /**
422      * Used to track what type of audio focus loss caused the playback to pause
423      */
424     private boolean mPausedByTransientLossOfFocus = false;
425
426     /**
427      * Used to track whether any of Apollo's activities is in the foreground
428      */
429     private boolean mAnyActivityInForeground = false;
430
431     /**
432      * Lock screen controls
433      */
434     private RemoteControlClient mRemoteControlClient;
435
436     private ComponentName mMediaButtonReceiverComponent;
437
438     // We use this to distinguish between different cards when saving/restoring
439     // playlists
440     private int mCardId;
441
442     private int mPlayListLen = 0;
443
444     private int mPlayPos = -1;
445
446     private int mNextPlayPos = -1;
447
448     private int mOpenFailedCounter = 0;
449
450     private int mMediaMountedCount = 0;
451
452     private int mShuffleMode = SHUFFLE_NONE;
453
454     private int mRepeatMode = REPEAT_NONE;
455
456     private int mServiceStartId = -1;
457
458     private long[] mPlayList = null;
459
460     private long[] mAutoShuffleList = null;
461
462     private MusicPlayerHandler mPlayerHandler;
463
464     private BroadcastReceiver mUnmountReceiver = null;
465
466     /**
467      * Image cache
468      */
469     private ImageFetcher mImageFetcher;
470
471     /**
472      * Used to build the notification
473      */
474     private NotificationHelper mNotificationHelper;
475
476     /**
477      * Recently listened database
478      */
479     private RecentStore mRecentsCache;
480
481     /**
482      * Favorites database
483      */
484     private FavoritesStore mFavoritesCache;
485
486     /**
487      * {@inheritDoc}
488      */
489     @Override
490     public IBinder onBind(final Intent intent) {
491         if (D) Log.d(TAG, "Service bound, intent = " + intent);
492         cancelShutdown();
493         mServiceInUse = true;
494         return mBinder;
495     }
496
497     /**
498      * {@inheritDoc}
499      */
500     @Override
501     public boolean onUnbind(final Intent intent) {
502         if (D) Log.d(TAG, "Service unbound");
503         mServiceInUse = false;
504         saveQueue(true);
505
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
509             // the service now.
510             return true;
511
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
515             // tracks.
516         } else if (mPlayListLen > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
517             scheduleDelayedShutdown();
518             return true;
519         }
520         stopSelf(mServiceStartId);
521         return true;
522     }
523
524     /**
525      * {@inheritDoc}
526      */
527     @Override
528     public void onRebind(final Intent intent) {
529         cancelShutdown();
530         mServiceInUse = true;
531     }
532
533     /**
534      * {@inheritDoc}
535      */
536     @Override
537     public void onCreate() {
538         if (D) Log.d(TAG, "Creating service");
539         super.onCreate();
540
541         // Initialize the favorites and recents databases
542         mRecentsCache = RecentStore.getInstance(this);
543         mFavoritesCache = FavoritesStore.getInstance(this);
544
545         // Initialize the notification helper
546         mNotificationHelper = new NotificationHelper(this);
547
548         // Initialize the image fetcher
549         mImageFetcher = ImageFetcher.getInstance(this);
550         // Initialize the image cache
551         mImageFetcher.setImageCache(ImageCache.getInstance(this));
552
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);
559         thread.start();
560
561         // Initialize the handler
562         mPlayerHandler = new MusicPlayerHandler(this, thread.getLooper());
563
564         // Initialize the audio manager and register any headset controls for
565         // playback
566         mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
567         mMediaButtonReceiverComponent = new ComponentName(getPackageName(),
568                 MediaButtonIntentReceiver.class.getName());
569         mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);
570
571         // Use the remote control APIs to set the playback state
572         setUpRemoteControlClient();
573
574         // Initialize the preferences
575         mPreferences = getSharedPreferences("Service", 0);
576         mCardId = getCardId();
577
578         registerExternalStorageListener();
579
580         // Initialize the media player
581         mPlayer = new MultiPlayer(this);
582         mPlayer.setHandler(mPlayerHandler);
583
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);
596
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);
601
602         // Initialize the delayed shutdown intent
603         final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
604         shutdownIntent.setAction(SHUTDOWN);
605
606         mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
607         mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
608
609         // Listen for the idle state
610         scheduleDelayedShutdown();
611
612         // Bring the queue back
613         reloadQueue();
614         notifyChange(QUEUE_CHANGED);
615         notifyChange(META_CHANGED);
616     }
617
618     /**
619      * Initializes the remote control client
620      */
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);
628
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;
636
637         if (ApolloUtils.hasJellyBeanMR2()) {
638             flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
639
640             mRemoteControlClient.setOnGetPlaybackPositionListener(
641                     new RemoteControlClient.OnGetPlaybackPositionListener() {
642                 @Override
643                 public long onGetPlaybackPosition() {
644                     return position();
645                 }
646             });
647             mRemoteControlClient.setPlaybackPositionUpdateListener(
648                     new RemoteControlClient.OnPlaybackPositionUpdateListener() {
649                 @Override
650                 public void onPlaybackPositionUpdate(long newPositionMs) {
651                     seek(newPositionMs);
652                 }
653             });
654         }
655
656         mRemoteControlClient.setTransportControlFlags(flags);
657     }
658
659     /**
660      * {@inheritDoc}
661      */
662     @Override
663     public void onDestroy() {
664         if (D) Log.d(TAG, "Destroying service");
665         super.onDestroy();
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);
672
673         // remove any pending alarms
674         mAlarmManager.cancel(mShutdownIntent);
675
676         // Release the player
677         mPlayer.release();
678         mPlayer = null;
679
680         // Remove the audio focus listener and lock screen controls
681         mAudioManager.abandonAudioFocus(mAudioFocusListener);
682         mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
683
684         // Remove any callbacks from the handler
685         mPlayerHandler.removeCallbacksAndMessages(null);
686
687         // Close the cursor
688         closeCursor();
689
690         // Unregister the mount listener
691         unregisterReceiver(mIntentReceiver);
692         if (mUnmountReceiver != null) {
693             unregisterReceiver(mUnmountReceiver);
694             mUnmountReceiver = null;
695         }
696
697         // Release the wake lock
698         mWakeLock.release();
699     }
700
701     /**
702      * {@inheritDoc}
703      */
704     @Override
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;
708
709         if (intent != null) {
710             final String action = intent.getAction();
711
712             if (intent.hasExtra(NOW_IN_FOREGROUND)) {
713                 mAnyActivityInForeground = intent.getBooleanExtra(NOW_IN_FOREGROUND, false);
714                 updateNotification();
715             }
716
717             if (SHUTDOWN.equals(action)) {
718                 mShutdownScheduled = false;
719                 releaseServiceUiAndStop();
720                 return START_NOT_STICKY;
721             }
722
723             handleCommandIntent(intent);
724         }
725
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();
729
730         if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
731             MediaButtonIntentReceiver.completeWakefulIntent(intent);
732         }
733
734         return START_STICKY;
735     }
736
737     private void releaseServiceUiAndStop() {
738         if (isPlaying()
739                 || mPausedByTransientLossOfFocus
740                 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
741             return;
742         }
743
744         if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
745         mNotificationHelper.killNotification();
746         mAudioManager.abandonAudioFocus(mAudioFocusListener);
747
748         if (!mServiceInUse) {
749             saveQueue(true);
750             stopSelf(mServiceStartId);
751         }
752     }
753
754     private void handleCommandIntent(Intent intent) {
755         final String action = intent.getAction();
756         final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
757
758         if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
759
760         if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
761             gotoNext(true);
762         } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) {
763             if (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD) {
764                 prev();
765             } else {
766                 seek(0);
767                 play();
768             }
769         } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
770             if (isPlaying()) {
771                 pause();
772                 mPausedByTransientLossOfFocus = false;
773             } else {
774                 play();
775             }
776         } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
777             pause();
778             mPausedByTransientLossOfFocus = false;
779         } else if (CMDPLAY.equals(command)) {
780             play();
781         } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
782             pause();
783             mPausedByTransientLossOfFocus = false;
784             seek(0);
785             releaseServiceUiAndStop();
786         } else if (REPEAT_ACTION.equals(action)) {
787             cycleRepeat();
788         } else if (SHUFFLE_ACTION.equals(action)) {
789             cycleShuffle();
790         }
791     }
792
793     /**
794      * Updates the notification, considering the current play and activity state
795      */
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();
802         }
803     }
804
805     /**
806      * @return A card ID used to save and restore playlists, i.e., the queue.
807      */
808     private int getCardId() {
809         final ContentResolver resolver = getContentResolver();
810         Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
811                 null, null);
812         int mCardId = -1;
813         if (cursor != null && cursor.moveToFirst()) {
814             mCardId = cursor.getInt(0);
815             cursor.close();
816             cursor = null;
817         }
818         return mCardId;
819     }
820
821     /**
822      * Called when we receive a ACTION_MEDIA_EJECT notification.
823      *
824      * @param storagePath The path to mount point for the removed media
825      */
826     public void closeExternalStorageFiles(final String storagePath) {
827         stop(true);
828         notifyChange(QUEUE_CHANGED);
829         notifyChange(META_CHANGED);
830     }
831
832     /**
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
836      * open.
837      */
838     public void registerExternalStorageListener() {
839         if (mUnmountReceiver == null) {
840             mUnmountReceiver = new BroadcastReceiver() {
841
842                 /**
843                  * {@inheritDoc}
844                  */
845                 @Override
846                 public void onReceive(final Context context, final Intent intent) {
847                     final String action = intent.getAction();
848                     if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
849                         saveQueue(true);
850                         mQueueIsSaveable = false;
851                         closeExternalStorageFiles(intent.getData().getPath());
852                     } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
853                         mMediaMountedCount++;
854                         mCardId = getCardId();
855                         reloadQueue();
856                         mQueueIsSaveable = true;
857                         notifyChange(QUEUE_CHANGED);
858                         notifyChange(META_CHANGED);
859                     }
860                 }
861             };
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);
867         }
868     }
869
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;
875     }
876
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;
882         }
883     }
884
885     /**
886      * Stops playback
887      *
888      * @param goToIdle True to go to the idle state, false otherwise
889      */
890     private void stop(final boolean goToIdle) {
891         if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
892         if (mPlayer.isInitialized()) {
893             mPlayer.stop();
894         }
895         mFileToPlay = null;
896         closeCursor();
897         if (goToIdle) {
898             scheduleDelayedShutdown();
899             mIsSupposedToBePlaying = false;
900         } else {
901             stopForeground(false);
902         }
903     }
904
905     /**
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.
909      *
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
913      */
914     private int removeTracksInternal(int first, int last) {
915         synchronized (this) {
916             if (last < first) {
917                 return 0;
918             } else if (first < 0) {
919                 first = 0;
920             } else if (last >= mPlayListLen) {
921                 last = mPlayListLen - 1;
922             }
923
924             boolean gotonext = false;
925             if (first <= mPlayPos && mPlayPos <= last) {
926                 mPlayPos = first;
927                 gotonext = true;
928             } else if (mPlayPos > last) {
929                 mPlayPos -= last - first + 1;
930             }
931             final int num = mPlayListLen - last - 1;
932             for (int i = 0; i < num; i++) {
933                 mPlayList[first + i] = mPlayList[last + 1 + i];
934             }
935             mPlayListLen -= last - first + 1;
936
937             if (gotonext) {
938                 if (mPlayListLen == 0) {
939                     stop(true);
940                     mPlayPos = -1;
941                     closeCursor();
942                 } else {
943                     if (mShuffleMode != SHUFFLE_NONE) {
944                         mPlayPos = getNextPosition(true);
945                     } else if (mPlayPos >= mPlayListLen) {
946                         mPlayPos = 0;
947                     }
948                     final boolean wasPlaying = isPlaying();
949                     stop(false);
950                     openCurrentAndNext();
951                     if (wasPlaying) {
952                         play();
953                     }
954                 }
955                 notifyChange(META_CHANGED);
956             }
957             return last - first + 1;
958         }
959     }
960
961     /**
962      * Adds a list to the playlist
963      *
964      * @param list The list to add
965      * @param position The position to place the tracks
966      */
967     private void addToPlayList(final long[] list, int position) {
968         final int addlen = list.length;
969         if (position < 0) {
970             mPlayListLen = 0;
971             position = 0;
972         }
973         ensurePlayListCapacity(mPlayListLen + addlen);
974         if (position > mPlayListLen) {
975             position = mPlayListLen;
976         }
977
978         final int tailsize = mPlayListLen - position;
979         for (int i = tailsize; i > 0; i--) {
980             mPlayList[position + i] = mPlayList[position + i - addlen];
981         }
982
983         for (int i = 0; i < addlen; i++) {
984             mPlayList[position + i] = list[i];
985         }
986         mPlayListLen += addlen;
987         if (mPlayListLen == 0) {
988             closeCursor();
989             notifyChange(META_CHANGED);
990         }
991     }
992
993     /**
994      * @param trackId The track ID
995      */
996     private void updateCursor(final long trackId) {
997         updateCursor("_id=" + trackId, null);
998     }
999
1000     private void updateCursor(final String selection, final String[] selectionArgs) {
1001         synchronized (this) {
1002             closeCursor();
1003             mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1004                     PROJECTION, selection, selectionArgs);
1005         }
1006         updateAlbumCursor();
1007     }
1008
1009     private void updateCursor(final Uri uri) {
1010         synchronized (this) {
1011             closeCursor();
1012             mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
1013         }
1014         updateAlbumCursor();
1015     }
1016
1017     private void updateAlbumCursor() {
1018         long albumId = getAlbumId();
1019         if (albumId >= 0) {
1020             mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
1021                     ALBUM_PROJECTION, "_id=" + albumId, null);
1022         } else {
1023             mAlbumCursor = null;
1024         }
1025     }
1026
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);
1031         if (c == null) {
1032             return null;
1033         }
1034         if (!c.moveToFirst()) {
1035             c.close();
1036             return null;
1037         }
1038         return c;
1039      }
1040
1041     private void closeCursor() {
1042         if (mCursor != null) {
1043             mCursor.close();
1044             mCursor = null;
1045         }
1046         if (mAlbumCursor != null) {
1047             mAlbumCursor.close();
1048             mAlbumCursor = null;
1049         }
1050     }
1051
1052     /**
1053      * Called to open a new file as the current track and prepare the next for
1054      * playback
1055      */
1056     private void openCurrentAndNext() {
1057         openCurrentAndMaybeNext(true);
1058     }
1059
1060     /**
1061      * Called to open a new file as the current track and prepare the next for
1062      * playback
1063      *
1064      * @param openNext True to prepare the next track for playback, false
1065      *            otherwise.
1066      */
1067     private void openCurrentAndMaybeNext(final boolean openNext) {
1068         synchronized (this) {
1069             closeCursor();
1070
1071             if (mPlayListLen == 0) {
1072                 return;
1073             }
1074             stop(false);
1075
1076             updateCursor(mPlayList[mPlayPos]);
1077             while (true) {
1078                 if (mCursor != null
1079                         && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
1080                                 + mCursor.getLong(IDCOLIDX))) {
1081                     break;
1082                 }
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
1086                 closeCursor();
1087                 if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) {
1088                     final int pos = getNextPosition(false);
1089                     if (pos < 0) {
1090                         scheduleDelayedShutdown();
1091                         if (mIsSupposedToBePlaying) {
1092                             mIsSupposedToBePlaying = false;
1093                             notifyChange(PLAYSTATE_CHANGED);
1094                         }
1095                         return;
1096                     }
1097                     mPlayPos = pos;
1098                     stop(false);
1099                     mPlayPos = pos;
1100                     updateCursor(mPlayList[mPlayPos]);
1101                 } else {
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);
1108                     }
1109                     return;
1110                 }
1111             }
1112             if (openNext) {
1113                 setNextTrack();
1114             }
1115         }
1116     }
1117
1118     /**
1119      * @param force True to force the player onto the track next, false
1120      *            otherwise.
1121      * @return The next position to play.
1122      */
1123     private int getNextPosition(final boolean force) {
1124         if (!force && mRepeatMode == REPEAT_CURRENT) {
1125             if (mPlayPos < 0) {
1126                 return 0;
1127             }
1128             return mPlayPos;
1129         } else if (mShuffleMode == SHUFFLE_NORMAL) {
1130             if (mPlayPos >= 0) {
1131                 mHistory.add(mPlayPos);
1132             }
1133             if (mHistory.size() > MAX_HISTORY_SIZE) {
1134                 mHistory.remove(0);
1135             }
1136             final int numTracks = mPlayListLen;
1137             final int[] tracks = new int[numTracks];
1138             for (int i = 0; i < numTracks; i++) {
1139                 tracks[i] = i;
1140             }
1141
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) {
1147                     numUnplayed--;
1148                     tracks[idx] = -1;
1149                 }
1150             }
1151             if (numUnplayed <= 0) {
1152                 if (mRepeatMode == REPEAT_ALL || force) {
1153                     numUnplayed = numTracks;
1154                     for (int i = 0; i < numTracks; i++) {
1155                         tracks[i] = i;
1156                     }
1157                 } else {
1158                     return -1;
1159                 }
1160             }
1161             int skip = 0;
1162             if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
1163                 skip = mShuffler.nextInt(numUnplayed);
1164             }
1165             int cnt = -1;
1166             while (true) {
1167                 while (tracks[++cnt] < 0) {
1168                     ;
1169                 }
1170                 skip--;
1171                 if (skip < 0) {
1172                     break;
1173                 }
1174             }
1175             return cnt;
1176         } else if (mShuffleMode == SHUFFLE_AUTO) {
1177             doAutoShuffleUpdate();
1178             return mPlayPos + 1;
1179         } else {
1180             if (mPlayPos >= mPlayListLen - 1) {
1181                 if (mRepeatMode == REPEAT_NONE && !force) {
1182                     return -1;
1183                 } else if (mRepeatMode == REPEAT_ALL || force) {
1184                     return 0;
1185                 }
1186                 return -1;
1187             } else {
1188                 return mPlayPos + 1;
1189             }
1190         }
1191     }
1192
1193     /**
1194      * Sets the track track to be played
1195      */
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);
1202         } else {
1203             mPlayer.setNextDataSource(null);
1204         }
1205     }
1206
1207     /**
1208      * Creates a shuffled playlist used for party mode
1209      */
1210     private boolean makeAutoShuffleList() {
1211         Cursor cursor = null;
1212         try {
1213             cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1214                     new String[] {
1215                         MediaStore.Audio.Media._ID
1216                     }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
1217             if (cursor == null || cursor.getCount() == 0) {
1218                 return false;
1219             }
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);
1225             }
1226             mAutoShuffleList = list;
1227             return true;
1228         } catch (final RuntimeException e) {
1229         } finally {
1230             if (cursor != null) {
1231                 cursor.close();
1232                 cursor = null;
1233             }
1234         }
1235         return false;
1236     }
1237
1238     /**
1239      * Creates the party shuffle playlist
1240      */
1241     private void doAutoShuffleUpdate() {
1242         boolean notify = false;
1243         if (mPlayPos > 10) {
1244             removeTracks(0, mPlayPos - 9);
1245             notify = true;
1246         }
1247         final int toAdd = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
1248         for (int i = 0; i < toAdd; i++) {
1249             int lookback = mHistory.size();
1250             int idx = -1;
1251             while (true) {
1252                 idx = mShuffler.nextInt(mAutoShuffleList.length);
1253                 if (!wasRecentlyUsed(idx, lookback)) {
1254                     break;
1255                 }
1256                 lookback /= 2;
1257             }
1258             mHistory.add(idx);
1259             if (mHistory.size() > MAX_HISTORY_SIZE) {
1260                 mHistory.remove(0);
1261             }
1262             ensurePlayListCapacity(mPlayListLen + 1);
1263             mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
1264             notify = true;
1265         }
1266         if (notify) {
1267             notifyChange(QUEUE_CHANGED);
1268         }
1269     }
1270
1271     /**/
1272     private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
1273         if (lookbacksize == 0) {
1274             return false;
1275         }
1276         final int histsize = mHistory.size();
1277         if (histsize < lookbacksize) {
1278             lookbacksize = histsize;
1279         }
1280         final int maxidx = histsize - 1;
1281         for (int i = 0; i < lookbacksize; i++) {
1282             final long entry = mHistory.get(maxidx - i);
1283             if (entry == idx) {
1284                 return true;
1285             }
1286         }
1287         return false;
1288     }
1289
1290     /**
1291      * Makes sure the playlist has enough space to hold all of the songs
1292      *
1293      * @param size The size of the playlist
1294      */
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
1299             // insert
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];
1304             }
1305             mPlayList = newlist;
1306         }
1307         // FIXME: shrink the array when the needed size is much smaller
1308         // than the allocated size
1309     }
1310
1311     /**
1312      * Notify the change-receivers that something has changed.
1313      */
1314     private void notifyChange(final String what) {
1315         if (D) Log.d(TAG, "notifyChange: what = " + what);
1316
1317         // Update the lockscreen controls
1318         updateRemoteControlClient(what);
1319
1320         if (what.equals(POSITION_CHANGED)) {
1321             return;
1322         }
1323
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);
1332
1333         final Intent musicIntent = new Intent(intent);
1334         musicIntent.setAction(what.replace(APOLLO_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
1335         sendStickyBroadcast(musicIntent);
1336
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(),
1341                         getArtistName());
1342             }
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)) {
1348             saveQueue(true);
1349             if (isPlaying()) {
1350                 setNextTrack();
1351             }
1352         } else {
1353             saveQueue(false);
1354         }
1355
1356         if (what.equals(PLAYSTATE_CHANGED)) {
1357             mNotificationHelper.updatePlayState(isPlaying());
1358         }
1359
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);
1365     }
1366
1367     /**
1368      * Updates the lockscreen controls.
1369      *
1370      * @param what The broadcast
1371      */
1372     private void updateRemoteControlClient(final String what) {
1373         int playState = mIsSupposedToBePlaying
1374                 ? RemoteControlClient.PLAYSTATE_PLAYING
1375                 : RemoteControlClient.PLAYSTATE_PAUSED;
1376
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;
1390                 }
1391                 albumArt = albumArt.copy(config, false);
1392             }
1393             mRemoteControlClient
1394                     .editMetadata(true)
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)
1402                     .apply();
1403
1404             if (ApolloUtils.hasJellyBeanMR2()) {
1405                 mRemoteControlClient.setPlaybackState(playState, position(), 1.0f);
1406             }
1407         }
1408     }
1409
1410     /**
1411      * Saves the queue
1412      *
1413      * @param full True if the queue is full
1414      */
1415     private void saveQueue(final boolean full) {
1416         if (!mQueueIsSaveable) {
1417             return;
1418         }
1419
1420         final SharedPreferences.Editor editor = mPreferences.edit();
1421         if (full) {
1422             final StringBuilder q = new StringBuilder();
1423             int len = mPlayListLen;
1424             for (int i = 0; i < len; i++) {
1425                 long n = mPlayList[i];
1426                 if (n < 0) {
1427                     continue;
1428                 } else if (n == 0) {
1429                     q.append("0;");
1430                 } else {
1431                     while (n != 0) {
1432                         final int digit = (int)(n & 0xf);
1433                         n >>>= 4;
1434                         q.append(HEX_DIGITS[digit]);
1435                     }
1436                     q.append(";");
1437                 }
1438             }
1439             editor.putString("queue", q.toString());
1440             editor.putInt("cardid", mCardId);
1441             if (mShuffleMode != SHUFFLE_NONE) {
1442                 len = mHistory.size();
1443                 q.setLength(0);
1444                 for (int i = 0; i < len; i++) {
1445                     int n = mHistory.get(i);
1446                     if (n == 0) {
1447                         q.append("0;");
1448                     } else {
1449                         while (n != 0) {
1450                             final int digit = n & 0xf;
1451                             n >>>= 4;
1452                             q.append(HEX_DIGITS[digit]);
1453                         }
1454                         q.append(";");
1455                     }
1456                 }
1457                 editor.putString("history", q.toString());
1458             }
1459         }
1460         editor.putInt("curpos", mPlayPos);
1461         if (mPlayer.isInitialized()) {
1462             editor.putLong("seekpos", mPlayer.position());
1463         }
1464         editor.putInt("repeatmode", mRepeatMode);
1465         editor.putInt("shufflemode", mShuffleMode);
1466         editor.apply();
1467     }
1468
1469     /**
1470      * Reloads the queue as the user left it the last time they stopped using
1471      * Apollo
1472      */
1473     private void reloadQueue() {
1474         String q = null;
1475         int id = mCardId;
1476         if (mPreferences.contains("cardid")) {
1477             id = mPreferences.getInt("cardid", ~mCardId);
1478         }
1479         if (id == mCardId) {
1480             q = mPreferences.getString("queue", "");
1481         }
1482         int qlen = q != null ? q.length() : 0;
1483         if (qlen > 1) {
1484             int plen = 0;
1485             int n = 0;
1486             int shift = 0;
1487             for (int i = 0; i < qlen; i++) {
1488                 final char c = q.charAt(i);
1489                 if (c == ';') {
1490                     ensurePlayListCapacity(plen + 1);
1491                     mPlayList[plen] = n;
1492                     plen++;
1493                     n = 0;
1494                     shift = 0;
1495                 } else {
1496                     if (c >= '0' && c <= '9') {
1497                         n += c - '0' << shift;
1498                     } else if (c >= 'a' && c <= 'f') {
1499                         n += 10 + c - 'a' << shift;
1500                     } else {
1501                         plen = 0;
1502                         break;
1503                     }
1504                     shift += 4;
1505                 }
1506             }
1507             mPlayListLen = plen;
1508             final int pos = mPreferences.getInt("curpos", 0);
1509             if (pos < 0 || pos >= mPlayListLen) {
1510                 mPlayListLen = 0;
1511                 return;
1512             }
1513             mPlayPos = pos;
1514             updateCursor(mPlayList[mPlayPos]);
1515             if (mCursor == null) {
1516                 SystemClock.sleep(3000);
1517                 updateCursor(mPlayList[mPlayPos]);
1518             }
1519             synchronized (this) {
1520                 closeCursor();
1521                 mOpenFailedCounter = 20;
1522                 openCurrentAndNext();
1523             }
1524             if (!mPlayer.isInitialized()) {
1525                 mPlayListLen = 0;
1526                 return;
1527             }
1528
1529             final long seekpos = mPreferences.getLong("seekpos", 0);
1530             seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
1531
1532             if (D) {
1533                 Log.d(TAG, "restored queue, currently at position "
1534                         + position() + "/" + duration()
1535                         + " (requested " + seekpos + ")");
1536             }
1537
1538             int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
1539             if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
1540                 repmode = REPEAT_NONE;
1541             }
1542             mRepeatMode = repmode;
1543
1544             int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
1545             if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
1546                 shufmode = SHUFFLE_NONE;
1547             }
1548             if (shufmode != SHUFFLE_NONE) {
1549                 q = mPreferences.getString("history", "");
1550                 qlen = q != null ? q.length() : 0;
1551                 if (qlen > 1) {
1552                     plen = 0;
1553                     n = 0;
1554                     shift = 0;
1555                     mHistory.clear();
1556                     for (int i = 0; i < qlen; i++) {
1557                         final char c = q.charAt(i);
1558                         if (c == ';') {
1559                             if (n >= mPlayListLen) {
1560                                 mHistory.clear();
1561                                 break;
1562                             }
1563                             mHistory.add(n);
1564                             n = 0;
1565                             shift = 0;
1566                         } else {
1567                             if (c >= '0' && c <= '9') {
1568                                 n += c - '0' << shift;
1569                             } else if (c >= 'a' && c <= 'f') {
1570                                 n += 10 + c - 'a' << shift;
1571                             } else {
1572                                 mHistory.clear();
1573                                 break;
1574                             }
1575                             shift += 4;
1576                         }
1577                     }
1578                 }
1579             }
1580             if (shufmode == SHUFFLE_AUTO) {
1581                 if (!makeAutoShuffleList()) {
1582                     shufmode = SHUFFLE_NONE;
1583                 }
1584             }
1585             mShuffleMode = shufmode;
1586         }
1587     }
1588
1589     /**
1590      * Opens a file and prepares it for playback
1591      *
1592      * @param path The path of the file to open
1593      */
1594     public boolean openFile(final String path) {
1595         if (D) Log.d(TAG, "openFile: path = " + path);
1596         synchronized (this) {
1597             if (path == null) {
1598                 return false;
1599             }
1600
1601             // If mCursor is null, try to associate path with a database cursor
1602             if (mCursor == null) {
1603                 Uri uri = Uri.parse(path);
1604                 long id = -1;
1605                 try  {
1606                     id = Long.valueOf(uri.getLastPathSegment());
1607                 } catch (NumberFormatException ex) {
1608                     // Ignore
1609                 }
1610
1611                 if (id != -1 && path.startsWith(MediaStore.Audio.Media.
1612                         EXTERNAL_CONTENT_URI.toString())) {
1613                     updateCursor(uri);
1614
1615                 } else if (id != -1 && path.startsWith(MediaStore.Files.getContentUri(
1616                         "external").toString())) {
1617                     updateCursor(id);
1618
1619                 } else {
1620                     String where = MediaStore.Audio.Media.DATA + "=?";
1621                     String[] selectionArgs = new String[] {path};
1622                     updateCursor(where, selectionArgs);
1623                 }
1624                 try {
1625                     if (mCursor != null) {
1626                         ensurePlayListCapacity(1);
1627                         mPlayListLen = 1;
1628                         mPlayList[0] = mCursor.getLong(IDCOLIDX);
1629                         mPlayPos = 0;
1630                     }
1631                 } catch (final UnsupportedOperationException ex) {
1632                 }
1633             }
1634             mFileToPlay = path;
1635             mPlayer.setDataSource(mFileToPlay);
1636             if (mPlayer.isInitialized()) {
1637                 mOpenFailedCounter = 0;
1638                 return true;
1639             }
1640             stop(true);
1641             return false;
1642         }
1643     }
1644
1645     /**
1646      * Returns the audio session ID
1647      *
1648      * @return The current media player audio session ID
1649      */
1650     public int getAudioSessionId() {
1651         synchronized (this) {
1652             return mPlayer.getAudioSessionId();
1653         }
1654     }
1655
1656     /**
1657      * Indicates if the media storeage device has been mounted or not
1658      *
1659      * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
1660      */
1661     public int getMediaMountedCount() {
1662         return mMediaMountedCount;
1663     }
1664
1665     /**
1666      * Returns the shuffle mode
1667      *
1668      * @return The current shuffle mode (all, party, none)
1669      */
1670     public int getShuffleMode() {
1671         return mShuffleMode;
1672     }
1673
1674     /**
1675      * Returns the repeat mode
1676      *
1677      * @return The current repeat mode (all, one, none)
1678      */
1679     public int getRepeatMode() {
1680         return mRepeatMode;
1681     }
1682
1683     /**
1684      * Removes all instances of the track with the given ID from the playlist.
1685      *
1686      * @param id The id to be removed
1687      * @return how many instances of the track were removed
1688      */
1689     public int removeTrack(final long id) {
1690         int numremoved = 0;
1691         synchronized (this) {
1692             for (int i = 0; i < mPlayListLen; i++) {
1693                 if (mPlayList[i] == id) {
1694                     numremoved += removeTracksInternal(i, i);
1695                     i--;
1696                 }
1697             }
1698         }
1699         if (numremoved > 0) {
1700             notifyChange(QUEUE_CHANGED);
1701         }
1702         return numremoved;
1703     }
1704
1705     /**
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.
1709      *
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
1713      */
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);
1718         }
1719         return numremoved;
1720     }
1721
1722     /**
1723      * Returns the position in the queue
1724      *
1725      * @return the current position in the queue
1726      */
1727     public int getQueuePosition() {
1728         synchronized (this) {
1729             return mPlayPos;
1730         }
1731     }
1732
1733     /**
1734      * Returns the path to current song
1735      *
1736      * @return The path to the current song
1737      */
1738     public String getPath() {
1739         synchronized (this) {
1740             if (mCursor == null) {
1741                 return null;
1742             }
1743             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
1744         }
1745     }
1746
1747     /**
1748      * Returns the album name
1749      *
1750      * @return The current song album Name
1751      */
1752     public String getAlbumName() {
1753         synchronized (this) {
1754             if (mCursor == null) {
1755                 return null;
1756             }
1757             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
1758         }
1759     }
1760
1761     /**
1762      * Returns the song name
1763      *
1764      * @return The current song name
1765      */
1766     public String getTrackName() {
1767         synchronized (this) {
1768             if (mCursor == null) {
1769                 return null;
1770             }
1771             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
1772         }
1773     }
1774
1775     /**
1776      * Returns the artist name
1777      *
1778      * @return The current song artist name
1779      */
1780     public String getArtistName() {
1781         synchronized (this) {
1782             if (mCursor == null) {
1783                 return null;
1784             }
1785             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
1786         }
1787     }
1788
1789     /**
1790      * Returns the artist name
1791      *
1792      * @return The current song artist name
1793      */
1794     public String getAlbumArtistName() {
1795         synchronized (this) {
1796             if (mAlbumCursor == null) {
1797                 return null;
1798             }
1799             return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(AlbumColumns.ARTIST));
1800         }
1801     }
1802
1803     /**
1804      * Returns the album ID
1805      *
1806      * @return The current song album ID
1807      */
1808     public long getAlbumId() {
1809         synchronized (this) {
1810             if (mCursor == null) {
1811                 return -1;
1812             }
1813             return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
1814         }
1815     }
1816
1817     /**
1818      * Returns the artist ID
1819      *
1820      * @return The current song artist ID
1821      */
1822     public long getArtistId() {
1823         synchronized (this) {
1824             if (mCursor == null) {
1825                 return -1;
1826             }
1827             return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
1828         }
1829     }
1830
1831     /**
1832      * Returns the current audio ID
1833      *
1834      * @return The current track ID
1835      */
1836     public long getAudioId() {
1837         synchronized (this) {
1838             if (mPlayPos >= 0 && mPlayer.isInitialized()) {
1839                 return mPlayList[mPlayPos];
1840             }
1841         }
1842         return -1;
1843     }
1844
1845     /**
1846      * Seeks the current track to a specific time
1847      *
1848      * @param position The time to seek to
1849      * @return The time to play the track at
1850      */
1851     public long seek(long position) {
1852         if (mPlayer.isInitialized()) {
1853             if (position < 0) {
1854                 position = 0;
1855             } else if (position > mPlayer.duration()) {
1856                 position = mPlayer.duration();
1857             }
1858             long result = mPlayer.seek(position);
1859             notifyChange(POSITION_CHANGED);
1860             return result;
1861         }
1862         return -1;
1863     }
1864
1865     /**
1866      * Returns the current position in time of the currenttrack
1867      *
1868      * @return The current playback position in miliseconds
1869      */
1870     public long position() {
1871         if (mPlayer.isInitialized()) {
1872             return mPlayer.position();
1873         }
1874         return -1;
1875     }
1876
1877     /**
1878      * Returns the full duration of the current track
1879      *
1880      * @return The duration of the current track in miliseconds
1881      */
1882     public long duration() {
1883         if (mPlayer.isInitialized()) {
1884             return mPlayer.duration();
1885         }
1886         return -1;
1887     }
1888
1889     /**
1890      * Returns the queue
1891      *
1892      * @return The queue as a long[]
1893      */
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];
1900             }
1901             return list;
1902         }
1903     }
1904
1905     /**
1906      * @return True if music is playing, false otherwise
1907      */
1908     public boolean isPlaying() {
1909         return mIsSupposedToBePlaying;
1910     }
1911
1912     /**
1913      * True if the current track is a "favorite", false otherwise
1914      */
1915     public boolean isFavorite() {
1916         if (mFavoritesCache != null) {
1917             synchronized (this) {
1918                 final Long id = mFavoritesCache.getSongId(getAudioId());
1919                 return id != null ? true : false;
1920             }
1921         }
1922         return false;
1923     }
1924
1925     /**
1926      * Opens a list for playback
1927      *
1928      * @param list The list of tracks to open
1929      * @param position The position to start playback at
1930      */
1931     public void open(final long[] list, final int position) {
1932         synchronized (this) {
1933             if (mShuffleMode == SHUFFLE_AUTO) {
1934                 mShuffleMode = SHUFFLE_NORMAL;
1935             }
1936             final long oldId = getAudioId();
1937             final int listlength = list.length;
1938             boolean newlist = true;
1939             if (mPlayListLen == listlength) {
1940                 newlist = false;
1941                 for (int i = 0; i < listlength; i++) {
1942                     if (list[i] != mPlayList[i]) {
1943                         newlist = true;
1944                         break;
1945                     }
1946                 }
1947             }
1948             if (newlist) {
1949                 addToPlayList(list, -1);
1950                 notifyChange(QUEUE_CHANGED);
1951             }
1952             if (position >= 0) {
1953                 mPlayPos = position;
1954             } else {
1955                 mPlayPos = mShuffler.nextInt(mPlayListLen);
1956             }
1957             mHistory.clear();
1958             openCurrentAndNext();
1959             if (oldId != getAudioId()) {
1960                 notifyChange(META_CHANGED);
1961             }
1962         }
1963     }
1964
1965     /**
1966      * Stops playback.
1967      */
1968     public void stop() {
1969         stop(true);
1970     }
1971
1972     /**
1973      * Resumes or starts playback.
1974      */
1975     public void play() {
1976         int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
1977                 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
1978
1979         if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
1980
1981         if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
1982             return;
1983         }
1984
1985         mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
1986                 MediaButtonIntentReceiver.class.getName()));
1987
1988         setNextTrack();
1989
1990         if (mPlayer.isInitialized()) {
1991             final long duration = mPlayer.duration();
1992             if (mRepeatMode != REPEAT_CURRENT && duration > 2000
1993                     && mPlayer.position() >= duration - 2000) {
1994                 gotoNext(true);
1995             }
1996
1997             mPlayer.start();
1998             mPlayerHandler.removeMessages(FADEDOWN);
1999             mPlayerHandler.sendEmptyMessage(FADEUP);
2000
2001             if (!mIsSupposedToBePlaying) {
2002                 mIsSupposedToBePlaying = true;
2003                 notifyChange(PLAYSTATE_CHANGED);
2004             }
2005
2006             cancelShutdown();
2007             updateNotification();
2008         } else if (mPlayListLen <= 0) {
2009             setShuffleMode(SHUFFLE_AUTO);
2010         }
2011     }
2012
2013     /**
2014      * Temporarily pauses playback.
2015      */
2016     public void pause() {
2017         if (D) Log.d(TAG, "Pausing playback");
2018         synchronized (this) {
2019             mPlayerHandler.removeMessages(FADEUP);
2020             if (mIsSupposedToBePlaying) {
2021                 mPlayer.pause();
2022                 scheduleDelayedShutdown();
2023                 mIsSupposedToBePlaying = false;
2024                 notifyChange(PLAYSTATE_CHANGED);
2025             }
2026         }
2027     }
2028
2029     /**
2030      * Changes from the current track to the next track
2031      */
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();
2038                 return;
2039             }
2040             final int pos = getNextPosition(force);
2041             if (pos < 0) {
2042                 scheduleDelayedShutdown();
2043                 if (mIsSupposedToBePlaying) {
2044                     mIsSupposedToBePlaying = false;
2045                     notifyChange(PLAYSTATE_CHANGED);
2046                 }
2047                 return;
2048             }
2049             mPlayPos = pos;
2050             stop(false);
2051             mPlayPos = pos;
2052             openCurrentAndNext();
2053             play();
2054             notifyChange(META_CHANGED);
2055         }
2056     }
2057
2058     /**
2059      * Changes from the current track to the previous played track
2060      */
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) {
2068                     return;
2069                 }
2070                 final Integer pos = mHistory.remove(histsize - 1);
2071                 mPlayPos = pos.intValue();
2072             } else {
2073                 if (mPlayPos > 0) {
2074                     mPlayPos--;
2075                 } else {
2076                     mPlayPos = mPlayListLen - 1;
2077                 }
2078             }
2079             stop(false);
2080             openCurrent();
2081             play();
2082             notifyChange(META_CHANGED);
2083         }
2084     }
2085
2086     /**
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.
2090      */
2091     private void openCurrent() {
2092         openCurrentAndMaybeNext(false);
2093     }
2094
2095     /**
2096      * Toggles the current song as a favorite.
2097      */
2098     public void toggleFavorite() {
2099         if (mFavoritesCache != null) {
2100             synchronized (this) {
2101                 mFavoritesCache.toggleSong(getAudioId(), getTrackName(), getAlbumName(),
2102                         getArtistName());
2103             }
2104         }
2105     }
2106
2107     /**
2108      * Moves an item in the queue from one position to another
2109      *
2110      * @param from The position the item is currently at
2111      * @param to The position the item is being moved to
2112      */
2113     public void moveQueueItem(int index1, int index2) {
2114         synchronized (this) {
2115             if (index1 >= mPlayListLen) {
2116                 index1 = mPlayListLen - 1;
2117             }
2118             if (index2 >= mPlayListLen) {
2119                 index2 = mPlayListLen - 1;
2120             }
2121             if (index1 < index2) {
2122                 final long tmp = mPlayList[index1];
2123                 for (int i = index1; i < index2; i++) {
2124                     mPlayList[i] = mPlayList[i + 1];
2125                 }
2126                 mPlayList[index2] = tmp;
2127                 if (mPlayPos == index1) {
2128                     mPlayPos = index2;
2129                 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
2130                     mPlayPos--;
2131                 }
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];
2136                 }
2137                 mPlayList[index2] = tmp;
2138                 if (mPlayPos == index1) {
2139                     mPlayPos = index2;
2140                 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
2141                     mPlayPos++;
2142                 }
2143             }
2144             notifyChange(QUEUE_CHANGED);
2145         }
2146     }
2147
2148     /**
2149      * Sets the repeat mode
2150      *
2151      * @param repeatmode The repeat mode to use
2152      */
2153     public void setRepeatMode(final int repeatmode) {
2154         synchronized (this) {
2155             mRepeatMode = repeatmode;
2156             setNextTrack();
2157             saveQueue(false);
2158             notifyChange(REPEATMODE_CHANGED);
2159         }
2160     }
2161
2162     /**
2163      * Sets the shuffle mode
2164      *
2165      * @param shufflemode The shuffle mode to use
2166      */
2167     public void setShuffleMode(final int shufflemode) {
2168         synchronized (this) {
2169             if (mShuffleMode == shufflemode && mPlayListLen > 0) {
2170                 return;
2171             }
2172             mShuffleMode = shufflemode;
2173             if (mShuffleMode == SHUFFLE_AUTO) {
2174                 if (makeAutoShuffleList()) {
2175                     mPlayListLen = 0;
2176                     doAutoShuffleUpdate();
2177                     mPlayPos = 0;
2178                     openCurrentAndNext();
2179                     play();
2180                     notifyChange(META_CHANGED);
2181                     return;
2182                 } else {
2183                     mShuffleMode = SHUFFLE_NONE;
2184                 }
2185             }
2186             saveQueue(false);
2187             notifyChange(SHUFFLEMODE_CHANGED);
2188         }
2189     }
2190
2191     /**
2192      * Sets the position of a track in the queue
2193      *
2194      * @param index The position to place the track
2195      */
2196     public void setQueuePosition(final int index) {
2197         synchronized (this) {
2198             stop(false);
2199             mPlayPos = index;
2200             openCurrentAndNext();
2201             play();
2202             notifyChange(META_CHANGED);
2203             if (mShuffleMode == SHUFFLE_AUTO) {
2204                 doAutoShuffleUpdate();
2205             }
2206         }
2207     }
2208
2209     /**
2210      * Queues a new list for playback
2211      *
2212      * @param list The list to queue
2213      * @param action The action to take
2214      */
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);
2220             } else {
2221                 addToPlayList(list, Integer.MAX_VALUE);
2222                 notifyChange(QUEUE_CHANGED);
2223                 if (action == NOW) {
2224                     mPlayPos = mPlayListLen - list.length;
2225                     openCurrentAndNext();
2226                     play();
2227                     notifyChange(META_CHANGED);
2228                     return;
2229                 }
2230             }
2231             if (mPlayPos < 0) {
2232                 mPlayPos = 0;
2233                 openCurrentAndNext();
2234                 play();
2235                 notifyChange(META_CHANGED);
2236             }
2237         }
2238     }
2239
2240     /**
2241      * Cycles through the different repeat modes
2242      */
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);
2250             }
2251         } else {
2252             setRepeatMode(REPEAT_NONE);
2253         }
2254     }
2255
2256     /**
2257      * Cycles through the different shuffle modes
2258      */
2259     private void cycleShuffle() {
2260         if (mShuffleMode == SHUFFLE_NONE) {
2261             setShuffleMode(SHUFFLE_NORMAL);
2262             if (mRepeatMode == REPEAT_CURRENT) {
2263                 setRepeatMode(REPEAT_ALL);
2264             }
2265         } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
2266             setShuffleMode(SHUFFLE_NONE);
2267         }
2268     }
2269
2270     /**
2271      * @return The album art for the current album.
2272      */
2273     public Bitmap getAlbumArt() {
2274         // Return the cached artwork
2275         final Bitmap bitmap = mImageFetcher.getArtwork(getAlbumName(),
2276                 getAlbumId(), getArtistName());
2277         return bitmap;
2278     }
2279
2280     /**
2281      * Called when one of the lists should refresh or requery.
2282      */
2283     public void refresh() {
2284         notifyChange(REFRESH);
2285     }
2286
2287     private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
2288         /**
2289          * {@inheritDoc}
2290          */
2291         @Override
2292         public void onReceive(final Context context, final Intent intent) {
2293             final String command = intent.getStringExtra(CMDNAME);
2294
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);
2308             } else {
2309                 handleCommandIntent(intent);
2310             }
2311         }
2312     };
2313
2314     private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
2315         /**
2316          * {@inheritDoc}
2317          */
2318         @Override
2319         public void onAudioFocusChange(final int focusChange) {
2320             mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
2321         }
2322     };
2323
2324     private static final class MusicPlayerHandler extends Handler {
2325         private final WeakReference<MusicPlaybackService> mService;
2326         private float mCurrentVolume = 1.0f;
2327
2328         /**
2329          * Constructor of <code>MusicPlayerHandler</code>
2330          *
2331          * @param service The service to use.
2332          * @param looper The thread to run on.
2333          */
2334         public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
2335             super(looper);
2336             mService = new WeakReference<MusicPlaybackService>(service);
2337         }
2338
2339         /**
2340          * {@inheritDoc}
2341          */
2342         @Override
2343         public void handleMessage(final Message msg) {
2344             final MusicPlaybackService service = mService.get();
2345             if (service == null) {
2346                 return;
2347             }
2348
2349             switch (msg.what) {
2350                 case FADEDOWN:
2351                     mCurrentVolume -= .05f;
2352                     if (mCurrentVolume > .2f) {
2353                         sendEmptyMessageDelayed(FADEDOWN, 10);
2354                     } else {
2355                         mCurrentVolume = .2f;
2356                     }
2357                     service.mPlayer.setVolume(mCurrentVolume);
2358                     break;
2359                 case FADEUP:
2360                     mCurrentVolume += .01f;
2361                     if (mCurrentVolume < 1.0f) {
2362                         sendEmptyMessageDelayed(FADEUP, 10);
2363                     } else {
2364                         mCurrentVolume = 1.0f;
2365                     }
2366                     service.mPlayer.setVolume(mCurrentVolume);
2367                     break;
2368                 case SERVER_DIED:
2369                     if (service.isPlaying()) {
2370                         service.gotoNext(true);
2371                     } else {
2372                         service.openCurrentAndNext();
2373                     }
2374                     break;
2375                 case TRACK_WENT_TO_NEXT:
2376                     service.mPlayPos = service.mNextPlayPos;
2377                     if (service.mCursor != null) {
2378                         service.mCursor.close();
2379                     }
2380                     service.updateCursor(service.mPlayList[service.mPlayPos]);
2381                     service.notifyChange(META_CHANGED);
2382                     service.updateNotification();
2383                     service.setNextTrack();
2384                     break;
2385                 case TRACK_ENDED:
2386                     if (service.mRepeatMode == REPEAT_CURRENT) {
2387                         service.seek(0);
2388                         service.play();
2389                     } else {
2390                         service.gotoNext(false);
2391                     }
2392                     break;
2393                 case RELEASE_WAKELOCK:
2394                     service.mWakeLock.release();
2395                     break;
2396                 case FOCUSCHANGE:
2397                     if (D) Log.d(TAG, "Received audio focus change event " + msg.arg1);
2398                     switch (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;
2404                             }
2405                             service.pause();
2406                             break;
2407                         case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
2408                             removeMessages(FADEUP);
2409                             sendEmptyMessage(FADEDOWN);
2410                             break;
2411                         case AudioManager.AUDIOFOCUS_GAIN:
2412                             if (!service.isPlaying()
2413                                     && service.mPausedByTransientLossOfFocus) {
2414                                 service.mPausedByTransientLossOfFocus = false;
2415                                 mCurrentVolume = 0f;
2416                                 service.mPlayer.setVolume(mCurrentVolume);
2417                                 service.play();
2418                             } else {
2419                                 removeMessages(FADEDOWN);
2420                                 sendEmptyMessage(FADEUP);
2421                             }
2422                             break;
2423                         default:
2424                     }
2425                     break;
2426                 default:
2427                     break;
2428             }
2429         }
2430     }
2431
2432     private static final class Shuffler {
2433
2434         private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<Integer>();
2435
2436         private final TreeSet<Integer> mPreviousNumbers = new TreeSet<Integer>();
2437
2438         private final Random mRandom = new Random();
2439
2440         private int mPrevious;
2441
2442         /**
2443          * Constructor of <code>Shuffler</code>
2444          */
2445         public Shuffler() {
2446             super();
2447         }
2448
2449         /**
2450          * @param interval The length the queue
2451          * @return The position of the next track to play
2452          */
2453         public int nextInt(final int interval) {
2454             int next;
2455             do {
2456                 next = mRandom.nextInt(interval);
2457             } while (next == mPrevious && interval > 1
2458                     && !mPreviousNumbers.contains(Integer.valueOf(next)));
2459             mPrevious = next;
2460             mHistoryOfNumbers.add(mPrevious);
2461             mPreviousNumbers.add(mPrevious);
2462             cleanUpHistory();
2463             return next;
2464         }
2465
2466         /**
2467          * Removes old tracks and cleans up the history preparing for new tracks
2468          * to be added to the mapping
2469          */
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());
2474                 }
2475             }
2476         }
2477     };
2478
2479     private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
2480             MediaPlayer.OnCompletionListener {
2481
2482         private final WeakReference<MusicPlaybackService> mService;
2483
2484         private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
2485
2486         private MediaPlayer mNextMediaPlayer;
2487
2488         private Handler mHandler;
2489
2490         private boolean mIsInitialized = false;
2491
2492         /**
2493          * Constructor of <code>MultiPlayer</code>
2494          */
2495         public MultiPlayer(final MusicPlaybackService service) {
2496             mService = new WeakReference<MusicPlaybackService>(service);
2497             mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
2498         }
2499
2500         /**
2501          * @param path The path of the file, or the http/rtsp URL of the stream
2502          *            you want to play
2503          */
2504         public void setDataSource(final String path) {
2505             mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
2506             if (mIsInitialized) {
2507                 setNextDataSource(null);
2508             }
2509         }
2510
2511         /**
2512          * @param player The {@link MediaPlayer} to use
2513          * @param path The path of the file, or the http/rtsp URL of the stream
2514          *            you want to play
2515          * @return True if the <code>player</code> has been prepared and is
2516          *         ready to play, false otherwise
2517          */
2518         private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
2519             try {
2520                 player.reset();
2521                 player.setOnPreparedListener(null);
2522                 if (path.startsWith("content://")) {
2523                     player.setDataSource(mService.get(), Uri.parse(path));
2524                 } else {
2525                     player.setDataSource(path);
2526                 }
2527                 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
2528                 player.prepare();
2529             } catch (final IOException todo) {
2530                 // TODO: notify the user why the file couldn't be opened
2531                 return false;
2532             } catch (final IllegalArgumentException todo) {
2533                 // TODO: notify the user why the file couldn't be opened
2534                 return false;
2535             }
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);
2542             return true;
2543         }
2544
2545         /**
2546          * Set the MediaPlayer to start when this MediaPlayer finishes playback.
2547          *
2548          * @param path The path of the file, or the http/rtsp URL of the stream
2549          *            you want to play
2550          */
2551         public void setNextDataSource(final String path) {
2552             try {
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!");
2558                 return;
2559             }
2560             if (mNextMediaPlayer != null) {
2561                 mNextMediaPlayer.release();
2562                 mNextMediaPlayer = null;
2563             }
2564             if (path == null) {
2565                 return;
2566             }
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);
2572             } else {
2573                 if (mNextMediaPlayer != null) {
2574                     mNextMediaPlayer.release();
2575                     mNextMediaPlayer = null;
2576                 }
2577             }
2578         }
2579
2580         /**
2581          * Sets the handler
2582          *
2583          * @param handler The handler to use
2584          */
2585         public void setHandler(final Handler handler) {
2586             mHandler = handler;
2587         }
2588
2589         /**
2590          * @return True if the player is ready to go, false otherwise
2591          */
2592         public boolean isInitialized() {
2593             return mIsInitialized;
2594         }
2595
2596         /**
2597          * Starts or resumes playback.
2598          */
2599         public void start() {
2600             mCurrentMediaPlayer.start();
2601         }
2602
2603         /**
2604          * Resets the MediaPlayer to its uninitialized state.
2605          */
2606         public void stop() {
2607             mCurrentMediaPlayer.reset();
2608             mIsInitialized = false;
2609         }
2610
2611         /**
2612          * Releases resources associated with this MediaPlayer object.
2613          */
2614         public void release() {
2615             stop();
2616             mCurrentMediaPlayer.release();
2617         }
2618
2619         /**
2620          * Pauses playback. Call start() to resume.
2621          */
2622         public void pause() {
2623             mCurrentMediaPlayer.pause();
2624         }
2625
2626         /**
2627          * Gets the duration of the file.
2628          *
2629          * @return The duration in milliseconds
2630          */
2631         public long duration() {
2632             return mCurrentMediaPlayer.getDuration();
2633         }
2634
2635         /**
2636          * Gets the current playback position.
2637          *
2638          * @return The current position in milliseconds
2639          */
2640         public long position() {
2641             return mCurrentMediaPlayer.getCurrentPosition();
2642         }
2643
2644         /**
2645          * Gets the current playback position.
2646          *
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
2649          */
2650         public long seek(final long whereto) {
2651             mCurrentMediaPlayer.seekTo((int)whereto);
2652             return whereto;
2653         }
2654
2655         /**
2656          * Sets the volume on this player.
2657          *
2658          * @param vol Left and right volume scalar
2659          */
2660         public void setVolume(final float vol) {
2661             mCurrentMediaPlayer.setVolume(vol, vol);
2662         }
2663
2664         /**
2665          * Sets the audio session ID.
2666          *
2667          * @param sessionId The audio session ID
2668          */
2669         public void setAudioSessionId(final int sessionId) {
2670             mCurrentMediaPlayer.setAudioSessionId(sessionId);
2671         }
2672
2673         /**
2674          * Returns the audio session ID.
2675          *
2676          * @return The current audio session ID.
2677          */
2678         public int getAudioSessionId() {
2679             return mCurrentMediaPlayer.getAudioSessionId();
2680         }
2681
2682         /**
2683          * {@inheritDoc}
2684          */
2685         @Override
2686         public boolean onError(final MediaPlayer mp, final int what, final int extra) {
2687             switch (what) {
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);
2694                     return true;
2695                 default:
2696                     break;
2697             }
2698             return false;
2699         }
2700
2701         /**
2702          * {@inheritDoc}
2703          */
2704         @Override
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);
2711             } else {
2712                 mService.get().mWakeLock.acquire(30000);
2713                 mHandler.sendEmptyMessage(TRACK_ENDED);
2714                 mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
2715             }
2716         }
2717     }
2718
2719     private static final class ServiceStub extends IApolloService.Stub {
2720
2721         private final WeakReference<MusicPlaybackService> mService;
2722
2723         private ServiceStub(final MusicPlaybackService service) {
2724             mService = new WeakReference<MusicPlaybackService>(service);
2725         }
2726
2727         /**
2728          * {@inheritDoc}
2729          */
2730         @Override
2731         public void openFile(final String path) throws RemoteException {
2732             mService.get().openFile(path);
2733         }
2734
2735         /**
2736          * {@inheritDoc}
2737          */
2738         @Override
2739         public void open(final long[] list, final int position) throws RemoteException {
2740             mService.get().open(list, position);
2741         }
2742
2743         /**
2744          * {@inheritDoc}
2745          */
2746         @Override
2747         public void stop() throws RemoteException {
2748             mService.get().stop();
2749         }
2750
2751         /**
2752          * {@inheritDoc}
2753          */
2754         @Override
2755         public void pause() throws RemoteException {
2756             mService.get().pause();
2757         }
2758
2759         /**
2760          * {@inheritDoc}
2761          */
2762         @Override
2763         public void play() throws RemoteException {
2764             mService.get().play();
2765         }
2766
2767         /**
2768          * {@inheritDoc}
2769          */
2770         @Override
2771         public void prev() throws RemoteException {
2772             mService.get().prev();
2773         }
2774
2775         /**
2776          * {@inheritDoc}
2777          */
2778         @Override
2779         public void next() throws RemoteException {
2780             mService.get().gotoNext(true);
2781         }
2782
2783         /**
2784          * {@inheritDoc}
2785          */
2786         @Override
2787         public void enqueue(final long[] list, final int action) throws RemoteException {
2788             mService.get().enqueue(list, action);
2789         }
2790
2791         /**
2792          * {@inheritDoc}
2793          */
2794         @Override
2795         public void setQueuePosition(final int index) throws RemoteException {
2796             mService.get().setQueuePosition(index);
2797         }
2798
2799         /**
2800          * {@inheritDoc}
2801          */
2802         @Override
2803         public void setShuffleMode(final int shufflemode) throws RemoteException {
2804             mService.get().setShuffleMode(shufflemode);
2805         }
2806
2807         /**
2808          * {@inheritDoc}
2809          */
2810         @Override
2811         public void setRepeatMode(final int repeatmode) throws RemoteException {
2812             mService.get().setRepeatMode(repeatmode);
2813         }
2814
2815         /**
2816          * {@inheritDoc}
2817          */
2818         @Override
2819         public void moveQueueItem(final int from, final int to) throws RemoteException {
2820             mService.get().moveQueueItem(from, to);
2821         }
2822
2823         /**
2824          * {@inheritDoc}
2825          */
2826         @Override
2827         public void toggleFavorite() throws RemoteException {
2828             mService.get().toggleFavorite();
2829         }
2830
2831         /**
2832          * {@inheritDoc}
2833          */
2834         @Override
2835         public void refresh() throws RemoteException {
2836             mService.get().refresh();
2837         }
2838
2839         /**
2840          * {@inheritDoc}
2841          */
2842         @Override
2843         public boolean isFavorite() throws RemoteException {
2844             return mService.get().isFavorite();
2845         }
2846
2847         /**
2848          * {@inheritDoc}
2849          */
2850         @Override
2851         public boolean isPlaying() throws RemoteException {
2852             return mService.get().isPlaying();
2853         }
2854
2855         /**
2856          * {@inheritDoc}
2857          */
2858         @Override
2859         public long[] getQueue() throws RemoteException {
2860             return mService.get().getQueue();
2861         }
2862
2863         /**
2864          * {@inheritDoc}
2865          */
2866         @Override
2867         public long duration() throws RemoteException {
2868             return mService.get().duration();
2869         }
2870
2871         /**
2872          * {@inheritDoc}
2873          */
2874         @Override
2875         public long position() throws RemoteException {
2876             return mService.get().position();
2877         }
2878
2879         /**
2880          * {@inheritDoc}
2881          */
2882         @Override
2883         public long seek(final long position) throws RemoteException {
2884             return mService.get().seek(position);
2885         }
2886
2887         /**
2888          * {@inheritDoc}
2889          */
2890         @Override
2891         public long getAudioId() throws RemoteException {
2892             return mService.get().getAudioId();
2893         }
2894
2895         /**
2896          * {@inheritDoc}
2897          */
2898         @Override
2899         public long getArtistId() throws RemoteException {
2900             return mService.get().getArtistId();
2901         }
2902
2903         /**
2904          * {@inheritDoc}
2905          */
2906         @Override
2907         public long getAlbumId() throws RemoteException {
2908             return mService.get().getAlbumId();
2909         }
2910
2911         /**
2912          * {@inheritDoc}
2913          */
2914         @Override
2915         public String getArtistName() throws RemoteException {
2916             return mService.get().getArtistName();
2917         }
2918
2919         /**
2920          * {@inheritDoc}
2921          */
2922         @Override
2923         public String getTrackName() throws RemoteException {
2924             return mService.get().getTrackName();
2925         }
2926
2927         /**
2928          * {@inheritDoc}
2929          */
2930         @Override
2931         public String getAlbumName() throws RemoteException {
2932             return mService.get().getAlbumName();
2933         }
2934
2935         /**
2936          * {@inheritDoc}
2937          */
2938         @Override
2939         public String getPath() throws RemoteException {
2940             return mService.get().getPath();
2941         }
2942
2943         /**
2944          * {@inheritDoc}
2945          */
2946         @Override
2947         public int getQueuePosition() throws RemoteException {
2948             return mService.get().getQueuePosition();
2949         }
2950
2951         /**
2952          * {@inheritDoc}
2953          */
2954         @Override
2955         public int getShuffleMode() throws RemoteException {
2956             return mService.get().getShuffleMode();
2957         }
2958
2959         /**
2960          * {@inheritDoc}
2961          */
2962         @Override
2963         public int getRepeatMode() throws RemoteException {
2964             return mService.get().getRepeatMode();
2965         }
2966
2967         /**
2968          * {@inheritDoc}
2969          */
2970         @Override
2971         public int removeTracks(final int first, final int last) throws RemoteException {
2972             return mService.get().removeTracks(first, last);
2973         }
2974
2975         /**
2976          * {@inheritDoc}
2977          */
2978         @Override
2979         public int removeTrack(final long id) throws RemoteException {
2980             return mService.get().removeTrack(id);
2981         }
2982
2983         /**
2984          * {@inheritDoc}
2985          */
2986         @Override
2987         public int getMediaMountedCount() throws RemoteException {
2988             return mService.get().getMediaMountedCount();
2989         }
2990
2991         /**
2992          * {@inheritDoc}
2993          */
2994         @Override
2995         public int getAudioSessionId() throws RemoteException {
2996             return mService.get().getAudioSessionId();
2997         }
2998
2999     }
3000
3001 }