OSDN Git Service

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