OSDN Git Service

17252347dbeba69303c594a5c976fe4f9c7a2260
[android-x86/packages-apps-Eleven.git] / src / org / lineageos / eleven / MusicPlaybackService.java
1 /*
2  * Copyright (C) 2012 Andrew Neal
3  * Copyright (C) 2014-2016 The CyanogenMod Project
4  * Licensed under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with the
6  * License. You may obtain a copy of the License at
7  * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
8  * or agreed to in writing, software distributed under the License is
9  * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
10  * KIND, either express or implied. See the License for the specific language
11  * governing permissions and limitations under the License.
12  */
13
14 package org.lineageos.eleven;
15
16 import android.Manifest.permission;
17 import android.annotation.SuppressLint;
18 import android.app.AlarmManager;
19 import android.app.Notification;
20 import android.app.NotificationChannel;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.app.Service;
24 import android.appwidget.AppWidgetManager;
25 import android.content.BroadcastReceiver;
26 import android.content.ComponentName;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.SharedPreferences;
32 import android.content.pm.PackageManager;
33 import android.database.ContentObserver;
34 import android.database.Cursor;
35 import android.database.MatrixCursor;
36 import android.graphics.Bitmap;
37 import android.hardware.SensorManager;
38 import android.media.AudioManager;
39 import android.media.AudioManager.OnAudioFocusChangeListener;
40 import android.media.MediaDescription;
41 import android.media.MediaMetadata;
42 import android.media.MediaPlayer;
43 import android.media.audiofx.AudioEffect;
44 import android.media.session.MediaSession;
45 import android.media.session.PlaybackState;
46 import android.net.Uri;
47 import android.os.AsyncTask;
48 import android.os.Handler;
49 import android.os.HandlerThread;
50 import android.os.IBinder;
51 import android.os.Looper;
52 import android.os.Message;
53 import android.os.PowerManager;
54 import android.os.RemoteException;
55 import android.os.SystemClock;
56 import android.provider.MediaStore;
57 import android.provider.MediaStore.Audio.AlbumColumns;
58 import android.provider.MediaStore.Audio.AudioColumns;
59 import android.support.annotation.NonNull;
60 import android.support.v4.os.BuildCompat;
61 import android.text.TextUtils;
62 import android.util.Log;
63 import android.util.LongSparseArray;
64 import android.view.KeyEvent;
65
66 import org.lineageos.eleven.Config.IdType;
67 import org.lineageos.eleven.appwidgets.AppWidgetLarge;
68 import org.lineageos.eleven.appwidgets.AppWidgetLargeAlternate;
69 import org.lineageos.eleven.appwidgets.AppWidgetSmall;
70 import org.lineageos.eleven.cache.ImageCache;
71 import org.lineageos.eleven.cache.ImageFetcher;
72 import org.lineageos.eleven.provider.MusicPlaybackState;
73 import org.lineageos.eleven.provider.RecentStore;
74 import org.lineageos.eleven.provider.SongPlayCount;
75 import org.lineageos.eleven.service.MusicPlaybackTrack;
76 import org.lineageos.eleven.utils.BitmapWithColors;
77 import org.lineageos.eleven.utils.Lists;
78 import org.lineageos.eleven.utils.PreferenceUtils;
79 import org.lineageos.eleven.utils.ShakeDetector;
80 import org.lineageos.eleven.utils.SrtManager;
81
82 import java.io.File;
83 import java.io.IOException;
84 import java.lang.ref.WeakReference;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.LinkedList;
88 import java.util.List;
89 import java.util.ListIterator;
90 import java.util.Random;
91 import java.util.TreeSet;
92
93 /**
94  * A backbround {@link Service} used to keep music playing between activities
95  * and when the user moves Eleven into the background.
96  */
97 @SuppressLint("NewApi")
98 public class MusicPlaybackService extends Service {
99     private static final String TAG = "MusicPlaybackService";
100     private static final boolean D = false;
101
102     /**
103      * Indicates that the music has paused or resumed
104      */
105     public static final String PLAYSTATE_CHANGED = BuildConstants.PACKAGE_NAME + ".playstatechanged";
106
107     /**
108      * Indicates that music playback position within
109      * a title was changed
110      */
111     public static final String POSITION_CHANGED = BuildConstants.PACKAGE_NAME + ".positionchanged";
112
113     /**
114      * Indicates the meta data has changed in some way, like a track change
115      */
116     public static final String META_CHANGED = BuildConstants.PACKAGE_NAME + ".metachanged";
117
118     /**
119      * Indicates the queue has been updated
120      */
121     public static final String QUEUE_CHANGED = BuildConstants.PACKAGE_NAME + ".queuechanged";
122
123     /**
124      * Indicates the queue has been updated
125      */
126     public static final String PLAYLIST_CHANGED = BuildConstants.PACKAGE_NAME + ".playlistchanged";
127
128     /**
129      * Indicates the repeat mode changed
130      */
131     public static final String REPEATMODE_CHANGED = BuildConstants.PACKAGE_NAME + ".repeatmodechanged";
132
133     /**
134      * Indicates the shuffle mode changed
135      */
136     public static final String SHUFFLEMODE_CHANGED = BuildConstants.PACKAGE_NAME + ".shufflemodechanged";
137
138     /**
139      * Indicates the track fails to play
140      */
141     public static final String TRACK_ERROR = BuildConstants.PACKAGE_NAME + ".trackerror";
142
143     /**
144      * For backwards compatibility reasons, also provide sticky
145      * broadcasts under the music package
146      */
147     public static final String ELEVEN_PACKAGE_NAME = BuildConstants.PACKAGE_NAME;
148     public static final String MUSIC_PACKAGE_NAME = "com.android.music";
149
150     /**
151      * Called to indicate a general service commmand. Used in
152      * {@link MediaButtonIntentReceiver}
153      */
154     public static final String SERVICECMD = BuildConstants.PACKAGE_NAME + ".musicservicecommand";
155
156     /**
157      * Called to go toggle between pausing and playing the music
158      */
159     public static final String TOGGLEPAUSE_ACTION = BuildConstants.PACKAGE_NAME + ".togglepause";
160
161     /**
162      * Called to go to pause the playback
163      */
164     public static final String PAUSE_ACTION = BuildConstants.PACKAGE_NAME + ".pause";
165
166     /**
167      * Called to go to stop the playback
168      */
169     public static final String STOP_ACTION = BuildConstants.PACKAGE_NAME + ".stop";
170
171     /**
172      * Called to go to the previous track or the beginning of the track if partway through the track
173      */
174     public static final String PREVIOUS_ACTION = BuildConstants.PACKAGE_NAME + ".previous";
175
176     /**
177      * Called to go to the previous track regardless of how far in the current track the playback is
178      */
179     public static final String PREVIOUS_FORCE_ACTION = BuildConstants.PACKAGE_NAME + ".previous.force";
180
181     /**
182      * Called to go to the next track
183      */
184     public static final String NEXT_ACTION = BuildConstants.PACKAGE_NAME + ".next";
185
186     /**
187      * Called to change the repeat mode
188      */
189     public static final String REPEAT_ACTION = BuildConstants.PACKAGE_NAME + ".repeat";
190
191     /**
192      * Called to change the shuffle mode
193      */
194     public static final String SHUFFLE_ACTION = BuildConstants.PACKAGE_NAME + ".shuffle";
195
196     public static final String FROM_MEDIA_BUTTON = "frommediabutton";
197
198     public static final String TIMESTAMP = "timestamp";
199
200     /**
201      * Used to easily notify a list that it should refresh. i.e. A playlist
202      * changes
203      */
204     public static final String REFRESH = BuildConstants.PACKAGE_NAME + ".refresh";
205
206     /**
207      * Used by the alarm intent to shutdown the service after being idle
208      */
209     private static final String SHUTDOWN = BuildConstants.PACKAGE_NAME + ".shutdown";
210
211     /**
212      * Called to notify of a timed text
213      */
214     public static final String NEW_LYRICS = BuildConstants.PACKAGE_NAME + ".lyrics";
215
216     /**
217      * Called to update the remote control client
218      */
219     public static final String UPDATE_LOCKSCREEN = BuildConstants.PACKAGE_NAME + ".updatelockscreen";
220
221     public static final String CMDNAME = "command";
222
223     public static final String CMDTOGGLEPAUSE = "togglepause";
224
225     public static final String CMDSTOP = "stop";
226
227     public static final String CMDPAUSE = "pause";
228
229     public static final String CMDPLAY = "play";
230
231     public static final String CMDPREVIOUS = "previous";
232
233     public static final String CMDNEXT = "next";
234
235     public static final String CMDHEADSETHOOK = "headsethook";
236
237     private static final int IDCOLIDX = 0;
238
239     /**
240      * Moves a list to the next position in the queue
241      */
242     public static final int NEXT = 2;
243
244     /**
245      * Moves a list to the last position in the queue
246      */
247     public static final int LAST = 3;
248
249     /**
250      * Shuffles no songs, turns shuffling off
251      */
252     public static final int SHUFFLE_NONE = 0;
253
254     /**
255      * Shuffles all songs
256      */
257     public static final int SHUFFLE_NORMAL = 1;
258
259     /**
260      * Party shuffle
261      */
262     public static final int SHUFFLE_AUTO = 2;
263
264     /**
265      * Turns repeat off
266      */
267     public static final int REPEAT_NONE = 0;
268
269     /**
270      * Repeats the current track in a list
271      */
272     public static final int REPEAT_CURRENT = 1;
273
274     /**
275      * Repeats all the tracks in a list
276      */
277     public static final int REPEAT_ALL = 2;
278
279     /**
280      * Indicates when the track ends
281      */
282     private static final int TRACK_ENDED = 1;
283
284     /**
285      * Indicates that the current track was changed the next track
286      */
287     private static final int TRACK_WENT_TO_NEXT = 2;
288
289     /**
290      * Indicates the player died
291      */
292     private static final int SERVER_DIED = 3;
293
294     /**
295      * Indicates some sort of focus change, maybe a phone call
296      */
297     private static final int FOCUSCHANGE = 4;
298
299     /**
300      * Indicates to fade the volume down
301      */
302     private static final int FADEDOWN = 5;
303
304     /**
305      * Indicates to fade the volume back up
306      */
307     private static final int FADEUP = 6;
308
309     /**
310      * Notifies that there is a new timed text string
311      */
312     private static final int LYRICS = 7;
313
314     /**
315      * Indicates a headset hook key event
316      */
317     private static final int HEADSET_HOOK_EVENT = 8;
318
319     /**
320      * Indicates waiting for another headset hook event has timed out
321      */
322     private static final int HEADSET_HOOK_MULTI_CLICK_TIMEOUT = 9;
323
324     /**
325      * Idle time before stopping the foreground notfication (5 minutes)
326      */
327     private static final int IDLE_DELAY = 5 * 60 * 1000;
328
329     /**
330      * Song play time used as threshold for rewinding to the beginning of the
331      * track instead of skipping to the previous track when getting the PREVIOUS
332      * command
333      */
334     private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
335
336     /**
337      * The max size allowed for the track history
338      * TODO: Comeback and rewrite/fix all the whole queue code bugs after demo
339      */
340     public static final int MAX_HISTORY_SIZE = 1000;
341
342     private static final String ACTION_AUDIO_PLAYER = BuildConstants.PACKAGE_NAME + ".AUDIO_PLAYER";
343
344     private static final String CHANNEL_NAME = "eleven_playback";
345
346     public interface TrackErrorExtra {
347         /**
348          * Name of the track that was unable to play
349          */
350         public static final String TRACK_NAME = "trackname";
351     }
352
353     /**
354      * The columns used to retrieve any info from the current track
355      */
356     private static final String[] PROJECTION = new String[] {
357             "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
358             MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
359             MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
360             MediaStore.Audio.Media.ARTIST_ID
361     };
362
363     /**
364      * The columns used to retrieve any info from the current album
365      */
366     private static final String[] ALBUM_PROJECTION = new String[] {
367             MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
368             MediaStore.Audio.Albums.LAST_YEAR
369     };
370
371     /**
372      * Keeps a mapping of the track history
373      */
374     private static LinkedList<Integer> mHistory = Lists.newLinkedList();
375
376     /**
377      * Used to shuffle the tracks
378      */
379     private static final Shuffler mShuffler = new Shuffler();
380
381     /**
382      * Service stub
383      */
384     private final IBinder mBinder = new ServiceStub(this);
385
386     /**
387      * 4x1 widget
388      */
389     private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
390
391     /**
392      * 4x2 widget
393      */
394     private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
395
396     /**
397      * 4x2 alternate widget
398      */
399     private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
400             .getInstance();
401
402     /**
403      * The media player
404      */
405     private MultiPlayer mPlayer;
406
407     /**
408      * The path of the current file to play
409      */
410     private String mFileToPlay;
411
412     /**
413      * Alarm intent for removing the notification when nothing is playing
414      * for some time
415      */
416     private AlarmManager mAlarmManager;
417     private PendingIntent mShutdownIntent;
418     private boolean mShutdownScheduled;
419
420     private NotificationManager mNotificationManager;
421
422     /**
423      * The cursor used to retrieve info on the current track and run the
424      * necessary queries to play audio files
425      */
426     private Cursor mCursor;
427
428     /**
429      * The cursor used to retrieve info on the album the current track is
430      * part of, if any.
431      */
432     private Cursor mAlbumCursor;
433
434     /**
435      * Monitors the audio state
436      */
437     private AudioManager mAudioManager;
438
439     /**
440      * Settings used to save and retrieve the queue and history
441      */
442     private SharedPreferences mPreferences;
443
444     /**
445      * Used to know when the service is active
446      */
447     private boolean mServiceInUse = false;
448
449     /**
450      * Used to know if something should be playing or not
451      */
452     private boolean mIsSupposedToBePlaying = false;
453
454     /**
455      * Gets the last played time to determine whether we still want notifications or not
456      */
457     private long mLastPlayedTime;
458
459     private int mNotifyMode = NOTIFY_MODE_NONE;
460     private long mNotificationPostTime = 0;
461
462     private static final int NOTIFY_MODE_NONE = 0;
463     private static final int NOTIFY_MODE_FOREGROUND = 1;
464     private static final int NOTIFY_MODE_BACKGROUND = 2;
465
466     /**
467      * Used to indicate if the queue can be saved
468      */
469     private boolean mQueueIsSaveable = true;
470
471     /**
472      * Used to track what type of audio focus loss caused the playback to pause
473      */
474     private boolean mPausedByTransientLossOfFocus = false;
475
476     /**
477      * Lock screen controls
478      */
479     private MediaSession mSession;
480
481     // We use this to distinguish between different cards when saving/restoring
482     // playlists
483     private int mCardId;
484
485     private int mPlayPos = -1;
486
487     private int mNextPlayPos = -1;
488
489     private int mOpenFailedCounter = 0;
490
491     private int mMediaMountedCount = 0;
492
493     private int mShuffleMode = SHUFFLE_NONE;
494
495     private int mRepeatMode = REPEAT_NONE;
496
497     private int mServiceStartId = -1;
498
499     private String mLyrics;
500
501     private ArrayList<MusicPlaybackTrack> mPlaylist = new ArrayList<>(100);
502
503     private long[] mAutoShuffleList = null;
504
505     private MusicPlayerHandler mPlayerHandler;
506     private HandlerThread mHandlerThread;
507
508     private BroadcastReceiver mUnmountReceiver = null;
509
510     // to improve perf, instead of hitting the disk cache or file cache, store the bitmaps in memory
511     private String mCachedKey;
512     private BitmapWithColors[] mCachedBitmapWithColors = new BitmapWithColors[2];
513
514     private QueueUpdateTask mQueueUpdateTask;
515
516     /**
517      * Image cache
518      */
519     private ImageFetcher mImageFetcher;
520
521     /**
522      * Recently listened database
523      */
524     private RecentStore mRecentsCache;
525
526     /**
527      * The song play count database
528      */
529     private SongPlayCount mSongPlayCountCache;
530
531     /**
532      * Stores the playback state
533      */
534     private MusicPlaybackState mPlaybackStateStore;
535
536     /**
537      * Shake detector class used for shake to switch song feature
538      */
539     private ShakeDetector mShakeDetector;
540
541     /**
542      * Switch for displaying album art on lockscreen
543      */
544     private boolean mShowAlbumArtOnLockscreen;
545
546     private boolean mReadGranted = false;
547
548     private PowerManager.WakeLock mHeadsetHookWakeLock;
549
550     private ShakeDetector.Listener mShakeDetectorListener=new ShakeDetector.Listener() {
551
552         @Override
553         public void hearShake() {
554             /*
555              * on shake detect, play next song
556              */
557             if (D) {
558                 Log.d(TAG,"Shake detected!!!");
559             }
560             gotoNext(true);
561         }
562     };
563
564     /**
565      * {@inheritDoc}
566      */
567     @Override
568     public IBinder onBind(final Intent intent) {
569         if (D) Log.d(TAG, "Service bound, intent = " + intent);
570         cancelShutdown();
571         mServiceInUse = true;
572         return mBinder;
573     }
574
575     /**
576      * {@inheritDoc}
577      */
578     @Override
579     public boolean onUnbind(final Intent intent) {
580         if (D) Log.d(TAG, "Service unbound");
581         mServiceInUse = false;
582         saveQueue(true);
583
584         if (mReadGranted) {
585             if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
586                 // Something is currently playing, or will be playing once
587                 // an in-progress action requesting audio focus ends, so don't stop
588                 // the service now.
589                 return true;
590
591                 // If there is a playlist but playback is paused, then wait a while
592                 // before stopping the service, so that pause/resume isn't slow.
593                 // Also delay stopping the service if we're transitioning between
594                 // tracks.
595             } else if (mPlaylist.size() > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
596                 scheduleDelayedShutdown();
597                 return true;
598             }
599         }
600         stopSelf(mServiceStartId);
601
602         return true;
603     }
604
605     /**
606      * {@inheritDoc}
607      */
608     @Override
609     public void onRebind(final Intent intent) {
610         cancelShutdown();
611         mServiceInUse = true;
612     }
613
614     /**
615      * {@inheritDoc}
616      */
617     @Override
618     public void onCreate() {
619         if (D) Log.d(TAG, "Creating service");
620         super.onCreate();
621
622         if (checkSelfPermission(permission.READ_EXTERNAL_STORAGE) !=
623                 PackageManager.PERMISSION_GRANTED) {
624             stopSelf();
625             return;
626         } else {
627             mReadGranted = true;
628         }
629
630         mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
631
632         // Initialize the favorites and recents databases
633         mRecentsCache = RecentStore.getInstance(this);
634
635         // gets the song play count cache
636         mSongPlayCountCache = SongPlayCount.getInstance(this);
637
638         // gets a pointer to the playback state store
639         mPlaybackStateStore = MusicPlaybackState.getInstance(this);
640
641         // Initialize the image fetcher
642         mImageFetcher = ImageFetcher.getInstance(this);
643         // Initialize the image cache
644         mImageFetcher.setImageCache(ImageCache.getInstance(this));
645
646         // Start up the thread running the service. Note that we create a
647         // separate thread because the service normally runs in the process's
648         // main thread, which we don't want to block. We also make it
649         // background priority so CPU-intensive work will not disrupt the UI.
650         mHandlerThread = new HandlerThread("MusicPlayerHandler",
651                 android.os.Process.THREAD_PRIORITY_BACKGROUND);
652         mHandlerThread.start();
653
654         // Initialize the handler
655         mPlayerHandler = new MusicPlayerHandler(this, mHandlerThread.getLooper());
656
657         // Initialize the audio manager and register any headset controls for
658         // playback
659         mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
660
661         // Use the remote control APIs to set the playback state
662         setUpMediaSession();
663
664         // Initialize the preferences
665         mPreferences = getSharedPreferences("Service", 0);
666         mCardId = getCardId();
667
668         mShowAlbumArtOnLockscreen = mPreferences.getBoolean(
669                 PreferenceUtils.SHOW_ALBUM_ART_ON_LOCKSCREEN, true);
670         setShakeToPlayEnabled(mPreferences.getBoolean(PreferenceUtils.SHAKE_TO_PLAY, false));
671
672         mRepeatMode = mPreferences.getInt("repeatmode", REPEAT_NONE);
673         mShuffleMode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
674
675         registerExternalStorageListener();
676
677         // Initialize the media player
678         mPlayer = new MultiPlayer(this);
679         mPlayer.setHandler(mPlayerHandler);
680
681         // Initialize the intent filter and each action
682         final IntentFilter filter = new IntentFilter();
683         filter.addAction(SERVICECMD);
684         filter.addAction(TOGGLEPAUSE_ACTION);
685         filter.addAction(PAUSE_ACTION);
686         filter.addAction(STOP_ACTION);
687         filter.addAction(NEXT_ACTION);
688         filter.addAction(PREVIOUS_ACTION);
689         filter.addAction(PREVIOUS_FORCE_ACTION);
690         filter.addAction(REPEAT_ACTION);
691         filter.addAction(SHUFFLE_ACTION);
692         // Attach the broadcast listener
693         registerReceiver(mIntentReceiver, filter);
694
695         // Get events when MediaStore content changes
696         mMediaStoreObserver = new MediaStoreObserver(mPlayerHandler);
697         getContentResolver().registerContentObserver(
698                 MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mMediaStoreObserver);
699         getContentResolver().registerContentObserver(
700                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mMediaStoreObserver);
701
702         // Initialize the delayed shutdown intent
703         final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
704         shutdownIntent.setAction(SHUTDOWN);
705
706         mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
707         mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
708
709         // Listen for the idle state
710         scheduleDelayedShutdown();
711
712         // Bring the queue back
713         reloadQueue();
714         notifyChange(QUEUE_CHANGED);
715         notifyChange(META_CHANGED);
716     }
717
718     private void setUpMediaSession() {
719         mSession = new MediaSession(this, "Eleven");
720         mSession.setCallback(new MediaSession.Callback() {
721             @Override
722             public void onPause() {
723                 pause();
724                 mPausedByTransientLossOfFocus = false;
725             }
726             @Override
727             public void onPlay() {
728                 play();
729             }
730             @Override
731             public void onSeekTo(long pos) {
732                 seek(pos);
733             }
734             @Override
735             public void onSkipToNext() {
736                 gotoNext(true);
737             }
738             @Override
739             public void onSkipToPrevious() {
740                 prev(false);
741             }
742             @Override
743             public void onStop() {
744                 pause();
745                 mPausedByTransientLossOfFocus = false;
746                 seek(0);
747                 releaseServiceUiAndStop();
748             }
749             @Override
750             public void onSkipToQueueItem(long id) {
751                 setQueuePosition((int) id);
752             }
753             @Override
754             public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
755                 if (Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
756                     KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
757                     if (ke != null && ke.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) {
758                         if (ke.getAction() == KeyEvent.ACTION_UP) {
759                             handleHeadsetHookClick(ke.getEventTime());
760                         }
761                         return true;
762                     }
763                 }
764                 return super.onMediaButtonEvent(mediaButtonIntent);
765             }
766         });
767
768         PendingIntent pi = PendingIntent.getBroadcast(this, 0,
769                 new Intent(this, MediaButtonIntentReceiver.class),
770                 PendingIntent.FLAG_UPDATE_CURRENT);
771         mSession.setMediaButtonReceiver(pi);
772
773         mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
774                 | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
775     }
776
777     /**
778      * {@inheritDoc}
779      */
780     @Override
781     public void onDestroy() {
782         if (D) Log.d(TAG, "Destroying service");
783         if (!mReadGranted) {
784             return;
785         }
786         super.onDestroy();
787         // Remove any sound effects
788         final Intent audioEffectsIntent = new Intent(
789                 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
790         audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
791         audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
792         sendBroadcast(audioEffectsIntent);
793
794         // remove any pending alarms
795         mAlarmManager.cancel(mShutdownIntent);
796
797         // Remove any callbacks from the handler
798         mPlayerHandler.removeCallbacksAndMessages(null);
799         // quit the thread so that anything that gets posted won't run
800         mHandlerThread.quitSafely();
801
802         // Release the player
803         mPlayer.release();
804         mPlayer = null;
805
806         // Remove the audio focus listener and lock screen controls
807         mAudioManager.abandonAudioFocus(mAudioFocusListener);
808         mSession.release();
809
810         // remove the media store observer
811         getContentResolver().unregisterContentObserver(mMediaStoreObserver);
812
813         // Close the cursor
814         closeCursor();
815
816         // Unregister the mount listener
817         unregisterReceiver(mIntentReceiver);
818         if (mUnmountReceiver != null) {
819             unregisterReceiver(mUnmountReceiver);
820             mUnmountReceiver = null;
821         }
822
823         // deinitialize shake detector
824         stopShakeDetector(true);
825     }
826
827     /**
828      * {@inheritDoc}
829      */
830     @Override
831     public int onStartCommand(final Intent intent, final int flags, final int startId) {
832         if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
833         mServiceStartId = startId;
834
835         if (intent != null) {
836             final String action = intent.getAction();
837
838             if (SHUTDOWN.equals(action)) {
839                 mShutdownScheduled = false;
840                 releaseServiceUiAndStop();
841                 return START_NOT_STICKY;
842             }
843
844             handleCommandIntent(intent);
845         }
846
847         // Make sure the service will shut down on its own if it was
848         // just started but not bound to and nothing is playing
849         scheduleDelayedShutdown();
850
851         if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
852             MediaButtonIntentReceiver.completeWakefulIntent(intent);
853         }
854
855         return START_NOT_STICKY;
856     }
857
858     private void releaseServiceUiAndStop() {
859         if (isPlaying()
860                 || mPausedByTransientLossOfFocus
861                 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
862             return;
863         }
864
865         if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
866         cancelNotification();
867         mAudioManager.abandonAudioFocus(mAudioFocusListener);
868         mSession.setActive(false);
869
870         if (!mServiceInUse) {
871             saveQueue(true);
872             stopSelf(mServiceStartId);
873         }
874     }
875
876     private void handleCommandIntent(Intent intent) {
877         final String action = intent.getAction();
878         final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
879
880         if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
881
882         if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
883             gotoNext(true);
884         } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)
885                 || PREVIOUS_FORCE_ACTION.equals(action)) {
886             prev(PREVIOUS_FORCE_ACTION.equals(action));
887         } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
888             togglePlayPause();
889         } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
890             pause();
891             mPausedByTransientLossOfFocus = false;
892         } else if (CMDPLAY.equals(command)) {
893             play();
894         } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
895             pause();
896             mPausedByTransientLossOfFocus = false;
897             seek(0);
898             releaseServiceUiAndStop();
899         } else if (REPEAT_ACTION.equals(action)) {
900             cycleRepeat();
901         } else if (SHUFFLE_ACTION.equals(action)) {
902             cycleShuffle();
903         } else if (CMDHEADSETHOOK.equals(command)) {
904             long timestamp = intent.getLongExtra(TIMESTAMP, 0);
905             handleHeadsetHookClick(timestamp);
906         }
907     }
908
909     private void handleHeadsetHookClick(long timestamp) {
910         if (mHeadsetHookWakeLock == null) {
911             PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
912             mHeadsetHookWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
913                     "Eleven headset button");
914             mHeadsetHookWakeLock.setReferenceCounted(false);
915         }
916         // Make sure we don't indefinitely hold the wake lock under any circumstances
917         mHeadsetHookWakeLock.acquire(10000);
918
919         Message msg = mPlayerHandler.obtainMessage(HEADSET_HOOK_EVENT, timestamp);
920         msg.sendToTarget();
921     }
922
923     /**
924      * Updates the notification, considering the current play and activity state
925      */
926     private void updateNotification() {
927         final int newNotifyMode;
928         if (isPlaying()) {
929             newNotifyMode = NOTIFY_MODE_FOREGROUND;
930         } else if (recentlyPlayed()) {
931             newNotifyMode = NOTIFY_MODE_BACKGROUND;
932         } else {
933             newNotifyMode = NOTIFY_MODE_NONE;
934         }
935
936         int notificationId = hashCode();
937         if (mNotifyMode != newNotifyMode) {
938             if (mNotifyMode == NOTIFY_MODE_FOREGROUND) {
939                 stopForeground(newNotifyMode == NOTIFY_MODE_NONE);
940             } else if (newNotifyMode == NOTIFY_MODE_NONE) {
941                 mNotificationManager.cancel(notificationId);
942                 mNotificationPostTime = 0;
943             }
944         }
945
946         if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
947             startForeground(notificationId, buildNotification());
948         } else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
949             mNotificationManager.notify(notificationId, buildNotification());
950         }
951
952         mNotifyMode = newNotifyMode;
953     }
954
955     private void cancelNotification() {
956         stopForeground(true);
957         mNotificationManager.cancel(hashCode());
958         mNotificationPostTime = 0;
959         mNotifyMode = NOTIFY_MODE_NONE;
960     }
961
962     /**
963      * @return A card ID used to save and restore playlists, i.e., the queue.
964      */
965     private int getCardId() {
966         final ContentResolver resolver = getContentResolver();
967         Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
968                 null, null);
969         int mCardId = -1;
970         if (cursor != null && cursor.moveToFirst()) {
971             mCardId = cursor.getInt(0);
972             cursor.close();
973             cursor = null;
974         }
975         return mCardId;
976     }
977
978     /**
979      * Called when we receive a ACTION_MEDIA_EJECT notification.
980      *
981      * @param storagePath The path to mount point for the removed media
982      */
983     public void closeExternalStorageFiles(final String storagePath) {
984         stop(true);
985         notifyChange(QUEUE_CHANGED);
986         notifyChange(META_CHANGED);
987     }
988
989     /**
990      * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
991      * intent will call closeExternalStorageFiles() if the external media is
992      * going to be ejected, so applications can clean up any files they have
993      * open.
994      */
995     public void registerExternalStorageListener() {
996         if (mUnmountReceiver == null) {
997             mUnmountReceiver = new BroadcastReceiver() {
998
999                 /**
1000                  * {@inheritDoc}
1001                  */
1002                 @Override
1003                 public void onReceive(final Context context, final Intent intent) {
1004                     final String action = intent.getAction();
1005                     if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
1006                         saveQueue(true);
1007                         mQueueIsSaveable = false;
1008                         closeExternalStorageFiles(intent.getData().getPath());
1009                     } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
1010                         mMediaMountedCount++;
1011                         mCardId = getCardId();
1012                         reloadQueue();
1013                         mQueueIsSaveable = true;
1014                         notifyChange(QUEUE_CHANGED);
1015                         notifyChange(META_CHANGED);
1016                     }
1017                 }
1018             };
1019             final IntentFilter filter = new IntentFilter();
1020             filter.addAction(Intent.ACTION_MEDIA_EJECT);
1021             filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
1022             filter.addDataScheme("file");
1023             registerReceiver(mUnmountReceiver, filter);
1024         }
1025     }
1026
1027     private void scheduleDelayedShutdown() {
1028         if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
1029         if (!mReadGranted) {
1030             return;
1031         }
1032         mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
1033                 SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
1034         mShutdownScheduled = true;
1035     }
1036
1037     private void cancelShutdown() {
1038         if (D) Log.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled);
1039         if (mShutdownScheduled) {
1040             mAlarmManager.cancel(mShutdownIntent);
1041             mShutdownScheduled = false;
1042         }
1043     }
1044
1045     /**
1046      * Stops playback
1047      *
1048      * @param goToIdle True to go to the idle state, false otherwise
1049      */
1050     private void stop(final boolean goToIdle) {
1051         if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
1052         if (mPlayer.isInitialized()) {
1053             mPlayer.stop();
1054         }
1055         mFileToPlay = null;
1056         closeCursor();
1057         if (goToIdle) {
1058             setIsSupposedToBePlaying(false, false);
1059         } else {
1060             stopForeground(false);
1061         }
1062     }
1063
1064     /**
1065      * Removes the range of tracks specified from the play list. If a file
1066      * within the range is the file currently being played, playback will move
1067      * to the next file after the range.
1068      *
1069      * @param first The first file to be removed
1070      * @param last The last file to be removed
1071      * @return the number of tracks deleted
1072      */
1073     private int removeTracksInternal(int first, int last) {
1074         synchronized (this) {
1075             if (last < first) {
1076                 return 0;
1077             } else if (first < 0) {
1078                 first = 0;
1079             } else if (last >= mPlaylist.size()) {
1080                 last = mPlaylist.size() - 1;
1081             }
1082
1083             boolean gotonext = false;
1084             if (first <= mPlayPos && mPlayPos <= last) {
1085                 mPlayPos = first;
1086                 gotonext = true;
1087             } else if (mPlayPos > last) {
1088                 mPlayPos -= last - first + 1;
1089             }
1090             final int numToRemove = last - first + 1;
1091
1092             if (first == 0 && last == mPlaylist.size() - 1) {
1093                 mPlayPos = -1;
1094                 mNextPlayPos = -1;
1095                 mPlaylist.clear();
1096                 mHistory.clear();
1097             } else {
1098                 for (int i = 0; i < numToRemove; i++) {
1099                     mPlaylist.remove(first);
1100                 }
1101
1102                 // remove the items from the history
1103                 // this is not ideal as the history shouldn't be impacted by this
1104                 // but since we are removing items from the array, it will throw
1105                 // an exception if we keep it around.
1106                 ListIterator<Integer> positionIterator = mHistory.listIterator();
1107                 while (positionIterator.hasNext()) {
1108                     int pos = positionIterator.next();
1109                     if (pos >= first && pos <= last) {
1110                         positionIterator.remove();
1111                     } else if (pos > last) {
1112                         positionIterator.set(pos - numToRemove);
1113                     }
1114                 }
1115             }
1116             if (gotonext) {
1117                 if (mPlaylist.size() == 0) {
1118                     stop(true);
1119                     mPlayPos = -1;
1120                     closeCursor();
1121                 } else {
1122                     if (mShuffleMode != SHUFFLE_NONE) {
1123                         mPlayPos = getNextPosition(true);
1124                     } else if (mPlayPos >= mPlaylist.size()) {
1125                         mPlayPos = 0;
1126                     }
1127                     final boolean wasPlaying = isPlaying();
1128                     stop(false);
1129                     openCurrentAndNext();
1130                     if (wasPlaying) {
1131                         play();
1132                     }
1133                 }
1134                 notifyChange(META_CHANGED);
1135             }
1136             return last - first + 1;
1137         }
1138     }
1139
1140     /**
1141      * Adds a list to the playlist
1142      *
1143      * @param list The list to add
1144      * @param position The position to place the tracks
1145      */
1146     private void addToPlayList(final long[] list, int position, long sourceId, IdType sourceType) {
1147         final int addlen = list.length;
1148         if (position < 0) {
1149             mPlaylist.clear();
1150             position = 0;
1151         }
1152
1153         mPlaylist.ensureCapacity(mPlaylist.size() + addlen);
1154         if (position > mPlaylist.size()) {
1155             position = mPlaylist.size();
1156         }
1157
1158         final ArrayList<MusicPlaybackTrack> arrayList = new ArrayList<>(addlen);
1159         for (int i = 0; i < list.length; i++) {
1160             arrayList.add(new MusicPlaybackTrack(list[i], sourceId, sourceType, i));
1161         }
1162
1163         mPlaylist.addAll(position, arrayList);
1164
1165         if (mPlaylist.size() == 0) {
1166             closeCursor();
1167             notifyChange(META_CHANGED);
1168         }
1169     }
1170
1171     /**
1172      * @param trackId The track ID
1173      */
1174     private void updateCursor(final long trackId) {
1175         updateCursor("_id=" + trackId, null);
1176     }
1177
1178     private void updateCursor(final String selection, final String[] selectionArgs) {
1179         synchronized (this) {
1180             closeCursor();
1181             mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1182                     PROJECTION, selection, selectionArgs);
1183         }
1184         updateAlbumCursor();
1185     }
1186
1187     private void updateCursor(final Uri uri) {
1188         synchronized (this) {
1189             closeCursor();
1190             mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
1191         }
1192         updateAlbumCursor();
1193     }
1194
1195     private void updateAlbumCursor() {
1196         long albumId = getAlbumId();
1197         if (albumId >= 0) {
1198             mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
1199                     ALBUM_PROJECTION, "_id=" + albumId, null);
1200         } else {
1201             mAlbumCursor = null;
1202         }
1203     }
1204
1205     private Cursor openCursorAndGoToFirst(Uri uri, String[] projection,
1206             String selection, String[] selectionArgs) {
1207         Cursor c = getContentResolver().query(uri, projection,
1208                 selection, selectionArgs, null, null);
1209         if (c == null) {
1210             return null;
1211         }
1212         if (!c.moveToFirst()) {
1213             c.close();
1214             return null;
1215         }
1216         return c;
1217      }
1218
1219     private synchronized void closeCursor() {
1220         if (mCursor != null) {
1221             mCursor.close();
1222             mCursor = null;
1223         }
1224         if (mAlbumCursor != null) {
1225             mAlbumCursor.close();
1226             mAlbumCursor = null;
1227         }
1228     }
1229
1230     /**
1231      * Called to open a new file as the current track and prepare the next for
1232      * playback
1233      */
1234     private void openCurrentAndNext() {
1235         openCurrentAndMaybeNext(true);
1236     }
1237
1238     /**
1239      * Called to open a new file as the current track and prepare the next for
1240      * playback
1241      *
1242      * @param openNext True to prepare the next track for playback, false
1243      *            otherwise.
1244      */
1245     private void openCurrentAndMaybeNext(final boolean openNext) {
1246         synchronized (this) {
1247             closeCursor();
1248
1249             if (mPlaylist.size() == 0) {
1250                 return;
1251             }
1252             stop(false);
1253
1254             boolean shutdown = false;
1255
1256             updateCursor(mPlaylist.get(mPlayPos).mId);
1257             while (true) {
1258                 if (mCursor != null
1259                         && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
1260                                 + mCursor.getLong(IDCOLIDX))) {
1261                     break;
1262                 }
1263
1264                 // if we get here then opening the file failed. We can close the
1265                 // cursor now, because
1266                 // we're either going to create a new one next, or stop trying
1267                 closeCursor();
1268                 if (mOpenFailedCounter++ < 10 && mPlaylist.size() > 1) {
1269                     final int pos = getNextPosition(false);
1270                     if (pos < 0) {
1271                         shutdown = true;
1272                         break;
1273                     }
1274                     mPlayPos = pos;
1275                     stop(false);
1276                     mPlayPos = pos;
1277                     updateCursor(mPlaylist.get(mPlayPos).mId);
1278                 } else {
1279                     mOpenFailedCounter = 0;
1280                     Log.w(TAG, "Failed to open file for playback");
1281                     shutdown = true;
1282                     break;
1283                 }
1284             }
1285
1286             if (shutdown) {
1287                 scheduleDelayedShutdown();
1288                 if (mIsSupposedToBePlaying) {
1289                     mIsSupposedToBePlaying = false;
1290                     notifyChange(PLAYSTATE_CHANGED);
1291                 }
1292             } else if (openNext) {
1293                 setNextTrack();
1294             }
1295         }
1296     }
1297
1298     private void sendErrorMessage(final String trackName) {
1299         final Intent i = new Intent(TRACK_ERROR);
1300         i.putExtra(TrackErrorExtra.TRACK_NAME, trackName);
1301         sendBroadcast(i);
1302     }
1303
1304     /**
1305      * @param force True to force the player onto the track next, false
1306      *            otherwise.
1307      * @param saveToHistory True to save the mPlayPos to the history
1308      * @return The next position to play.
1309      */
1310     private int getNextPosition(final boolean force) {
1311         // as a base case, if the playlist is empty just return -1
1312         if (mPlaylist == null || mPlaylist.isEmpty()) {
1313             return -1;
1314         }
1315         // if we're not forced to go to the next track and we are only playing the current track
1316         if (!force && mRepeatMode == REPEAT_CURRENT) {
1317             if (mPlayPos < 0) {
1318                 return 0;
1319             }
1320             return mPlayPos;
1321         } else if (mShuffleMode == SHUFFLE_NORMAL) {
1322             final int numTracks = mPlaylist.size();
1323
1324             // count the number of times a track has been played
1325             final int[] trackNumPlays = new int[numTracks];
1326             for (int i = 0; i < numTracks; i++) {
1327                 // set it all to 0
1328                 trackNumPlays[i] = 0;
1329             }
1330
1331             // walk through the history and add up the number of times the track
1332             // has been played
1333             final int numHistory = mHistory.size();
1334             for (int i = 0; i < numHistory; i++) {
1335                 final int idx = mHistory.get(i);
1336                 if (idx >= 0 && idx < numTracks) {
1337                     trackNumPlays[idx]++;
1338                 }
1339             }
1340
1341             // also add the currently playing track to the count
1342             if (mPlayPos >= 0 && mPlayPos < numTracks) {
1343                 trackNumPlays[mPlayPos]++;
1344             }
1345
1346             // figure out the least # of times a track has a played as well as
1347             // how many tracks share that count
1348             int minNumPlays = Integer.MAX_VALUE;
1349             int numTracksWithMinNumPlays = 0;
1350             for (final int trackNumPlay : trackNumPlays) {
1351                 // if we found a new track that has less number of plays, reset the counters
1352                 if (trackNumPlay < minNumPlays) {
1353                     minNumPlays = trackNumPlay;
1354                     numTracksWithMinNumPlays = 1;
1355                 } else if (trackNumPlay == minNumPlays) {
1356                     // increment this track shares the # of tracks
1357                     numTracksWithMinNumPlays++;
1358                 }
1359             }
1360
1361             // if we've played each track at least once and all tracks have been played an equal
1362             // # of times and we aren't repeating all and we're not forcing a track, then
1363             // return no more tracks
1364             if (minNumPlays > 0 && numTracksWithMinNumPlays == numTracks
1365                     && mRepeatMode != REPEAT_ALL && !force) {
1366                     return -1;
1367             }
1368
1369             // else pick a track from the least number of played tracks
1370             int skip = mShuffler.nextInt(numTracksWithMinNumPlays);
1371             for (int i = 0; i < trackNumPlays.length; i++) {
1372                 if (trackNumPlays[i] == minNumPlays) {
1373                     if (skip == 0) {
1374                         return i;
1375                     } else {
1376                         skip--;
1377                     }
1378                 }
1379             }
1380
1381             // Unexpected to land here
1382             if (D) Log.e(TAG, "Getting the next position resulted did not get a result when it should have");
1383             return -1;
1384         } else if (mShuffleMode == SHUFFLE_AUTO) {
1385             doAutoShuffleUpdate();
1386             return mPlayPos + 1;
1387         } else {
1388             if (mPlayPos >= mPlaylist.size() - 1) {
1389                 if (mRepeatMode == REPEAT_NONE && !force) {
1390                     return -1;
1391                 } else if (mRepeatMode == REPEAT_ALL || force) {
1392                     return 0;
1393                 }
1394                 return -1;
1395             } else {
1396                 return mPlayPos + 1;
1397             }
1398         }
1399     }
1400
1401     /**
1402      * Sets the track to be played
1403      */
1404     private void setNextTrack() {
1405         setNextTrack(getNextPosition(false));
1406     }
1407
1408     /**
1409      * Sets the next track to be played
1410      * @param position the target position we want
1411      */
1412     private void setNextTrack(int position) {
1413         mNextPlayPos = position;
1414         if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
1415         if (mNextPlayPos >= 0 && mPlaylist != null && mNextPlayPos < mPlaylist.size()) {
1416             final long id = mPlaylist.get(mNextPlayPos).mId;
1417             mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
1418         } else {
1419             mPlayer.setNextDataSource(null);
1420         }
1421     }
1422
1423     /**
1424      * Creates a shuffled playlist used for party mode
1425      */
1426     private boolean makeAutoShuffleList() {
1427         Cursor cursor = null;
1428         try {
1429             cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1430                     new String[] {
1431                         MediaStore.Audio.Media._ID
1432                     }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
1433             if (cursor == null || cursor.getCount() == 0) {
1434                 return false;
1435             }
1436             final int len = cursor.getCount();
1437             final long[] list = new long[len];
1438             for (int i = 0; i < len; i++) {
1439                 cursor.moveToNext();
1440                 list[i] = cursor.getLong(0);
1441             }
1442             mAutoShuffleList = list;
1443             return true;
1444         } catch (final RuntimeException e) {
1445         } finally {
1446             if (cursor != null) {
1447                 cursor.close();
1448                 cursor = null;
1449             }
1450         }
1451         return false;
1452     }
1453
1454     /**
1455      * Creates the party shuffle playlist
1456      */
1457     private void doAutoShuffleUpdate() {
1458         boolean notify = false;
1459         if (mPlayPos > 10) {
1460             removeTracks(0, mPlayPos - 9);
1461             notify = true;
1462         }
1463         final int toAdd = 7 - (mPlaylist.size() - (mPlayPos < 0 ? -1 : mPlayPos));
1464         for (int i = 0; i < toAdd; i++) {
1465             int lookback = mHistory.size();
1466             int idx = -1;
1467             while (true) {
1468                 idx = mShuffler.nextInt(mAutoShuffleList.length);
1469                 if (!wasRecentlyUsed(idx, lookback)) {
1470                     break;
1471                 }
1472                 lookback /= 2;
1473             }
1474             mHistory.add(idx);
1475             if (mHistory.size() > MAX_HISTORY_SIZE) {
1476                 mHistory.remove(0);
1477             }
1478             mPlaylist.add(new MusicPlaybackTrack(mAutoShuffleList[idx], -1, IdType.NA, -1));
1479             notify = true;
1480         }
1481         if (notify) {
1482             notifyChange(QUEUE_CHANGED);
1483         }
1484     }
1485
1486     /**/
1487     private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
1488         if (lookbacksize == 0) {
1489             return false;
1490         }
1491         final int histsize = mHistory.size();
1492         if (histsize < lookbacksize) {
1493             lookbacksize = histsize;
1494         }
1495         final int maxidx = histsize - 1;
1496         for (int i = 0; i < lookbacksize; i++) {
1497             final long entry = mHistory.get(maxidx - i);
1498             if (entry == idx) {
1499                 return true;
1500             }
1501         }
1502         return false;
1503     }
1504
1505     /**
1506      * Notify the change-receivers that something has changed.
1507      */
1508     private void notifyChange(final String what) {
1509         if (D) Log.d(TAG, "notifyChange: what = " + what);
1510
1511         // Update the lockscreen controls
1512         updateMediaSession(what);
1513
1514         if (what.equals(POSITION_CHANGED)) {
1515             return;
1516         }
1517
1518         final Intent intent = new Intent(what);
1519         intent.putExtra("id", getAudioId());
1520         intent.putExtra("artist", getArtistName());
1521         intent.putExtra("album", getAlbumName());
1522         intent.putExtra("track", getTrackName());
1523         intent.putExtra("playing", isPlaying());
1524
1525         if (NEW_LYRICS.equals(what)) {
1526             intent.putExtra("lyrics", mLyrics);
1527         }
1528
1529         sendStickyBroadcast(intent);
1530
1531         final Intent musicIntent = new Intent(intent);
1532         musicIntent.setAction(what.replace(ELEVEN_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
1533         sendStickyBroadcast(musicIntent);
1534
1535         switch (what) {
1536             case META_CHANGED:
1537                 // Add the track to the recently played list.
1538                 mRecentsCache.addSongId(getAudioId());
1539
1540                 mSongPlayCountCache.bumpSongCount(getAudioId());
1541                 break;
1542             case QUEUE_CHANGED:
1543                 saveQueue(true);
1544                 if (isPlaying()) {
1545                     // if we are in shuffle mode and our next track is still valid,
1546                     // try to re-use the track
1547                     // We need to reimplement the queue to prevent hacky solutions like this
1548                     if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size()
1549                             && getShuffleMode() != SHUFFLE_NONE) {
1550                         setNextTrack(mNextPlayPos);
1551                     } else {
1552                         setNextTrack();
1553                     }
1554                 }
1555                 break;
1556             default:
1557                 saveQueue(false);
1558                 break;
1559         }
1560
1561         if (what.equals(PLAYSTATE_CHANGED)) {
1562             updateNotification();
1563         }
1564
1565         // Update the app-widgets
1566         mAppWidgetSmall.notifyChange(this, what);
1567         mAppWidgetLarge.notifyChange(this, what);
1568         mAppWidgetLargeAlternate.notifyChange(this, what);
1569     }
1570
1571     private void updateMediaSession(final String what) {
1572         int playState = mIsSupposedToBePlaying
1573                 ? PlaybackState.STATE_PLAYING
1574                 : PlaybackState.STATE_PAUSED;
1575
1576         long playBackStateActions = PlaybackState.ACTION_PLAY |
1577                 PlaybackState.ACTION_PLAY_PAUSE |
1578                 PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
1579                 PlaybackState.ACTION_PAUSE |
1580                 PlaybackState.ACTION_SKIP_TO_NEXT |
1581                 PlaybackState.ACTION_SKIP_TO_PREVIOUS |
1582                 PlaybackState.ACTION_STOP;
1583
1584         if (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED)) {
1585             mSession.setPlaybackState(new PlaybackState.Builder()
1586                     .setActions(playBackStateActions)
1587                     .setActiveQueueItemId(getAudioId())
1588                     .setState(playState, position(), 1.0f).build());
1589         } else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
1590             Bitmap albumArt = getAlbumArt(false).getBitmap();
1591             if (albumArt != null) {
1592                 // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
1593                 // to make sure not to hand out our cache copy
1594                 Bitmap.Config config = albumArt.getConfig();
1595                 if (config == null) {
1596                     config = Bitmap.Config.ARGB_8888;
1597                 }
1598                 albumArt = albumArt.copy(config, false);
1599             }
1600
1601             mSession.setMetadata(new MediaMetadata.Builder()
1602                     .putString(MediaMetadata.METADATA_KEY_ARTIST, getArtistName())
1603                     .putString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST, getAlbumArtistName())
1604                     .putString(MediaMetadata.METADATA_KEY_ALBUM, getAlbumName())
1605                     .putString(MediaMetadata.METADATA_KEY_TITLE, getTrackName())
1606                     .putLong(MediaMetadata.METADATA_KEY_DURATION, duration())
1607                     .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, getQueuePosition() + 1)
1608                     .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, getQueue().length)
1609                     .putString(MediaMetadata.METADATA_KEY_GENRE, getGenreName())
1610                     .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART,
1611                             mShowAlbumArtOnLockscreen ? albumArt : null)
1612                     .build());
1613
1614             if (what.equals(QUEUE_CHANGED)) {
1615                 updateMediaSessionQueue();
1616             }
1617
1618             mSession.setPlaybackState(new PlaybackState.Builder()
1619                     .setActions(playBackStateActions)
1620                     .setActiveQueueItemId(getAudioId())
1621                     .setState(playState, position(), 1.0f).build());
1622         }
1623     }
1624
1625     private synchronized void updateMediaSessionQueue() {
1626         if (mQueueUpdateTask != null) {
1627             mQueueUpdateTask.cancel(true);
1628         }
1629         mQueueUpdateTask = new QueueUpdateTask(getQueue());
1630         mQueueUpdateTask.execute();
1631     }
1632
1633     private Notification buildNotification() {
1634         final String albumName = getAlbumName();
1635         final String artistName = getArtistName();
1636         final boolean isPlaying = isPlaying();
1637         String text = TextUtils.isEmpty(albumName)
1638                 ? artistName : artistName + " - " + albumName;
1639
1640         int playButtonResId = isPlaying
1641                 ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play;
1642         int playButtonTitleResId = isPlaying
1643                 ? R.string.accessibility_pause : R.string.accessibility_play;
1644
1645         Notification.MediaStyle style = new Notification.MediaStyle()
1646                 .setMediaSession(mSession.getSessionToken())
1647                 .setShowActionsInCompactView(0, 1, 2);
1648
1649         Intent nowPlayingIntent = new Intent(ACTION_AUDIO_PLAYER)
1650                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1651         PendingIntent clickIntent = PendingIntent.getActivity(this, 0, nowPlayingIntent, 0);
1652         BitmapWithColors artwork = getAlbumArt(false);
1653
1654         if (mNotificationPostTime == 0) {
1655             mNotificationPostTime = System.currentTimeMillis();
1656         }
1657
1658         Notification.Builder builder = new Notification.Builder(this, CHANNEL_NAME)
1659                 .setSmallIcon(R.drawable.ic_notification)
1660                 .setLargeIcon(artwork.getBitmap())
1661                 .setContentIntent(clickIntent)
1662                 .setContentTitle(getTrackName())
1663                 .setContentText(text)
1664                 .setWhen(mNotificationPostTime)
1665                 .setShowWhen(false)
1666                 .setStyle(style)
1667                 .setVisibility(Notification.VISIBILITY_PUBLIC)
1668                 .addAction(R.drawable.btn_playback_previous,
1669                         getString(R.string.accessibility_prev),
1670                         retrievePlaybackAction(PREVIOUS_ACTION))
1671                 .addAction(playButtonResId, getString(playButtonTitleResId),
1672                         retrievePlaybackAction(TOGGLEPAUSE_ACTION))
1673                 .addAction(R.drawable.btn_playback_next,
1674                         getString(R.string.accessibility_next),
1675                         retrievePlaybackAction(NEXT_ACTION));
1676
1677         builder.setColor(artwork.getVibrantDarkColor());
1678
1679         if (BuildCompat.isAtLeastO()) {
1680             NotificationChannel channel = mNotificationManager
1681                     .getNotificationChannel(CHANNEL_NAME);
1682
1683             if (channel == null) {
1684                 String name = getString(R.string.channel_music);
1685
1686                 channel = new NotificationChannel(CHANNEL_NAME, name,
1687                         mNotificationManager.IMPORTANCE_DEFAULT);
1688                 channel.setShowBadge(false);
1689                 channel.enableVibration(false);
1690                 channel.setSound(null, null);
1691                 mNotificationManager.createNotificationChannel(channel);
1692             }
1693
1694             builder.setChannelId(channel.getId());
1695         }
1696
1697         return builder.build();
1698     }
1699
1700     private final PendingIntent retrievePlaybackAction(final String action) {
1701         final ComponentName serviceName = new ComponentName(this, MusicPlaybackService.class);
1702         Intent intent = new Intent(action);
1703         intent.setComponent(serviceName);
1704
1705         return PendingIntent.getService(this, 0, intent, 0);
1706     }
1707
1708     /**
1709      * Saves the queue
1710      *
1711      * @param full True if the queue is full
1712      */
1713     private void saveQueue(final boolean full) {
1714         if (!mQueueIsSaveable || mPreferences == null) {
1715             return;
1716         }
1717
1718         final SharedPreferences.Editor editor = mPreferences.edit();
1719         if (full) {
1720             mPlaybackStateStore.saveState(mPlaylist,
1721                     mShuffleMode != SHUFFLE_NONE ? mHistory : null);
1722             editor.putInt("cardid", mCardId);
1723         }
1724         editor.putInt("curpos", mPlayPos);
1725         if (mPlayer.isInitialized()) {
1726             editor.putLong("seekpos", mPlayer.position());
1727         }
1728         editor.putInt("repeatmode", mRepeatMode);
1729         editor.putInt("shufflemode", mShuffleMode);
1730         editor.apply();
1731     }
1732
1733     /**
1734      * Reloads the queue as the user left it the last time they stopped using
1735      * Eleven
1736      */
1737     private void reloadQueue() {
1738         int id = mCardId;
1739         if (mPreferences.contains("cardid")) {
1740             id = mPreferences.getInt("cardid", ~mCardId);
1741         }
1742         if (id == mCardId) {
1743             mPlaylist = mPlaybackStateStore.getQueue();
1744         }
1745         if (mPlaylist.size() > 0) {
1746             final int pos = mPreferences.getInt("curpos", 0);
1747             if (pos < 0 || pos >= mPlaylist.size()) {
1748                 mPlaylist.clear();
1749                 return;
1750             }
1751             mPlayPos = pos;
1752             updateCursor(mPlaylist.get(mPlayPos).mId);
1753             if (mCursor == null) {
1754                 SystemClock.sleep(3000);
1755                 updateCursor(mPlaylist.get(mPlayPos).mId);
1756             }
1757             synchronized (this) {
1758                 closeCursor();
1759                 mOpenFailedCounter = 20;
1760                 openCurrentAndNext();
1761             }
1762             if (!mPlayer.isInitialized()) {
1763                 mPlaylist.clear();
1764                 return;
1765             }
1766
1767             final long seekpos = mPreferences.getLong("seekpos", 0);
1768             seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
1769
1770             if (D) {
1771                 Log.d(TAG, "restored queue, currently at position "
1772                         + position() + "/" + duration()
1773                         + " (requested " + seekpos + ")");
1774             }
1775
1776             int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
1777             if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
1778                 repmode = REPEAT_NONE;
1779             }
1780             mRepeatMode = repmode;
1781
1782             int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
1783             if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
1784                 shufmode = SHUFFLE_NONE;
1785             }
1786             if (shufmode != SHUFFLE_NONE) {
1787                 mHistory = mPlaybackStateStore.getHistory(mPlaylist.size());
1788             }
1789             if (shufmode == SHUFFLE_AUTO) {
1790                 if (!makeAutoShuffleList()) {
1791                     shufmode = SHUFFLE_NONE;
1792                 }
1793             }
1794             mShuffleMode = shufmode;
1795         }
1796     }
1797
1798     /**
1799      * Opens a file and prepares it for playback
1800      *
1801      * @param path The path of the file to open
1802      */
1803     public boolean openFile(final String path) {
1804         if (D) Log.d(TAG, "openFile: path = " + path);
1805         synchronized (this) {
1806             if (path == null) {
1807                 return false;
1808             }
1809
1810             // If mCursor is null, try to associate path with a database cursor
1811             if (mCursor == null) {
1812                 Uri uri = Uri.parse(path);
1813                 boolean shouldAddToPlaylist = true;     // should try adding audio info to playlist
1814                 long id = -1;
1815                 try {
1816                     id = Long.valueOf(uri.getLastPathSegment());
1817                 } catch (NumberFormatException ex) {
1818                     // Ignore
1819                 }
1820
1821                 if (id != -1 && path.startsWith(
1822                                     MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
1823                     updateCursor(uri);
1824
1825                 } else if (id != -1 && path.startsWith(
1826                                     MediaStore.Files.getContentUri("external").toString())) {
1827                     updateCursor(id);
1828
1829                 // handle downloaded media files
1830                 } else if ( path.startsWith("content://downloads/") ) {
1831
1832                     // extract MediaProvider(MP) uri , if available
1833                     // Downloads.Impl.COLUMN_MEDIAPROVIDER_URI
1834                     String mpUri = getValueForDownloadedFile(this, uri, "mediaprovider_uri");
1835                     if (D) Log.i(TAG, "Downloaded file's MP uri : " + mpUri);
1836                     if ( !TextUtils.isEmpty(mpUri) ) {
1837                         // if mpUri is valid, play that URI instead
1838                         if (openFile(mpUri)) {
1839                             // notify impending change in track
1840                             notifyChange(META_CHANGED);
1841                             return true;
1842                         } else {
1843                             return false;
1844                         }
1845                     } else {
1846                         // create phantom cursor with download info, if a MP uri wasn't found
1847                         updateCursorForDownloadedFile(this, uri);
1848                         shouldAddToPlaylist = false;    // song info isn't available in MediaStore
1849                     }
1850
1851                 } else {
1852                     // assuming a "file://" uri by this point ...
1853                     String where = MediaStore.Audio.Media.DATA + "=?";
1854                     String[] selectionArgs = new String[]{path};
1855                     updateCursor(where, selectionArgs);
1856                 }
1857                 try {
1858                     if (mCursor != null && shouldAddToPlaylist) {
1859                         mPlaylist.clear();
1860                         mPlaylist.add(new MusicPlaybackTrack(
1861                                                 mCursor.getLong(IDCOLIDX), -1, IdType.NA, -1));
1862                         // propagate the change in playlist state
1863                         notifyChange(QUEUE_CHANGED);
1864                         mPlayPos = 0;
1865                         mHistory.clear();
1866                     }
1867                 } catch (final UnsupportedOperationException ex) {
1868                     // Ignore
1869                 }
1870             }
1871
1872             mFileToPlay = path;
1873             mPlayer.setDataSource(mFileToPlay);
1874             if (mPlayer.isInitialized()) {
1875                 mOpenFailedCounter = 0;
1876                 return true;
1877             }
1878
1879             String trackName = getTrackName();
1880             if (TextUtils.isEmpty(trackName)) {
1881                 trackName = path;
1882             }
1883             sendErrorMessage(trackName);
1884
1885             stop(true);
1886             return false;
1887         }
1888     }
1889
1890     /*
1891         Columns for a pseudo cursor we are creating for downloaded songs
1892         Modeled after mCursor to be able to respond to respond to the same queries as it
1893      */
1894     private static final String[] PROJECTION_MATRIX = new String[] {
1895             "_id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
1896             MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
1897             MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
1898             MediaStore.Audio.Media.ARTIST_ID
1899     };
1900
1901     /**
1902      * Creates a pseudo cursor for downloaded audio files with minimal info
1903      * @param context needed to query the download uri
1904      * @param uri the uri of the downloaded file
1905      */
1906     private void updateCursorForDownloadedFile(Context context, Uri uri) {
1907         synchronized (this) {
1908             closeCursor();  // clear mCursor
1909             MatrixCursor cursor = new MatrixCursor(PROJECTION_MATRIX);
1910             // get title of the downloaded file ; Downloads.Impl.COLUMN_TITLE
1911             String title = getValueForDownloadedFile(this, uri, "title" );
1912             // populating the cursor with bare minimum info
1913             cursor.addRow(new Object[] {
1914                     null ,
1915                     null ,
1916                     null ,
1917                     title ,
1918                     null ,
1919                     null ,
1920                     null ,
1921                     null
1922             });
1923             mCursor = cursor;
1924             mCursor.moveToFirst();
1925         }
1926     }
1927
1928     /**
1929      * Query the DownloadProvider to get the value in the specified column
1930      * @param context
1931      * @param uri the uri of the downloaded file
1932      * @param column
1933      * @return
1934      */
1935     private String getValueForDownloadedFile(Context context, Uri uri, String column) {
1936
1937         final String[] projection = {
1938                 column
1939         };
1940         try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null)) {
1941             if (cursor != null && cursor.moveToFirst()) {
1942                 return cursor.getString(0);
1943             }
1944         }
1945         return null;
1946     }
1947
1948     /**
1949      * Returns the audio session ID
1950      *
1951      * @return The current media player audio session ID
1952      */
1953     public int getAudioSessionId() {
1954         synchronized (this) {
1955             return mPlayer.getAudioSessionId();
1956         }
1957     }
1958
1959     /**
1960      * Indicates if the media storeage device has been mounted or not
1961      *
1962      * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
1963      */
1964     public int getMediaMountedCount() {
1965         return mMediaMountedCount;
1966     }
1967
1968     /**
1969      * Returns the shuffle mode
1970      *
1971      * @return The current shuffle mode (all, party, none)
1972      */
1973     public int getShuffleMode() {
1974         return mShuffleMode;
1975     }
1976
1977     /**
1978      * Returns the repeat mode
1979      *
1980      * @return The current repeat mode (all, one, none)
1981      */
1982     public int getRepeatMode() {
1983         return mRepeatMode;
1984     }
1985
1986     /**
1987      * Removes all instances of the track with the given ID from the playlist.
1988      *
1989      * @param id The id to be removed
1990      * @return how many instances of the track were removed
1991      */
1992     public int removeTrack(final long id) {
1993         int numremoved = 0;
1994         synchronized (this) {
1995             for (int i = 0; i < mPlaylist.size(); i++) {
1996                 if (mPlaylist.get(i).mId == id) {
1997                     numremoved += removeTracksInternal(i, i);
1998                     i--;
1999                 }
2000             }
2001         }
2002         if (numremoved > 0) {
2003             notifyChange(QUEUE_CHANGED);
2004         }
2005         return numremoved;
2006     }
2007
2008     /**
2009      * Removes a song from the playlist at the specified position.
2010      *
2011      * @param id The song id to be removed
2012      * @param position The position of the song in the playlist
2013      * @return true if successful
2014      */
2015     public boolean removeTrackAtPosition(final long id, final int position) {
2016         synchronized (this) {
2017             if (    position >=0 &&
2018                     position < mPlaylist.size() &&
2019                     mPlaylist.get(position).mId == id  ) {
2020
2021                 return removeTracks(position, position) > 0;
2022             }
2023         }
2024         return false;
2025     }
2026
2027     /**
2028      * Removes the range of tracks specified from the play list. If a file
2029      * within the range is the file currently being played, playback will move
2030      * to the next file after the range.
2031      *
2032      * @param first The first file to be removed
2033      * @param last The last file to be removed
2034      * @return the number of tracks deleted
2035      */
2036     public int removeTracks(final int first, final int last) {
2037         final int numremoved = removeTracksInternal(first, last);
2038         if (numremoved > 0) {
2039             notifyChange(QUEUE_CHANGED);
2040         }
2041         return numremoved;
2042     }
2043
2044     /**
2045      * Returns the position in the queue
2046      *
2047      * @return the current position in the queue
2048      */
2049     public int getQueuePosition() {
2050         synchronized (this) {
2051             return mPlayPos;
2052         }
2053     }
2054
2055     /**
2056      * @return the size of the queue history cache
2057      */
2058     public int getQueueHistorySize() {
2059         synchronized (this) {
2060             return mHistory.size();
2061         }
2062     }
2063
2064     /**
2065      * @return the position in the history
2066      */
2067     public int getQueueHistoryPosition(int position) {
2068         synchronized (this) {
2069             if (position >= 0 && position < mHistory.size()) {
2070                 return mHistory.get(position);
2071             }
2072         }
2073
2074         return -1;
2075     }
2076
2077     /**
2078      * @return the queue of history positions
2079      */
2080     public int[] getQueueHistoryList() {
2081         synchronized (this) {
2082             int[] history = new int[mHistory.size()];
2083             for (int i = 0; i < mHistory.size(); i++) {
2084                 history[i] = mHistory.get(i);
2085             }
2086
2087             return history;
2088         }
2089     }
2090
2091     /**
2092      * Returns the path to current song
2093      *
2094      * @return The path to the current song
2095      */
2096     public String getPath() {
2097         synchronized (this) {
2098             if (mCursor == null) {
2099                 return null;
2100             }
2101             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
2102         }
2103     }
2104
2105     /**
2106      * Returns the album name
2107      *
2108      * @return The current song album Name
2109      */
2110     public String getAlbumName() {
2111         synchronized (this) {
2112             if (mCursor == null) {
2113                 return null;
2114             }
2115             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
2116         }
2117     }
2118
2119     /**
2120      * Returns the song name
2121      *
2122      * @return The current song name
2123      */
2124     public String getTrackName() {
2125         synchronized (this) {
2126             if (mCursor == null) {
2127                 return null;
2128             }
2129             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
2130         }
2131     }
2132
2133     /**
2134      * Returns the genre name of song
2135      *
2136      * @return The current song genre name
2137      */
2138     public String getGenreName() {
2139         synchronized (this) {
2140             if (mCursor == null || mPlayPos < 0 || mPlayPos >= mPlaylist.size()) {
2141                 return null;
2142             }
2143             String[] genreProjection = { MediaStore.Audio.Genres.NAME };
2144             Uri genreUri = MediaStore.Audio.Genres.getContentUriForAudioId("external",
2145                     (int) mPlaylist.get(mPlayPos).mId);
2146             Cursor genreCursor = getContentResolver().query(genreUri, genreProjection,
2147                     null, null, null);
2148             if (genreCursor != null) {
2149                 try {
2150                     if (genreCursor.moveToFirst()) {
2151                         return genreCursor.getString(
2152                             genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME));
2153                     }
2154                 } finally {
2155                     genreCursor.close();
2156                 }
2157             }
2158             return null;
2159         }
2160     }
2161
2162     /**
2163      * Returns the artist name
2164      *
2165      * @return The current song artist name
2166      */
2167     public String getArtistName() {
2168         synchronized (this) {
2169             if (mCursor == null) {
2170                 return null;
2171             }
2172             return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
2173         }
2174     }
2175
2176     /**
2177      * Returns the artist name
2178      *
2179      * @return The current song artist name
2180      */
2181     public String getAlbumArtistName() {
2182         synchronized (this) {
2183             if (mAlbumCursor == null) {
2184                 return null;
2185             }
2186             return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(AlbumColumns.ARTIST));
2187         }
2188     }
2189
2190     /**
2191      * Returns the album ID
2192      *
2193      * @return The current song album ID
2194      */
2195     public long getAlbumId() {
2196         synchronized (this) {
2197             if (mCursor == null) {
2198                 return -1;
2199             }
2200             return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
2201         }
2202     }
2203
2204     /**
2205      * Returns the artist ID
2206      *
2207      * @return The current song artist ID
2208      */
2209     public long getArtistId() {
2210         synchronized (this) {
2211             if (mCursor == null) {
2212                 return -1;
2213             }
2214             return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
2215         }
2216     }
2217
2218     /**
2219      * @return The audio id of the track
2220      */
2221     public long getAudioId() {
2222         MusicPlaybackTrack track = getCurrentTrack();
2223         if (track != null) {
2224             return track.mId;
2225         }
2226
2227         return -1;
2228     }
2229
2230     /**
2231      * Gets the currently playing music track
2232      */
2233     public MusicPlaybackTrack getCurrentTrack() {
2234         return getTrack(mPlayPos);
2235     }
2236
2237     /**
2238      * Gets the music track from the queue at the specified index
2239      * @param index position
2240      * @return music track or null
2241      */
2242     public synchronized MusicPlaybackTrack getTrack(int index) {
2243         if (index >= 0 && index < mPlaylist.size() && mPlayer.isInitialized()) {
2244             return mPlaylist.get(index);
2245         }
2246
2247         return null;
2248     }
2249
2250     /**
2251      * Returns the next audio ID
2252      *
2253      * @return The next track ID
2254      */
2255     public long getNextAudioId() {
2256         synchronized (this) {
2257             if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size() && mPlayer.isInitialized()) {
2258                 return mPlaylist.get(mNextPlayPos).mId;
2259             }
2260         }
2261         return -1;
2262     }
2263
2264     /**
2265      * Returns the previous audio ID
2266      *
2267      * @return The previous track ID
2268      */
2269     public long getPreviousAudioId() {
2270         synchronized (this) {
2271             if (mPlayer.isInitialized()) {
2272                 int pos = getPreviousPlayPosition(false);
2273                 if (pos >= 0 && pos < mPlaylist.size()) {
2274                     return mPlaylist.get(pos).mId;
2275                 }
2276             }
2277         }
2278         return -1;
2279     }
2280
2281     /**
2282      * Seeks the current track to a specific time
2283      *
2284      * @param position The time to seek to
2285      * @return The time to play the track at
2286      */
2287     public long seek(long position) {
2288         if (mPlayer.isInitialized()) {
2289             if (position < 0) {
2290                 position = 0;
2291             } else if (position > mPlayer.duration()) {
2292                 position = mPlayer.duration();
2293             }
2294             long result = mPlayer.seek(position);
2295             notifyChange(POSITION_CHANGED);
2296             return result;
2297         }
2298         return -1;
2299     }
2300
2301     /**
2302      * Seeks the current track to a position relative to its current position
2303      * If the relative position is after or before the track, it will also automatically
2304      * jump to the previous or next track respectively
2305      *
2306      * @param deltaInMs The delta time to seek to in milliseconds
2307      */
2308     public void seekRelative(long deltaInMs) {
2309         synchronized (this) {
2310             if (mPlayer.isInitialized()) {
2311                 final long newPos = position() + deltaInMs;
2312                 final long duration = duration();
2313                 if (newPos < 0) {
2314                     prev(true);
2315                     // seek to the new duration + the leftover position
2316                     seek(duration() + newPos);
2317                 } else if (newPos >= duration) {
2318                     gotoNext(true);
2319                     // seek to the leftover duration
2320                     seek(newPos - duration);
2321                 } else {
2322                     seek(newPos);
2323                 }
2324             }
2325         }
2326     }
2327
2328     /**
2329      * Returns the current position in time of the currenttrack
2330      *
2331      * @return The current playback position in miliseconds
2332      */
2333     public long position() {
2334         if (mPlayer.isInitialized()) {
2335             return mPlayer.position();
2336         }
2337         return -1;
2338     }
2339
2340     /**
2341      * Returns the full duration of the current track
2342      *
2343      * @return The duration of the current track in miliseconds
2344      */
2345     public long duration() {
2346         if (mPlayer.isInitialized()) {
2347             return mPlayer.duration();
2348         }
2349         return -1;
2350     }
2351
2352     /**
2353      * Returns the queue
2354      *
2355      * @return The queue as a long[]
2356      */
2357     public long[] getQueue() {
2358         synchronized (this) {
2359             final int len = mPlaylist.size();
2360             final long[] list = new long[len];
2361             for (int i = 0; i < len; i++) {
2362                 list[i] = mPlaylist.get(i).mId;
2363             }
2364             return list;
2365         }
2366     }
2367
2368     /**
2369      * Gets the track id at a given position in the queue
2370      * @param position
2371      * @return track id in the queue position
2372      */
2373     public long getQueueItemAtPosition(int position) {
2374         synchronized (this) {
2375             if (position >= 0 && position < mPlaylist.size()) {
2376                 return mPlaylist.get(position).mId;
2377             }
2378         }
2379
2380         return -1;
2381     }
2382
2383     /**
2384      * @return the size of the queue
2385      */
2386     public int getQueueSize() {
2387         synchronized (this) {
2388             return mPlaylist.size();
2389         }
2390     }
2391
2392     /**
2393      * @return True if music is playing, false otherwise
2394      */
2395     public boolean isPlaying() {
2396         return mIsSupposedToBePlaying;
2397     }
2398
2399     /**
2400      * Helper function to wrap the logic around mIsSupposedToBePlaying for consistentcy
2401      * @param value to set mIsSupposedToBePlaying to
2402      * @param notify whether we want to fire PLAYSTATE_CHANGED event
2403      */
2404     private void setIsSupposedToBePlaying(boolean value, boolean notify) {
2405         if (mIsSupposedToBePlaying != value) {
2406             mIsSupposedToBePlaying = value;
2407
2408             // Update mLastPlayed time first and notify afterwards, as
2409             // the notification listener method needs the up-to-date value
2410             // for the recentlyPlayed() method to work
2411             if (!mIsSupposedToBePlaying) {
2412                 scheduleDelayedShutdown();
2413                 mLastPlayedTime = System.currentTimeMillis();
2414             }
2415
2416             if (notify) {
2417                 notifyChange(PLAYSTATE_CHANGED);
2418             }
2419         }
2420     }
2421
2422     /**
2423      * @return true if is playing or has played within the last IDLE_DELAY time
2424      */
2425     private boolean recentlyPlayed() {
2426         return isPlaying() || System.currentTimeMillis() - mLastPlayedTime < IDLE_DELAY;
2427     }
2428
2429     /**
2430      * Opens a list for playback
2431      *
2432      * @param list The list of tracks to open
2433      * @param position The position to start playback at
2434      */
2435     public void open(final long[] list, final int position, long sourceId, IdType sourceType) {
2436         synchronized (this) {
2437             if (mShuffleMode == SHUFFLE_AUTO) {
2438                 mShuffleMode = SHUFFLE_NORMAL;
2439             }
2440             final long oldId = getAudioId();
2441             final int listlength = list.length;
2442             boolean newlist = true;
2443             if (mPlaylist.size() == listlength) {
2444                 newlist = false;
2445                 for (int i = 0; i < listlength; i++) {
2446                     if (list[i] != mPlaylist.get(i).mId) {
2447                         newlist = true;
2448                         break;
2449                     }
2450                 }
2451             }
2452             if (newlist) {
2453                 addToPlayList(list, -1, sourceId, sourceType);
2454                 notifyChange(QUEUE_CHANGED);
2455             }
2456             if (position >= 0) {
2457                 mPlayPos = position;
2458             } else {
2459                 mPlayPos = mShuffler.nextInt(mPlaylist.size());
2460             }
2461             mHistory.clear();
2462             openCurrentAndNext();
2463             if (oldId != getAudioId()) {
2464                 notifyChange(META_CHANGED);
2465             }
2466         }
2467     }
2468
2469     /**
2470      * Stops playback.
2471      */
2472     public void stop() {
2473         stopShakeDetector(false);
2474         stop(true);
2475     }
2476
2477     /**
2478      * Resumes or starts playback.
2479      */
2480     public void play() {
2481         startShakeDetector();
2482         play(true);
2483     }
2484
2485     /**
2486      * Resumes or starts playback.
2487      * @param createNewNextTrack True if you want to figure out the next track, false
2488      *                           if you want to re-use the existing next track (used for going back)
2489      */
2490     public void play(boolean createNewNextTrack) {
2491         int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
2492                 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
2493
2494         if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
2495
2496         if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
2497             return;
2498         }
2499
2500         final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
2501         intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2502         intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2503         sendBroadcast(intent);
2504
2505         mSession.setActive(true);
2506
2507         if (createNewNextTrack) {
2508             setNextTrack();
2509         } else {
2510             setNextTrack(mNextPlayPos);
2511         }
2512
2513         if (mPlayer.isInitialized()) {
2514             final long duration = mPlayer.duration();
2515             if (mRepeatMode != REPEAT_CURRENT && duration > 2000
2516                     && mPlayer.position() >= duration - 2000) {
2517                 gotoNext(true);
2518             }
2519
2520             mPlayer.start();
2521             mPlayerHandler.removeMessages(FADEDOWN);
2522             mPlayerHandler.sendEmptyMessage(FADEUP);
2523
2524             setIsSupposedToBePlaying(true, true);
2525
2526             cancelShutdown();
2527             updateNotification();
2528         } else if (mPlaylist.size() <= 0) {
2529             setShuffleMode(SHUFFLE_AUTO);
2530         }
2531     }
2532
2533     private void togglePlayPause() {
2534         if (isPlaying()) {
2535             pause();
2536             mPausedByTransientLossOfFocus = false;
2537         } else {
2538             play();
2539         }
2540     }
2541
2542     /**
2543      * Temporarily pauses playback.
2544      */
2545     public void pause() {
2546         if (mPlayerHandler == null) return;
2547         if (D) Log.d(TAG, "Pausing playback");
2548         synchronized (this) {
2549             if (mPlayerHandler != null) {
2550                 mPlayerHandler.removeMessages(FADEUP);
2551             }
2552             if (mIsSupposedToBePlaying) {
2553                 final Intent intent = new Intent(
2554                         AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
2555                 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2556                 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2557                 sendBroadcast(intent);
2558
2559                 if (mPlayer != null) {
2560                     mPlayer.pause();
2561                 }
2562                 setIsSupposedToBePlaying(false, true);
2563                 stopShakeDetector(false);
2564             }
2565         }
2566     }
2567
2568     /**
2569      * Changes from the current track to the next track
2570      */
2571     public void gotoNext(final boolean force) {
2572         if (D) Log.d(TAG, "Going to next track");
2573         synchronized (this) {
2574             if (mPlaylist.size() <= 0) {
2575                 if (D) Log.d(TAG, "No play queue");
2576                 scheduleDelayedShutdown();
2577                 return;
2578             }
2579             int pos = mNextPlayPos;
2580             if (pos < 0) {
2581                 pos = getNextPosition(force);
2582             }
2583
2584             if (pos < 0) {
2585                 setIsSupposedToBePlaying(false, true);
2586                 return;
2587             }
2588
2589             stop(false);
2590             setAndRecordPlayPos(pos);
2591             openCurrentAndNext();
2592             play();
2593             notifyChange(META_CHANGED);
2594         }
2595     }
2596
2597     public void setAndRecordPlayPos(int nextPos) {
2598         synchronized (this) {
2599             // save to the history
2600             if (mShuffleMode != SHUFFLE_NONE) {
2601                 mHistory.add(mPlayPos);
2602                 if (mHistory.size() > MAX_HISTORY_SIZE) {
2603                     mHistory.remove(0);
2604                 }
2605             }
2606
2607             mPlayPos = nextPos;
2608         }
2609     }
2610
2611     /**
2612      * Changes from the current track to the previous played track
2613      */
2614     public void prev(boolean forcePrevious) {
2615         synchronized (this) {
2616             // if we aren't repeating 1, and we are either early in the song
2617             // or we want to force go back, then go to the prevous track
2618             boolean goPrevious = getRepeatMode() != REPEAT_CURRENT &&
2619                     (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD || forcePrevious);
2620
2621             if (goPrevious) {
2622                 if (D) Log.d(TAG, "Going to previous track");
2623                 int pos = getPreviousPlayPosition(true);
2624                 // if we have no more previous tracks, quit
2625                 if (pos < 0) {
2626                     return;
2627                 }
2628                 mNextPlayPos = mPlayPos;
2629                 mPlayPos = pos;
2630                 stop(false);
2631                 openCurrent();
2632                 play(false);
2633                 notifyChange(META_CHANGED);
2634             } else {
2635                 if (D) Log.d(TAG, "Going to beginning of track");
2636                 seek(0);
2637                 play(false);
2638             }
2639         }
2640     }
2641
2642     public int getPreviousPlayPosition(boolean removeFromHistory) {
2643         synchronized (this) {
2644             if (mShuffleMode == SHUFFLE_NORMAL) {
2645                 // Go to previously-played track and remove it from the history
2646                 final int histsize = mHistory.size();
2647                 if (histsize == 0) {
2648                     return -1;
2649                 }
2650                 final Integer pos = mHistory.get(histsize - 1);
2651                 if (removeFromHistory) {
2652                     mHistory.remove(histsize - 1);
2653                 }
2654                 return pos;
2655             } else {
2656                 if (mPlayPos > 0) {
2657                     return mPlayPos - 1;
2658                 } else {
2659                     return mPlaylist.size() - 1;
2660                 }
2661             }
2662         }
2663     }
2664
2665     /**
2666      * We don't want to open the current and next track when the user is using
2667      * the {@code #prev()} method because they won't be able to travel back to
2668      * the previously listened track if they're shuffling.
2669      */
2670     private void openCurrent() {
2671         openCurrentAndMaybeNext(false);
2672     }
2673
2674     /**
2675      * Moves an item in the queue from one position to another
2676      *
2677      * @param from The position the item is currently at
2678      * @param to The position the item is being moved to
2679      */
2680     public void moveQueueItem(int index1, int index2) {
2681         synchronized (this) {
2682             if (index1 >= mPlaylist.size()) {
2683                 index1 = mPlaylist.size() - 1;
2684             }
2685             if (index2 >= mPlaylist.size()) {
2686                 index2 = mPlaylist.size() - 1;
2687             }
2688
2689             if (index1 == index2) {
2690                 return;
2691             }
2692
2693             final MusicPlaybackTrack track = mPlaylist.remove(index1);
2694             if (index1 < index2) {
2695                 mPlaylist.add(index2, track);
2696                 if (mPlayPos == index1) {
2697                     mPlayPos = index2;
2698                 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
2699                     mPlayPos--;
2700                 }
2701             } else if (index2 < index1) {
2702                 mPlaylist.add(index2, track);
2703                 if (mPlayPos == index1) {
2704                     mPlayPos = index2;
2705                 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
2706                     mPlayPos++;
2707                 }
2708             }
2709             notifyChange(QUEUE_CHANGED);
2710         }
2711     }
2712
2713     /**
2714      * Sets the repeat mode
2715      *
2716      * @param repeatmode The repeat mode to use
2717      */
2718     public void setRepeatMode(final int repeatmode) {
2719         synchronized (this) {
2720             mRepeatMode = repeatmode;
2721             setNextTrack();
2722             saveQueue(false);
2723             notifyChange(REPEATMODE_CHANGED);
2724         }
2725     }
2726
2727     /**
2728      * Sets the shuffle mode
2729      *
2730      * @param shufflemode The shuffle mode to use
2731      */
2732     public void setShuffleMode(final int shufflemode) {
2733         synchronized (this) {
2734             if (mShuffleMode == shufflemode && mPlaylist.size() > 0) {
2735                 return;
2736             }
2737
2738             mShuffleMode = shufflemode;
2739             if (mShuffleMode == SHUFFLE_AUTO) {
2740                 if (makeAutoShuffleList()) {
2741                     mPlaylist.clear();
2742                     doAutoShuffleUpdate();
2743                     mPlayPos = 0;
2744                     openCurrentAndNext();
2745                     play();
2746                     notifyChange(META_CHANGED);
2747                     return;
2748                 } else {
2749                     mShuffleMode = SHUFFLE_NONE;
2750                 }
2751             } else {
2752                 setNextTrack();
2753             }
2754             saveQueue(false);
2755             notifyChange(SHUFFLEMODE_CHANGED);
2756         }
2757     }
2758
2759     /**
2760      * Sets the position of a track in the queue
2761      *
2762      * @param index The position to place the track
2763      */
2764     public void setQueuePosition(final int index) {
2765         synchronized (this) {
2766             stop(false);
2767             mPlayPos = index;
2768             openCurrentAndNext();
2769             play();
2770             notifyChange(META_CHANGED);
2771             if (mShuffleMode == SHUFFLE_AUTO) {
2772                 doAutoShuffleUpdate();
2773             }
2774         }
2775     }
2776
2777     /**
2778      * Queues a new list for playback
2779      *
2780      * @param list The list to queue
2781      * @param action The action to take
2782      */
2783     public void enqueue(final long[] list, final int action, long sourceId, IdType sourceType) {
2784         synchronized (this) {
2785             if (action == NEXT && mPlayPos + 1 < mPlaylist.size()) {
2786                 addToPlayList(list, mPlayPos + 1, sourceId, sourceType);
2787                 mNextPlayPos = mPlayPos + 1;
2788                 notifyChange(QUEUE_CHANGED);
2789             } else {
2790                 addToPlayList(list, Integer.MAX_VALUE, sourceId, sourceType);
2791                 notifyChange(QUEUE_CHANGED);
2792             }
2793
2794             if (mPlayPos < 0) {
2795                 mPlayPos = 0;
2796                 openCurrentAndNext();
2797                 play();
2798                 notifyChange(META_CHANGED);
2799             }
2800         }
2801     }
2802
2803     /**
2804      * Cycles through the different repeat modes
2805      */
2806     private void cycleRepeat() {
2807         if (mRepeatMode == REPEAT_NONE) {
2808             setRepeatMode(REPEAT_ALL);
2809         } else if (mRepeatMode == REPEAT_ALL) {
2810             setRepeatMode(REPEAT_CURRENT);
2811             if (mShuffleMode != SHUFFLE_NONE) {
2812                 setShuffleMode(SHUFFLE_NONE);
2813             }
2814         } else {
2815             setRepeatMode(REPEAT_NONE);
2816         }
2817     }
2818
2819     /**
2820      * Cycles through the different shuffle modes
2821      */
2822     private void cycleShuffle() {
2823         if (mShuffleMode == SHUFFLE_NONE) {
2824             setShuffleMode(SHUFFLE_NORMAL);
2825             if (mRepeatMode == REPEAT_CURRENT) {
2826                 setRepeatMode(REPEAT_ALL);
2827             }
2828         } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
2829             setShuffleMode(SHUFFLE_NONE);
2830         }
2831     }
2832
2833     /**
2834      * @param smallBitmap true to return a smaller version of the default artwork image.
2835      *                    Currently Has no impact on the artwork size if one exists
2836      * @return The album art for the current album.
2837      */
2838     public BitmapWithColors getAlbumArt(boolean smallBitmap) {
2839         final String albumName = getAlbumName();
2840         final String artistName = getArtistName();
2841         final long albumId = getAlbumId();
2842         final String key = albumName + "_" + artistName + "_" + albumId;
2843         final int targetIndex = smallBitmap ? 0 : 1;
2844
2845         // if the cached key matches and we have the bitmap, return it
2846         if (key.equals(mCachedKey) && mCachedBitmapWithColors[targetIndex] != null) {
2847             return mCachedBitmapWithColors[targetIndex];
2848         }
2849
2850         // otherwise get the artwork (or defaultartwork if none found)
2851         final BitmapWithColors bitmap = mImageFetcher.getArtwork(albumName,
2852                 albumId, artistName, smallBitmap);
2853
2854         // if the key is different, clear the bitmaps first
2855         if (!key.equals(mCachedKey)) {
2856             mCachedBitmapWithColors[0] = null;
2857             mCachedBitmapWithColors[1] = null;
2858         }
2859
2860         // store the new key and bitmap
2861         mCachedKey = key;
2862         mCachedBitmapWithColors[targetIndex] = bitmap;
2863         return bitmap;
2864     }
2865
2866     /**
2867      * Called when one of the lists should refresh or requery.
2868      */
2869     public void refresh() {
2870         notifyChange(REFRESH);
2871     }
2872
2873     /**
2874      * Called when one of the playlists have changed (renamed, added/removed tracks)
2875      */
2876     public void playlistChanged() {
2877         notifyChange(PLAYLIST_CHANGED);
2878     }
2879
2880     /**
2881      * Called to set the status of shake to play feature
2882      */
2883     public void setShakeToPlayEnabled(boolean enabled) {
2884         if (D) {
2885             Log.d(TAG, "ShakeToPlay status: " + enabled);
2886         }
2887         if (enabled) {
2888             if (mShakeDetector == null) {
2889                 mShakeDetector = new ShakeDetector(mShakeDetectorListener);
2890             }
2891             // if song is already playing, start listening immediately
2892             if (isPlaying()) {
2893                 startShakeDetector();
2894             }
2895         }
2896         else {
2897             stopShakeDetector(true);
2898         }
2899     }
2900
2901     /**
2902      * Called to set visibility of album art on lockscreen
2903      */
2904     public void setLockscreenAlbumArt(boolean enabled) {
2905         mShowAlbumArtOnLockscreen = enabled;
2906         notifyChange(META_CHANGED);
2907     }
2908
2909     /**
2910      * Called to start listening to shakes
2911      */
2912     private void startShakeDetector() {
2913         if (mShakeDetector != null) {
2914             mShakeDetector.start((SensorManager)getSystemService(SENSOR_SERVICE));
2915         }
2916     }
2917
2918     /**
2919      * Called to stop listening to shakes
2920      */
2921     private void stopShakeDetector(final boolean destroyShakeDetector) {
2922         if (mShakeDetector != null) {
2923             mShakeDetector.stop();
2924         }
2925         if(destroyShakeDetector){
2926             mShakeDetector = null;
2927             if (D) {
2928                 Log.d(TAG, "ShakeToPlay destroyed!!!");
2929             }
2930         }
2931     }
2932
2933     private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
2934         /**
2935          * {@inheritDoc}
2936          */
2937         @Override
2938         public void onReceive(final Context context, final Intent intent) {
2939             final String command = intent.getStringExtra(CMDNAME);
2940
2941             if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
2942                 final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2943                 mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
2944             } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
2945                 final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2946                 mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
2947             } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
2948                 final int[] largeAlt = intent
2949                         .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2950                 mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
2951             } else {
2952                 handleCommandIntent(intent);
2953             }
2954         }
2955     };
2956
2957     private ContentObserver mMediaStoreObserver;
2958
2959     private class MediaStoreObserver extends ContentObserver implements Runnable {
2960         // milliseconds to delay before calling refresh to aggregate events
2961         private static final long REFRESH_DELAY = 500;
2962         private Handler mHandler;
2963
2964         public MediaStoreObserver(Handler handler) {
2965             super(handler);
2966             mHandler = handler;
2967         }
2968
2969         @Override
2970         public void onChange(boolean selfChange) {
2971             // if a change is detected, remove any scheduled callback
2972             // then post a new one. This is intended to prevent closely
2973             // spaced events from generating multiple refresh calls
2974             mHandler.removeCallbacks(this);
2975             mHandler.postDelayed(this, REFRESH_DELAY);
2976         }
2977
2978         @Override
2979         public void run() {
2980             // actually call refresh when the delayed callback fires
2981             Log.e("ELEVEN", "calling refresh!");
2982             refresh();
2983         }
2984     };
2985
2986     private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
2987         /**
2988          * {@inheritDoc}
2989          */
2990         @Override
2991         public void onAudioFocusChange(final int focusChange) {
2992             mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
2993         }
2994     };
2995
2996     private static final class MusicPlayerHandler extends Handler {
2997         private final WeakReference<MusicPlaybackService> mService;
2998         private float mCurrentVolume = 1.0f;
2999
3000         private static final int DOUBLE_CLICK_TIMEOUT = 800;
3001         private int mHeadsetHookClickCounter = 0;
3002
3003         /**
3004          * Constructor of <code>MusicPlayerHandler</code>
3005          *
3006          * @param service The service to use.
3007          * @param looper The thread to run on.
3008          */
3009         public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
3010             super(looper);
3011             mService = new WeakReference<>(service);
3012         }
3013
3014         /**
3015          * {@inheritDoc}
3016          */
3017         @Override
3018         public void handleMessage(final Message msg) {
3019             final MusicPlaybackService service = mService.get();
3020             if (service == null) {
3021                 return;
3022             }
3023
3024             synchronized (service) {
3025                 switch (msg.what) {
3026                     case FADEDOWN:
3027                         mCurrentVolume -= .05f;
3028                         if (mCurrentVolume > .2f) {
3029                             sendEmptyMessageDelayed(FADEDOWN, 10);
3030                         } else {
3031                             mCurrentVolume = .2f;
3032                         }
3033                         service.mPlayer.setVolume(mCurrentVolume);
3034                         break;
3035                     case FADEUP:
3036                         mCurrentVolume += .01f;
3037                         if (mCurrentVolume < 1.0f) {
3038                             sendEmptyMessageDelayed(FADEUP, 10);
3039                         } else {
3040                             mCurrentVolume = 1.0f;
3041                         }
3042                         service.mPlayer.setVolume(mCurrentVolume);
3043                         break;
3044                     case SERVER_DIED:
3045                         if (service.isPlaying()) {
3046                             final TrackErrorInfo info = (TrackErrorInfo)msg.obj;
3047                             service.sendErrorMessage(info.mTrackName);
3048
3049                             // since the service isPlaying(), we only need to remove the offending
3050                             // audio track, and the code will automatically play the next track
3051                             service.removeTrack(info.mId);
3052                         } else {
3053                             service.openCurrentAndNext();
3054                         }
3055                         break;
3056                     case TRACK_WENT_TO_NEXT:
3057                         service.setAndRecordPlayPos(service.mNextPlayPos);
3058                         service.setNextTrack();
3059                         if (service.mCursor != null) {
3060                             service.mCursor.close();
3061                             service.mCursor = null;
3062                         }
3063                         service.updateCursor(service.mPlaylist.get(service.mPlayPos).mId);
3064                         service.notifyChange(META_CHANGED);
3065                         service.updateNotification();
3066                         break;
3067                     case TRACK_ENDED:
3068                         if (service.mRepeatMode == REPEAT_CURRENT) {
3069                             service.seek(0);
3070                             service.play();
3071                         } else {
3072                             service.gotoNext(false);
3073                         }
3074                         break;
3075                     case LYRICS:
3076                         service.mLyrics = (String) msg.obj;
3077                         service.notifyChange(NEW_LYRICS);
3078                         break;
3079                     case FOCUSCHANGE:
3080                         if (D) Log.d(TAG, "Received audio focus change event " + msg.arg1);
3081                         switch (msg.arg1) {
3082                             case AudioManager.AUDIOFOCUS_LOSS:
3083                             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
3084                                 if (service.isPlaying()) {
3085                                     service.mPausedByTransientLossOfFocus =
3086                                             msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
3087                                 }
3088                                 service.pause();
3089                                 break;
3090                             case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
3091                                 removeMessages(FADEUP);
3092                                 sendEmptyMessage(FADEDOWN);
3093                                 break;
3094                             case AudioManager.AUDIOFOCUS_GAIN:
3095                                 if (!service.isPlaying()
3096                                         && service.mPausedByTransientLossOfFocus) {
3097                                     service.mPausedByTransientLossOfFocus = false;
3098                                     mCurrentVolume = 0f;
3099                                     service.mPlayer.setVolume(mCurrentVolume);
3100                                     service.play();
3101                                 } else {
3102                                     removeMessages(FADEDOWN);
3103                                     sendEmptyMessage(FADEUP);
3104                                 }
3105                                 break;
3106                             default:
3107                         }
3108                         break;
3109                     case HEADSET_HOOK_EVENT: {
3110                         long eventTime = (Long) msg.obj;
3111
3112                         mHeadsetHookClickCounter = Math.min(mHeadsetHookClickCounter + 1, 3);
3113                         if (D) Log.d(TAG, "Got headset click, count = " + mHeadsetHookClickCounter);
3114                         removeMessages(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3115
3116                         if (mHeadsetHookClickCounter == 3) {
3117                             sendEmptyMessage(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3118                         } else {
3119                             sendEmptyMessageAtTime(HEADSET_HOOK_MULTI_CLICK_TIMEOUT,
3120                                     eventTime + DOUBLE_CLICK_TIMEOUT);
3121                         }
3122                         break;
3123                     }
3124                     case HEADSET_HOOK_MULTI_CLICK_TIMEOUT:
3125                         if (D) Log.d(TAG, "Handling headset click");
3126                         switch (mHeadsetHookClickCounter) {
3127                             case 1: service.togglePlayPause(); break;
3128                             case 2: service.gotoNext(true); break;
3129                             case 3: service.prev(false); break;
3130                         }
3131                         mHeadsetHookClickCounter = 0;
3132                         service.mHeadsetHookWakeLock.release();
3133                         break;
3134                     default:
3135                         break;
3136                 }
3137             }
3138         }
3139     }
3140
3141     private static final class Shuffler {
3142
3143         private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<>();
3144
3145         private final TreeSet<Integer> mPreviousNumbers = new TreeSet<>();
3146
3147         private final Random mRandom = new Random();
3148
3149         private int mPrevious;
3150
3151         /**
3152          * Constructor of <code>Shuffler</code>
3153          */
3154         public Shuffler() {
3155             super();
3156         }
3157
3158         /**
3159          * @param interval The length the queue
3160          * @return The position of the next track to play
3161          */
3162         public int nextInt(final int interval) {
3163             int next;
3164             do {
3165                 next = mRandom.nextInt(interval);
3166             } while (next == mPrevious && interval > 1 && !mPreviousNumbers.contains(next));
3167             mPrevious = next;
3168             mHistoryOfNumbers.add(mPrevious);
3169             mPreviousNumbers.add(mPrevious);
3170             cleanUpHistory();
3171             return next;
3172         }
3173
3174         /**
3175          * Removes old tracks and cleans up the history preparing for new tracks
3176          * to be added to the mapping
3177          */
3178         private void cleanUpHistory() {
3179             if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
3180                 for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
3181                     mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
3182                 }
3183             }
3184         }
3185     };
3186
3187     private static final class TrackErrorInfo {
3188         public long mId;
3189         public String mTrackName;
3190
3191         public TrackErrorInfo(long id, String trackName) {
3192             mId = id;
3193             mTrackName = trackName;
3194         }
3195     }
3196
3197     private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
3198             MediaPlayer.OnCompletionListener {
3199
3200         private final WeakReference<MusicPlaybackService> mService;
3201
3202         private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
3203
3204         private MediaPlayer mNextMediaPlayer;
3205
3206         private Handler mHandler;
3207
3208         private boolean mIsInitialized = false;
3209
3210         private SrtManager mSrtManager;
3211
3212         private String mNextMediaPath;
3213
3214         /**
3215          * Constructor of <code>MultiPlayer</code>
3216          */
3217         public MultiPlayer(final MusicPlaybackService service) {
3218             mService = new WeakReference<>(service);
3219             mSrtManager = new SrtManager() {
3220                 @Override
3221                 public void onTimedText(String text) {
3222                     mHandler.obtainMessage(LYRICS, text).sendToTarget();
3223                 }
3224             };
3225         }
3226
3227         /**
3228          * @param path The path of the file, or the http/rtsp URL of the stream
3229          *            you want to play
3230          */
3231         public void setDataSource(final String path) {
3232             mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
3233             if (mIsInitialized) {
3234                 loadSrt(path);
3235                 setNextDataSource(null);
3236             }
3237         }
3238
3239         private void loadSrt(final String path) {
3240             mSrtManager.reset();
3241
3242             Uri uri = Uri.parse(path);
3243             String filePath = null;
3244
3245             if (path.startsWith("content://")) {
3246                 // resolve the content resolver path to a file path
3247                 Cursor cursor = null;
3248                 try {
3249                     final String[] proj = {MediaStore.Audio.Media.DATA};
3250                     cursor = mService.get().getContentResolver().query(uri, proj,
3251                             null, null, null);
3252                     if (cursor != null && cursor.moveToFirst()) {
3253                         filePath = cursor.getString(0);
3254                     }
3255                 } finally {
3256                     if (cursor != null) {
3257                         cursor.close();
3258                         cursor = null;
3259                     }
3260                 }
3261             } else {
3262                 filePath = uri.getPath();
3263             }
3264
3265             if (!TextUtils.isEmpty(filePath)) {
3266                 final int lastIndex = filePath.lastIndexOf('.');
3267                 if (lastIndex != -1) {
3268                     String newPath = filePath.substring(0, lastIndex) + ".srt";
3269                     final File f = new File(newPath);
3270
3271                     mSrtManager.initialize(mCurrentMediaPlayer, f);
3272                 }
3273             }
3274         }
3275
3276         /**
3277          * @param player The {@link MediaPlayer} to use
3278          * @param path The path of the file, or the http/rtsp URL of the stream
3279          *            you want to play
3280          * @return True if the <code>player</code> has been prepared and is
3281          *         ready to play, false otherwise
3282          */
3283         private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
3284             try {
3285                 player.reset();
3286                 player.setOnPreparedListener(null);
3287                 if (path.startsWith("content://")) {
3288                     player.setDataSource(mService.get(), Uri.parse(path));
3289                 } else {
3290                     player.setDataSource(path);
3291                 }
3292                 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
3293
3294                 player.prepare();
3295             } catch (final IOException todo) {
3296                 // TODO: notify the user why the file couldn't be opened
3297                 return false;
3298             } catch (final IllegalArgumentException todo) {
3299                 // TODO: notify the user why the file couldn't be opened
3300                 return false;
3301             }
3302             player.setOnCompletionListener(this);
3303             player.setOnErrorListener(this);
3304             return true;
3305         }
3306
3307         /**
3308          * Set the MediaPlayer to start when this MediaPlayer finishes playback.
3309          *
3310          * @param path The path of the file, or the http/rtsp URL of the stream
3311          *            you want to play
3312          */
3313         public void setNextDataSource(final String path) {
3314             mNextMediaPath = null;
3315             try {
3316                 mCurrentMediaPlayer.setNextMediaPlayer(null);
3317             } catch (IllegalArgumentException e) {
3318                 Log.i(TAG, "Next media player is current one, continuing");
3319             } catch (IllegalStateException e) {
3320                 Log.e(TAG, "Media player not initialized!");
3321                 return;
3322             }
3323             if (mNextMediaPlayer != null) {
3324                 mNextMediaPlayer.release();
3325                 mNextMediaPlayer = null;
3326             }
3327             if (path == null) {
3328                 return;
3329             }
3330             mNextMediaPlayer = new MediaPlayer();
3331             mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
3332             if (setDataSourceImpl(mNextMediaPlayer, path)) {
3333                 mNextMediaPath = path;
3334                 mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
3335             } else {
3336                 if (mNextMediaPlayer != null) {
3337                     mNextMediaPlayer.release();
3338                     mNextMediaPlayer = null;
3339                 }
3340             }
3341         }
3342
3343         /**
3344          * Sets the handler
3345          *
3346          * @param handler The handler to use
3347          */
3348         public void setHandler(final Handler handler) {
3349             mHandler = handler;
3350         }
3351
3352         /**
3353          * @return True if the player is ready to go, false otherwise
3354          */
3355         public boolean isInitialized() {
3356             return mIsInitialized;
3357         }
3358
3359         /**
3360          * Starts or resumes playback.
3361          */
3362         public void start() {
3363             mCurrentMediaPlayer.start();
3364             mSrtManager.play();
3365         }
3366
3367         /**
3368          * Resets the MediaPlayer to its uninitialized state.
3369          */
3370         public void stop() {
3371             mCurrentMediaPlayer.reset();
3372             mSrtManager.reset();
3373             mIsInitialized = false;
3374         }
3375
3376         /**
3377          * Releases resources associated with this MediaPlayer object.
3378          */
3379         public void release() {
3380             mCurrentMediaPlayer.release();
3381             mSrtManager.release();
3382             mSrtManager = null;
3383         }
3384
3385         /**
3386          * Pauses playback. Call start() to resume.
3387          */
3388         public void pause() {
3389             mCurrentMediaPlayer.pause();
3390             mSrtManager.pause();
3391         }
3392
3393         /**
3394          * Gets the duration of the file.
3395          *
3396          * @return The duration in milliseconds
3397          */
3398         public long duration() {
3399             return mCurrentMediaPlayer.getDuration();
3400         }
3401
3402         /**
3403          * Gets the current playback position.
3404          *
3405          * @return The current position in milliseconds
3406          */
3407         public long position() {
3408             return mCurrentMediaPlayer.getCurrentPosition();
3409         }
3410
3411         /**
3412          * Gets the current playback position.
3413          *
3414          * @param whereto The offset in milliseconds from the start to seek to
3415          * @return The offset in milliseconds from the start to seek to
3416          */
3417         public long seek(final long whereto) {
3418             mCurrentMediaPlayer.seekTo((int)whereto);
3419             mSrtManager.seekTo(whereto);
3420             return whereto;
3421         }
3422
3423         /**
3424          * Sets the volume on this player.
3425          *
3426          * @param vol Left and right volume scalar
3427          */
3428         public void setVolume(final float vol) {
3429             mCurrentMediaPlayer.setVolume(vol, vol);
3430         }
3431
3432         /**
3433          * Sets the audio session ID.
3434          *
3435          * @param sessionId The audio session ID
3436          */
3437         public void setAudioSessionId(final int sessionId) {
3438             mCurrentMediaPlayer.setAudioSessionId(sessionId);
3439         }
3440
3441         /**
3442          * Returns the audio session ID.
3443          *
3444          * @return The current audio session ID.
3445          */
3446         public int getAudioSessionId() {
3447             return mCurrentMediaPlayer.getAudioSessionId();
3448         }
3449
3450         /**
3451          * {@inheritDoc}
3452          */
3453         @Override
3454         public boolean onError(final MediaPlayer mp, final int what, final int extra) {
3455             Log.w(TAG, "Music Server Error what: " + what + " extra: " + extra);
3456             switch (what) {
3457                 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
3458                     final MusicPlaybackService service = mService.get();
3459                     if (service == null) {
3460                         return false;
3461                     }
3462                     final TrackErrorInfo errorInfo = new TrackErrorInfo(service.getAudioId(),
3463                             service.getTrackName());
3464
3465                     mIsInitialized = false;
3466                     mCurrentMediaPlayer.release();
3467                     mCurrentMediaPlayer = new MediaPlayer();
3468                     Message msg = mHandler.obtainMessage(SERVER_DIED, errorInfo);
3469                     mHandler.sendMessageDelayed(msg, 2000);
3470                     return true;
3471                 default:
3472                     break;
3473             }
3474             return false;
3475         }
3476
3477         /**
3478          * {@inheritDoc}
3479          */
3480         @Override
3481         public void onCompletion(final MediaPlayer mp) {
3482             if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
3483                 mCurrentMediaPlayer.release();
3484                 mCurrentMediaPlayer = mNextMediaPlayer;
3485                 loadSrt(mNextMediaPath);
3486                 mNextMediaPath = null;
3487                 mNextMediaPlayer = null;
3488                 mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
3489             } else {
3490                 mHandler.sendEmptyMessage(TRACK_ENDED);
3491             }
3492         }
3493     }
3494
3495     private static final class ServiceStub extends IElevenService.Stub {
3496
3497         private final WeakReference<MusicPlaybackService> mService;
3498
3499         private ServiceStub(final MusicPlaybackService service) {
3500             mService = new WeakReference<>(service);
3501         }
3502
3503         /**
3504          * {@inheritDoc}
3505          */
3506         @Override
3507         public void openFile(final String path) throws RemoteException {
3508             mService.get().openFile(path);
3509         }
3510
3511         /**
3512          * {@inheritDoc}
3513          */
3514         @Override
3515         public void open(final long[] list, final int position, long sourceId, int sourceType)
3516                 throws RemoteException {
3517             mService.get().open(list, position, sourceId, IdType.getTypeById(sourceType));
3518         }
3519
3520         /**
3521          * {@inheritDoc}
3522          */
3523         @Override
3524         public void stop() throws RemoteException {
3525             mService.get().stop();
3526         }
3527
3528         /**
3529          * {@inheritDoc}
3530          */
3531         @Override
3532         public void pause() throws RemoteException {
3533             mService.get().pause();
3534         }
3535
3536         /**
3537          * {@inheritDoc}
3538          */
3539         @Override
3540         public void play() throws RemoteException {
3541             mService.get().play();
3542         }
3543
3544         /**
3545          * {@inheritDoc}
3546          */
3547         @Override
3548         public void prev(boolean forcePrevious) throws RemoteException {
3549             mService.get().prev(forcePrevious);
3550         }
3551
3552         /**
3553          * {@inheritDoc}
3554          */
3555         @Override
3556         public void next() throws RemoteException {
3557             mService.get().gotoNext(true);
3558         }
3559
3560         /**
3561          * {@inheritDoc}
3562          */
3563         @Override
3564         public void enqueue(final long[] list, final int action, long sourceId, int sourceType)
3565                 throws RemoteException {
3566             mService.get().enqueue(list, action, sourceId, IdType.getTypeById(sourceType));
3567         }
3568
3569         /**
3570          * {@inheritDoc}
3571          */
3572         @Override
3573         public void setQueuePosition(final int index) throws RemoteException {
3574             mService.get().setQueuePosition(index);
3575         }
3576
3577         /**
3578          * {@inheritDoc}
3579          */
3580         @Override
3581         public void setShuffleMode(final int shufflemode) throws RemoteException {
3582             mService.get().setShuffleMode(shufflemode);
3583         }
3584
3585         /**
3586          * {@inheritDoc}
3587          */
3588         @Override
3589         public void setRepeatMode(final int repeatmode) throws RemoteException {
3590             mService.get().setRepeatMode(repeatmode);
3591         }
3592
3593         /**
3594          * {@inheritDoc}
3595          */
3596         @Override
3597         public void moveQueueItem(final int from, final int to) throws RemoteException {
3598             mService.get().moveQueueItem(from, to);
3599         }
3600
3601         /**
3602          * {@inheritDoc}
3603          */
3604         @Override
3605         public void refresh() throws RemoteException {
3606             mService.get().refresh();
3607         }
3608
3609         /**
3610          * {@inheritDoc}
3611          */
3612         @Override
3613         public void playlistChanged() throws RemoteException {
3614             mService.get().playlistChanged();
3615         }
3616
3617         /**
3618          * {@inheritDoc}
3619          */
3620         @Override
3621         public boolean isPlaying() throws RemoteException {
3622             return mService.get().isPlaying();
3623         }
3624
3625         /**
3626          * {@inheritDoc}
3627          */
3628         @Override
3629         public long[] getQueue() throws RemoteException {
3630             return mService.get().getQueue();
3631         }
3632
3633         /**
3634          * {@inheritDoc}
3635          */
3636         @Override
3637         public long getQueueItemAtPosition(int position) throws RemoteException {
3638             return mService.get().getQueueItemAtPosition(position);
3639         }
3640
3641         /**
3642          * {@inheritDoc}
3643          */
3644         @Override
3645         public int getQueueSize() throws RemoteException {
3646             return mService.get().getQueueSize();
3647         }
3648
3649         /**
3650          * {@inheritDoc}
3651          */
3652         @Override
3653         public int getQueueHistoryPosition(int position) throws RemoteException {
3654             return mService.get().getQueueHistoryPosition(position);
3655         }
3656
3657         /**
3658          * {@inheritDoc}
3659          */
3660         @Override
3661         public int getQueueHistorySize() throws RemoteException {
3662             return mService.get().getQueueHistorySize();
3663         }
3664
3665         /**
3666          * {@inheritDoc}
3667          */
3668         @Override
3669         public int[] getQueueHistoryList() throws RemoteException {
3670             return mService.get().getQueueHistoryList();
3671         }
3672
3673         /**
3674          * {@inheritDoc}
3675          */
3676         @Override
3677         public long duration() throws RemoteException {
3678             return mService.get().duration();
3679         }
3680
3681         /**
3682          * {@inheritDoc}
3683          */
3684         @Override
3685         public long position() throws RemoteException {
3686             return mService.get().position();
3687         }
3688
3689         /**
3690          * {@inheritDoc}
3691          */
3692         @Override
3693         public long seek(final long position) throws RemoteException {
3694             return mService.get().seek(position);
3695         }
3696
3697         /**
3698          * {@inheritDoc}
3699          */
3700         @Override
3701         public void seekRelative(final long deltaInMs) throws RemoteException {
3702             mService.get().seekRelative(deltaInMs);
3703         }
3704
3705         /**
3706          * {@inheritDoc}
3707          */
3708         @Override
3709         public long getAudioId() throws RemoteException {
3710             return mService.get().getAudioId();
3711         }
3712
3713         /**
3714          * {@inheritDoc}
3715          */
3716         @Override
3717         public MusicPlaybackTrack getCurrentTrack() throws RemoteException {
3718             return mService.get().getCurrentTrack();
3719         }
3720
3721         /**
3722          * {@inheritDoc}
3723          */
3724         @Override
3725         public MusicPlaybackTrack getTrack(int index) throws RemoteException {
3726             return mService.get().getTrack(index);
3727         }
3728
3729                 /**
3730          * {@inheritDoc}
3731          */
3732         @Override
3733         public long getNextAudioId() throws RemoteException {
3734             return mService.get().getNextAudioId();
3735         }
3736
3737         /**
3738          * {@inheritDoc}
3739          */
3740         @Override
3741         public long getPreviousAudioId() throws RemoteException {
3742             return mService.get().getPreviousAudioId();
3743         }
3744
3745         /**
3746          * {@inheritDoc}
3747          */
3748         @Override
3749         public long getArtistId() throws RemoteException {
3750             return mService.get().getArtistId();
3751         }
3752
3753         /**
3754          * {@inheritDoc}
3755          */
3756         @Override
3757         public long getAlbumId() throws RemoteException {
3758             return mService.get().getAlbumId();
3759         }
3760
3761         /**
3762          * {@inheritDoc}
3763          */
3764         @Override
3765         public String getArtistName() throws RemoteException {
3766             return mService.get().getArtistName();
3767         }
3768
3769         /**
3770          * {@inheritDoc}
3771          */
3772         @Override
3773         public String getTrackName() throws RemoteException {
3774             return mService.get().getTrackName();
3775         }
3776
3777         /**
3778          * {@inheritDoc}
3779          */
3780         @Override
3781         public String getAlbumName() throws RemoteException {
3782             return mService.get().getAlbumName();
3783         }
3784
3785         /**
3786          * {@inheritDoc}
3787          */
3788         @Override
3789         public String getPath() throws RemoteException {
3790             return mService.get().getPath();
3791         }
3792
3793         /**
3794          * {@inheritDoc}
3795          */
3796         @Override
3797         public int getQueuePosition() throws RemoteException {
3798             return mService.get().getQueuePosition();
3799         }
3800
3801         /**
3802          * {@inheritDoc}
3803          */
3804         @Override
3805         public int getShuffleMode() throws RemoteException {
3806             return mService.get().getShuffleMode();
3807         }
3808
3809         /**
3810          * {@inheritDoc}
3811          */
3812         @Override
3813         public int getRepeatMode() throws RemoteException {
3814             return mService.get().getRepeatMode();
3815         }
3816
3817         /**
3818          * {@inheritDoc}
3819          */
3820         @Override
3821         public int removeTracks(final int first, final int last) throws RemoteException {
3822             return mService.get().removeTracks(first, last);
3823         }
3824
3825         /**
3826          * {@inheritDoc}
3827          */
3828         @Override
3829         public int removeTrack(final long id) throws RemoteException {
3830             return mService.get().removeTrack(id);
3831         }
3832
3833         /**
3834          * {@inheritDoc}
3835          */
3836         @Override
3837         public boolean removeTrackAtPosition(final long id, final int position)
3838                 throws RemoteException {
3839             return mService.get().removeTrackAtPosition(id, position);
3840         }
3841
3842         /**
3843          * {@inheritDoc}
3844          */
3845         @Override
3846         public int getMediaMountedCount() throws RemoteException {
3847             return mService.get().getMediaMountedCount();
3848         }
3849
3850         /**
3851          * {@inheritDoc}
3852          */
3853         @Override
3854         public int getAudioSessionId() throws RemoteException {
3855             return mService.get().getAudioSessionId();
3856         }
3857
3858         /**
3859          * {@inheritDoc}
3860          */
3861         @Override
3862         public void setShakeToPlayEnabled(boolean enabled) {
3863             mService.get().setShakeToPlayEnabled(enabled);
3864         }
3865
3866         /**
3867          * {@inheritDoc}
3868          */
3869         @Override
3870         public void setLockscreenAlbumArt(boolean enabled) {
3871             mService.get().setLockscreenAlbumArt(enabled);
3872         }
3873
3874     }
3875
3876     private class QueueUpdateTask extends AsyncTask<Void, Void, List<MediaSession.QueueItem>> {
3877         private long[] mQueue;
3878
3879         public QueueUpdateTask(long[] queue) {
3880             mQueue = queue != null ? Arrays.copyOf(queue, queue.length) : null;
3881         }
3882
3883         @Override
3884         protected List<MediaSession.QueueItem> doInBackground(Void... params) {
3885             if (mQueue == null || mQueue.length == 0) {
3886                 return null;
3887             }
3888
3889             final StringBuilder selection = new StringBuilder();
3890             selection.append(MediaStore.Audio.Media._ID).append(" IN (");
3891             for (int i = 0; i < mQueue.length; i++) {
3892                 if (i != 0) {
3893                     selection.append(",");
3894                 }
3895                 selection.append(mQueue[i]);
3896             }
3897             selection.append(")");
3898
3899             Cursor c = getContentResolver().query(
3900                     MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
3901                     new String[] { AudioColumns._ID, AudioColumns.TITLE, AudioColumns.ARTIST },
3902                     selection.toString(), null, null);
3903             if (c == null) {
3904                 return null;
3905             }
3906
3907             try {
3908                 LongSparseArray<MediaDescription> descsById = new LongSparseArray<>();
3909                 final int idColumnIndex = c.getColumnIndexOrThrow(AudioColumns._ID);
3910                 final int titleColumnIndex = c.getColumnIndexOrThrow(AudioColumns.TITLE);
3911                 final int artistColumnIndex = c.getColumnIndexOrThrow(AudioColumns.ARTIST);
3912
3913                 while (c.moveToNext() && !isCancelled()) {
3914                     final MediaDescription desc = new MediaDescription.Builder()
3915                             .setTitle(c.getString(titleColumnIndex))
3916                             .setSubtitle(c.getString(artistColumnIndex))
3917                             .build();
3918                     final long id = c.getLong(idColumnIndex);
3919                     descsById.put(id, desc);
3920                 }
3921
3922                 List<MediaSession.QueueItem> items = new ArrayList<>();
3923                 for (int i = 0; i < mQueue.length; i++) {
3924                     MediaDescription desc = descsById.get(mQueue[i]);
3925                     if (desc == null) {
3926                         // shouldn't happen except in corner cases like
3927                         // music being deleted while we were processing
3928                         desc = new MediaDescription.Builder().build();
3929                     }
3930                     items.add(new MediaSession.QueueItem(desc, i));
3931                 }
3932                 return items;
3933             } finally {
3934                 c.close();
3935             }
3936         }
3937
3938         @Override
3939         protected void onPostExecute(List<MediaSession.QueueItem> items) {
3940             if (!isCancelled()) {
3941                 mSession.setQueue(items);
3942             }
3943         }
3944     }
3945 }