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.SuppressLint;
18 import android.app.AlarmManager;
19 import android.app.Notification;
20 import android.app.NotificationChannel;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.app.Service;
24 import android.appwidget.AppWidgetManager;
25 import android.content.BroadcastReceiver;
26 import android.content.ComponentName;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.SharedPreferences;
32 import android.content.pm.PackageManager;
33 import android.database.ContentObserver;
34 import android.database.Cursor;
35 import android.database.MatrixCursor;
36 import android.graphics.Bitmap;
37 import android.hardware.SensorManager;
38 import android.media.AudioManager;
39 import android.media.AudioManager.OnAudioFocusChangeListener;
40 import android.media.MediaDescription;
41 import android.media.MediaMetadata;
42 import android.media.MediaPlayer;
43 import android.media.audiofx.AudioEffect;
44 import android.media.session.MediaSession;
45 import android.media.session.PlaybackState;
46 import android.net.Uri;
47 import android.os.AsyncTask;
48 import android.os.Handler;
49 import android.os.HandlerThread;
50 import android.os.IBinder;
51 import android.os.Looper;
52 import android.os.Message;
53 import android.os.PowerManager;
54 import android.os.RemoteException;
55 import android.os.SystemClock;
56 import android.provider.MediaStore;
57 import android.provider.MediaStore.Audio.AlbumColumns;
58 import android.provider.MediaStore.Audio.AudioColumns;
59 import android.support.annotation.NonNull;
60 import android.support.v4.os.BuildCompat;
61 import android.text.TextUtils;
62 import android.util.Log;
63 import android.util.LongSparseArray;
64 import android.view.KeyEvent;
66 import org.lineageos.eleven.Config.IdType;
67 import org.lineageos.eleven.appwidgets.AppWidgetLarge;
68 import org.lineageos.eleven.appwidgets.AppWidgetLargeAlternate;
69 import org.lineageos.eleven.appwidgets.AppWidgetSmall;
70 import org.lineageos.eleven.cache.ImageCache;
71 import org.lineageos.eleven.cache.ImageFetcher;
72 import org.lineageos.eleven.provider.MusicPlaybackState;
73 import org.lineageos.eleven.provider.RecentStore;
74 import org.lineageos.eleven.provider.SongPlayCount;
75 import org.lineageos.eleven.service.MusicPlaybackTrack;
76 import org.lineageos.eleven.utils.BitmapWithColors;
77 import org.lineageos.eleven.utils.Lists;
78 import org.lineageos.eleven.utils.PreferenceUtils;
79 import org.lineageos.eleven.utils.ShakeDetector;
80 import org.lineageos.eleven.utils.SrtManager;
83 import java.io.IOException;
84 import java.lang.ref.WeakReference;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.LinkedList;
88 import java.util.List;
89 import java.util.ListIterator;
90 import java.util.Random;
91 import java.util.TreeSet;
94 * A backbround {@link Service} used to keep music playing between activities
95 * and when the user moves Eleven into the background.
97 @SuppressLint("NewApi")
98 public class MusicPlaybackService extends Service {
99 private static final String TAG = "MusicPlaybackService";
100 private static final boolean D = false;
103 * Indicates that the music has paused or resumed
105 public static final String PLAYSTATE_CHANGED = BuildConstants.PACKAGE_NAME + ".playstatechanged";
108 * Indicates that music playback position within
109 * a title was changed
111 public static final String POSITION_CHANGED = BuildConstants.PACKAGE_NAME + ".positionchanged";
114 * Indicates the meta data has changed in some way, like a track change
116 public static final String META_CHANGED = BuildConstants.PACKAGE_NAME + ".metachanged";
119 * Indicates the queue has been updated
121 public static final String QUEUE_CHANGED = BuildConstants.PACKAGE_NAME + ".queuechanged";
124 * Indicates the queue has been updated
126 public static final String PLAYLIST_CHANGED = BuildConstants.PACKAGE_NAME + ".playlistchanged";
129 * Indicates the repeat mode changed
131 public static final String REPEATMODE_CHANGED = BuildConstants.PACKAGE_NAME + ".repeatmodechanged";
134 * Indicates the shuffle mode changed
136 public static final String SHUFFLEMODE_CHANGED = BuildConstants.PACKAGE_NAME + ".shufflemodechanged";
139 * Indicates the track fails to play
141 public static final String TRACK_ERROR = BuildConstants.PACKAGE_NAME + ".trackerror";
144 * For backwards compatibility reasons, also provide sticky
145 * broadcasts under the music package
147 public static final String ELEVEN_PACKAGE_NAME = BuildConstants.PACKAGE_NAME;
148 public static final String MUSIC_PACKAGE_NAME = "com.android.music";
151 * Called to indicate a general service commmand. Used in
152 * {@link MediaButtonIntentReceiver}
154 public static final String SERVICECMD = BuildConstants.PACKAGE_NAME + ".musicservicecommand";
157 * Called to go toggle between pausing and playing the music
159 public static final String TOGGLEPAUSE_ACTION = BuildConstants.PACKAGE_NAME + ".togglepause";
162 * Called to go to pause the playback
164 public static final String PAUSE_ACTION = BuildConstants.PACKAGE_NAME + ".pause";
167 * Called to go to stop the playback
169 public static final String STOP_ACTION = BuildConstants.PACKAGE_NAME + ".stop";
172 * Called to go to the previous track or the beginning of the track if partway through the track
174 public static final String PREVIOUS_ACTION = BuildConstants.PACKAGE_NAME + ".previous";
177 * Called to go to the previous track regardless of how far in the current track the playback is
179 public static final String PREVIOUS_FORCE_ACTION = BuildConstants.PACKAGE_NAME + ".previous.force";
182 * Called to go to the next track
184 public static final String NEXT_ACTION = BuildConstants.PACKAGE_NAME + ".next";
187 * Called to change the repeat mode
189 public static final String REPEAT_ACTION = BuildConstants.PACKAGE_NAME + ".repeat";
192 * Called to change the shuffle mode
194 public static final String SHUFFLE_ACTION = BuildConstants.PACKAGE_NAME + ".shuffle";
196 public static final String FROM_MEDIA_BUTTON = "frommediabutton";
198 public static final String TIMESTAMP = "timestamp";
201 * Used to easily notify a list that it should refresh. i.e. A playlist
204 public static final String REFRESH = BuildConstants.PACKAGE_NAME + ".refresh";
207 * Used by the alarm intent to shutdown the service after being idle
209 private static final String SHUTDOWN = BuildConstants.PACKAGE_NAME + ".shutdown";
212 * Called to notify of a timed text
214 public static final String NEW_LYRICS = BuildConstants.PACKAGE_NAME + ".lyrics";
217 * Called to update the remote control client
219 public static final String UPDATE_LOCKSCREEN = BuildConstants.PACKAGE_NAME + ".updatelockscreen";
221 public static final String CMDNAME = "command";
223 public static final String CMDTOGGLEPAUSE = "togglepause";
225 public static final String CMDSTOP = "stop";
227 public static final String CMDPAUSE = "pause";
229 public static final String CMDPLAY = "play";
231 public static final String CMDPREVIOUS = "previous";
233 public static final String CMDNEXT = "next";
235 public static final String CMDHEADSETHOOK = "headsethook";
237 private static final int IDCOLIDX = 0;
240 * Moves a list to the next position in the queue
242 public static final int NEXT = 2;
245 * Moves a list to the last position in the queue
247 public static final int LAST = 3;
250 * Shuffles no songs, turns shuffling off
252 public static final int SHUFFLE_NONE = 0;
257 public static final int SHUFFLE_NORMAL = 1;
262 public static final int SHUFFLE_AUTO = 2;
267 public static final int REPEAT_NONE = 0;
270 * Repeats the current track in a list
272 public static final int REPEAT_CURRENT = 1;
275 * Repeats all the tracks in a list
277 public static final int REPEAT_ALL = 2;
280 * Indicates when the track ends
282 private static final int TRACK_ENDED = 1;
285 * Indicates that the current track was changed the next track
287 private static final int TRACK_WENT_TO_NEXT = 2;
290 * Indicates the player died
292 private static final int SERVER_DIED = 3;
295 * Indicates some sort of focus change, maybe a phone call
297 private static final int FOCUSCHANGE = 4;
300 * Indicates to fade the volume down
302 private static final int FADEDOWN = 5;
305 * Indicates to fade the volume back up
307 private static final int FADEUP = 6;
310 * Notifies that there is a new timed text string
312 private static final int LYRICS = 7;
315 * Indicates a headset hook key event
317 private static final int HEADSET_HOOK_EVENT = 8;
320 * Indicates waiting for another headset hook event has timed out
322 private static final int HEADSET_HOOK_MULTI_CLICK_TIMEOUT = 9;
325 * Idle time before stopping the foreground notfication (5 minutes)
327 private static final int IDLE_DELAY = 5 * 60 * 1000;
330 * Song play time used as threshold for rewinding to the beginning of the
331 * track instead of skipping to the previous track when getting the PREVIOUS
334 private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
337 * The max size allowed for the track history
338 * TODO: Comeback and rewrite/fix all the whole queue code bugs after demo
340 public static final int MAX_HISTORY_SIZE = 1000;
342 private static final String ACTION_AUDIO_PLAYER = BuildConstants.PACKAGE_NAME + ".AUDIO_PLAYER";
344 private static final String CHANNEL_NAME = "eleven_playback";
346 public interface TrackErrorExtra {
348 * Name of the track that was unable to play
350 public static final String TRACK_NAME = "trackname";
354 * The columns used to retrieve any info from the current track
356 private static final String[] PROJECTION = new String[] {
357 "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
358 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
359 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
360 MediaStore.Audio.Media.ARTIST_ID
364 * The columns used to retrieve any info from the current album
366 private static final String[] ALBUM_PROJECTION = new String[] {
367 MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
368 MediaStore.Audio.Albums.LAST_YEAR
372 * Keeps a mapping of the track history
374 private static LinkedList<Integer> mHistory = Lists.newLinkedList();
377 * Used to shuffle the tracks
379 private static final Shuffler mShuffler = new Shuffler();
384 private final IBinder mBinder = new ServiceStub(this);
389 private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
394 private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
397 * 4x2 alternate widget
399 private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
405 private MultiPlayer mPlayer;
408 * The path of the current file to play
410 private String mFileToPlay;
413 * Alarm intent for removing the notification when nothing is playing
416 private AlarmManager mAlarmManager;
417 private PendingIntent mShutdownIntent;
418 private boolean mShutdownScheduled;
420 private NotificationManager mNotificationManager;
423 * The cursor used to retrieve info on the current track and run the
424 * necessary queries to play audio files
426 private Cursor mCursor;
429 * The cursor used to retrieve info on the album the current track is
432 private Cursor mAlbumCursor;
435 * Monitors the audio state
437 private AudioManager mAudioManager;
440 * Settings used to save and retrieve the queue and history
442 private SharedPreferences mPreferences;
445 * Used to know when the service is active
447 private boolean mServiceInUse = false;
450 * Used to know if something should be playing or not
452 private boolean mIsSupposedToBePlaying = false;
455 * Gets the last played time to determine whether we still want notifications or not
457 private long mLastPlayedTime;
459 private int mNotifyMode = NOTIFY_MODE_NONE;
460 private long mNotificationPostTime = 0;
462 private static final int NOTIFY_MODE_NONE = 0;
463 private static final int NOTIFY_MODE_FOREGROUND = 1;
464 private static final int NOTIFY_MODE_BACKGROUND = 2;
467 * Used to indicate if the queue can be saved
469 private boolean mQueueIsSaveable = true;
472 * Used to track what type of audio focus loss caused the playback to pause
474 private boolean mPausedByTransientLossOfFocus = false;
477 * Lock screen controls
479 private MediaSession mSession;
481 // We use this to distinguish between different cards when saving/restoring
485 private int mPlayPos = -1;
487 private int mNextPlayPos = -1;
489 private int mOpenFailedCounter = 0;
491 private int mMediaMountedCount = 0;
493 private int mShuffleMode = SHUFFLE_NONE;
495 private int mRepeatMode = REPEAT_NONE;
497 private int mServiceStartId = -1;
499 private String mLyrics;
501 private ArrayList<MusicPlaybackTrack> mPlaylist = new ArrayList<>(100);
503 private long[] mAutoShuffleList = null;
505 private MusicPlayerHandler mPlayerHandler;
506 private HandlerThread mHandlerThread;
508 private BroadcastReceiver mUnmountReceiver = null;
510 // to improve perf, instead of hitting the disk cache or file cache, store the bitmaps in memory
511 private String mCachedKey;
512 private BitmapWithColors[] mCachedBitmapWithColors = new BitmapWithColors[2];
514 private QueueUpdateTask mQueueUpdateTask;
519 private ImageFetcher mImageFetcher;
522 * Recently listened database
524 private RecentStore mRecentsCache;
527 * The song play count database
529 private SongPlayCount mSongPlayCountCache;
532 * Stores the playback state
534 private MusicPlaybackState mPlaybackStateStore;
537 * Shake detector class used for shake to switch song feature
539 private ShakeDetector mShakeDetector;
542 * Switch for displaying album art on lockscreen
544 private boolean mShowAlbumArtOnLockscreen;
546 private boolean mReadGranted = false;
548 private PowerManager.WakeLock mHeadsetHookWakeLock;
550 private ShakeDetector.Listener mShakeDetectorListener=new ShakeDetector.Listener() {
553 public void hearShake() {
555 * on shake detect, play next song
558 Log.d(TAG,"Shake detected!!!");
568 public IBinder onBind(final Intent intent) {
569 if (D) Log.d(TAG, "Service bound, intent = " + intent);
571 mServiceInUse = true;
579 public boolean onUnbind(final Intent intent) {
580 if (D) Log.d(TAG, "Service unbound");
581 mServiceInUse = false;
585 if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
586 // Something is currently playing, or will be playing once
587 // an in-progress action requesting audio focus ends, so don't stop
591 // If there is a playlist but playback is paused, then wait a while
592 // before stopping the service, so that pause/resume isn't slow.
593 // Also delay stopping the service if we're transitioning between
595 } else if (mPlaylist.size() > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
596 scheduleDelayedShutdown();
600 stopSelf(mServiceStartId);
609 public void onRebind(final Intent intent) {
611 mServiceInUse = true;
618 public void onCreate() {
619 if (D) Log.d(TAG, "Creating service");
622 if (checkSelfPermission(permission.READ_EXTERNAL_STORAGE) !=
623 PackageManager.PERMISSION_GRANTED) {
630 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
632 // Initialize the favorites and recents databases
633 mRecentsCache = RecentStore.getInstance(this);
635 // gets the song play count cache
636 mSongPlayCountCache = SongPlayCount.getInstance(this);
638 // gets a pointer to the playback state store
639 mPlaybackStateStore = MusicPlaybackState.getInstance(this);
641 // Initialize the image fetcher
642 mImageFetcher = ImageFetcher.getInstance(this);
643 // Initialize the image cache
644 mImageFetcher.setImageCache(ImageCache.getInstance(this));
646 // Start up the thread running the service. Note that we create a
647 // separate thread because the service normally runs in the process's
648 // main thread, which we don't want to block. We also make it
649 // background priority so CPU-intensive work will not disrupt the UI.
650 mHandlerThread = new HandlerThread("MusicPlayerHandler",
651 android.os.Process.THREAD_PRIORITY_BACKGROUND);
652 mHandlerThread.start();
654 // Initialize the handler
655 mPlayerHandler = new MusicPlayerHandler(this, mHandlerThread.getLooper());
657 // Initialize the audio manager and register any headset controls for
659 mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
661 // Use the remote control APIs to set the playback state
664 // Initialize the preferences
665 mPreferences = getSharedPreferences("Service", 0);
666 mCardId = getCardId();
668 mShowAlbumArtOnLockscreen = mPreferences.getBoolean(
669 PreferenceUtils.SHOW_ALBUM_ART_ON_LOCKSCREEN, true);
670 setShakeToPlayEnabled(mPreferences.getBoolean(PreferenceUtils.SHAKE_TO_PLAY, false));
672 mRepeatMode = mPreferences.getInt("repeatmode", REPEAT_NONE);
673 mShuffleMode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
675 registerExternalStorageListener();
677 // Initialize the media player
678 mPlayer = new MultiPlayer(this);
679 mPlayer.setHandler(mPlayerHandler);
681 // Initialize the intent filter and each action
682 final IntentFilter filter = new IntentFilter();
683 filter.addAction(SERVICECMD);
684 filter.addAction(TOGGLEPAUSE_ACTION);
685 filter.addAction(PAUSE_ACTION);
686 filter.addAction(STOP_ACTION);
687 filter.addAction(NEXT_ACTION);
688 filter.addAction(PREVIOUS_ACTION);
689 filter.addAction(PREVIOUS_FORCE_ACTION);
690 filter.addAction(REPEAT_ACTION);
691 filter.addAction(SHUFFLE_ACTION);
692 // Attach the broadcast listener
693 registerReceiver(mIntentReceiver, filter);
695 // Get events when MediaStore content changes
696 mMediaStoreObserver = new MediaStoreObserver(mPlayerHandler);
697 getContentResolver().registerContentObserver(
698 MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mMediaStoreObserver);
699 getContentResolver().registerContentObserver(
700 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mMediaStoreObserver);
702 // Initialize the delayed shutdown intent
703 final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
704 shutdownIntent.setAction(SHUTDOWN);
706 mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
707 mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
709 // Listen for the idle state
710 scheduleDelayedShutdown();
712 // Bring the queue back
714 notifyChange(QUEUE_CHANGED);
715 notifyChange(META_CHANGED);
718 private void setUpMediaSession() {
719 mSession = new MediaSession(this, "Eleven");
720 mSession.setCallback(new MediaSession.Callback() {
722 public void onPause() {
724 mPausedByTransientLossOfFocus = false;
727 public void onPlay() {
731 public void onSeekTo(long pos) {
735 public void onSkipToNext() {
739 public void onSkipToPrevious() {
743 public void onStop() {
745 mPausedByTransientLossOfFocus = false;
747 releaseServiceUiAndStop();
750 public void onSkipToQueueItem(long id) {
751 setQueuePosition((int) id);
754 public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
755 if (Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
756 KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
757 if (ke != null && ke.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) {
758 if (ke.getAction() == KeyEvent.ACTION_UP) {
759 handleHeadsetHookClick(ke.getEventTime());
764 return super.onMediaButtonEvent(mediaButtonIntent);
768 PendingIntent pi = PendingIntent.getBroadcast(this, 0,
769 new Intent(this, MediaButtonIntentReceiver.class),
770 PendingIntent.FLAG_UPDATE_CURRENT);
771 mSession.setMediaButtonReceiver(pi);
773 mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
774 | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
781 public void onDestroy() {
782 if (D) Log.d(TAG, "Destroying service");
787 // Remove any sound effects
788 final Intent audioEffectsIntent = new Intent(
789 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
790 audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
791 audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
792 sendBroadcast(audioEffectsIntent);
794 // remove any pending alarms
795 mAlarmManager.cancel(mShutdownIntent);
797 // Remove any callbacks from the handler
798 mPlayerHandler.removeCallbacksAndMessages(null);
799 // quit the thread so that anything that gets posted won't run
800 mHandlerThread.quitSafely();
802 // Release the player
806 // Remove the audio focus listener and lock screen controls
807 mAudioManager.abandonAudioFocus(mAudioFocusListener);
810 // remove the media store observer
811 getContentResolver().unregisterContentObserver(mMediaStoreObserver);
816 // Unregister the mount listener
817 unregisterReceiver(mIntentReceiver);
818 if (mUnmountReceiver != null) {
819 unregisterReceiver(mUnmountReceiver);
820 mUnmountReceiver = null;
823 // deinitialize shake detector
824 stopShakeDetector(true);
831 public int onStartCommand(final Intent intent, final int flags, final int startId) {
832 if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
833 mServiceStartId = startId;
835 if (intent != null) {
836 final String action = intent.getAction();
838 if (SHUTDOWN.equals(action)) {
839 mShutdownScheduled = false;
840 releaseServiceUiAndStop();
841 return START_NOT_STICKY;
844 handleCommandIntent(intent);
847 // Make sure the service will shut down on its own if it was
848 // just started but not bound to and nothing is playing
849 scheduleDelayedShutdown();
851 if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
852 MediaButtonIntentReceiver.completeWakefulIntent(intent);
855 return START_NOT_STICKY;
858 private void releaseServiceUiAndStop() {
860 || mPausedByTransientLossOfFocus
861 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
865 if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
866 cancelNotification();
867 mAudioManager.abandonAudioFocus(mAudioFocusListener);
868 mSession.setActive(false);
870 if (!mServiceInUse) {
872 stopSelf(mServiceStartId);
876 private void handleCommandIntent(Intent intent) {
877 final String action = intent.getAction();
878 final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
880 if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
882 if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
884 } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)
885 || PREVIOUS_FORCE_ACTION.equals(action)) {
886 prev(PREVIOUS_FORCE_ACTION.equals(action));
887 } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
889 } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
891 mPausedByTransientLossOfFocus = false;
892 } else if (CMDPLAY.equals(command)) {
894 } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
896 mPausedByTransientLossOfFocus = false;
898 releaseServiceUiAndStop();
899 } else if (REPEAT_ACTION.equals(action)) {
901 } else if (SHUFFLE_ACTION.equals(action)) {
903 } else if (CMDHEADSETHOOK.equals(command)) {
904 long timestamp = intent.getLongExtra(TIMESTAMP, 0);
905 handleHeadsetHookClick(timestamp);
909 private void handleHeadsetHookClick(long timestamp) {
910 if (mHeadsetHookWakeLock == null) {
911 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
912 mHeadsetHookWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
913 "Eleven headset button");
914 mHeadsetHookWakeLock.setReferenceCounted(false);
916 // Make sure we don't indefinitely hold the wake lock under any circumstances
917 mHeadsetHookWakeLock.acquire(10000);
919 Message msg = mPlayerHandler.obtainMessage(HEADSET_HOOK_EVENT, timestamp);
924 * Updates the notification, considering the current play and activity state
926 private void updateNotification() {
927 final int newNotifyMode;
929 newNotifyMode = NOTIFY_MODE_FOREGROUND;
930 } else if (recentlyPlayed()) {
931 newNotifyMode = NOTIFY_MODE_BACKGROUND;
933 newNotifyMode = NOTIFY_MODE_NONE;
936 int notificationId = hashCode();
937 if (mNotifyMode != newNotifyMode) {
938 if (mNotifyMode == NOTIFY_MODE_FOREGROUND) {
939 stopForeground(newNotifyMode == NOTIFY_MODE_NONE);
940 } else if (newNotifyMode == NOTIFY_MODE_NONE) {
941 mNotificationManager.cancel(notificationId);
942 mNotificationPostTime = 0;
946 if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
947 startForeground(notificationId, buildNotification());
948 } else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
949 mNotificationManager.notify(notificationId, buildNotification());
952 mNotifyMode = newNotifyMode;
955 private void cancelNotification() {
956 stopForeground(true);
957 mNotificationManager.cancel(hashCode());
958 mNotificationPostTime = 0;
959 mNotifyMode = NOTIFY_MODE_NONE;
963 * @return A card ID used to save and restore playlists, i.e., the queue.
965 private int getCardId() {
966 final ContentResolver resolver = getContentResolver();
967 Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
970 if (cursor != null && cursor.moveToFirst()) {
971 mCardId = cursor.getInt(0);
979 * Called when we receive a ACTION_MEDIA_EJECT notification.
981 * @param storagePath The path to mount point for the removed media
983 public void closeExternalStorageFiles(final String storagePath) {
985 notifyChange(QUEUE_CHANGED);
986 notifyChange(META_CHANGED);
990 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
991 * intent will call closeExternalStorageFiles() if the external media is
992 * going to be ejected, so applications can clean up any files they have
995 public void registerExternalStorageListener() {
996 if (mUnmountReceiver == null) {
997 mUnmountReceiver = new BroadcastReceiver() {
1003 public void onReceive(final Context context, final Intent intent) {
1004 final String action = intent.getAction();
1005 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
1007 mQueueIsSaveable = false;
1008 closeExternalStorageFiles(intent.getData().getPath());
1009 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
1010 mMediaMountedCount++;
1011 mCardId = getCardId();
1013 mQueueIsSaveable = true;
1014 notifyChange(QUEUE_CHANGED);
1015 notifyChange(META_CHANGED);
1019 final IntentFilter filter = new IntentFilter();
1020 filter.addAction(Intent.ACTION_MEDIA_EJECT);
1021 filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
1022 filter.addDataScheme("file");
1023 registerReceiver(mUnmountReceiver, filter);
1027 private void scheduleDelayedShutdown() {
1028 if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
1029 if (!mReadGranted) {
1032 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
1033 SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
1034 mShutdownScheduled = true;
1037 private void cancelShutdown() {
1038 if (D) Log.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled);
1039 if (mShutdownScheduled) {
1040 mAlarmManager.cancel(mShutdownIntent);
1041 mShutdownScheduled = false;
1048 * @param goToIdle True to go to the idle state, false otherwise
1050 private void stop(final boolean goToIdle) {
1051 if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
1052 if (mPlayer.isInitialized()) {
1058 setIsSupposedToBePlaying(false, false);
1060 stopForeground(false);
1065 * Removes the range of tracks specified from the play list. If a file
1066 * within the range is the file currently being played, playback will move
1067 * to the next file after the range.
1069 * @param first The first file to be removed
1070 * @param last The last file to be removed
1071 * @return the number of tracks deleted
1073 private int removeTracksInternal(int first, int last) {
1074 synchronized (this) {
1077 } else if (first < 0) {
1079 } else if (last >= mPlaylist.size()) {
1080 last = mPlaylist.size() - 1;
1083 boolean gotonext = false;
1084 if (first <= mPlayPos && mPlayPos <= last) {
1087 } else if (mPlayPos > last) {
1088 mPlayPos -= last - first + 1;
1090 final int numToRemove = last - first + 1;
1092 if (first == 0 && last == mPlaylist.size() - 1) {
1098 for (int i = 0; i < numToRemove; i++) {
1099 mPlaylist.remove(first);
1102 // remove the items from the history
1103 // this is not ideal as the history shouldn't be impacted by this
1104 // but since we are removing items from the array, it will throw
1105 // an exception if we keep it around.
1106 ListIterator<Integer> positionIterator = mHistory.listIterator();
1107 while (positionIterator.hasNext()) {
1108 int pos = positionIterator.next();
1109 if (pos >= first && pos <= last) {
1110 positionIterator.remove();
1111 } else if (pos > last) {
1112 positionIterator.set(pos - numToRemove);
1117 if (mPlaylist.size() == 0) {
1122 if (mShuffleMode != SHUFFLE_NONE) {
1123 mPlayPos = getNextPosition(true);
1124 } else if (mPlayPos >= mPlaylist.size()) {
1127 final boolean wasPlaying = isPlaying();
1129 openCurrentAndNext();
1134 notifyChange(META_CHANGED);
1136 return last - first + 1;
1141 * Adds a list to the playlist
1143 * @param list The list to add
1144 * @param position The position to place the tracks
1146 private void addToPlayList(final long[] list, int position, long sourceId, IdType sourceType) {
1147 final int addlen = list.length;
1153 mPlaylist.ensureCapacity(mPlaylist.size() + addlen);
1154 if (position > mPlaylist.size()) {
1155 position = mPlaylist.size();
1158 final ArrayList<MusicPlaybackTrack> arrayList = new ArrayList<>(addlen);
1159 for (int i = 0; i < list.length; i++) {
1160 arrayList.add(new MusicPlaybackTrack(list[i], sourceId, sourceType, i));
1163 mPlaylist.addAll(position, arrayList);
1165 if (mPlaylist.size() == 0) {
1167 notifyChange(META_CHANGED);
1172 * @param trackId The track ID
1174 private void updateCursor(final long trackId) {
1175 updateCursor("_id=" + trackId, null);
1178 private void updateCursor(final String selection, final String[] selectionArgs) {
1179 synchronized (this) {
1181 mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1182 PROJECTION, selection, selectionArgs);
1184 updateAlbumCursor();
1187 private void updateCursor(final Uri uri) {
1188 synchronized (this) {
1190 mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
1192 updateAlbumCursor();
1195 private void updateAlbumCursor() {
1196 long albumId = getAlbumId();
1198 mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
1199 ALBUM_PROJECTION, "_id=" + albumId, null);
1201 mAlbumCursor = null;
1205 private Cursor openCursorAndGoToFirst(Uri uri, String[] projection,
1206 String selection, String[] selectionArgs) {
1207 Cursor c = getContentResolver().query(uri, projection,
1208 selection, selectionArgs, null, null);
1212 if (!c.moveToFirst()) {
1219 private synchronized void closeCursor() {
1220 if (mCursor != null) {
1224 if (mAlbumCursor != null) {
1225 mAlbumCursor.close();
1226 mAlbumCursor = null;
1231 * Called to open a new file as the current track and prepare the next for
1234 private void openCurrentAndNext() {
1235 openCurrentAndMaybeNext(true);
1239 * Called to open a new file as the current track and prepare the next for
1242 * @param openNext True to prepare the next track for playback, false
1245 private void openCurrentAndMaybeNext(final boolean openNext) {
1246 synchronized (this) {
1249 if (mPlaylist.size() == 0) {
1254 boolean shutdown = false;
1256 updateCursor(mPlaylist.get(mPlayPos).mId);
1259 && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
1260 + mCursor.getLong(IDCOLIDX))) {
1264 // if we get here then opening the file failed. We can close the
1265 // cursor now, because
1266 // we're either going to create a new one next, or stop trying
1268 if (mOpenFailedCounter++ < 10 && mPlaylist.size() > 1) {
1269 final int pos = getNextPosition(false);
1277 updateCursor(mPlaylist.get(mPlayPos).mId);
1279 mOpenFailedCounter = 0;
1280 Log.w(TAG, "Failed to open file for playback");
1287 scheduleDelayedShutdown();
1288 if (mIsSupposedToBePlaying) {
1289 mIsSupposedToBePlaying = false;
1290 notifyChange(PLAYSTATE_CHANGED);
1292 } else if (openNext) {
1298 private void sendErrorMessage(final String trackName) {
1299 final Intent i = new Intent(TRACK_ERROR);
1300 i.putExtra(TrackErrorExtra.TRACK_NAME, trackName);
1305 * @param force True to force the player onto the track next, false
1307 * @param saveToHistory True to save the mPlayPos to the history
1308 * @return The next position to play.
1310 private int getNextPosition(final boolean force) {
1311 // as a base case, if the playlist is empty just return -1
1312 if (mPlaylist == null || mPlaylist.isEmpty()) {
1315 // if we're not forced to go to the next track and we are only playing the current track
1316 if (!force && mRepeatMode == REPEAT_CURRENT) {
1321 } else if (mShuffleMode == SHUFFLE_NORMAL) {
1322 final int numTracks = mPlaylist.size();
1324 // count the number of times a track has been played
1325 final int[] trackNumPlays = new int[numTracks];
1326 for (int i = 0; i < numTracks; i++) {
1328 trackNumPlays[i] = 0;
1331 // walk through the history and add up the number of times the track
1333 final int numHistory = mHistory.size();
1334 for (int i = 0; i < numHistory; i++) {
1335 final int idx = mHistory.get(i);
1336 if (idx >= 0 && idx < numTracks) {
1337 trackNumPlays[idx]++;
1341 // also add the currently playing track to the count
1342 if (mPlayPos >= 0 && mPlayPos < numTracks) {
1343 trackNumPlays[mPlayPos]++;
1346 // figure out the least # of times a track has a played as well as
1347 // how many tracks share that count
1348 int minNumPlays = Integer.MAX_VALUE;
1349 int numTracksWithMinNumPlays = 0;
1350 for (final int trackNumPlay : trackNumPlays) {
1351 // if we found a new track that has less number of plays, reset the counters
1352 if (trackNumPlay < minNumPlays) {
1353 minNumPlays = trackNumPlay;
1354 numTracksWithMinNumPlays = 1;
1355 } else if (trackNumPlay == minNumPlays) {
1356 // increment this track shares the # of tracks
1357 numTracksWithMinNumPlays++;
1361 // if we've played each track at least once and all tracks have been played an equal
1362 // # of times and we aren't repeating all and we're not forcing a track, then
1363 // return no more tracks
1364 if (minNumPlays > 0 && numTracksWithMinNumPlays == numTracks
1365 && mRepeatMode != REPEAT_ALL && !force) {
1369 // else pick a track from the least number of played tracks
1370 int skip = mShuffler.nextInt(numTracksWithMinNumPlays);
1371 for (int i = 0; i < trackNumPlays.length; i++) {
1372 if (trackNumPlays[i] == minNumPlays) {
1381 // Unexpected to land here
1382 if (D) Log.e(TAG, "Getting the next position resulted did not get a result when it should have");
1384 } else if (mShuffleMode == SHUFFLE_AUTO) {
1385 doAutoShuffleUpdate();
1386 return mPlayPos + 1;
1388 if (mPlayPos >= mPlaylist.size() - 1) {
1389 if (mRepeatMode == REPEAT_NONE && !force) {
1391 } else if (mRepeatMode == REPEAT_ALL || force) {
1396 return mPlayPos + 1;
1402 * Sets the track to be played
1404 private void setNextTrack() {
1405 setNextTrack(getNextPosition(false));
1409 * Sets the next track to be played
1410 * @param position the target position we want
1412 private void setNextTrack(int position) {
1413 mNextPlayPos = position;
1414 if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
1415 if (mNextPlayPos >= 0 && mPlaylist != null && mNextPlayPos < mPlaylist.size()) {
1416 final long id = mPlaylist.get(mNextPlayPos).mId;
1417 mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
1419 mPlayer.setNextDataSource(null);
1424 * Creates a shuffled playlist used for party mode
1426 private boolean makeAutoShuffleList() {
1427 Cursor cursor = null;
1429 cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1431 MediaStore.Audio.Media._ID
1432 }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
1433 if (cursor == null || cursor.getCount() == 0) {
1436 final int len = cursor.getCount();
1437 final long[] list = new long[len];
1438 for (int i = 0; i < len; i++) {
1439 cursor.moveToNext();
1440 list[i] = cursor.getLong(0);
1442 mAutoShuffleList = list;
1444 } catch (final RuntimeException e) {
1446 if (cursor != null) {
1455 * Creates the party shuffle playlist
1457 private void doAutoShuffleUpdate() {
1458 boolean notify = false;
1459 if (mPlayPos > 10) {
1460 removeTracks(0, mPlayPos - 9);
1463 final int toAdd = 7 - (mPlaylist.size() - (mPlayPos < 0 ? -1 : mPlayPos));
1464 for (int i = 0; i < toAdd; i++) {
1465 int lookback = mHistory.size();
1468 idx = mShuffler.nextInt(mAutoShuffleList.length);
1469 if (!wasRecentlyUsed(idx, lookback)) {
1475 if (mHistory.size() > MAX_HISTORY_SIZE) {
1478 mPlaylist.add(new MusicPlaybackTrack(mAutoShuffleList[idx], -1, IdType.NA, -1));
1482 notifyChange(QUEUE_CHANGED);
1487 private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
1488 if (lookbacksize == 0) {
1491 final int histsize = mHistory.size();
1492 if (histsize < lookbacksize) {
1493 lookbacksize = histsize;
1495 final int maxidx = histsize - 1;
1496 for (int i = 0; i < lookbacksize; i++) {
1497 final long entry = mHistory.get(maxidx - i);
1506 * Notify the change-receivers that something has changed.
1508 private void notifyChange(final String what) {
1509 if (D) Log.d(TAG, "notifyChange: what = " + what);
1511 // Update the lockscreen controls
1512 updateMediaSession(what);
1514 if (what.equals(POSITION_CHANGED)) {
1518 final Intent intent = new Intent(what);
1519 intent.putExtra("id", getAudioId());
1520 intent.putExtra("artist", getArtistName());
1521 intent.putExtra("album", getAlbumName());
1522 intent.putExtra("track", getTrackName());
1523 intent.putExtra("playing", isPlaying());
1525 if (NEW_LYRICS.equals(what)) {
1526 intent.putExtra("lyrics", mLyrics);
1529 sendStickyBroadcast(intent);
1531 final Intent musicIntent = new Intent(intent);
1532 musicIntent.setAction(what.replace(ELEVEN_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
1533 sendStickyBroadcast(musicIntent);
1537 // Add the track to the recently played list.
1538 mRecentsCache.addSongId(getAudioId());
1540 mSongPlayCountCache.bumpSongCount(getAudioId());
1545 // if we are in shuffle mode and our next track is still valid,
1546 // try to re-use the track
1547 // We need to reimplement the queue to prevent hacky solutions like this
1548 if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size()
1549 && getShuffleMode() != SHUFFLE_NONE) {
1550 setNextTrack(mNextPlayPos);
1561 if (what.equals(PLAYSTATE_CHANGED)) {
1562 updateNotification();
1565 // Update the app-widgets
1566 mAppWidgetSmall.notifyChange(this, what);
1567 mAppWidgetLarge.notifyChange(this, what);
1568 mAppWidgetLargeAlternate.notifyChange(this, what);
1571 private void updateMediaSession(final String what) {
1572 int playState = mIsSupposedToBePlaying
1573 ? PlaybackState.STATE_PLAYING
1574 : PlaybackState.STATE_PAUSED;
1576 long playBackStateActions = PlaybackState.ACTION_PLAY |
1577 PlaybackState.ACTION_PLAY_PAUSE |
1578 PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
1579 PlaybackState.ACTION_PAUSE |
1580 PlaybackState.ACTION_SKIP_TO_NEXT |
1581 PlaybackState.ACTION_SKIP_TO_PREVIOUS |
1582 PlaybackState.ACTION_STOP;
1584 if (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED)) {
1585 mSession.setPlaybackState(new PlaybackState.Builder()
1586 .setActions(playBackStateActions)
1587 .setActiveQueueItemId(getAudioId())
1588 .setState(playState, position(), 1.0f).build());
1589 } else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
1590 Bitmap albumArt = getAlbumArt(false).getBitmap();
1591 if (albumArt != null) {
1592 // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
1593 // to make sure not to hand out our cache copy
1594 Bitmap.Config config = albumArt.getConfig();
1595 if (config == null) {
1596 config = Bitmap.Config.ARGB_8888;
1598 albumArt = albumArt.copy(config, false);
1601 mSession.setMetadata(new MediaMetadata.Builder()
1602 .putString(MediaMetadata.METADATA_KEY_ARTIST, getArtistName())
1603 .putString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST, getAlbumArtistName())
1604 .putString(MediaMetadata.METADATA_KEY_ALBUM, getAlbumName())
1605 .putString(MediaMetadata.METADATA_KEY_TITLE, getTrackName())
1606 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration())
1607 .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, getQueuePosition() + 1)
1608 .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, getQueue().length)
1609 .putString(MediaMetadata.METADATA_KEY_GENRE, getGenreName())
1610 .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART,
1611 mShowAlbumArtOnLockscreen ? albumArt : null)
1614 if (what.equals(QUEUE_CHANGED)) {
1615 updateMediaSessionQueue();
1618 mSession.setPlaybackState(new PlaybackState.Builder()
1619 .setActions(playBackStateActions)
1620 .setActiveQueueItemId(getAudioId())
1621 .setState(playState, position(), 1.0f).build());
1625 private synchronized void updateMediaSessionQueue() {
1626 if (mQueueUpdateTask != null) {
1627 mQueueUpdateTask.cancel(true);
1629 mQueueUpdateTask = new QueueUpdateTask(getQueue());
1630 mQueueUpdateTask.execute();
1633 private Notification buildNotification() {
1634 final String albumName = getAlbumName();
1635 final String artistName = getArtistName();
1636 final boolean isPlaying = isPlaying();
1637 String text = TextUtils.isEmpty(albumName)
1638 ? artistName : artistName + " - " + albumName;
1640 int playButtonResId = isPlaying
1641 ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play;
1642 int playButtonTitleResId = isPlaying
1643 ? R.string.accessibility_pause : R.string.accessibility_play;
1645 Notification.MediaStyle style = new Notification.MediaStyle()
1646 .setMediaSession(mSession.getSessionToken())
1647 .setShowActionsInCompactView(0, 1, 2);
1649 Intent nowPlayingIntent = new Intent(ACTION_AUDIO_PLAYER)
1650 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1651 PendingIntent clickIntent = PendingIntent.getActivity(this, 0, nowPlayingIntent, 0);
1652 BitmapWithColors artwork = getAlbumArt(false);
1654 if (mNotificationPostTime == 0) {
1655 mNotificationPostTime = System.currentTimeMillis();
1658 Notification.Builder builder = new Notification.Builder(this, CHANNEL_NAME)
1659 .setSmallIcon(R.drawable.ic_notification)
1660 .setLargeIcon(artwork.getBitmap())
1661 .setContentIntent(clickIntent)
1662 .setContentTitle(getTrackName())
1663 .setContentText(text)
1664 .setWhen(mNotificationPostTime)
1667 .setVisibility(Notification.VISIBILITY_PUBLIC)
1668 .addAction(R.drawable.btn_playback_previous,
1669 getString(R.string.accessibility_prev),
1670 retrievePlaybackAction(PREVIOUS_ACTION))
1671 .addAction(playButtonResId, getString(playButtonTitleResId),
1672 retrievePlaybackAction(TOGGLEPAUSE_ACTION))
1673 .addAction(R.drawable.btn_playback_next,
1674 getString(R.string.accessibility_next),
1675 retrievePlaybackAction(NEXT_ACTION));
1677 builder.setColor(artwork.getVibrantDarkColor());
1679 if (BuildCompat.isAtLeastO()) {
1680 NotificationChannel channel = mNotificationManager
1681 .getNotificationChannel(CHANNEL_NAME);
1683 if (channel == null) {
1684 String name = getString(R.string.channel_music);
1686 channel = new NotificationChannel(CHANNEL_NAME, name,
1687 mNotificationManager.IMPORTANCE_DEFAULT);
1688 channel.setShowBadge(false);
1689 channel.enableVibration(false);
1690 channel.setSound(null, null);
1691 mNotificationManager.createNotificationChannel(channel);
1694 builder.setChannelId(channel.getId());
1697 return builder.build();
1700 private final PendingIntent retrievePlaybackAction(final String action) {
1701 final ComponentName serviceName = new ComponentName(this, MusicPlaybackService.class);
1702 Intent intent = new Intent(action);
1703 intent.setComponent(serviceName);
1705 return PendingIntent.getService(this, 0, intent, 0);
1711 * @param full True if the queue is full
1713 private void saveQueue(final boolean full) {
1714 if (!mQueueIsSaveable || mPreferences == null) {
1718 final SharedPreferences.Editor editor = mPreferences.edit();
1720 mPlaybackStateStore.saveState(mPlaylist,
1721 mShuffleMode != SHUFFLE_NONE ? mHistory : null);
1722 editor.putInt("cardid", mCardId);
1724 editor.putInt("curpos", mPlayPos);
1725 if (mPlayer.isInitialized()) {
1726 editor.putLong("seekpos", mPlayer.position());
1728 editor.putInt("repeatmode", mRepeatMode);
1729 editor.putInt("shufflemode", mShuffleMode);
1734 * Reloads the queue as the user left it the last time they stopped using
1737 private void reloadQueue() {
1739 if (mPreferences.contains("cardid")) {
1740 id = mPreferences.getInt("cardid", ~mCardId);
1742 if (id == mCardId) {
1743 mPlaylist = mPlaybackStateStore.getQueue();
1745 if (mPlaylist.size() > 0) {
1746 final int pos = mPreferences.getInt("curpos", 0);
1747 if (pos < 0 || pos >= mPlaylist.size()) {
1752 updateCursor(mPlaylist.get(mPlayPos).mId);
1753 if (mCursor == null) {
1754 SystemClock.sleep(3000);
1755 updateCursor(mPlaylist.get(mPlayPos).mId);
1757 synchronized (this) {
1759 mOpenFailedCounter = 20;
1760 openCurrentAndNext();
1762 if (!mPlayer.isInitialized()) {
1767 final long seekpos = mPreferences.getLong("seekpos", 0);
1768 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
1771 Log.d(TAG, "restored queue, currently at position "
1772 + position() + "/" + duration()
1773 + " (requested " + seekpos + ")");
1776 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
1777 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
1778 repmode = REPEAT_NONE;
1780 mRepeatMode = repmode;
1782 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
1783 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
1784 shufmode = SHUFFLE_NONE;
1786 if (shufmode != SHUFFLE_NONE) {
1787 mHistory = mPlaybackStateStore.getHistory(mPlaylist.size());
1789 if (shufmode == SHUFFLE_AUTO) {
1790 if (!makeAutoShuffleList()) {
1791 shufmode = SHUFFLE_NONE;
1794 mShuffleMode = shufmode;
1799 * Opens a file and prepares it for playback
1801 * @param path The path of the file to open
1803 public boolean openFile(final String path) {
1804 if (D) Log.d(TAG, "openFile: path = " + path);
1805 synchronized (this) {
1810 // If mCursor is null, try to associate path with a database cursor
1811 if (mCursor == null) {
1812 Uri uri = Uri.parse(path);
1813 boolean shouldAddToPlaylist = true; // should try adding audio info to playlist
1816 id = Long.valueOf(uri.getLastPathSegment());
1817 } catch (NumberFormatException ex) {
1821 if (id != -1 && path.startsWith(
1822 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
1825 } else if (id != -1 && path.startsWith(
1826 MediaStore.Files.getContentUri("external").toString())) {
1829 // handle downloaded media files
1830 } else if ( path.startsWith("content://downloads/") ) {
1832 // extract MediaProvider(MP) uri , if available
1833 // Downloads.Impl.COLUMN_MEDIAPROVIDER_URI
1834 String mpUri = getValueForDownloadedFile(this, uri, "mediaprovider_uri");
1835 if (D) Log.i(TAG, "Downloaded file's MP uri : " + mpUri);
1836 if ( !TextUtils.isEmpty(mpUri) ) {
1837 // if mpUri is valid, play that URI instead
1838 if (openFile(mpUri)) {
1839 // notify impending change in track
1840 notifyChange(META_CHANGED);
1846 // create phantom cursor with download info, if a MP uri wasn't found
1847 updateCursorForDownloadedFile(this, uri);
1848 shouldAddToPlaylist = false; // song info isn't available in MediaStore
1852 // assuming a "file://" uri by this point ...
1853 String where = MediaStore.Audio.Media.DATA + "=?";
1854 String[] selectionArgs = new String[]{path};
1855 updateCursor(where, selectionArgs);
1858 if (mCursor != null && shouldAddToPlaylist) {
1860 mPlaylist.add(new MusicPlaybackTrack(
1861 mCursor.getLong(IDCOLIDX), -1, IdType.NA, -1));
1862 // propagate the change in playlist state
1863 notifyChange(QUEUE_CHANGED);
1867 } catch (final UnsupportedOperationException ex) {
1873 mPlayer.setDataSource(mFileToPlay);
1874 if (mPlayer.isInitialized()) {
1875 mOpenFailedCounter = 0;
1879 String trackName = getTrackName();
1880 if (TextUtils.isEmpty(trackName)) {
1883 sendErrorMessage(trackName);
1891 Columns for a pseudo cursor we are creating for downloaded songs
1892 Modeled after mCursor to be able to respond to respond to the same queries as it
1894 private static final String[] PROJECTION_MATRIX = new String[] {
1895 "_id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
1896 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
1897 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
1898 MediaStore.Audio.Media.ARTIST_ID
1902 * Creates a pseudo cursor for downloaded audio files with minimal info
1903 * @param context needed to query the download uri
1904 * @param uri the uri of the downloaded file
1906 private void updateCursorForDownloadedFile(Context context, Uri uri) {
1907 synchronized (this) {
1908 closeCursor(); // clear mCursor
1909 MatrixCursor cursor = new MatrixCursor(PROJECTION_MATRIX);
1910 // get title of the downloaded file ; Downloads.Impl.COLUMN_TITLE
1911 String title = getValueForDownloadedFile(this, uri, "title" );
1912 // populating the cursor with bare minimum info
1913 cursor.addRow(new Object[] {
1924 mCursor.moveToFirst();
1929 * Query the DownloadProvider to get the value in the specified column
1931 * @param uri the uri of the downloaded file
1935 private String getValueForDownloadedFile(Context context, Uri uri, String column) {
1937 final String[] projection = {
1940 try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null)) {
1941 if (cursor != null && cursor.moveToFirst()) {
1942 return cursor.getString(0);
1949 * Returns the audio session ID
1951 * @return The current media player audio session ID
1953 public int getAudioSessionId() {
1954 synchronized (this) {
1955 return mPlayer.getAudioSessionId();
1960 * Indicates if the media storeage device has been mounted or not
1962 * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
1964 public int getMediaMountedCount() {
1965 return mMediaMountedCount;
1969 * Returns the shuffle mode
1971 * @return The current shuffle mode (all, party, none)
1973 public int getShuffleMode() {
1974 return mShuffleMode;
1978 * Returns the repeat mode
1980 * @return The current repeat mode (all, one, none)
1982 public int getRepeatMode() {
1987 * Removes all instances of the track with the given ID from the playlist.
1989 * @param id The id to be removed
1990 * @return how many instances of the track were removed
1992 public int removeTrack(final long id) {
1994 synchronized (this) {
1995 for (int i = 0; i < mPlaylist.size(); i++) {
1996 if (mPlaylist.get(i).mId == id) {
1997 numremoved += removeTracksInternal(i, i);
2002 if (numremoved > 0) {
2003 notifyChange(QUEUE_CHANGED);
2009 * Removes a song from the playlist at the specified position.
2011 * @param id The song id to be removed
2012 * @param position The position of the song in the playlist
2013 * @return true if successful
2015 public boolean removeTrackAtPosition(final long id, final int position) {
2016 synchronized (this) {
2017 if ( position >=0 &&
2018 position < mPlaylist.size() &&
2019 mPlaylist.get(position).mId == id ) {
2021 return removeTracks(position, position) > 0;
2028 * Removes the range of tracks specified from the play list. If a file
2029 * within the range is the file currently being played, playback will move
2030 * to the next file after the range.
2032 * @param first The first file to be removed
2033 * @param last The last file to be removed
2034 * @return the number of tracks deleted
2036 public int removeTracks(final int first, final int last) {
2037 final int numremoved = removeTracksInternal(first, last);
2038 if (numremoved > 0) {
2039 notifyChange(QUEUE_CHANGED);
2045 * Returns the position in the queue
2047 * @return the current position in the queue
2049 public int getQueuePosition() {
2050 synchronized (this) {
2056 * @return the size of the queue history cache
2058 public int getQueueHistorySize() {
2059 synchronized (this) {
2060 return mHistory.size();
2065 * @return the position in the history
2067 public int getQueueHistoryPosition(int position) {
2068 synchronized (this) {
2069 if (position >= 0 && position < mHistory.size()) {
2070 return mHistory.get(position);
2078 * @return the queue of history positions
2080 public int[] getQueueHistoryList() {
2081 synchronized (this) {
2082 int[] history = new int[mHistory.size()];
2083 for (int i = 0; i < mHistory.size(); i++) {
2084 history[i] = mHistory.get(i);
2092 * Returns the path to current song
2094 * @return The path to the current song
2096 public String getPath() {
2097 synchronized (this) {
2098 if (mCursor == null) {
2101 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
2106 * Returns the album name
2108 * @return The current song album Name
2110 public String getAlbumName() {
2111 synchronized (this) {
2112 if (mCursor == null) {
2115 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
2120 * Returns the song name
2122 * @return The current song name
2124 public String getTrackName() {
2125 synchronized (this) {
2126 if (mCursor == null) {
2129 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
2134 * Returns the genre name of song
2136 * @return The current song genre name
2138 public String getGenreName() {
2139 synchronized (this) {
2140 if (mCursor == null || mPlayPos < 0 || mPlayPos >= mPlaylist.size()) {
2143 String[] genreProjection = { MediaStore.Audio.Genres.NAME };
2144 Uri genreUri = MediaStore.Audio.Genres.getContentUriForAudioId("external",
2145 (int) mPlaylist.get(mPlayPos).mId);
2146 Cursor genreCursor = getContentResolver().query(genreUri, genreProjection,
2148 if (genreCursor != null) {
2150 if (genreCursor.moveToFirst()) {
2151 return genreCursor.getString(
2152 genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME));
2155 genreCursor.close();
2163 * Returns the artist name
2165 * @return The current song artist name
2167 public String getArtistName() {
2168 synchronized (this) {
2169 if (mCursor == null) {
2172 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
2177 * Returns the artist name
2179 * @return The current song artist name
2181 public String getAlbumArtistName() {
2182 synchronized (this) {
2183 if (mAlbumCursor == null) {
2186 return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(AlbumColumns.ARTIST));
2191 * Returns the album ID
2193 * @return The current song album ID
2195 public long getAlbumId() {
2196 synchronized (this) {
2197 if (mCursor == null) {
2200 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
2205 * Returns the artist ID
2207 * @return The current song artist ID
2209 public long getArtistId() {
2210 synchronized (this) {
2211 if (mCursor == null) {
2214 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
2219 * @return The audio id of the track
2221 public long getAudioId() {
2222 MusicPlaybackTrack track = getCurrentTrack();
2223 if (track != null) {
2231 * Gets the currently playing music track
2233 public MusicPlaybackTrack getCurrentTrack() {
2234 return getTrack(mPlayPos);
2238 * Gets the music track from the queue at the specified index
2239 * @param index position
2240 * @return music track or null
2242 public synchronized MusicPlaybackTrack getTrack(int index) {
2243 if (index >= 0 && index < mPlaylist.size() && mPlayer.isInitialized()) {
2244 return mPlaylist.get(index);
2251 * Returns the next audio ID
2253 * @return The next track ID
2255 public long getNextAudioId() {
2256 synchronized (this) {
2257 if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size() && mPlayer.isInitialized()) {
2258 return mPlaylist.get(mNextPlayPos).mId;
2265 * Returns the previous audio ID
2267 * @return The previous track ID
2269 public long getPreviousAudioId() {
2270 synchronized (this) {
2271 if (mPlayer.isInitialized()) {
2272 int pos = getPreviousPlayPosition(false);
2273 if (pos >= 0 && pos < mPlaylist.size()) {
2274 return mPlaylist.get(pos).mId;
2282 * Seeks the current track to a specific time
2284 * @param position The time to seek to
2285 * @return The time to play the track at
2287 public long seek(long position) {
2288 if (mPlayer.isInitialized()) {
2291 } else if (position > mPlayer.duration()) {
2292 position = mPlayer.duration();
2294 long result = mPlayer.seek(position);
2295 notifyChange(POSITION_CHANGED);
2302 * Seeks the current track to a position relative to its current position
2303 * If the relative position is after or before the track, it will also automatically
2304 * jump to the previous or next track respectively
2306 * @param deltaInMs The delta time to seek to in milliseconds
2308 public void seekRelative(long deltaInMs) {
2309 synchronized (this) {
2310 if (mPlayer.isInitialized()) {
2311 final long newPos = position() + deltaInMs;
2312 final long duration = duration();
2315 // seek to the new duration + the leftover position
2316 seek(duration() + newPos);
2317 } else if (newPos >= duration) {
2319 // seek to the leftover duration
2320 seek(newPos - duration);
2329 * Returns the current position in time of the currenttrack
2331 * @return The current playback position in miliseconds
2333 public long position() {
2334 if (mPlayer.isInitialized()) {
2335 return mPlayer.position();
2341 * Returns the full duration of the current track
2343 * @return The duration of the current track in miliseconds
2345 public long duration() {
2346 if (mPlayer.isInitialized()) {
2347 return mPlayer.duration();
2355 * @return The queue as a long[]
2357 public long[] getQueue() {
2358 synchronized (this) {
2359 final int len = mPlaylist.size();
2360 final long[] list = new long[len];
2361 for (int i = 0; i < len; i++) {
2362 list[i] = mPlaylist.get(i).mId;
2369 * Gets the track id at a given position in the queue
2371 * @return track id in the queue position
2373 public long getQueueItemAtPosition(int position) {
2374 synchronized (this) {
2375 if (position >= 0 && position < mPlaylist.size()) {
2376 return mPlaylist.get(position).mId;
2384 * @return the size of the queue
2386 public int getQueueSize() {
2387 synchronized (this) {
2388 return mPlaylist.size();
2393 * @return True if music is playing, false otherwise
2395 public boolean isPlaying() {
2396 return mIsSupposedToBePlaying;
2400 * Helper function to wrap the logic around mIsSupposedToBePlaying for consistentcy
2401 * @param value to set mIsSupposedToBePlaying to
2402 * @param notify whether we want to fire PLAYSTATE_CHANGED event
2404 private void setIsSupposedToBePlaying(boolean value, boolean notify) {
2405 if (mIsSupposedToBePlaying != value) {
2406 mIsSupposedToBePlaying = value;
2408 // Update mLastPlayed time first and notify afterwards, as
2409 // the notification listener method needs the up-to-date value
2410 // for the recentlyPlayed() method to work
2411 if (!mIsSupposedToBePlaying) {
2412 scheduleDelayedShutdown();
2413 mLastPlayedTime = System.currentTimeMillis();
2417 notifyChange(PLAYSTATE_CHANGED);
2423 * @return true if is playing or has played within the last IDLE_DELAY time
2425 private boolean recentlyPlayed() {
2426 return isPlaying() || System.currentTimeMillis() - mLastPlayedTime < IDLE_DELAY;
2430 * Opens a list for playback
2432 * @param list The list of tracks to open
2433 * @param position The position to start playback at
2435 public void open(final long[] list, final int position, long sourceId, IdType sourceType) {
2436 synchronized (this) {
2437 if (mShuffleMode == SHUFFLE_AUTO) {
2438 mShuffleMode = SHUFFLE_NORMAL;
2440 final long oldId = getAudioId();
2441 final int listlength = list.length;
2442 boolean newlist = true;
2443 if (mPlaylist.size() == listlength) {
2445 for (int i = 0; i < listlength; i++) {
2446 if (list[i] != mPlaylist.get(i).mId) {
2453 addToPlayList(list, -1, sourceId, sourceType);
2454 notifyChange(QUEUE_CHANGED);
2456 if (position >= 0) {
2457 mPlayPos = position;
2459 mPlayPos = mShuffler.nextInt(mPlaylist.size());
2462 openCurrentAndNext();
2463 if (oldId != getAudioId()) {
2464 notifyChange(META_CHANGED);
2472 public void stop() {
2473 stopShakeDetector(false);
2478 * Resumes or starts playback.
2480 public void play() {
2481 startShakeDetector();
2486 * Resumes or starts playback.
2487 * @param createNewNextTrack True if you want to figure out the next track, false
2488 * if you want to re-use the existing next track (used for going back)
2490 public void play(boolean createNewNextTrack) {
2491 int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
2492 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
2494 if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
2496 if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
2500 final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
2501 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2502 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2503 sendBroadcast(intent);
2505 mSession.setActive(true);
2507 if (createNewNextTrack) {
2510 setNextTrack(mNextPlayPos);
2513 if (mPlayer.isInitialized()) {
2514 final long duration = mPlayer.duration();
2515 if (mRepeatMode != REPEAT_CURRENT && duration > 2000
2516 && mPlayer.position() >= duration - 2000) {
2521 mPlayerHandler.removeMessages(FADEDOWN);
2522 mPlayerHandler.sendEmptyMessage(FADEUP);
2524 setIsSupposedToBePlaying(true, true);
2527 updateNotification();
2528 } else if (mPlaylist.size() <= 0) {
2529 setShuffleMode(SHUFFLE_AUTO);
2533 private void togglePlayPause() {
2536 mPausedByTransientLossOfFocus = false;
2543 * Temporarily pauses playback.
2545 public void pause() {
2546 if (mPlayerHandler == null) return;
2547 if (D) Log.d(TAG, "Pausing playback");
2548 synchronized (this) {
2549 if (mPlayerHandler != null) {
2550 mPlayerHandler.removeMessages(FADEUP);
2552 if (mIsSupposedToBePlaying) {
2553 final Intent intent = new Intent(
2554 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
2555 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2556 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2557 sendBroadcast(intent);
2559 if (mPlayer != null) {
2562 setIsSupposedToBePlaying(false, true);
2563 stopShakeDetector(false);
2569 * Changes from the current track to the next track
2571 public void gotoNext(final boolean force) {
2572 if (D) Log.d(TAG, "Going to next track");
2573 synchronized (this) {
2574 if (mPlaylist.size() <= 0) {
2575 if (D) Log.d(TAG, "No play queue");
2576 scheduleDelayedShutdown();
2579 int pos = mNextPlayPos;
2581 pos = getNextPosition(force);
2585 setIsSupposedToBePlaying(false, true);
2590 setAndRecordPlayPos(pos);
2591 openCurrentAndNext();
2593 notifyChange(META_CHANGED);
2597 public void setAndRecordPlayPos(int nextPos) {
2598 synchronized (this) {
2599 // save to the history
2600 if (mShuffleMode != SHUFFLE_NONE) {
2601 mHistory.add(mPlayPos);
2602 if (mHistory.size() > MAX_HISTORY_SIZE) {
2612 * Changes from the current track to the previous played track
2614 public void prev(boolean forcePrevious) {
2615 synchronized (this) {
2616 // if we aren't repeating 1, and we are either early in the song
2617 // or we want to force go back, then go to the prevous track
2618 boolean goPrevious = getRepeatMode() != REPEAT_CURRENT &&
2619 (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD || forcePrevious);
2622 if (D) Log.d(TAG, "Going to previous track");
2623 int pos = getPreviousPlayPosition(true);
2624 // if we have no more previous tracks, quit
2628 mNextPlayPos = mPlayPos;
2633 notifyChange(META_CHANGED);
2635 if (D) Log.d(TAG, "Going to beginning of track");
2642 public int getPreviousPlayPosition(boolean removeFromHistory) {
2643 synchronized (this) {
2644 if (mShuffleMode == SHUFFLE_NORMAL) {
2645 // Go to previously-played track and remove it from the history
2646 final int histsize = mHistory.size();
2647 if (histsize == 0) {
2650 final Integer pos = mHistory.get(histsize - 1);
2651 if (removeFromHistory) {
2652 mHistory.remove(histsize - 1);
2657 return mPlayPos - 1;
2659 return mPlaylist.size() - 1;
2666 * We don't want to open the current and next track when the user is using
2667 * the {@code #prev()} method because they won't be able to travel back to
2668 * the previously listened track if they're shuffling.
2670 private void openCurrent() {
2671 openCurrentAndMaybeNext(false);
2675 * Moves an item in the queue from one position to another
2677 * @param from The position the item is currently at
2678 * @param to The position the item is being moved to
2680 public void moveQueueItem(int index1, int index2) {
2681 synchronized (this) {
2682 if (index1 >= mPlaylist.size()) {
2683 index1 = mPlaylist.size() - 1;
2685 if (index2 >= mPlaylist.size()) {
2686 index2 = mPlaylist.size() - 1;
2689 if (index1 == index2) {
2693 final MusicPlaybackTrack track = mPlaylist.remove(index1);
2694 if (index1 < index2) {
2695 mPlaylist.add(index2, track);
2696 if (mPlayPos == index1) {
2698 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
2701 } else if (index2 < index1) {
2702 mPlaylist.add(index2, track);
2703 if (mPlayPos == index1) {
2705 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
2709 notifyChange(QUEUE_CHANGED);
2714 * Sets the repeat mode
2716 * @param repeatmode The repeat mode to use
2718 public void setRepeatMode(final int repeatmode) {
2719 synchronized (this) {
2720 mRepeatMode = repeatmode;
2723 notifyChange(REPEATMODE_CHANGED);
2728 * Sets the shuffle mode
2730 * @param shufflemode The shuffle mode to use
2732 public void setShuffleMode(final int shufflemode) {
2733 synchronized (this) {
2734 if (mShuffleMode == shufflemode && mPlaylist.size() > 0) {
2738 mShuffleMode = shufflemode;
2739 if (mShuffleMode == SHUFFLE_AUTO) {
2740 if (makeAutoShuffleList()) {
2742 doAutoShuffleUpdate();
2744 openCurrentAndNext();
2746 notifyChange(META_CHANGED);
2749 mShuffleMode = SHUFFLE_NONE;
2755 notifyChange(SHUFFLEMODE_CHANGED);
2760 * Sets the position of a track in the queue
2762 * @param index The position to place the track
2764 public void setQueuePosition(final int index) {
2765 synchronized (this) {
2768 openCurrentAndNext();
2770 notifyChange(META_CHANGED);
2771 if (mShuffleMode == SHUFFLE_AUTO) {
2772 doAutoShuffleUpdate();
2778 * Queues a new list for playback
2780 * @param list The list to queue
2781 * @param action The action to take
2783 public void enqueue(final long[] list, final int action, long sourceId, IdType sourceType) {
2784 synchronized (this) {
2785 if (action == NEXT && mPlayPos + 1 < mPlaylist.size()) {
2786 addToPlayList(list, mPlayPos + 1, sourceId, sourceType);
2787 mNextPlayPos = mPlayPos + 1;
2788 notifyChange(QUEUE_CHANGED);
2790 addToPlayList(list, Integer.MAX_VALUE, sourceId, sourceType);
2791 notifyChange(QUEUE_CHANGED);
2796 openCurrentAndNext();
2798 notifyChange(META_CHANGED);
2804 * Cycles through the different repeat modes
2806 private void cycleRepeat() {
2807 if (mRepeatMode == REPEAT_NONE) {
2808 setRepeatMode(REPEAT_ALL);
2809 } else if (mRepeatMode == REPEAT_ALL) {
2810 setRepeatMode(REPEAT_CURRENT);
2811 if (mShuffleMode != SHUFFLE_NONE) {
2812 setShuffleMode(SHUFFLE_NONE);
2815 setRepeatMode(REPEAT_NONE);
2820 * Cycles through the different shuffle modes
2822 private void cycleShuffle() {
2823 if (mShuffleMode == SHUFFLE_NONE) {
2824 setShuffleMode(SHUFFLE_NORMAL);
2825 if (mRepeatMode == REPEAT_CURRENT) {
2826 setRepeatMode(REPEAT_ALL);
2828 } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
2829 setShuffleMode(SHUFFLE_NONE);
2834 * @param smallBitmap true to return a smaller version of the default artwork image.
2835 * Currently Has no impact on the artwork size if one exists
2836 * @return The album art for the current album.
2838 public BitmapWithColors getAlbumArt(boolean smallBitmap) {
2839 final String albumName = getAlbumName();
2840 final String artistName = getArtistName();
2841 final long albumId = getAlbumId();
2842 final String key = albumName + "_" + artistName + "_" + albumId;
2843 final int targetIndex = smallBitmap ? 0 : 1;
2845 // if the cached key matches and we have the bitmap, return it
2846 if (key.equals(mCachedKey) && mCachedBitmapWithColors[targetIndex] != null) {
2847 return mCachedBitmapWithColors[targetIndex];
2850 // otherwise get the artwork (or defaultartwork if none found)
2851 final BitmapWithColors bitmap = mImageFetcher.getArtwork(albumName,
2852 albumId, artistName, smallBitmap);
2854 // if the key is different, clear the bitmaps first
2855 if (!key.equals(mCachedKey)) {
2856 mCachedBitmapWithColors[0] = null;
2857 mCachedBitmapWithColors[1] = null;
2860 // store the new key and bitmap
2862 mCachedBitmapWithColors[targetIndex] = bitmap;
2867 * Called when one of the lists should refresh or requery.
2869 public void refresh() {
2870 notifyChange(REFRESH);
2874 * Called when one of the playlists have changed (renamed, added/removed tracks)
2876 public void playlistChanged() {
2877 notifyChange(PLAYLIST_CHANGED);
2881 * Called to set the status of shake to play feature
2883 public void setShakeToPlayEnabled(boolean enabled) {
2885 Log.d(TAG, "ShakeToPlay status: " + enabled);
2888 if (mShakeDetector == null) {
2889 mShakeDetector = new ShakeDetector(mShakeDetectorListener);
2891 // if song is already playing, start listening immediately
2893 startShakeDetector();
2897 stopShakeDetector(true);
2902 * Called to set visibility of album art on lockscreen
2904 public void setLockscreenAlbumArt(boolean enabled) {
2905 mShowAlbumArtOnLockscreen = enabled;
2906 notifyChange(META_CHANGED);
2910 * Called to start listening to shakes
2912 private void startShakeDetector() {
2913 if (mShakeDetector != null) {
2914 mShakeDetector.start((SensorManager)getSystemService(SENSOR_SERVICE));
2919 * Called to stop listening to shakes
2921 private void stopShakeDetector(final boolean destroyShakeDetector) {
2922 if (mShakeDetector != null) {
2923 mShakeDetector.stop();
2925 if(destroyShakeDetector){
2926 mShakeDetector = null;
2928 Log.d(TAG, "ShakeToPlay destroyed!!!");
2933 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
2938 public void onReceive(final Context context, final Intent intent) {
2939 final String command = intent.getStringExtra(CMDNAME);
2941 if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
2942 final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2943 mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
2944 } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
2945 final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2946 mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
2947 } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
2948 final int[] largeAlt = intent
2949 .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2950 mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
2952 handleCommandIntent(intent);
2957 private ContentObserver mMediaStoreObserver;
2959 private class MediaStoreObserver extends ContentObserver implements Runnable {
2960 // milliseconds to delay before calling refresh to aggregate events
2961 private static final long REFRESH_DELAY = 500;
2962 private Handler mHandler;
2964 public MediaStoreObserver(Handler handler) {
2970 public void onChange(boolean selfChange) {
2971 // if a change is detected, remove any scheduled callback
2972 // then post a new one. This is intended to prevent closely
2973 // spaced events from generating multiple refresh calls
2974 mHandler.removeCallbacks(this);
2975 mHandler.postDelayed(this, REFRESH_DELAY);
2980 // actually call refresh when the delayed callback fires
2981 Log.e("ELEVEN", "calling refresh!");
2986 private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
2991 public void onAudioFocusChange(final int focusChange) {
2992 mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
2996 private static final class MusicPlayerHandler extends Handler {
2997 private final WeakReference<MusicPlaybackService> mService;
2998 private float mCurrentVolume = 1.0f;
3000 private static final int DOUBLE_CLICK_TIMEOUT = 800;
3001 private int mHeadsetHookClickCounter = 0;
3004 * Constructor of <code>MusicPlayerHandler</code>
3006 * @param service The service to use.
3007 * @param looper The thread to run on.
3009 public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
3011 mService = new WeakReference<>(service);
3018 public void handleMessage(final Message msg) {
3019 final MusicPlaybackService service = mService.get();
3020 if (service == null) {
3024 synchronized (service) {
3027 mCurrentVolume -= .05f;
3028 if (mCurrentVolume > .2f) {
3029 sendEmptyMessageDelayed(FADEDOWN, 10);
3031 mCurrentVolume = .2f;
3033 service.mPlayer.setVolume(mCurrentVolume);
3036 mCurrentVolume += .01f;
3037 if (mCurrentVolume < 1.0f) {
3038 sendEmptyMessageDelayed(FADEUP, 10);
3040 mCurrentVolume = 1.0f;
3042 service.mPlayer.setVolume(mCurrentVolume);
3045 if (service.isPlaying()) {
3046 final TrackErrorInfo info = (TrackErrorInfo)msg.obj;
3047 service.sendErrorMessage(info.mTrackName);
3049 // since the service isPlaying(), we only need to remove the offending
3050 // audio track, and the code will automatically play the next track
3051 service.removeTrack(info.mId);
3053 service.openCurrentAndNext();
3056 case TRACK_WENT_TO_NEXT:
3057 service.setAndRecordPlayPos(service.mNextPlayPos);
3058 service.setNextTrack();
3059 if (service.mCursor != null) {
3060 service.mCursor.close();
3061 service.mCursor = null;
3063 service.updateCursor(service.mPlaylist.get(service.mPlayPos).mId);
3064 service.notifyChange(META_CHANGED);
3065 service.updateNotification();
3068 if (service.mRepeatMode == REPEAT_CURRENT) {
3072 service.gotoNext(false);
3076 service.mLyrics = (String) msg.obj;
3077 service.notifyChange(NEW_LYRICS);
3080 if (D) Log.d(TAG, "Received audio focus change event " + msg.arg1);
3082 case AudioManager.AUDIOFOCUS_LOSS:
3083 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
3084 if (service.isPlaying()) {
3085 service.mPausedByTransientLossOfFocus =
3086 msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
3090 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
3091 removeMessages(FADEUP);
3092 sendEmptyMessage(FADEDOWN);
3094 case AudioManager.AUDIOFOCUS_GAIN:
3095 if (!service.isPlaying()
3096 && service.mPausedByTransientLossOfFocus) {
3097 service.mPausedByTransientLossOfFocus = false;
3098 mCurrentVolume = 0f;
3099 service.mPlayer.setVolume(mCurrentVolume);
3102 removeMessages(FADEDOWN);
3103 sendEmptyMessage(FADEUP);
3109 case HEADSET_HOOK_EVENT: {
3110 long eventTime = (Long) msg.obj;
3112 mHeadsetHookClickCounter = Math.min(mHeadsetHookClickCounter + 1, 3);
3113 if (D) Log.d(TAG, "Got headset click, count = " + mHeadsetHookClickCounter);
3114 removeMessages(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3116 if (mHeadsetHookClickCounter == 3) {
3117 sendEmptyMessage(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3119 sendEmptyMessageAtTime(HEADSET_HOOK_MULTI_CLICK_TIMEOUT,
3120 eventTime + DOUBLE_CLICK_TIMEOUT);
3124 case HEADSET_HOOK_MULTI_CLICK_TIMEOUT:
3125 if (D) Log.d(TAG, "Handling headset click");
3126 switch (mHeadsetHookClickCounter) {
3127 case 1: service.togglePlayPause(); break;
3128 case 2: service.gotoNext(true); break;
3129 case 3: service.prev(false); break;
3131 mHeadsetHookClickCounter = 0;
3132 service.mHeadsetHookWakeLock.release();
3141 private static final class Shuffler {
3143 private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<>();
3145 private final TreeSet<Integer> mPreviousNumbers = new TreeSet<>();
3147 private final Random mRandom = new Random();
3149 private int mPrevious;
3152 * Constructor of <code>Shuffler</code>
3159 * @param interval The length the queue
3160 * @return The position of the next track to play
3162 public int nextInt(final int interval) {
3165 next = mRandom.nextInt(interval);
3166 } while (next == mPrevious && interval > 1 && !mPreviousNumbers.contains(next));
3168 mHistoryOfNumbers.add(mPrevious);
3169 mPreviousNumbers.add(mPrevious);
3175 * Removes old tracks and cleans up the history preparing for new tracks
3176 * to be added to the mapping
3178 private void cleanUpHistory() {
3179 if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
3180 for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
3181 mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
3187 private static final class TrackErrorInfo {
3189 public String mTrackName;
3191 public TrackErrorInfo(long id, String trackName) {
3193 mTrackName = trackName;
3197 private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
3198 MediaPlayer.OnCompletionListener {
3200 private final WeakReference<MusicPlaybackService> mService;
3202 private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
3204 private MediaPlayer mNextMediaPlayer;
3206 private Handler mHandler;
3208 private boolean mIsInitialized = false;
3210 private SrtManager mSrtManager;
3212 private String mNextMediaPath;
3215 * Constructor of <code>MultiPlayer</code>
3217 public MultiPlayer(final MusicPlaybackService service) {
3218 mService = new WeakReference<>(service);
3219 mSrtManager = new SrtManager() {
3221 public void onTimedText(String text) {
3222 mHandler.obtainMessage(LYRICS, text).sendToTarget();
3228 * @param path The path of the file, or the http/rtsp URL of the stream
3231 public void setDataSource(final String path) {
3232 mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
3233 if (mIsInitialized) {
3235 setNextDataSource(null);
3239 private void loadSrt(final String path) {
3240 mSrtManager.reset();
3242 Uri uri = Uri.parse(path);
3243 String filePath = null;
3245 if (path.startsWith("content://")) {
3246 // resolve the content resolver path to a file path
3247 Cursor cursor = null;
3249 final String[] proj = {MediaStore.Audio.Media.DATA};
3250 cursor = mService.get().getContentResolver().query(uri, proj,
3252 if (cursor != null && cursor.moveToFirst()) {
3253 filePath = cursor.getString(0);
3256 if (cursor != null) {
3262 filePath = uri.getPath();
3265 if (!TextUtils.isEmpty(filePath)) {
3266 final int lastIndex = filePath.lastIndexOf('.');
3267 if (lastIndex != -1) {
3268 String newPath = filePath.substring(0, lastIndex) + ".srt";
3269 final File f = new File(newPath);
3271 mSrtManager.initialize(mCurrentMediaPlayer, f);
3277 * @param player The {@link MediaPlayer} to use
3278 * @param path The path of the file, or the http/rtsp URL of the stream
3280 * @return True if the <code>player</code> has been prepared and is
3281 * ready to play, false otherwise
3283 private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
3286 player.setOnPreparedListener(null);
3287 if (path.startsWith("content://")) {
3288 player.setDataSource(mService.get(), Uri.parse(path));
3290 player.setDataSource(path);
3292 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
3295 } catch (final IOException todo) {
3296 // TODO: notify the user why the file couldn't be opened
3298 } catch (final IllegalArgumentException todo) {
3299 // TODO: notify the user why the file couldn't be opened
3302 player.setOnCompletionListener(this);
3303 player.setOnErrorListener(this);
3308 * Set the MediaPlayer to start when this MediaPlayer finishes playback.
3310 * @param path The path of the file, or the http/rtsp URL of the stream
3313 public void setNextDataSource(final String path) {
3314 mNextMediaPath = null;
3316 mCurrentMediaPlayer.setNextMediaPlayer(null);
3317 } catch (IllegalArgumentException e) {
3318 Log.i(TAG, "Next media player is current one, continuing");
3319 } catch (IllegalStateException e) {
3320 Log.e(TAG, "Media player not initialized!");
3323 if (mNextMediaPlayer != null) {
3324 mNextMediaPlayer.release();
3325 mNextMediaPlayer = null;
3330 mNextMediaPlayer = new MediaPlayer();
3331 mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
3332 if (setDataSourceImpl(mNextMediaPlayer, path)) {
3333 mNextMediaPath = path;
3334 mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
3336 if (mNextMediaPlayer != null) {
3337 mNextMediaPlayer.release();
3338 mNextMediaPlayer = null;
3346 * @param handler The handler to use
3348 public void setHandler(final Handler handler) {
3353 * @return True if the player is ready to go, false otherwise
3355 public boolean isInitialized() {
3356 return mIsInitialized;
3360 * Starts or resumes playback.
3362 public void start() {
3363 mCurrentMediaPlayer.start();
3368 * Resets the MediaPlayer to its uninitialized state.
3370 public void stop() {
3371 mCurrentMediaPlayer.reset();
3372 mSrtManager.reset();
3373 mIsInitialized = false;
3377 * Releases resources associated with this MediaPlayer object.
3379 public void release() {
3380 mCurrentMediaPlayer.release();
3381 mSrtManager.release();
3386 * Pauses playback. Call start() to resume.
3388 public void pause() {
3389 mCurrentMediaPlayer.pause();
3390 mSrtManager.pause();
3394 * Gets the duration of the file.
3396 * @return The duration in milliseconds
3398 public long duration() {
3399 return mCurrentMediaPlayer.getDuration();
3403 * Gets the current playback position.
3405 * @return The current position in milliseconds
3407 public long position() {
3408 return mCurrentMediaPlayer.getCurrentPosition();
3412 * Gets the current playback position.
3414 * @param whereto The offset in milliseconds from the start to seek to
3415 * @return The offset in milliseconds from the start to seek to
3417 public long seek(final long whereto) {
3418 mCurrentMediaPlayer.seekTo((int)whereto);
3419 mSrtManager.seekTo(whereto);
3424 * Sets the volume on this player.
3426 * @param vol Left and right volume scalar
3428 public void setVolume(final float vol) {
3429 mCurrentMediaPlayer.setVolume(vol, vol);
3433 * Sets the audio session ID.
3435 * @param sessionId The audio session ID
3437 public void setAudioSessionId(final int sessionId) {
3438 mCurrentMediaPlayer.setAudioSessionId(sessionId);
3442 * Returns the audio session ID.
3444 * @return The current audio session ID.
3446 public int getAudioSessionId() {
3447 return mCurrentMediaPlayer.getAudioSessionId();
3454 public boolean onError(final MediaPlayer mp, final int what, final int extra) {
3455 Log.w(TAG, "Music Server Error what: " + what + " extra: " + extra);
3457 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
3458 final MusicPlaybackService service = mService.get();
3459 if (service == null) {
3462 final TrackErrorInfo errorInfo = new TrackErrorInfo(service.getAudioId(),
3463 service.getTrackName());
3465 mIsInitialized = false;
3466 mCurrentMediaPlayer.release();
3467 mCurrentMediaPlayer = new MediaPlayer();
3468 Message msg = mHandler.obtainMessage(SERVER_DIED, errorInfo);
3469 mHandler.sendMessageDelayed(msg, 2000);
3481 public void onCompletion(final MediaPlayer mp) {
3482 if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
3483 mCurrentMediaPlayer.release();
3484 mCurrentMediaPlayer = mNextMediaPlayer;
3485 loadSrt(mNextMediaPath);
3486 mNextMediaPath = null;
3487 mNextMediaPlayer = null;
3488 mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
3490 mHandler.sendEmptyMessage(TRACK_ENDED);
3495 private static final class ServiceStub extends IElevenService.Stub {
3497 private final WeakReference<MusicPlaybackService> mService;
3499 private ServiceStub(final MusicPlaybackService service) {
3500 mService = new WeakReference<>(service);
3507 public void openFile(final String path) throws RemoteException {
3508 mService.get().openFile(path);
3515 public void open(final long[] list, final int position, long sourceId, int sourceType)
3516 throws RemoteException {
3517 mService.get().open(list, position, sourceId, IdType.getTypeById(sourceType));
3524 public void stop() throws RemoteException {
3525 mService.get().stop();
3532 public void pause() throws RemoteException {
3533 mService.get().pause();
3540 public void play() throws RemoteException {
3541 mService.get().play();
3548 public void prev(boolean forcePrevious) throws RemoteException {
3549 mService.get().prev(forcePrevious);
3556 public void next() throws RemoteException {
3557 mService.get().gotoNext(true);
3564 public void enqueue(final long[] list, final int action, long sourceId, int sourceType)
3565 throws RemoteException {
3566 mService.get().enqueue(list, action, sourceId, IdType.getTypeById(sourceType));
3573 public void setQueuePosition(final int index) throws RemoteException {
3574 mService.get().setQueuePosition(index);
3581 public void setShuffleMode(final int shufflemode) throws RemoteException {
3582 mService.get().setShuffleMode(shufflemode);
3589 public void setRepeatMode(final int repeatmode) throws RemoteException {
3590 mService.get().setRepeatMode(repeatmode);
3597 public void moveQueueItem(final int from, final int to) throws RemoteException {
3598 mService.get().moveQueueItem(from, to);
3605 public void refresh() throws RemoteException {
3606 mService.get().refresh();
3613 public void playlistChanged() throws RemoteException {
3614 mService.get().playlistChanged();
3621 public boolean isPlaying() throws RemoteException {
3622 return mService.get().isPlaying();
3629 public long[] getQueue() throws RemoteException {
3630 return mService.get().getQueue();
3637 public long getQueueItemAtPosition(int position) throws RemoteException {
3638 return mService.get().getQueueItemAtPosition(position);
3645 public int getQueueSize() throws RemoteException {
3646 return mService.get().getQueueSize();
3653 public int getQueueHistoryPosition(int position) throws RemoteException {
3654 return mService.get().getQueueHistoryPosition(position);
3661 public int getQueueHistorySize() throws RemoteException {
3662 return mService.get().getQueueHistorySize();
3669 public int[] getQueueHistoryList() throws RemoteException {
3670 return mService.get().getQueueHistoryList();
3677 public long duration() throws RemoteException {
3678 return mService.get().duration();
3685 public long position() throws RemoteException {
3686 return mService.get().position();
3693 public long seek(final long position) throws RemoteException {
3694 return mService.get().seek(position);
3701 public void seekRelative(final long deltaInMs) throws RemoteException {
3702 mService.get().seekRelative(deltaInMs);
3709 public long getAudioId() throws RemoteException {
3710 return mService.get().getAudioId();
3717 public MusicPlaybackTrack getCurrentTrack() throws RemoteException {
3718 return mService.get().getCurrentTrack();
3725 public MusicPlaybackTrack getTrack(int index) throws RemoteException {
3726 return mService.get().getTrack(index);
3733 public long getNextAudioId() throws RemoteException {
3734 return mService.get().getNextAudioId();
3741 public long getPreviousAudioId() throws RemoteException {
3742 return mService.get().getPreviousAudioId();
3749 public long getArtistId() throws RemoteException {
3750 return mService.get().getArtistId();
3757 public long getAlbumId() throws RemoteException {
3758 return mService.get().getAlbumId();
3765 public String getArtistName() throws RemoteException {
3766 return mService.get().getArtistName();
3773 public String getTrackName() throws RemoteException {
3774 return mService.get().getTrackName();
3781 public String getAlbumName() throws RemoteException {
3782 return mService.get().getAlbumName();
3789 public String getPath() throws RemoteException {
3790 return mService.get().getPath();
3797 public int getQueuePosition() throws RemoteException {
3798 return mService.get().getQueuePosition();
3805 public int getShuffleMode() throws RemoteException {
3806 return mService.get().getShuffleMode();
3813 public int getRepeatMode() throws RemoteException {
3814 return mService.get().getRepeatMode();
3821 public int removeTracks(final int first, final int last) throws RemoteException {
3822 return mService.get().removeTracks(first, last);
3829 public int removeTrack(final long id) throws RemoteException {
3830 return mService.get().removeTrack(id);
3837 public boolean removeTrackAtPosition(final long id, final int position)
3838 throws RemoteException {
3839 return mService.get().removeTrackAtPosition(id, position);
3846 public int getMediaMountedCount() throws RemoteException {
3847 return mService.get().getMediaMountedCount();
3854 public int getAudioSessionId() throws RemoteException {
3855 return mService.get().getAudioSessionId();
3862 public void setShakeToPlayEnabled(boolean enabled) {
3863 mService.get().setShakeToPlayEnabled(enabled);
3870 public void setLockscreenAlbumArt(boolean enabled) {
3871 mService.get().setLockscreenAlbumArt(enabled);
3876 private class QueueUpdateTask extends AsyncTask<Void, Void, List<MediaSession.QueueItem>> {
3877 private long[] mQueue;
3879 public QueueUpdateTask(long[] queue) {
3880 mQueue = queue != null ? Arrays.copyOf(queue, queue.length) : null;
3884 protected List<MediaSession.QueueItem> doInBackground(Void... params) {
3885 if (mQueue == null || mQueue.length == 0) {
3889 final StringBuilder selection = new StringBuilder();
3890 selection.append(MediaStore.Audio.Media._ID).append(" IN (");
3891 for (int i = 0; i < mQueue.length; i++) {
3893 selection.append(",");
3895 selection.append(mQueue[i]);
3897 selection.append(")");
3899 Cursor c = getContentResolver().query(
3900 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
3901 new String[] { AudioColumns._ID, AudioColumns.TITLE, AudioColumns.ARTIST },
3902 selection.toString(), null, null);
3908 LongSparseArray<MediaDescription> descsById = new LongSparseArray<>();
3909 final int idColumnIndex = c.getColumnIndexOrThrow(AudioColumns._ID);
3910 final int titleColumnIndex = c.getColumnIndexOrThrow(AudioColumns.TITLE);
3911 final int artistColumnIndex = c.getColumnIndexOrThrow(AudioColumns.ARTIST);
3913 while (c.moveToNext() && !isCancelled()) {
3914 final MediaDescription desc = new MediaDescription.Builder()
3915 .setTitle(c.getString(titleColumnIndex))
3916 .setSubtitle(c.getString(artistColumnIndex))
3918 final long id = c.getLong(idColumnIndex);
3919 descsById.put(id, desc);
3922 List<MediaSession.QueueItem> items = new ArrayList<>();
3923 for (int i = 0; i < mQueue.length; i++) {
3924 MediaDescription desc = descsById.get(mQueue[i]);
3926 // shouldn't happen except in corner cases like
3927 // music being deleted while we were processing
3928 desc = new MediaDescription.Builder().build();
3930 items.add(new MediaSession.QueueItem(desc, i));
3939 protected void onPostExecute(List<MediaSession.QueueItem> items) {
3940 if (!isCancelled()) {
3941 mSession.setQueue(items);