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.
14 package org.lineageos.eleven;
16 import android.Manifest.permission;
17 import android.annotation.NonNull;
18 import android.annotation.SuppressLint;
19 import android.app.AlarmManager;
20 import android.app.Notification;
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;
64 import org.lineageos.eleven.Config.IdType;
65 import org.lineageos.eleven.appwidgets.AppWidgetLarge;
66 import org.lineageos.eleven.appwidgets.AppWidgetLargeAlternate;
67 import org.lineageos.eleven.appwidgets.AppWidgetSmall;
68 import org.lineageos.eleven.cache.ImageCache;
69 import org.lineageos.eleven.cache.ImageFetcher;
70 import org.lineageos.eleven.provider.MusicPlaybackState;
71 import org.lineageos.eleven.provider.RecentStore;
72 import org.lineageos.eleven.provider.SongPlayCount;
73 import org.lineageos.eleven.service.MusicPlaybackTrack;
74 import org.lineageos.eleven.utils.BitmapWithColors;
75 import org.lineageos.eleven.utils.Lists;
76 import org.lineageos.eleven.utils.PreferenceUtils;
77 import org.lineageos.eleven.utils.ShakeDetector;
78 import org.lineageos.eleven.utils.SrtManager;
81 import java.io.IOException;
82 import java.lang.ref.WeakReference;
83 import java.util.ArrayList;
84 import java.util.Arrays;
85 import java.util.LinkedList;
86 import java.util.List;
87 import java.util.ListIterator;
88 import java.util.Random;
89 import java.util.TreeSet;
92 * A backbround {@link Service} used to keep music playing between activities
93 * and when the user moves Apollo into the background.
95 @SuppressLint("NewApi")
96 public class MusicPlaybackService extends Service {
97 private static final String TAG = "MusicPlaybackService";
98 private static final boolean D = false;
101 * Indicates that the music has paused or resumed
103 public static final String PLAYSTATE_CHANGED = "org.lineageos.eleven.playstatechanged";
106 * Indicates that music playback position within
107 * a title was changed
109 public static final String POSITION_CHANGED = "org.lineageos.eleven.positionchanged";
112 * Indicates the meta data has changed in some way, like a track change
114 public static final String META_CHANGED = "org.lineageos.eleven.metachanged";
117 * Indicates the queue has been updated
119 public static final String QUEUE_CHANGED = "org.lineageos.eleven.queuechanged";
122 * Indicates the queue has been updated
124 public static final String PLAYLIST_CHANGED = "org.lineageos.eleven.playlistchanged";
127 * Indicates the repeat mode changed
129 public static final String REPEATMODE_CHANGED = "org.lineageos.eleven.repeatmodechanged";
132 * Indicates the shuffle mode changed
134 public static final String SHUFFLEMODE_CHANGED = "org.lineageos.eleven.shufflemodechanged";
137 * Indicates the track fails to play
139 public static final String TRACK_ERROR = "org.lineageos.eleven.trackerror";
142 * For backwards compatibility reasons, also provide sticky
143 * broadcasts under the music package
145 public static final String ELEVEN_PACKAGE_NAME = "org.lineageos.eleven";
146 public static final String MUSIC_PACKAGE_NAME = "com.android.music";
149 * Called to indicate a general service commmand. Used in
150 * {@link MediaButtonIntentReceiver}
152 public static final String SERVICECMD = "org.lineageos.eleven.musicservicecommand";
155 * Called to go toggle between pausing and playing the music
157 public static final String TOGGLEPAUSE_ACTION = "org.lineageos.eleven.togglepause";
160 * Called to go to pause the playback
162 public static final String PAUSE_ACTION = "org.lineageos.eleven.pause";
165 * Called to go to stop the playback
167 public static final String STOP_ACTION = "org.lineageos.eleven.stop";
170 * Called to go to the previous track or the beginning of the track if partway through the track
172 public static final String PREVIOUS_ACTION = "org.lineageos.eleven.previous";
175 * Called to go to the previous track regardless of how far in the current track the playback is
177 public static final String PREVIOUS_FORCE_ACTION = "org.lineageos.eleven.previous.force";
180 * Called to go to the next track
182 public static final String NEXT_ACTION = "org.lineageos.eleven.next";
185 * Called to change the repeat mode
187 public static final String REPEAT_ACTION = "org.lineageos.eleven.repeat";
190 * Called to change the shuffle mode
192 public static final String SHUFFLE_ACTION = "org.lineageos.eleven.shuffle";
194 public static final String FROM_MEDIA_BUTTON = "frommediabutton";
196 public static final String TIMESTAMP = "timestamp";
199 * Used to easily notify a list that it should refresh. i.e. A playlist
202 public static final String REFRESH = "org.lineageos.eleven.refresh";
205 * Used by the alarm intent to shutdown the service after being idle
207 private static final String SHUTDOWN = "org.lineageos.eleven.shutdown";
210 * Called to notify of a timed text
212 public static final String NEW_LYRICS = "org.lineageos.eleven.lyrics";
215 * Called to update the remote control client
217 public static final String UPDATE_LOCKSCREEN = "org.lineageos.eleven.updatelockscreen";
219 public static final String CMDNAME = "command";
221 public static final String CMDTOGGLEPAUSE = "togglepause";
223 public static final String CMDSTOP = "stop";
225 public static final String CMDPAUSE = "pause";
227 public static final String CMDPLAY = "play";
229 public static final String CMDPREVIOUS = "previous";
231 public static final String CMDNEXT = "next";
233 public static final String CMDHEADSETHOOK = "headsethook";
235 private static final int IDCOLIDX = 0;
238 * Moves a list to the next position in the queue
240 public static final int NEXT = 2;
243 * Moves a list to the last position in the queue
245 public static final int LAST = 3;
248 * Shuffles no songs, turns shuffling off
250 public static final int SHUFFLE_NONE = 0;
255 public static final int SHUFFLE_NORMAL = 1;
260 public static final int SHUFFLE_AUTO = 2;
265 public static final int REPEAT_NONE = 0;
268 * Repeats the current track in a list
270 public static final int REPEAT_CURRENT = 1;
273 * Repeats all the tracks in a list
275 public static final int REPEAT_ALL = 2;
278 * Indicates when the track ends
280 private static final int TRACK_ENDED = 1;
283 * Indicates that the current track was changed the next track
285 private static final int TRACK_WENT_TO_NEXT = 2;
288 * Indicates the player died
290 private static final int SERVER_DIED = 3;
293 * Indicates some sort of focus change, maybe a phone call
295 private static final int FOCUSCHANGE = 4;
298 * Indicates to fade the volume down
300 private static final int FADEDOWN = 5;
303 * Indicates to fade the volume back up
305 private static final int FADEUP = 6;
308 * Notifies that there is a new timed text string
310 private static final int LYRICS = 7;
313 * Indicates a headset hook key event
315 private static final int HEADSET_HOOK_EVENT = 8;
318 * Indicates waiting for another headset hook event has timed out
320 private static final int HEADSET_HOOK_MULTI_CLICK_TIMEOUT = 9;
323 * Idle time before stopping the foreground notfication (5 minutes)
325 private static final int IDLE_DELAY = 5 * 60 * 1000;
328 * Song play time used as threshold for rewinding to the beginning of the
329 * track instead of skipping to the previous track when getting the PREVIOUS
332 private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
335 * The max size allowed for the track history
336 * TODO: Comeback and rewrite/fix all the whole queue code bugs after demo
337 * https://cyanogen.atlassian.net/browse/MUSIC-175
338 * https://cyanogen.atlassian.net/browse/MUSIC-44
340 public static final int MAX_HISTORY_SIZE = 1000;
342 public interface TrackErrorExtra {
344 * Name of the track that was unable to play
346 public static final String TRACK_NAME = "trackname";
350 * The columns used to retrieve any info from the current track
352 private static final String[] PROJECTION = new String[] {
353 "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
354 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
355 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
356 MediaStore.Audio.Media.ARTIST_ID
360 * The columns used to retrieve any info from the current album
362 private static final String[] ALBUM_PROJECTION = new String[] {
363 MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
364 MediaStore.Audio.Albums.LAST_YEAR
368 * Keeps a mapping of the track history
370 private static LinkedList<Integer> mHistory = Lists.newLinkedList();
373 * Used to shuffle the tracks
375 private static final Shuffler mShuffler = new Shuffler();
380 private final IBinder mBinder = new ServiceStub(this);
385 private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
390 private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
393 * 4x2 alternate widget
395 private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
401 private MultiPlayer mPlayer;
404 * The path of the current file to play
406 private String mFileToPlay;
409 * Alarm intent for removing the notification when nothing is playing
412 private AlarmManager mAlarmManager;
413 private PendingIntent mShutdownIntent;
414 private boolean mShutdownScheduled;
416 private NotificationManager mNotificationManager;
419 * The cursor used to retrieve info on the current track and run the
420 * necessary queries to play audio files
422 private Cursor mCursor;
425 * The cursor used to retrieve info on the album the current track is
428 private Cursor mAlbumCursor;
431 * Monitors the audio state
433 private AudioManager mAudioManager;
436 * Settings used to save and retrieve the queue and history
438 private SharedPreferences mPreferences;
441 * Used to know when the service is active
443 private boolean mServiceInUse = false;
446 * Used to know if something should be playing or not
448 private boolean mIsSupposedToBePlaying = false;
451 * Gets the last played time to determine whether we still want notifications or not
453 private long mLastPlayedTime;
455 private int mNotifyMode = NOTIFY_MODE_NONE;
456 private long mNotificationPostTime = 0;
458 private static final int NOTIFY_MODE_NONE = 0;
459 private static final int NOTIFY_MODE_FOREGROUND = 1;
460 private static final int NOTIFY_MODE_BACKGROUND = 2;
463 * Used to indicate if the queue can be saved
465 private boolean mQueueIsSaveable = true;
468 * Used to track what type of audio focus loss caused the playback to pause
470 private boolean mPausedByTransientLossOfFocus = false;
473 * Lock screen controls
475 private MediaSession mSession;
477 // We use this to distinguish between different cards when saving/restoring
481 private int mPlayPos = -1;
483 private int mNextPlayPos = -1;
485 private int mOpenFailedCounter = 0;
487 private int mMediaMountedCount = 0;
489 private int mShuffleMode = SHUFFLE_NONE;
491 private int mRepeatMode = REPEAT_NONE;
493 private int mServiceStartId = -1;
495 private String mLyrics;
497 private ArrayList<MusicPlaybackTrack> mPlaylist = new ArrayList<MusicPlaybackTrack>(100);
499 private long[] mAutoShuffleList = null;
501 private MusicPlayerHandler mPlayerHandler;
502 private HandlerThread mHandlerThread;
504 private BroadcastReceiver mUnmountReceiver = null;
506 // to improve perf, instead of hitting the disk cache or file cache, store the bitmaps in memory
507 private String mCachedKey;
508 private BitmapWithColors[] mCachedBitmapWithColors = new BitmapWithColors[2];
510 private QueueUpdateTask mQueueUpdateTask;
515 private ImageFetcher mImageFetcher;
518 * Recently listened database
520 private RecentStore mRecentsCache;
523 * The song play count database
525 private SongPlayCount mSongPlayCountCache;
528 * Stores the playback state
530 private MusicPlaybackState mPlaybackStateStore;
533 * Shake detector class used for shake to switch song feature
535 private ShakeDetector mShakeDetector;
538 * Switch for displaying album art on lockscreen
540 private boolean mShowAlbumArtOnLockscreen;
542 private boolean mReadGranted = false;
544 private PowerManager.WakeLock mHeadsetHookWakeLock;
546 private ShakeDetector.Listener mShakeDetectorListener=new ShakeDetector.Listener() {
549 public void hearShake() {
551 * on shake detect, play next song
554 Log.d(TAG,"Shake detected!!!");
564 public IBinder onBind(final Intent intent) {
565 if (D) Log.d(TAG, "Service bound, intent = " + intent);
567 mServiceInUse = true;
575 public boolean onUnbind(final Intent intent) {
576 if (D) Log.d(TAG, "Service unbound");
577 mServiceInUse = false;
581 if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
582 // Something is currently playing, or will be playing once
583 // an in-progress action requesting audio focus ends, so don't stop
587 // If there is a playlist but playback is paused, then wait a while
588 // before stopping the service, so that pause/resume isn't slow.
589 // Also delay stopping the service if we're transitioning between
591 } else if (mPlaylist.size() > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
592 scheduleDelayedShutdown();
596 stopSelf(mServiceStartId);
605 public void onRebind(final Intent intent) {
607 mServiceInUse = true;
614 public void onCreate() {
615 if (D) Log.d(TAG, "Creating service");
618 if (checkSelfPermission(permission.READ_EXTERNAL_STORAGE) !=
619 PackageManager.PERMISSION_GRANTED) {
626 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
628 // Initialize the favorites and recents databases
629 mRecentsCache = RecentStore.getInstance(this);
631 // gets the song play count cache
632 mSongPlayCountCache = SongPlayCount.getInstance(this);
634 // gets a pointer to the playback state store
635 mPlaybackStateStore = MusicPlaybackState.getInstance(this);
637 // Initialize the image fetcher
638 mImageFetcher = ImageFetcher.getInstance(this);
639 // Initialize the image cache
640 mImageFetcher.setImageCache(ImageCache.getInstance(this));
642 // Start up the thread running the service. Note that we create a
643 // separate thread because the service normally runs in the process's
644 // main thread, which we don't want to block. We also make it
645 // background priority so CPU-intensive work will not disrupt the UI.
646 mHandlerThread = new HandlerThread("MusicPlayerHandler",
647 android.os.Process.THREAD_PRIORITY_BACKGROUND);
648 mHandlerThread.start();
650 // Initialize the handler
651 mPlayerHandler = new MusicPlayerHandler(this, mHandlerThread.getLooper());
653 // Initialize the audio manager and register any headset controls for
655 mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
657 // Use the remote control APIs to set the playback state
660 // Initialize the preferences
661 mPreferences = getSharedPreferences("Service", 0);
662 mCardId = getCardId();
664 mShowAlbumArtOnLockscreen = mPreferences.getBoolean(
665 PreferenceUtils.SHOW_ALBUM_ART_ON_LOCKSCREEN, true);
666 setShakeToPlayEnabled(mPreferences.getBoolean(PreferenceUtils.SHAKE_TO_PLAY, false));
668 mRepeatMode = mPreferences.getInt("repeatmode", REPEAT_NONE);
669 mShuffleMode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
671 registerExternalStorageListener();
673 // Initialize the media player
674 mPlayer = new MultiPlayer(this);
675 mPlayer.setHandler(mPlayerHandler);
677 // Initialize the intent filter and each action
678 final IntentFilter filter = new IntentFilter();
679 filter.addAction(SERVICECMD);
680 filter.addAction(TOGGLEPAUSE_ACTION);
681 filter.addAction(PAUSE_ACTION);
682 filter.addAction(STOP_ACTION);
683 filter.addAction(NEXT_ACTION);
684 filter.addAction(PREVIOUS_ACTION);
685 filter.addAction(PREVIOUS_FORCE_ACTION);
686 filter.addAction(REPEAT_ACTION);
687 filter.addAction(SHUFFLE_ACTION);
688 // Attach the broadcast listener
689 registerReceiver(mIntentReceiver, filter);
691 // Get events when MediaStore content changes
692 mMediaStoreObserver = new MediaStoreObserver(mPlayerHandler);
693 getContentResolver().registerContentObserver(
694 MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mMediaStoreObserver);
695 getContentResolver().registerContentObserver(
696 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mMediaStoreObserver);
698 // Initialize the delayed shutdown intent
699 final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
700 shutdownIntent.setAction(SHUTDOWN);
702 mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
703 mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
705 // Listen for the idle state
706 scheduleDelayedShutdown();
708 // Bring the queue back
710 notifyChange(QUEUE_CHANGED);
711 notifyChange(META_CHANGED);
714 private void setUpMediaSession() {
715 mSession = new MediaSession(this, "Eleven");
716 mSession.setCallback(new MediaSession.Callback() {
718 public void onPause() {
720 mPausedByTransientLossOfFocus = false;
723 public void onPlay() {
727 public void onSeekTo(long pos) {
731 public void onSkipToNext() {
735 public void onSkipToPrevious() {
739 public void onStop() {
741 mPausedByTransientLossOfFocus = false;
743 releaseServiceUiAndStop();
746 public void onSkipToQueueItem(long id) {
747 setQueuePosition((int) id);
750 public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
751 if (Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
752 KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
753 if (ke != null && ke.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) {
754 if (ke.getAction() == KeyEvent.ACTION_UP) {
755 handleHeadsetHookClick(ke.getEventTime());
760 return super.onMediaButtonEvent(mediaButtonIntent);
764 PendingIntent pi = PendingIntent.getBroadcast(this, 0,
765 new Intent(this, MediaButtonIntentReceiver.class),
766 PendingIntent.FLAG_UPDATE_CURRENT);
767 mSession.setMediaButtonReceiver(pi);
769 mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
770 | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
777 public void onDestroy() {
778 if (D) Log.d(TAG, "Destroying service");
783 // Remove any sound effects
784 final Intent audioEffectsIntent = new Intent(
785 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
786 audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
787 audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
788 sendBroadcast(audioEffectsIntent);
790 // remove any pending alarms
791 mAlarmManager.cancel(mShutdownIntent);
793 // Remove any callbacks from the handler
794 mPlayerHandler.removeCallbacksAndMessages(null);
795 // quit the thread so that anything that gets posted won't run
796 mHandlerThread.quitSafely();
798 // Release the player
802 // Remove the audio focus listener and lock screen controls
803 mAudioManager.abandonAudioFocus(mAudioFocusListener);
806 // remove the media store observer
807 getContentResolver().unregisterContentObserver(mMediaStoreObserver);
812 // Unregister the mount listener
813 unregisterReceiver(mIntentReceiver);
814 if (mUnmountReceiver != null) {
815 unregisterReceiver(mUnmountReceiver);
816 mUnmountReceiver = null;
819 // deinitialize shake detector
820 stopShakeDetector(true);
827 public int onStartCommand(final Intent intent, final int flags, final int startId) {
828 if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
829 mServiceStartId = startId;
831 if (intent != null) {
832 final String action = intent.getAction();
834 if (SHUTDOWN.equals(action)) {
835 mShutdownScheduled = false;
836 releaseServiceUiAndStop();
837 return START_NOT_STICKY;
840 handleCommandIntent(intent);
843 // Make sure the service will shut down on its own if it was
844 // just started but not bound to and nothing is playing
845 scheduleDelayedShutdown();
847 if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
848 MediaButtonIntentReceiver.completeWakefulIntent(intent);
851 return START_NOT_STICKY;
854 private void releaseServiceUiAndStop() {
856 || mPausedByTransientLossOfFocus
857 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
861 if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
862 cancelNotification();
863 mAudioManager.abandonAudioFocus(mAudioFocusListener);
864 mSession.setActive(false);
866 if (!mServiceInUse) {
868 stopSelf(mServiceStartId);
872 private void handleCommandIntent(Intent intent) {
873 final String action = intent.getAction();
874 final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
876 if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
878 if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
880 } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)
881 || PREVIOUS_FORCE_ACTION.equals(action)) {
882 prev(PREVIOUS_FORCE_ACTION.equals(action));
883 } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
885 } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
887 mPausedByTransientLossOfFocus = false;
888 } else if (CMDPLAY.equals(command)) {
890 } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
892 mPausedByTransientLossOfFocus = false;
894 releaseServiceUiAndStop();
895 } else if (REPEAT_ACTION.equals(action)) {
897 } else if (SHUFFLE_ACTION.equals(action)) {
899 } else if (CMDHEADSETHOOK.equals(command)) {
900 long timestamp = intent.getLongExtra(TIMESTAMP, 0);
901 handleHeadsetHookClick(timestamp);
905 private void handleHeadsetHookClick(long timestamp) {
906 if (mHeadsetHookWakeLock == null) {
907 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
908 mHeadsetHookWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
909 "Eleven headset button");
910 mHeadsetHookWakeLock.setReferenceCounted(false);
912 // Make sure we don't indefinitely hold the wake lock under any circumstances
913 mHeadsetHookWakeLock.acquire(10000);
915 Message msg = mPlayerHandler.obtainMessage(HEADSET_HOOK_EVENT, Long.valueOf(timestamp));
920 * Updates the notification, considering the current play and activity state
922 private void updateNotification() {
923 final int newNotifyMode;
925 newNotifyMode = NOTIFY_MODE_FOREGROUND;
926 } else if (recentlyPlayed()) {
927 newNotifyMode = NOTIFY_MODE_BACKGROUND;
929 newNotifyMode = NOTIFY_MODE_NONE;
932 int notificationId = hashCode();
933 if (mNotifyMode != newNotifyMode) {
934 if (mNotifyMode == NOTIFY_MODE_FOREGROUND) {
935 stopForeground(newNotifyMode == NOTIFY_MODE_NONE);
936 } else if (newNotifyMode == NOTIFY_MODE_NONE) {
937 mNotificationManager.cancel(notificationId);
938 mNotificationPostTime = 0;
942 if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
943 startForeground(notificationId, buildNotification());
944 } else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
945 mNotificationManager.notify(notificationId, buildNotification());
948 mNotifyMode = newNotifyMode;
951 private void cancelNotification() {
952 stopForeground(true);
953 mNotificationManager.cancel(hashCode());
954 mNotificationPostTime = 0;
955 mNotifyMode = NOTIFY_MODE_NONE;
959 * @return A card ID used to save and restore playlists, i.e., the queue.
961 private int getCardId() {
962 final ContentResolver resolver = getContentResolver();
963 Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
966 if (cursor != null && cursor.moveToFirst()) {
967 mCardId = cursor.getInt(0);
975 * Called when we receive a ACTION_MEDIA_EJECT notification.
977 * @param storagePath The path to mount point for the removed media
979 public void closeExternalStorageFiles(final String storagePath) {
981 notifyChange(QUEUE_CHANGED);
982 notifyChange(META_CHANGED);
986 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
987 * intent will call closeExternalStorageFiles() if the external media is
988 * going to be ejected, so applications can clean up any files they have
991 public void registerExternalStorageListener() {
992 if (mUnmountReceiver == null) {
993 mUnmountReceiver = new BroadcastReceiver() {
999 public void onReceive(final Context context, final Intent intent) {
1000 final String action = intent.getAction();
1001 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
1003 mQueueIsSaveable = false;
1004 closeExternalStorageFiles(intent.getData().getPath());
1005 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
1006 mMediaMountedCount++;
1007 mCardId = getCardId();
1009 mQueueIsSaveable = true;
1010 notifyChange(QUEUE_CHANGED);
1011 notifyChange(META_CHANGED);
1015 final IntentFilter filter = new IntentFilter();
1016 filter.addAction(Intent.ACTION_MEDIA_EJECT);
1017 filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
1018 filter.addDataScheme("file");
1019 registerReceiver(mUnmountReceiver, filter);
1023 private void scheduleDelayedShutdown() {
1024 if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
1025 if (!mReadGranted) {
1028 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
1029 SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
1030 mShutdownScheduled = true;
1033 private void cancelShutdown() {
1034 if (D) Log.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled);
1035 if (mShutdownScheduled) {
1036 mAlarmManager.cancel(mShutdownIntent);
1037 mShutdownScheduled = false;
1044 * @param goToIdle True to go to the idle state, false otherwise
1046 private void stop(final boolean goToIdle) {
1047 if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
1048 if (mPlayer.isInitialized()) {
1054 setIsSupposedToBePlaying(false, false);
1056 stopForeground(false);
1061 * Removes the range of tracks specified from the play list. If a file
1062 * within the range is the file currently being played, playback will move
1063 * to the next file after the range.
1065 * @param first The first file to be removed
1066 * @param last The last file to be removed
1067 * @return the number of tracks deleted
1069 private int removeTracksInternal(int first, int last) {
1070 synchronized (this) {
1073 } else if (first < 0) {
1075 } else if (last >= mPlaylist.size()) {
1076 last = mPlaylist.size() - 1;
1079 boolean gotonext = false;
1080 if (first <= mPlayPos && mPlayPos <= last) {
1083 } else if (mPlayPos > last) {
1084 mPlayPos -= last - first + 1;
1086 final int numToRemove = last - first + 1;
1088 if (first == 0 && last == mPlaylist.size() - 1) {
1094 for (int i = 0; i < numToRemove; i++) {
1095 mPlaylist.remove(first);
1098 // remove the items from the history
1099 // this is not ideal as the history shouldn't be impacted by this
1100 // but since we are removing items from the array, it will throw
1101 // an exception if we keep it around. Idealistically with the queue
1102 // rewrite this should be all be fixed
1103 // https://cyanogen.atlassian.net/browse/MUSIC-44
1104 ListIterator<Integer> positionIterator = mHistory.listIterator();
1105 while (positionIterator.hasNext()) {
1106 int pos = positionIterator.next();
1107 if (pos >= first && pos <= last) {
1108 positionIterator.remove();
1109 } else if (pos > last) {
1110 positionIterator.set(pos - numToRemove);
1115 if (mPlaylist.size() == 0) {
1120 if (mShuffleMode != SHUFFLE_NONE) {
1121 mPlayPos = getNextPosition(true);
1122 } else if (mPlayPos >= mPlaylist.size()) {
1125 final boolean wasPlaying = isPlaying();
1127 openCurrentAndNext();
1132 notifyChange(META_CHANGED);
1134 return last - first + 1;
1139 * Adds a list to the playlist
1141 * @param list The list to add
1142 * @param position The position to place the tracks
1144 private void addToPlayList(final long[] list, int position, long sourceId, IdType sourceType) {
1145 final int addlen = list.length;
1151 mPlaylist.ensureCapacity(mPlaylist.size() + addlen);
1152 if (position > mPlaylist.size()) {
1153 position = mPlaylist.size();
1156 final ArrayList<MusicPlaybackTrack> arrayList = new ArrayList<MusicPlaybackTrack>(addlen);
1157 for (int i = 0; i < list.length; i++) {
1158 arrayList.add(new MusicPlaybackTrack(list[i], sourceId, sourceType, i));
1161 mPlaylist.addAll(position, arrayList);
1163 if (mPlaylist.size() == 0) {
1165 notifyChange(META_CHANGED);
1170 * @param trackId The track ID
1172 private void updateCursor(final long trackId) {
1173 updateCursor("_id=" + trackId, null);
1176 private void updateCursor(final String selection, final String[] selectionArgs) {
1177 synchronized (this) {
1179 mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1180 PROJECTION, selection, selectionArgs);
1182 updateAlbumCursor();
1185 private void updateCursor(final Uri uri) {
1186 synchronized (this) {
1188 mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
1190 updateAlbumCursor();
1193 private void updateAlbumCursor() {
1194 long albumId = getAlbumId();
1196 mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
1197 ALBUM_PROJECTION, "_id=" + albumId, null);
1199 mAlbumCursor = null;
1203 private Cursor openCursorAndGoToFirst(Uri uri, String[] projection,
1204 String selection, String[] selectionArgs) {
1205 Cursor c = getContentResolver().query(uri, projection,
1206 selection, selectionArgs, null, null);
1210 if (!c.moveToFirst()) {
1217 private synchronized void closeCursor() {
1218 if (mCursor != null) {
1222 if (mAlbumCursor != null) {
1223 mAlbumCursor.close();
1224 mAlbumCursor = null;
1229 * Called to open a new file as the current track and prepare the next for
1232 private void openCurrentAndNext() {
1233 openCurrentAndMaybeNext(true);
1237 * Called to open a new file as the current track and prepare the next for
1240 * @param openNext True to prepare the next track for playback, false
1243 private void openCurrentAndMaybeNext(final boolean openNext) {
1244 synchronized (this) {
1247 if (mPlaylist.size() == 0) {
1252 boolean shutdown = false;
1254 updateCursor(mPlaylist.get(mPlayPos).mId);
1257 && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
1258 + mCursor.getLong(IDCOLIDX))) {
1262 // if we get here then opening the file failed. We can close the
1263 // cursor now, because
1264 // we're either going to create a new one next, or stop trying
1266 if (mOpenFailedCounter++ < 10 && mPlaylist.size() > 1) {
1267 final int pos = getNextPosition(false);
1275 updateCursor(mPlaylist.get(mPlayPos).mId);
1277 mOpenFailedCounter = 0;
1278 Log.w(TAG, "Failed to open file for playback");
1285 scheduleDelayedShutdown();
1286 if (mIsSupposedToBePlaying) {
1287 mIsSupposedToBePlaying = false;
1288 notifyChange(PLAYSTATE_CHANGED);
1290 } else if (openNext) {
1296 private void sendErrorMessage(final String trackName) {
1297 final Intent i = new Intent(TRACK_ERROR);
1298 i.putExtra(TrackErrorExtra.TRACK_NAME, trackName);
1303 * @param force True to force the player onto the track next, false
1305 * @param saveToHistory True to save the mPlayPos to the history
1306 * @return The next position to play.
1308 private int getNextPosition(final boolean force) {
1309 // as a base case, if the playlist is empty just return -1
1310 if (mPlaylist == null || mPlaylist.isEmpty()) {
1313 // if we're not forced to go to the next track and we are only playing the current track
1314 if (!force && mRepeatMode == REPEAT_CURRENT) {
1319 } else if (mShuffleMode == SHUFFLE_NORMAL) {
1320 final int numTracks = mPlaylist.size();
1322 // count the number of times a track has been played
1323 final int[] trackNumPlays = new int[numTracks];
1324 for (int i = 0; i < numTracks; i++) {
1326 trackNumPlays[i] = 0;
1329 // walk through the history and add up the number of times the track
1331 final int numHistory = mHistory.size();
1332 for (int i = 0; i < numHistory; i++) {
1333 final int idx = mHistory.get(i).intValue();
1334 if (idx >= 0 && idx < numTracks) {
1335 trackNumPlays[idx]++;
1339 // also add the currently playing track to the count
1340 if (mPlayPos >= 0 && mPlayPos < numTracks) {
1341 trackNumPlays[mPlayPos]++;
1344 // figure out the least # of times a track has a played as well as
1345 // how many tracks share that count
1346 int minNumPlays = Integer.MAX_VALUE;
1347 int numTracksWithMinNumPlays = 0;
1348 for (int i = 0; i < trackNumPlays.length; i++) {
1349 // if we found a new track that has less number of plays, reset the counters
1350 if (trackNumPlays[i] < minNumPlays) {
1351 minNumPlays = trackNumPlays[i];
1352 numTracksWithMinNumPlays = 1;
1353 } else if (trackNumPlays[i] == minNumPlays) {
1354 // increment this track shares the # of tracks
1355 numTracksWithMinNumPlays++;
1359 // if we've played each track at least once and all tracks have been played an equal
1360 // # of times and we aren't repeating all and we're not forcing a track, then
1361 // return no more tracks
1362 if (minNumPlays > 0 && numTracksWithMinNumPlays == numTracks
1363 && mRepeatMode != REPEAT_ALL && !force) {
1367 // else pick a track from the least number of played tracks
1368 int skip = mShuffler.nextInt(numTracksWithMinNumPlays);
1369 for (int i = 0; i < trackNumPlays.length; i++) {
1370 if (trackNumPlays[i] == minNumPlays) {
1379 // Unexpected to land here
1380 if (D) Log.e(TAG, "Getting the next position resulted did not get a result when it should have");
1382 } else if (mShuffleMode == SHUFFLE_AUTO) {
1383 doAutoShuffleUpdate();
1384 return mPlayPos + 1;
1386 if (mPlayPos >= mPlaylist.size() - 1) {
1387 if (mRepeatMode == REPEAT_NONE && !force) {
1389 } else if (mRepeatMode == REPEAT_ALL || force) {
1394 return mPlayPos + 1;
1400 * Sets the track to be played
1402 private void setNextTrack() {
1403 setNextTrack(getNextPosition(false));
1407 * Sets the next track to be played
1408 * @param position the target position we want
1410 private void setNextTrack(int position) {
1411 mNextPlayPos = position;
1412 if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
1413 if (mNextPlayPos >= 0 && mPlaylist != null && mNextPlayPos < mPlaylist.size()) {
1414 final long id = mPlaylist.get(mNextPlayPos).mId;
1415 mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
1417 mPlayer.setNextDataSource(null);
1422 * Creates a shuffled playlist used for party mode
1424 private boolean makeAutoShuffleList() {
1425 Cursor cursor = null;
1427 cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1429 MediaStore.Audio.Media._ID
1430 }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
1431 if (cursor == null || cursor.getCount() == 0) {
1434 final int len = cursor.getCount();
1435 final long[] list = new long[len];
1436 for (int i = 0; i < len; i++) {
1437 cursor.moveToNext();
1438 list[i] = cursor.getLong(0);
1440 mAutoShuffleList = list;
1442 } catch (final RuntimeException e) {
1444 if (cursor != null) {
1453 * Creates the party shuffle playlist
1455 private void doAutoShuffleUpdate() {
1456 boolean notify = false;
1457 if (mPlayPos > 10) {
1458 removeTracks(0, mPlayPos - 9);
1461 final int toAdd = 7 - (mPlaylist.size() - (mPlayPos < 0 ? -1 : mPlayPos));
1462 for (int i = 0; i < toAdd; i++) {
1463 int lookback = mHistory.size();
1466 idx = mShuffler.nextInt(mAutoShuffleList.length);
1467 if (!wasRecentlyUsed(idx, lookback)) {
1473 if (mHistory.size() > MAX_HISTORY_SIZE) {
1476 mPlaylist.add(new MusicPlaybackTrack(mAutoShuffleList[idx], -1, IdType.NA, -1));
1480 notifyChange(QUEUE_CHANGED);
1485 private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
1486 if (lookbacksize == 0) {
1489 final int histsize = mHistory.size();
1490 if (histsize < lookbacksize) {
1491 lookbacksize = histsize;
1493 final int maxidx = histsize - 1;
1494 for (int i = 0; i < lookbacksize; i++) {
1495 final long entry = mHistory.get(maxidx - i);
1504 * Notify the change-receivers that something has changed.
1506 private void notifyChange(final String what) {
1507 if (D) Log.d(TAG, "notifyChange: what = " + what);
1509 // Update the lockscreen controls
1510 updateMediaSession(what);
1512 if (what.equals(POSITION_CHANGED)) {
1516 final Intent intent = new Intent(what);
1517 intent.putExtra("id", getAudioId());
1518 intent.putExtra("artist", getArtistName());
1519 intent.putExtra("album", getAlbumName());
1520 intent.putExtra("track", getTrackName());
1521 intent.putExtra("playing", isPlaying());
1523 if (NEW_LYRICS.equals(what)) {
1524 intent.putExtra("lyrics", mLyrics);
1527 sendStickyBroadcast(intent);
1529 final Intent musicIntent = new Intent(intent);
1530 musicIntent.setAction(what.replace(ELEVEN_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
1531 sendStickyBroadcast(musicIntent);
1533 if (what.equals(META_CHANGED)) {
1534 // Add the track to the recently played list.
1535 mRecentsCache.addSongId(getAudioId());
1537 mSongPlayCountCache.bumpSongCount(getAudioId());
1538 } else if (what.equals(QUEUE_CHANGED)) {
1541 // if we are in shuffle mode and our next track is still valid,
1542 // try to re-use the track
1543 // We need to reimplement the queue to prevent hacky solutions like this
1544 // https://cyanogen.atlassian.net/browse/MUSIC-175
1545 // https://cyanogen.atlassian.net/browse/MUSIC-44
1546 if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size()
1547 && getShuffleMode() != SHUFFLE_NONE) {
1548 setNextTrack(mNextPlayPos);
1557 if (what.equals(PLAYSTATE_CHANGED)) {
1558 updateNotification();
1561 // Update the app-widgets
1562 mAppWidgetSmall.notifyChange(this, what);
1563 mAppWidgetLarge.notifyChange(this, what);
1564 mAppWidgetLargeAlternate.notifyChange(this, what);
1567 private void updateMediaSession(final String what) {
1568 int playState = mIsSupposedToBePlaying
1569 ? PlaybackState.STATE_PLAYING
1570 : PlaybackState.STATE_PAUSED;
1572 long playBackStateActions = PlaybackState.ACTION_PLAY |
1573 PlaybackState.ACTION_PLAY_PAUSE |
1574 PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
1575 PlaybackState.ACTION_PAUSE |
1576 PlaybackState.ACTION_SKIP_TO_NEXT |
1577 PlaybackState.ACTION_SKIP_TO_PREVIOUS |
1578 PlaybackState.ACTION_STOP;
1580 if (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED)) {
1581 mSession.setPlaybackState(new PlaybackState.Builder()
1582 .setActions(playBackStateActions)
1583 .setActiveQueueItemId(getAudioId())
1584 .setState(playState, position(), 1.0f).build());
1585 } else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
1586 Bitmap albumArt = getAlbumArt(false).getBitmap();
1587 if (albumArt != null) {
1588 // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
1589 // to make sure not to hand out our cache copy
1590 Bitmap.Config config = albumArt.getConfig();
1591 if (config == null) {
1592 config = Bitmap.Config.ARGB_8888;
1594 albumArt = albumArt.copy(config, false);
1597 mSession.setMetadata(new MediaMetadata.Builder()
1598 .putString(MediaMetadata.METADATA_KEY_ARTIST, getArtistName())
1599 .putString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST, getAlbumArtistName())
1600 .putString(MediaMetadata.METADATA_KEY_ALBUM, getAlbumName())
1601 .putString(MediaMetadata.METADATA_KEY_TITLE, getTrackName())
1602 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration())
1603 .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, getQueuePosition() + 1)
1604 .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, getQueue().length)
1605 .putString(MediaMetadata.METADATA_KEY_GENRE, getGenreName())
1606 .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART,
1607 mShowAlbumArtOnLockscreen ? albumArt : null)
1610 if (what.equals(QUEUE_CHANGED)) {
1611 updateMediaSessionQueue();
1614 mSession.setPlaybackState(new PlaybackState.Builder()
1615 .setActions(playBackStateActions)
1616 .setActiveQueueItemId(getAudioId())
1617 .setState(playState, position(), 1.0f).build());
1621 private synchronized void updateMediaSessionQueue() {
1622 if (mQueueUpdateTask != null) {
1623 mQueueUpdateTask.cancel(true);
1625 mQueueUpdateTask = new QueueUpdateTask(getQueue());
1626 mQueueUpdateTask.execute();
1629 private Notification buildNotification() {
1630 final String albumName = getAlbumName();
1631 final String artistName = getArtistName();
1632 final boolean isPlaying = isPlaying();
1633 String text = TextUtils.isEmpty(albumName)
1634 ? artistName : artistName + " - " + albumName;
1636 int playButtonResId = isPlaying
1637 ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play;
1638 int playButtonTitleResId = isPlaying
1639 ? R.string.accessibility_pause : R.string.accessibility_play;
1641 Notification.MediaStyle style = new Notification.MediaStyle()
1642 .setMediaSession(mSession.getSessionToken())
1643 .setShowActionsInCompactView(0, 1, 2);
1645 Intent nowPlayingIntent = new Intent("org.lineageos.eleven.AUDIO_PLAYER")
1646 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1647 PendingIntent clickIntent = PendingIntent.getActivity(this, 0, nowPlayingIntent, 0);
1648 BitmapWithColors artwork = getAlbumArt(false);
1650 if (mNotificationPostTime == 0) {
1651 mNotificationPostTime = System.currentTimeMillis();
1654 Notification.Builder builder = new Notification.Builder(this)
1655 .setSmallIcon(R.drawable.ic_notification)
1656 .setLargeIcon(artwork.getBitmap())
1657 .setContentIntent(clickIntent)
1658 .setContentTitle(getTrackName())
1659 .setContentText(text)
1660 .setWhen(mNotificationPostTime)
1663 .setVisibility(Notification.VISIBILITY_PUBLIC)
1664 .addAction(R.drawable.btn_playback_previous,
1665 getString(R.string.accessibility_prev),
1666 retrievePlaybackAction(PREVIOUS_ACTION))
1667 .addAction(playButtonResId, getString(playButtonTitleResId),
1668 retrievePlaybackAction(TOGGLEPAUSE_ACTION))
1669 .addAction(R.drawable.btn_playback_next,
1670 getString(R.string.accessibility_next),
1671 retrievePlaybackAction(NEXT_ACTION));
1673 builder.setColor(artwork.getVibrantDarkColor());
1675 return builder.build();
1678 private final PendingIntent retrievePlaybackAction(final String action) {
1679 final ComponentName serviceName = new ComponentName(this, MusicPlaybackService.class);
1680 Intent intent = new Intent(action);
1681 intent.setComponent(serviceName);
1683 return PendingIntent.getService(this, 0, intent, 0);
1689 * @param full True if the queue is full
1691 private void saveQueue(final boolean full) {
1692 if (!mQueueIsSaveable || mPreferences == null) {
1696 final SharedPreferences.Editor editor = mPreferences.edit();
1698 mPlaybackStateStore.saveState(mPlaylist,
1699 mShuffleMode != SHUFFLE_NONE ? mHistory : null);
1700 editor.putInt("cardid", mCardId);
1702 editor.putInt("curpos", mPlayPos);
1703 if (mPlayer.isInitialized()) {
1704 editor.putLong("seekpos", mPlayer.position());
1706 editor.putInt("repeatmode", mRepeatMode);
1707 editor.putInt("shufflemode", mShuffleMode);
1712 * Reloads the queue as the user left it the last time they stopped using
1715 private void reloadQueue() {
1717 if (mPreferences.contains("cardid")) {
1718 id = mPreferences.getInt("cardid", ~mCardId);
1720 if (id == mCardId) {
1721 mPlaylist = mPlaybackStateStore.getQueue();
1723 if (mPlaylist.size() > 0) {
1724 final int pos = mPreferences.getInt("curpos", 0);
1725 if (pos < 0 || pos >= mPlaylist.size()) {
1730 updateCursor(mPlaylist.get(mPlayPos).mId);
1731 if (mCursor == null) {
1732 SystemClock.sleep(3000);
1733 updateCursor(mPlaylist.get(mPlayPos).mId);
1735 synchronized (this) {
1737 mOpenFailedCounter = 20;
1738 openCurrentAndNext();
1740 if (!mPlayer.isInitialized()) {
1745 final long seekpos = mPreferences.getLong("seekpos", 0);
1746 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
1749 Log.d(TAG, "restored queue, currently at position "
1750 + position() + "/" + duration()
1751 + " (requested " + seekpos + ")");
1754 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
1755 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
1756 repmode = REPEAT_NONE;
1758 mRepeatMode = repmode;
1760 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
1761 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
1762 shufmode = SHUFFLE_NONE;
1764 if (shufmode != SHUFFLE_NONE) {
1765 mHistory = mPlaybackStateStore.getHistory(mPlaylist.size());
1767 if (shufmode == SHUFFLE_AUTO) {
1768 if (!makeAutoShuffleList()) {
1769 shufmode = SHUFFLE_NONE;
1772 mShuffleMode = shufmode;
1777 * Opens a file and prepares it for playback
1779 * @param path The path of the file to open
1781 public boolean openFile(final String path) {
1782 if (D) Log.d(TAG, "openFile: path = " + path);
1783 synchronized (this) {
1788 // If mCursor is null, try to associate path with a database cursor
1789 if (mCursor == null) {
1790 Uri uri = Uri.parse(path);
1791 boolean shouldAddToPlaylist = true; // should try adding audio info to playlist
1794 id = Long.valueOf(uri.getLastPathSegment());
1795 } catch (NumberFormatException ex) {
1799 if (id != -1 && path.startsWith(
1800 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
1803 } else if (id != -1 && path.startsWith(
1804 MediaStore.Files.getContentUri("external").toString())) {
1807 // handle downloaded media files
1808 } else if ( path.startsWith("content://downloads/") ) {
1810 // extract MediaProvider(MP) uri , if available
1811 // Downloads.Impl.COLUMN_MEDIAPROVIDER_URI
1812 String mpUri = getValueForDownloadedFile(this, uri, "mediaprovider_uri");
1813 if (D) Log.i(TAG, "Downloaded file's MP uri : " + mpUri);
1814 if ( !TextUtils.isEmpty(mpUri) ) {
1815 // if mpUri is valid, play that URI instead
1816 if (openFile(mpUri)) {
1817 // notify impending change in track
1818 notifyChange(META_CHANGED);
1824 // create phantom cursor with download info, if a MP uri wasn't found
1825 updateCursorForDownloadedFile(this, uri);
1826 shouldAddToPlaylist = false; // song info isn't available in MediaStore
1830 // assuming a "file://" uri by this point ...
1831 String where = MediaStore.Audio.Media.DATA + "=?";
1832 String[] selectionArgs = new String[]{path};
1833 updateCursor(where, selectionArgs);
1836 if (mCursor != null && shouldAddToPlaylist) {
1838 mPlaylist.add(new MusicPlaybackTrack(
1839 mCursor.getLong(IDCOLIDX), -1, IdType.NA, -1));
1840 // propagate the change in playlist state
1841 notifyChange(QUEUE_CHANGED);
1845 } catch (final UnsupportedOperationException ex) {
1851 mPlayer.setDataSource(mFileToPlay);
1852 if (mPlayer.isInitialized()) {
1853 mOpenFailedCounter = 0;
1857 String trackName = getTrackName();
1858 if (TextUtils.isEmpty(trackName)) {
1861 sendErrorMessage(trackName);
1869 Columns for a pseudo cursor we are creating for downloaded songs
1870 Modeled after mCursor to be able to respond to respond to the same queries as it
1872 private static final String[] PROJECTION_MATRIX = new String[] {
1873 "_id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
1874 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
1875 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
1876 MediaStore.Audio.Media.ARTIST_ID
1880 * Creates a pseudo cursor for downloaded audio files with minimal info
1881 * @param context needed to query the download uri
1882 * @param uri the uri of the downloaded file
1884 private void updateCursorForDownloadedFile(Context context, Uri uri) {
1885 synchronized (this) {
1886 closeCursor(); // clear mCursor
1887 MatrixCursor cursor = new MatrixCursor(PROJECTION_MATRIX);
1888 // get title of the downloaded file ; Downloads.Impl.COLUMN_TITLE
1889 String title = getValueForDownloadedFile(this, uri, "title" );
1890 // populating the cursor with bare minimum info
1891 cursor.addRow(new Object[] {
1902 mCursor.moveToFirst();
1907 * Query the DownloadProvider to get the value in the specified column
1909 * @param uri the uri of the downloaded file
1913 private String getValueForDownloadedFile(Context context, Uri uri, String column) {
1915 Cursor cursor = null;
1916 final String[] projection = {
1921 cursor = context.getContentResolver().query(uri, projection, null, null, null);
1922 if (cursor != null && cursor.moveToFirst()) {
1923 return cursor.getString(0);
1926 if (cursor != null) {
1934 * Returns the audio session ID
1936 * @return The current media player audio session ID
1938 public int getAudioSessionId() {
1939 synchronized (this) {
1940 return mPlayer.getAudioSessionId();
1945 * Indicates if the media storeage device has been mounted or not
1947 * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
1949 public int getMediaMountedCount() {
1950 return mMediaMountedCount;
1954 * Returns the shuffle mode
1956 * @return The current shuffle mode (all, party, none)
1958 public int getShuffleMode() {
1959 return mShuffleMode;
1963 * Returns the repeat mode
1965 * @return The current repeat mode (all, one, none)
1967 public int getRepeatMode() {
1972 * Removes all instances of the track with the given ID from the playlist.
1974 * @param id The id to be removed
1975 * @return how many instances of the track were removed
1977 public int removeTrack(final long id) {
1979 synchronized (this) {
1980 for (int i = 0; i < mPlaylist.size(); i++) {
1981 if (mPlaylist.get(i).mId == id) {
1982 numremoved += removeTracksInternal(i, i);
1987 if (numremoved > 0) {
1988 notifyChange(QUEUE_CHANGED);
1994 * Removes a song from the playlist at the specified position.
1996 * @param id The song id to be removed
1997 * @param position The position of the song in the playlist
1998 * @return true if successful
2000 public boolean removeTrackAtPosition(final long id, final int position) {
2001 synchronized (this) {
2002 if ( position >=0 &&
2003 position < mPlaylist.size() &&
2004 mPlaylist.get(position).mId == id ) {
2006 return removeTracks(position, position) > 0;
2013 * Removes the range of tracks specified from the play list. If a file
2014 * within the range is the file currently being played, playback will move
2015 * to the next file after the range.
2017 * @param first The first file to be removed
2018 * @param last The last file to be removed
2019 * @return the number of tracks deleted
2021 public int removeTracks(final int first, final int last) {
2022 final int numremoved = removeTracksInternal(first, last);
2023 if (numremoved > 0) {
2024 notifyChange(QUEUE_CHANGED);
2030 * Returns the position in the queue
2032 * @return the current position in the queue
2034 public int getQueuePosition() {
2035 synchronized (this) {
2041 * @return the size of the queue history cache
2043 public int getQueueHistorySize() {
2044 synchronized (this) {
2045 return mHistory.size();
2050 * @return the position in the history
2052 public int getQueueHistoryPosition(int position) {
2053 synchronized (this) {
2054 if (position >= 0 && position < mHistory.size()) {
2055 return mHistory.get(position);
2063 * @return the queue of history positions
2065 public int[] getQueueHistoryList() {
2066 synchronized (this) {
2067 int[] history = new int[mHistory.size()];
2068 for (int i = 0; i < mHistory.size(); i++) {
2069 history[i] = mHistory.get(i);
2077 * Returns the path to current song
2079 * @return The path to the current song
2081 public String getPath() {
2082 synchronized (this) {
2083 if (mCursor == null) {
2086 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
2091 * Returns the album name
2093 * @return The current song album Name
2095 public String getAlbumName() {
2096 synchronized (this) {
2097 if (mCursor == null) {
2100 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
2105 * Returns the song name
2107 * @return The current song name
2109 public String getTrackName() {
2110 synchronized (this) {
2111 if (mCursor == null) {
2114 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
2119 * Returns the genre name of song
2121 * @return The current song genre name
2123 public String getGenreName() {
2124 synchronized (this) {
2125 if (mCursor == null || mPlayPos < 0 || mPlayPos >= mPlaylist.size()) {
2128 String[] genreProjection = { MediaStore.Audio.Genres.NAME };
2129 Uri genreUri = MediaStore.Audio.Genres.getContentUriForAudioId("external",
2130 (int) mPlaylist.get(mPlayPos).mId);
2131 Cursor genreCursor = getContentResolver().query(genreUri, genreProjection,
2133 if (genreCursor != null) {
2135 if (genreCursor.moveToFirst()) {
2136 return genreCursor.getString(
2137 genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME));
2140 genreCursor.close();
2148 * Returns the artist name
2150 * @return The current song artist name
2152 public String getArtistName() {
2153 synchronized (this) {
2154 if (mCursor == null) {
2157 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
2162 * Returns the artist name
2164 * @return The current song artist name
2166 public String getAlbumArtistName() {
2167 synchronized (this) {
2168 if (mAlbumCursor == null) {
2171 return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(AlbumColumns.ARTIST));
2176 * Returns the album ID
2178 * @return The current song album ID
2180 public long getAlbumId() {
2181 synchronized (this) {
2182 if (mCursor == null) {
2185 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
2190 * Returns the artist ID
2192 * @return The current song artist ID
2194 public long getArtistId() {
2195 synchronized (this) {
2196 if (mCursor == null) {
2199 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
2204 * @return The audio id of the track
2206 public long getAudioId() {
2207 MusicPlaybackTrack track = getCurrentTrack();
2208 if (track != null) {
2216 * Gets the currently playing music track
2218 public MusicPlaybackTrack getCurrentTrack() {
2219 return getTrack(mPlayPos);
2223 * Gets the music track from the queue at the specified index
2224 * @param index position
2225 * @return music track or null
2227 public synchronized MusicPlaybackTrack getTrack(int index) {
2228 if (index >= 0 && index < mPlaylist.size() && mPlayer.isInitialized()) {
2229 return mPlaylist.get(index);
2236 * Returns the next audio ID
2238 * @return The next track ID
2240 public long getNextAudioId() {
2241 synchronized (this) {
2242 if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size() && mPlayer.isInitialized()) {
2243 return mPlaylist.get(mNextPlayPos).mId;
2250 * Returns the previous audio ID
2252 * @return The previous track ID
2254 public long getPreviousAudioId() {
2255 synchronized (this) {
2256 if (mPlayer.isInitialized()) {
2257 int pos = getPreviousPlayPosition(false);
2258 if (pos >= 0 && pos < mPlaylist.size()) {
2259 return mPlaylist.get(pos).mId;
2267 * Seeks the current track to a specific time
2269 * @param position The time to seek to
2270 * @return The time to play the track at
2272 public long seek(long position) {
2273 if (mPlayer.isInitialized()) {
2276 } else if (position > mPlayer.duration()) {
2277 position = mPlayer.duration();
2279 long result = mPlayer.seek(position);
2280 notifyChange(POSITION_CHANGED);
2287 * Seeks the current track to a position relative to its current position
2288 * If the relative position is after or before the track, it will also automatically
2289 * jump to the previous or next track respectively
2291 * @param deltaInMs The delta time to seek to in milliseconds
2293 public void seekRelative(long deltaInMs) {
2294 synchronized (this) {
2295 if (mPlayer.isInitialized()) {
2296 final long newPos = position() + deltaInMs;
2297 final long duration = duration();
2300 // seek to the new duration + the leftover position
2301 seek(duration() + newPos);
2302 } else if (newPos >= duration) {
2304 // seek to the leftover duration
2305 seek(newPos - duration);
2314 * Returns the current position in time of the currenttrack
2316 * @return The current playback position in miliseconds
2318 public long position() {
2319 if (mPlayer.isInitialized()) {
2320 return mPlayer.position();
2326 * Returns the full duration of the current track
2328 * @return The duration of the current track in miliseconds
2330 public long duration() {
2331 if (mPlayer.isInitialized()) {
2332 return mPlayer.duration();
2340 * @return The queue as a long[]
2342 public long[] getQueue() {
2343 synchronized (this) {
2344 final int len = mPlaylist.size();
2345 final long[] list = new long[len];
2346 for (int i = 0; i < len; i++) {
2347 list[i] = mPlaylist.get(i).mId;
2354 * Gets the track id at a given position in the queue
2356 * @return track id in the queue position
2358 public long getQueueItemAtPosition(int position) {
2359 synchronized (this) {
2360 if (position >= 0 && position < mPlaylist.size()) {
2361 return mPlaylist.get(position).mId;
2369 * @return the size of the queue
2371 public int getQueueSize() {
2372 synchronized (this) {
2373 return mPlaylist.size();
2378 * @return True if music is playing, false otherwise
2380 public boolean isPlaying() {
2381 return mIsSupposedToBePlaying;
2385 * Helper function to wrap the logic around mIsSupposedToBePlaying for consistentcy
2386 * @param value to set mIsSupposedToBePlaying to
2387 * @param notify whether we want to fire PLAYSTATE_CHANGED event
2389 private void setIsSupposedToBePlaying(boolean value, boolean notify) {
2390 if (mIsSupposedToBePlaying != value) {
2391 mIsSupposedToBePlaying = value;
2393 // Update mLastPlayed time first and notify afterwards, as
2394 // the notification listener method needs the up-to-date value
2395 // for the recentlyPlayed() method to work
2396 if (!mIsSupposedToBePlaying) {
2397 scheduleDelayedShutdown();
2398 mLastPlayedTime = System.currentTimeMillis();
2402 notifyChange(PLAYSTATE_CHANGED);
2408 * @return true if is playing or has played within the last IDLE_DELAY time
2410 private boolean recentlyPlayed() {
2411 return isPlaying() || System.currentTimeMillis() - mLastPlayedTime < IDLE_DELAY;
2415 * Opens a list for playback
2417 * @param list The list of tracks to open
2418 * @param position The position to start playback at
2420 public void open(final long[] list, final int position, long sourceId, IdType sourceType) {
2421 synchronized (this) {
2422 if (mShuffleMode == SHUFFLE_AUTO) {
2423 mShuffleMode = SHUFFLE_NORMAL;
2425 final long oldId = getAudioId();
2426 final int listlength = list.length;
2427 boolean newlist = true;
2428 if (mPlaylist.size() == listlength) {
2430 for (int i = 0; i < listlength; i++) {
2431 if (list[i] != mPlaylist.get(i).mId) {
2438 addToPlayList(list, -1, sourceId, sourceType);
2439 notifyChange(QUEUE_CHANGED);
2441 if (position >= 0) {
2442 mPlayPos = position;
2444 mPlayPos = mShuffler.nextInt(mPlaylist.size());
2447 openCurrentAndNext();
2448 if (oldId != getAudioId()) {
2449 notifyChange(META_CHANGED);
2457 public void stop() {
2458 stopShakeDetector(false);
2463 * Resumes or starts playback.
2465 public void play() {
2466 startShakeDetector();
2471 * Resumes or starts playback.
2472 * @param createNewNextTrack True if you want to figure out the next track, false
2473 * if you want to re-use the existing next track (used for going back)
2475 public void play(boolean createNewNextTrack) {
2476 int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
2477 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
2479 if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
2481 if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
2485 final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
2486 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2487 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2488 sendBroadcast(intent);
2490 mSession.setActive(true);
2492 if (createNewNextTrack) {
2495 setNextTrack(mNextPlayPos);
2498 if (mPlayer.isInitialized()) {
2499 final long duration = mPlayer.duration();
2500 if (mRepeatMode != REPEAT_CURRENT && duration > 2000
2501 && mPlayer.position() >= duration - 2000) {
2506 mPlayerHandler.removeMessages(FADEDOWN);
2507 mPlayerHandler.sendEmptyMessage(FADEUP);
2509 setIsSupposedToBePlaying(true, true);
2512 updateNotification();
2513 } else if (mPlaylist.size() <= 0) {
2514 setShuffleMode(SHUFFLE_AUTO);
2518 private void togglePlayPause() {
2521 mPausedByTransientLossOfFocus = false;
2528 * Temporarily pauses playback.
2530 public void pause() {
2531 if (mPlayerHandler == null) return;
2532 if (D) Log.d(TAG, "Pausing playback");
2533 synchronized (this) {
2534 if (mPlayerHandler != null) {
2535 mPlayerHandler.removeMessages(FADEUP);
2537 if (mIsSupposedToBePlaying) {
2538 final Intent intent = new Intent(
2539 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
2540 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2541 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2542 sendBroadcast(intent);
2544 if (mPlayer != null) {
2547 setIsSupposedToBePlaying(false, true);
2548 stopShakeDetector(false);
2554 * Changes from the current track to the next track
2556 public void gotoNext(final boolean force) {
2557 if (D) Log.d(TAG, "Going to next track");
2558 synchronized (this) {
2559 if (mPlaylist.size() <= 0) {
2560 if (D) Log.d(TAG, "No play queue");
2561 scheduleDelayedShutdown();
2564 int pos = mNextPlayPos;
2566 pos = getNextPosition(force);
2570 setIsSupposedToBePlaying(false, true);
2575 setAndRecordPlayPos(pos);
2576 openCurrentAndNext();
2578 notifyChange(META_CHANGED);
2582 public void setAndRecordPlayPos(int nextPos) {
2583 synchronized (this) {
2584 // save to the history
2585 if (mShuffleMode != SHUFFLE_NONE) {
2586 mHistory.add(mPlayPos);
2587 if (mHistory.size() > MAX_HISTORY_SIZE) {
2597 * Changes from the current track to the previous played track
2599 public void prev(boolean forcePrevious) {
2600 synchronized (this) {
2601 // if we aren't repeating 1, and we are either early in the song
2602 // or we want to force go back, then go to the prevous track
2603 boolean goPrevious = getRepeatMode() != REPEAT_CURRENT &&
2604 (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD || forcePrevious);
2607 if (D) Log.d(TAG, "Going to previous track");
2608 int pos = getPreviousPlayPosition(true);
2609 // if we have no more previous tracks, quit
2613 mNextPlayPos = mPlayPos;
2618 notifyChange(META_CHANGED);
2620 if (D) Log.d(TAG, "Going to beginning of track");
2627 public int getPreviousPlayPosition(boolean removeFromHistory) {
2628 synchronized (this) {
2629 if (mShuffleMode == SHUFFLE_NORMAL) {
2630 // Go to previously-played track and remove it from the history
2631 final int histsize = mHistory.size();
2632 if (histsize == 0) {
2635 final Integer pos = mHistory.get(histsize - 1);
2636 if (removeFromHistory) {
2637 mHistory.remove(histsize - 1);
2639 return pos.intValue();
2642 return mPlayPos - 1;
2644 return mPlaylist.size() - 1;
2651 * We don't want to open the current and next track when the user is using
2652 * the {@code #prev()} method because they won't be able to travel back to
2653 * the previously listened track if they're shuffling.
2655 private void openCurrent() {
2656 openCurrentAndMaybeNext(false);
2660 * Moves an item in the queue from one position to another
2662 * @param from The position the item is currently at
2663 * @param to The position the item is being moved to
2665 public void moveQueueItem(int index1, int index2) {
2666 synchronized (this) {
2667 if (index1 >= mPlaylist.size()) {
2668 index1 = mPlaylist.size() - 1;
2670 if (index2 >= mPlaylist.size()) {
2671 index2 = mPlaylist.size() - 1;
2674 if (index1 == index2) {
2678 final MusicPlaybackTrack track = mPlaylist.remove(index1);
2679 if (index1 < index2) {
2680 mPlaylist.add(index2, track);
2681 if (mPlayPos == index1) {
2683 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
2686 } else if (index2 < index1) {
2687 mPlaylist.add(index2, track);
2688 if (mPlayPos == index1) {
2690 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
2694 notifyChange(QUEUE_CHANGED);
2699 * Sets the repeat mode
2701 * @param repeatmode The repeat mode to use
2703 public void setRepeatMode(final int repeatmode) {
2704 synchronized (this) {
2705 mRepeatMode = repeatmode;
2708 notifyChange(REPEATMODE_CHANGED);
2713 * Sets the shuffle mode
2715 * @param shufflemode The shuffle mode to use
2717 public void setShuffleMode(final int shufflemode) {
2718 synchronized (this) {
2719 if (mShuffleMode == shufflemode && mPlaylist.size() > 0) {
2723 mShuffleMode = shufflemode;
2724 if (mShuffleMode == SHUFFLE_AUTO) {
2725 if (makeAutoShuffleList()) {
2727 doAutoShuffleUpdate();
2729 openCurrentAndNext();
2731 notifyChange(META_CHANGED);
2734 mShuffleMode = SHUFFLE_NONE;
2740 notifyChange(SHUFFLEMODE_CHANGED);
2745 * Sets the position of a track in the queue
2747 * @param index The position to place the track
2749 public void setQueuePosition(final int index) {
2750 synchronized (this) {
2753 openCurrentAndNext();
2755 notifyChange(META_CHANGED);
2756 if (mShuffleMode == SHUFFLE_AUTO) {
2757 doAutoShuffleUpdate();
2763 * Queues a new list for playback
2765 * @param list The list to queue
2766 * @param action The action to take
2768 public void enqueue(final long[] list, final int action, long sourceId, IdType sourceType) {
2769 synchronized (this) {
2770 if (action == NEXT && mPlayPos + 1 < mPlaylist.size()) {
2771 addToPlayList(list, mPlayPos + 1, sourceId, sourceType);
2772 mNextPlayPos = mPlayPos + 1;
2773 notifyChange(QUEUE_CHANGED);
2775 addToPlayList(list, Integer.MAX_VALUE, sourceId, sourceType);
2776 notifyChange(QUEUE_CHANGED);
2781 openCurrentAndNext();
2783 notifyChange(META_CHANGED);
2789 * Cycles through the different repeat modes
2791 private void cycleRepeat() {
2792 if (mRepeatMode == REPEAT_NONE) {
2793 setRepeatMode(REPEAT_ALL);
2794 } else if (mRepeatMode == REPEAT_ALL) {
2795 setRepeatMode(REPEAT_CURRENT);
2796 if (mShuffleMode != SHUFFLE_NONE) {
2797 setShuffleMode(SHUFFLE_NONE);
2800 setRepeatMode(REPEAT_NONE);
2805 * Cycles through the different shuffle modes
2807 private void cycleShuffle() {
2808 if (mShuffleMode == SHUFFLE_NONE) {
2809 setShuffleMode(SHUFFLE_NORMAL);
2810 if (mRepeatMode == REPEAT_CURRENT) {
2811 setRepeatMode(REPEAT_ALL);
2813 } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
2814 setShuffleMode(SHUFFLE_NONE);
2819 * @param smallBitmap true to return a smaller version of the default artwork image.
2820 * Currently Has no impact on the artwork size if one exists
2821 * @return The album art for the current album.
2823 public BitmapWithColors getAlbumArt(boolean smallBitmap) {
2824 final String albumName = getAlbumName();
2825 final String artistName = getArtistName();
2826 final long albumId = getAlbumId();
2827 final String key = albumName + "_" + artistName + "_" + albumId;
2828 final int targetIndex = smallBitmap ? 0 : 1;
2830 // if the cached key matches and we have the bitmap, return it
2831 if (key.equals(mCachedKey) && mCachedBitmapWithColors[targetIndex] != null) {
2832 return mCachedBitmapWithColors[targetIndex];
2835 // otherwise get the artwork (or defaultartwork if none found)
2836 final BitmapWithColors bitmap = mImageFetcher.getArtwork(albumName,
2837 albumId, artistName, smallBitmap);
2839 // if the key is different, clear the bitmaps first
2840 if (!key.equals(mCachedKey)) {
2841 mCachedBitmapWithColors[0] = null;
2842 mCachedBitmapWithColors[1] = null;
2845 // store the new key and bitmap
2847 mCachedBitmapWithColors[targetIndex] = bitmap;
2852 * Called when one of the lists should refresh or requery.
2854 public void refresh() {
2855 notifyChange(REFRESH);
2859 * Called when one of the playlists have changed (renamed, added/removed tracks)
2861 public void playlistChanged() {
2862 notifyChange(PLAYLIST_CHANGED);
2866 * Called to set the status of shake to play feature
2868 public void setShakeToPlayEnabled(boolean enabled) {
2870 Log.d(TAG, "ShakeToPlay status: " + enabled);
2873 if (mShakeDetector == null) {
2874 mShakeDetector = new ShakeDetector(mShakeDetectorListener);
2876 // if song is already playing, start listening immediately
2878 startShakeDetector();
2882 stopShakeDetector(true);
2887 * Called to set visibility of album art on lockscreen
2889 public void setLockscreenAlbumArt(boolean enabled) {
2890 mShowAlbumArtOnLockscreen = enabled;
2891 notifyChange(META_CHANGED);
2895 * Called to start listening to shakes
2897 private void startShakeDetector() {
2898 if (mShakeDetector != null) {
2899 mShakeDetector.start((SensorManager)getSystemService(SENSOR_SERVICE));
2904 * Called to stop listening to shakes
2906 private void stopShakeDetector(final boolean destroyShakeDetector) {
2907 if (mShakeDetector != null) {
2908 mShakeDetector.stop();
2910 if(destroyShakeDetector){
2911 mShakeDetector = null;
2913 Log.d(TAG, "ShakeToPlay destroyed!!!");
2918 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
2923 public void onReceive(final Context context, final Intent intent) {
2924 final String command = intent.getStringExtra(CMDNAME);
2926 if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
2927 final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2928 mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
2929 } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
2930 final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2931 mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
2932 } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
2933 final int[] largeAlt = intent
2934 .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2935 mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
2937 handleCommandIntent(intent);
2942 private ContentObserver mMediaStoreObserver;
2944 private class MediaStoreObserver extends ContentObserver implements Runnable {
2945 // milliseconds to delay before calling refresh to aggregate events
2946 private static final long REFRESH_DELAY = 500;
2947 private Handler mHandler;
2949 public MediaStoreObserver(Handler handler) {
2955 public void onChange(boolean selfChange) {
2956 // if a change is detected, remove any scheduled callback
2957 // then post a new one. This is intended to prevent closely
2958 // spaced events from generating multiple refresh calls
2959 mHandler.removeCallbacks(this);
2960 mHandler.postDelayed(this, REFRESH_DELAY);
2965 // actually call refresh when the delayed callback fires
2966 Log.e("ELEVEN", "calling refresh!");
2971 private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
2976 public void onAudioFocusChange(final int focusChange) {
2977 mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
2981 private static final class MusicPlayerHandler extends Handler {
2982 private final WeakReference<MusicPlaybackService> mService;
2983 private float mCurrentVolume = 1.0f;
2985 private static final int DOUBLE_CLICK_TIMEOUT = 800;
2986 private int mHeadsetHookClickCounter = 0;
2989 * Constructor of <code>MusicPlayerHandler</code>
2991 * @param service The service to use.
2992 * @param looper The thread to run on.
2994 public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
2996 mService = new WeakReference<MusicPlaybackService>(service);
3003 public void handleMessage(final Message msg) {
3004 final MusicPlaybackService service = mService.get();
3005 if (service == null) {
3009 synchronized (service) {
3012 mCurrentVolume -= .05f;
3013 if (mCurrentVolume > .2f) {
3014 sendEmptyMessageDelayed(FADEDOWN, 10);
3016 mCurrentVolume = .2f;
3018 service.mPlayer.setVolume(mCurrentVolume);
3021 mCurrentVolume += .01f;
3022 if (mCurrentVolume < 1.0f) {
3023 sendEmptyMessageDelayed(FADEUP, 10);
3025 mCurrentVolume = 1.0f;
3027 service.mPlayer.setVolume(mCurrentVolume);
3030 if (service.isPlaying()) {
3031 final TrackErrorInfo info = (TrackErrorInfo)msg.obj;
3032 service.sendErrorMessage(info.mTrackName);
3034 // since the service isPlaying(), we only need to remove the offending
3035 // audio track, and the code will automatically play the next track
3036 service.removeTrack(info.mId);
3038 service.openCurrentAndNext();
3041 case TRACK_WENT_TO_NEXT:
3042 service.setAndRecordPlayPos(service.mNextPlayPos);
3043 service.setNextTrack();
3044 if (service.mCursor != null) {
3045 service.mCursor.close();
3046 service.mCursor = null;
3048 service.updateCursor(service.mPlaylist.get(service.mPlayPos).mId);
3049 service.notifyChange(META_CHANGED);
3050 service.updateNotification();
3053 if (service.mRepeatMode == REPEAT_CURRENT) {
3057 service.gotoNext(false);
3061 service.mLyrics = (String) msg.obj;
3062 service.notifyChange(NEW_LYRICS);
3065 if (D) Log.d(TAG, "Received audio focus change event " + msg.arg1);
3067 case AudioManager.AUDIOFOCUS_LOSS:
3068 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
3069 if (service.isPlaying()) {
3070 service.mPausedByTransientLossOfFocus =
3071 msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
3075 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
3076 removeMessages(FADEUP);
3077 sendEmptyMessage(FADEDOWN);
3079 case AudioManager.AUDIOFOCUS_GAIN:
3080 if (!service.isPlaying()
3081 && service.mPausedByTransientLossOfFocus) {
3082 service.mPausedByTransientLossOfFocus = false;
3083 mCurrentVolume = 0f;
3084 service.mPlayer.setVolume(mCurrentVolume);
3087 removeMessages(FADEDOWN);
3088 sendEmptyMessage(FADEUP);
3094 case HEADSET_HOOK_EVENT: {
3095 long eventTime = (Long) msg.obj;
3097 mHeadsetHookClickCounter = Math.min(mHeadsetHookClickCounter + 1, 3);
3098 if (D) Log.d(TAG, "Got headset click, count = " + mHeadsetHookClickCounter);
3099 removeMessages(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3101 if (mHeadsetHookClickCounter == 3) {
3102 sendEmptyMessage(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3104 sendEmptyMessageAtTime(HEADSET_HOOK_MULTI_CLICK_TIMEOUT,
3105 eventTime + DOUBLE_CLICK_TIMEOUT);
3109 case HEADSET_HOOK_MULTI_CLICK_TIMEOUT:
3110 if (D) Log.d(TAG, "Handling headset click");
3111 switch (mHeadsetHookClickCounter) {
3112 case 1: service.togglePlayPause(); break;
3113 case 2: service.gotoNext(true); break;
3114 case 3: service.prev(false); break;
3116 mHeadsetHookClickCounter = 0;
3117 service.mHeadsetHookWakeLock.release();
3126 private static final class Shuffler {
3128 private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<Integer>();
3130 private final TreeSet<Integer> mPreviousNumbers = new TreeSet<Integer>();
3132 private final Random mRandom = new Random();
3134 private int mPrevious;
3137 * Constructor of <code>Shuffler</code>
3144 * @param interval The length the queue
3145 * @return The position of the next track to play
3147 public int nextInt(final int interval) {
3150 next = mRandom.nextInt(interval);
3151 } while (next == mPrevious && interval > 1
3152 && !mPreviousNumbers.contains(Integer.valueOf(next)));
3154 mHistoryOfNumbers.add(mPrevious);
3155 mPreviousNumbers.add(mPrevious);
3161 * Removes old tracks and cleans up the history preparing for new tracks
3162 * to be added to the mapping
3164 private void cleanUpHistory() {
3165 if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
3166 for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
3167 mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
3173 private static final class TrackErrorInfo {
3175 public String mTrackName;
3177 public TrackErrorInfo(long id, String trackName) {
3179 mTrackName = trackName;
3183 private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
3184 MediaPlayer.OnCompletionListener {
3186 private final WeakReference<MusicPlaybackService> mService;
3188 private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
3190 private MediaPlayer mNextMediaPlayer;
3192 private Handler mHandler;
3194 private boolean mIsInitialized = false;
3196 private SrtManager mSrtManager;
3198 private String mNextMediaPath;
3201 * Constructor of <code>MultiPlayer</code>
3203 public MultiPlayer(final MusicPlaybackService service) {
3204 mService = new WeakReference<MusicPlaybackService>(service);
3205 mSrtManager = new SrtManager() {
3207 public void onTimedText(String text) {
3208 mHandler.obtainMessage(LYRICS, text).sendToTarget();
3214 * @param path The path of the file, or the http/rtsp URL of the stream
3217 public void setDataSource(final String path) {
3218 mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
3219 if (mIsInitialized) {
3221 setNextDataSource(null);
3225 private void loadSrt(final String path) {
3226 mSrtManager.reset();
3228 Uri uri = Uri.parse(path);
3229 String filePath = null;
3231 if (path.startsWith("content://")) {
3232 // resolve the content resolver path to a file path
3233 Cursor cursor = null;
3235 final String[] proj = {MediaStore.Audio.Media.DATA};
3236 cursor = mService.get().getContentResolver().query(uri, proj,
3238 if (cursor != null && cursor.moveToFirst()) {
3239 filePath = cursor.getString(0);
3242 if (cursor != null) {
3248 filePath = uri.getPath();
3251 if (!TextUtils.isEmpty(filePath)) {
3252 final int lastIndex = filePath.lastIndexOf('.');
3253 if (lastIndex != -1) {
3254 String newPath = filePath.substring(0, lastIndex) + ".srt";
3255 final File f = new File(newPath);
3257 mSrtManager.initialize(mCurrentMediaPlayer, f);
3263 * @param player The {@link MediaPlayer} to use
3264 * @param path The path of the file, or the http/rtsp URL of the stream
3266 * @return True if the <code>player</code> has been prepared and is
3267 * ready to play, false otherwise
3269 private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
3272 player.setOnPreparedListener(null);
3273 if (path.startsWith("content://")) {
3274 player.setDataSource(mService.get(), Uri.parse(path));
3276 player.setDataSource(path);
3278 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
3281 } catch (final IOException todo) {
3282 // TODO: notify the user why the file couldn't be opened
3284 } catch (final IllegalArgumentException todo) {
3285 // TODO: notify the user why the file couldn't be opened
3288 player.setOnCompletionListener(this);
3289 player.setOnErrorListener(this);
3294 * Set the MediaPlayer to start when this MediaPlayer finishes playback.
3296 * @param path The path of the file, or the http/rtsp URL of the stream
3299 public void setNextDataSource(final String path) {
3300 mNextMediaPath = null;
3302 mCurrentMediaPlayer.setNextMediaPlayer(null);
3303 } catch (IllegalArgumentException e) {
3304 Log.i(TAG, "Next media player is current one, continuing");
3305 } catch (IllegalStateException e) {
3306 Log.e(TAG, "Media player not initialized!");
3309 if (mNextMediaPlayer != null) {
3310 mNextMediaPlayer.release();
3311 mNextMediaPlayer = null;
3316 mNextMediaPlayer = new MediaPlayer();
3317 mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
3318 if (setDataSourceImpl(mNextMediaPlayer, path)) {
3319 mNextMediaPath = path;
3320 mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
3322 if (mNextMediaPlayer != null) {
3323 mNextMediaPlayer.release();
3324 mNextMediaPlayer = null;
3332 * @param handler The handler to use
3334 public void setHandler(final Handler handler) {
3339 * @return True if the player is ready to go, false otherwise
3341 public boolean isInitialized() {
3342 return mIsInitialized;
3346 * Starts or resumes playback.
3348 public void start() {
3349 mCurrentMediaPlayer.start();
3354 * Resets the MediaPlayer to its uninitialized state.
3356 public void stop() {
3357 mCurrentMediaPlayer.reset();
3358 mSrtManager.reset();
3359 mIsInitialized = false;
3363 * Releases resources associated with this MediaPlayer object.
3365 public void release() {
3366 mCurrentMediaPlayer.release();
3367 mSrtManager.release();
3372 * Pauses playback. Call start() to resume.
3374 public void pause() {
3375 mCurrentMediaPlayer.pause();
3376 mSrtManager.pause();
3380 * Gets the duration of the file.
3382 * @return The duration in milliseconds
3384 public long duration() {
3385 return mCurrentMediaPlayer.getDuration();
3389 * Gets the current playback position.
3391 * @return The current position in milliseconds
3393 public long position() {
3394 return mCurrentMediaPlayer.getCurrentPosition();
3398 * Gets the current playback position.
3400 * @param whereto The offset in milliseconds from the start to seek to
3401 * @return The offset in milliseconds from the start to seek to
3403 public long seek(final long whereto) {
3404 mCurrentMediaPlayer.seekTo((int)whereto);
3405 mSrtManager.seekTo(whereto);
3410 * Sets the volume on this player.
3412 * @param vol Left and right volume scalar
3414 public void setVolume(final float vol) {
3415 mCurrentMediaPlayer.setVolume(vol, vol);
3419 * Sets the audio session ID.
3421 * @param sessionId The audio session ID
3423 public void setAudioSessionId(final int sessionId) {
3424 mCurrentMediaPlayer.setAudioSessionId(sessionId);
3428 * Returns the audio session ID.
3430 * @return The current audio session ID.
3432 public int getAudioSessionId() {
3433 return mCurrentMediaPlayer.getAudioSessionId();
3440 public boolean onError(final MediaPlayer mp, final int what, final int extra) {
3441 Log.w(TAG, "Music Server Error what: " + what + " extra: " + extra);
3443 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
3444 final MusicPlaybackService service = mService.get();
3445 if (service == null) {
3448 final TrackErrorInfo errorInfo = new TrackErrorInfo(service.getAudioId(),
3449 service.getTrackName());
3451 mIsInitialized = false;
3452 mCurrentMediaPlayer.release();
3453 mCurrentMediaPlayer = new MediaPlayer();
3454 Message msg = mHandler.obtainMessage(SERVER_DIED, errorInfo);
3455 mHandler.sendMessageDelayed(msg, 2000);
3467 public void onCompletion(final MediaPlayer mp) {
3468 if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
3469 mCurrentMediaPlayer.release();
3470 mCurrentMediaPlayer = mNextMediaPlayer;
3471 loadSrt(mNextMediaPath);
3472 mNextMediaPath = null;
3473 mNextMediaPlayer = null;
3474 mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
3476 mHandler.sendEmptyMessage(TRACK_ENDED);
3481 private static final class ServiceStub extends IElevenService.Stub {
3483 private final WeakReference<MusicPlaybackService> mService;
3485 private ServiceStub(final MusicPlaybackService service) {
3486 mService = new WeakReference<MusicPlaybackService>(service);
3493 public void openFile(final String path) throws RemoteException {
3494 mService.get().openFile(path);
3501 public void open(final long[] list, final int position, long sourceId, int sourceType)
3502 throws RemoteException {
3503 mService.get().open(list, position, sourceId, IdType.getTypeById(sourceType));
3510 public void stop() throws RemoteException {
3511 mService.get().stop();
3518 public void pause() throws RemoteException {
3519 mService.get().pause();
3526 public void play() throws RemoteException {
3527 mService.get().play();
3534 public void prev(boolean forcePrevious) throws RemoteException {
3535 mService.get().prev(forcePrevious);
3542 public void next() throws RemoteException {
3543 mService.get().gotoNext(true);
3550 public void enqueue(final long[] list, final int action, long sourceId, int sourceType)
3551 throws RemoteException {
3552 mService.get().enqueue(list, action, sourceId, IdType.getTypeById(sourceType));
3559 public void setQueuePosition(final int index) throws RemoteException {
3560 mService.get().setQueuePosition(index);
3567 public void setShuffleMode(final int shufflemode) throws RemoteException {
3568 mService.get().setShuffleMode(shufflemode);
3575 public void setRepeatMode(final int repeatmode) throws RemoteException {
3576 mService.get().setRepeatMode(repeatmode);
3583 public void moveQueueItem(final int from, final int to) throws RemoteException {
3584 mService.get().moveQueueItem(from, to);
3591 public void refresh() throws RemoteException {
3592 mService.get().refresh();
3599 public void playlistChanged() throws RemoteException {
3600 mService.get().playlistChanged();
3607 public boolean isPlaying() throws RemoteException {
3608 return mService.get().isPlaying();
3615 public long[] getQueue() throws RemoteException {
3616 return mService.get().getQueue();
3623 public long getQueueItemAtPosition(int position) throws RemoteException {
3624 return mService.get().getQueueItemAtPosition(position);
3631 public int getQueueSize() throws RemoteException {
3632 return mService.get().getQueueSize();
3639 public int getQueueHistoryPosition(int position) throws RemoteException {
3640 return mService.get().getQueueHistoryPosition(position);
3647 public int getQueueHistorySize() throws RemoteException {
3648 return mService.get().getQueueHistorySize();
3655 public int[] getQueueHistoryList() throws RemoteException {
3656 return mService.get().getQueueHistoryList();
3663 public long duration() throws RemoteException {
3664 return mService.get().duration();
3671 public long position() throws RemoteException {
3672 return mService.get().position();
3679 public long seek(final long position) throws RemoteException {
3680 return mService.get().seek(position);
3687 public void seekRelative(final long deltaInMs) throws RemoteException {
3688 mService.get().seekRelative(deltaInMs);
3695 public long getAudioId() throws RemoteException {
3696 return mService.get().getAudioId();
3703 public MusicPlaybackTrack getCurrentTrack() throws RemoteException {
3704 return mService.get().getCurrentTrack();
3711 public MusicPlaybackTrack getTrack(int index) throws RemoteException {
3712 return mService.get().getTrack(index);
3719 public long getNextAudioId() throws RemoteException {
3720 return mService.get().getNextAudioId();
3727 public long getPreviousAudioId() throws RemoteException {
3728 return mService.get().getPreviousAudioId();
3735 public long getArtistId() throws RemoteException {
3736 return mService.get().getArtistId();
3743 public long getAlbumId() throws RemoteException {
3744 return mService.get().getAlbumId();
3751 public String getArtistName() throws RemoteException {
3752 return mService.get().getArtistName();
3759 public String getTrackName() throws RemoteException {
3760 return mService.get().getTrackName();
3767 public String getAlbumName() throws RemoteException {
3768 return mService.get().getAlbumName();
3775 public String getPath() throws RemoteException {
3776 return mService.get().getPath();
3783 public int getQueuePosition() throws RemoteException {
3784 return mService.get().getQueuePosition();
3791 public int getShuffleMode() throws RemoteException {
3792 return mService.get().getShuffleMode();
3799 public int getRepeatMode() throws RemoteException {
3800 return mService.get().getRepeatMode();
3807 public int removeTracks(final int first, final int last) throws RemoteException {
3808 return mService.get().removeTracks(first, last);
3815 public int removeTrack(final long id) throws RemoteException {
3816 return mService.get().removeTrack(id);
3823 public boolean removeTrackAtPosition(final long id, final int position)
3824 throws RemoteException {
3825 return mService.get().removeTrackAtPosition(id, position);
3832 public int getMediaMountedCount() throws RemoteException {
3833 return mService.get().getMediaMountedCount();
3840 public int getAudioSessionId() throws RemoteException {
3841 return mService.get().getAudioSessionId();
3848 public void setShakeToPlayEnabled(boolean enabled) {
3849 mService.get().setShakeToPlayEnabled(enabled);
3856 public void setLockscreenAlbumArt(boolean enabled) {
3857 mService.get().setLockscreenAlbumArt(enabled);
3862 private class QueueUpdateTask extends AsyncTask<Void, Void, List<MediaSession.QueueItem>> {
3863 private long[] mQueue;
3865 public QueueUpdateTask(long[] queue) {
3866 mQueue = queue != null ? Arrays.copyOf(queue, queue.length) : null;
3870 protected List<MediaSession.QueueItem> doInBackground(Void... params) {
3871 if (mQueue == null || mQueue.length == 0) {
3875 final StringBuilder selection = new StringBuilder();
3876 selection.append(MediaStore.Audio.Media._ID).append(" IN (");
3877 for (int i = 0; i < mQueue.length; i++) {
3879 selection.append(",");
3881 selection.append(mQueue[i]);
3883 selection.append(")");
3885 Cursor c = getContentResolver().query(
3886 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
3887 new String[] { AudioColumns._ID, AudioColumns.TITLE, AudioColumns.ARTIST },
3888 selection.toString(), null, null);
3894 LongSparseArray<MediaDescription> descsById = new LongSparseArray<>();
3895 final int idColumnIndex = c.getColumnIndexOrThrow(AudioColumns._ID);
3896 final int titleColumnIndex = c.getColumnIndexOrThrow(AudioColumns.TITLE);
3897 final int artistColumnIndex = c.getColumnIndexOrThrow(AudioColumns.ARTIST);
3899 while (c.moveToNext() && !isCancelled()) {
3900 final MediaDescription desc = new MediaDescription.Builder()
3901 .setTitle(c.getString(titleColumnIndex))
3902 .setSubtitle(c.getString(artistColumnIndex))
3904 final long id = c.getLong(idColumnIndex);
3905 descsById.put(id, desc);
3908 List<MediaSession.QueueItem> items = new ArrayList<>();
3909 for (int i = 0; i < mQueue.length; i++) {
3910 MediaDescription desc = descsById.get(mQueue[i]);
3912 // shouldn't happen except in corner cases like
3913 // music being deleted while we were processing
3914 desc = new MediaDescription.Builder().build();
3916 items.add(new MediaSession.QueueItem(desc, i));
3925 protected void onPostExecute(List<MediaSession.QueueItem> items) {
3926 if (!isCancelled()) {
3927 mSession.setQueue(items);