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.text.TextUtils;
60 import android.util.Log;
61 import android.util.LongSparseArray;
62 import android.view.KeyEvent;
64 import androidx.annotation.NonNull;
65 import androidx.core.os.BuildCompat;
67 import org.lineageos.eleven.Config.IdType;
68 import org.lineageos.eleven.appwidgets.AppWidgetLarge;
69 import org.lineageos.eleven.appwidgets.AppWidgetLargeAlternate;
70 import org.lineageos.eleven.appwidgets.AppWidgetSmall;
71 import org.lineageos.eleven.cache.ImageCache;
72 import org.lineageos.eleven.cache.ImageFetcher;
73 import org.lineageos.eleven.provider.MusicPlaybackState;
74 import org.lineageos.eleven.provider.RecentStore;
75 import org.lineageos.eleven.provider.SongPlayCount;
76 import org.lineageos.eleven.service.MusicPlaybackTrack;
77 import org.lineageos.eleven.utils.BitmapWithColors;
78 import org.lineageos.eleven.utils.Lists;
79 import org.lineageos.eleven.utils.PreferenceUtils;
80 import org.lineageos.eleven.utils.ShakeDetector;
81 import org.lineageos.eleven.utils.SrtManager;
84 import java.io.IOException;
85 import java.lang.ref.WeakReference;
86 import java.util.ArrayList;
87 import java.util.Arrays;
88 import java.util.LinkedList;
89 import java.util.List;
90 import java.util.ListIterator;
91 import java.util.Random;
92 import java.util.TreeSet;
95 * A backbround {@link Service} used to keep music playing between activities
96 * and when the user moves Eleven into the background.
98 @SuppressLint("NewApi")
99 public class MusicPlaybackService extends Service {
100 private static final String TAG = "MusicPlaybackService";
101 private static final boolean D = false;
104 * Indicates that the music has paused or resumed
106 public static final String PLAYSTATE_CHANGED = BuildConstants.PACKAGE_NAME + ".playstatechanged";
109 * Indicates that music playback position within
110 * a title was changed
112 public static final String POSITION_CHANGED = BuildConstants.PACKAGE_NAME + ".positionchanged";
115 * Indicates the meta data has changed in some way, like a track change
117 public static final String META_CHANGED = BuildConstants.PACKAGE_NAME + ".metachanged";
120 * Indicates the queue has been updated
122 public static final String QUEUE_CHANGED = BuildConstants.PACKAGE_NAME + ".queuechanged";
125 * Indicates the queue has been updated
127 public static final String PLAYLIST_CHANGED = BuildConstants.PACKAGE_NAME + ".playlistchanged";
130 * Indicates the repeat mode changed
132 public static final String REPEATMODE_CHANGED = BuildConstants.PACKAGE_NAME + ".repeatmodechanged";
135 * Indicates the shuffle mode changed
137 public static final String SHUFFLEMODE_CHANGED = BuildConstants.PACKAGE_NAME + ".shufflemodechanged";
140 * Indicates the track fails to play
142 public static final String TRACK_ERROR = BuildConstants.PACKAGE_NAME + ".trackerror";
145 * For backwards compatibility reasons, also provide sticky
146 * broadcasts under the music package
148 public static final String ELEVEN_PACKAGE_NAME = BuildConstants.PACKAGE_NAME;
149 public static final String MUSIC_PACKAGE_NAME = "com.android.music";
152 * Called to indicate a general service commmand. Used in
153 * {@link MediaButtonIntentReceiver}
155 public static final String SERVICECMD = BuildConstants.PACKAGE_NAME + ".musicservicecommand";
158 * Called to go toggle between pausing and playing the music
160 public static final String TOGGLEPAUSE_ACTION = BuildConstants.PACKAGE_NAME + ".togglepause";
163 * Called to go to pause the playback
165 public static final String PAUSE_ACTION = BuildConstants.PACKAGE_NAME + ".pause";
168 * Called to go to stop the playback
170 public static final String STOP_ACTION = BuildConstants.PACKAGE_NAME + ".stop";
173 * Called to go to the previous track or the beginning of the track if partway through the track
175 public static final String PREVIOUS_ACTION = BuildConstants.PACKAGE_NAME + ".previous";
178 * Called to go to the previous track regardless of how far in the current track the playback is
180 public static final String PREVIOUS_FORCE_ACTION = BuildConstants.PACKAGE_NAME + ".previous.force";
183 * Called to go to the next track
185 public static final String NEXT_ACTION = BuildConstants.PACKAGE_NAME + ".next";
188 * Called to change the repeat mode
190 public static final String REPEAT_ACTION = BuildConstants.PACKAGE_NAME + ".repeat";
193 * Called to change the shuffle mode
195 public static final String SHUFFLE_ACTION = BuildConstants.PACKAGE_NAME + ".shuffle";
197 public static final String FROM_MEDIA_BUTTON = "frommediabutton";
199 public static final String TIMESTAMP = "timestamp";
202 * Used to easily notify a list that it should refresh. i.e. A playlist
205 public static final String REFRESH = BuildConstants.PACKAGE_NAME + ".refresh";
208 * Used by the alarm intent to shutdown the service after being idle
210 private static final String SHUTDOWN = BuildConstants.PACKAGE_NAME + ".shutdown";
213 * Called to notify of a timed text
215 public static final String NEW_LYRICS = BuildConstants.PACKAGE_NAME + ".lyrics";
218 * Called to update the remote control client
220 public static final String UPDATE_LOCKSCREEN = BuildConstants.PACKAGE_NAME + ".updatelockscreen";
222 public static final String CMDNAME = "command";
224 public static final String CMDTOGGLEPAUSE = "togglepause";
226 public static final String CMDSTOP = "stop";
228 public static final String CMDPAUSE = "pause";
230 public static final String CMDPLAY = "play";
232 public static final String CMDPREVIOUS = "previous";
234 public static final String CMDNEXT = "next";
236 public static final String CMDHEADSETHOOK = "headsethook";
238 private static final int IDCOLIDX = 0;
241 * Moves a list to the next position in the queue
243 public static final int NEXT = 2;
246 * Moves a list to the last position in the queue
248 public static final int LAST = 3;
251 * Shuffles no songs, turns shuffling off
253 public static final int SHUFFLE_NONE = 0;
258 public static final int SHUFFLE_NORMAL = 1;
263 public static final int SHUFFLE_AUTO = 2;
268 public static final int REPEAT_NONE = 0;
271 * Repeats the current track in a list
273 public static final int REPEAT_CURRENT = 1;
276 * Repeats all the tracks in a list
278 public static final int REPEAT_ALL = 2;
281 * Indicates when the track ends
283 private static final int TRACK_ENDED = 1;
286 * Indicates that the current track was changed the next track
288 private static final int TRACK_WENT_TO_NEXT = 2;
291 * Indicates the player died
293 private static final int SERVER_DIED = 3;
296 * Indicates some sort of focus change, maybe a phone call
298 private static final int FOCUSCHANGE = 4;
301 * Indicates to fade the volume down
303 private static final int FADEDOWN = 5;
306 * Indicates to fade the volume back up
308 private static final int FADEUP = 6;
311 * Notifies that there is a new timed text string
313 private static final int LYRICS = 7;
316 * Indicates a headset hook key event
318 private static final int HEADSET_HOOK_EVENT = 8;
321 * Indicates waiting for another headset hook event has timed out
323 private static final int HEADSET_HOOK_MULTI_CLICK_TIMEOUT = 9;
326 * Idle time before stopping the foreground notfication (5 minutes)
328 private static final int IDLE_DELAY = 5 * 60 * 1000;
331 * Song play time used as threshold for rewinding to the beginning of the
332 * track instead of skipping to the previous track when getting the PREVIOUS
335 private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
338 * The max size allowed for the track history
339 * TODO: Comeback and rewrite/fix all the whole queue code bugs after demo
341 public static final int MAX_HISTORY_SIZE = 1000;
343 private static final String ACTION_AUDIO_PLAYER = BuildConstants.PACKAGE_NAME + ".AUDIO_PLAYER";
345 private static final String CHANNEL_NAME = "eleven_playback";
347 public interface TrackErrorExtra {
349 * Name of the track that was unable to play
351 public static final String TRACK_NAME = "trackname";
355 * The columns used to retrieve any info from the current track
357 private static final String[] PROJECTION = new String[] {
358 "audio._id AS _id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
359 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
360 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
361 MediaStore.Audio.Media.ARTIST_ID
365 * The columns used to retrieve any info from the current album
367 private static final String[] ALBUM_PROJECTION = new String[] {
368 MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
369 MediaStore.Audio.Albums.LAST_YEAR
373 * Keeps a mapping of the track history
375 private static LinkedList<Integer> mHistory = Lists.newLinkedList();
378 * Used to shuffle the tracks
380 private static final Shuffler mShuffler = new Shuffler();
385 private final IBinder mBinder = new ServiceStub(this);
390 private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance();
395 private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance();
398 * 4x2 alternate widget
400 private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate
406 private MultiPlayer mPlayer;
409 * The path of the current file to play
411 private String mFileToPlay;
414 * Alarm intent for removing the notification when nothing is playing
417 private AlarmManager mAlarmManager;
418 private PendingIntent mShutdownIntent;
419 private boolean mShutdownScheduled;
421 private NotificationManager mNotificationManager;
424 * The cursor used to retrieve info on the current track and run the
425 * necessary queries to play audio files
427 private Cursor mCursor;
430 * The cursor used to retrieve info on the album the current track is
433 private Cursor mAlbumCursor;
436 * Monitors the audio state
438 private AudioManager mAudioManager;
441 * Settings used to save and retrieve the queue and history
443 private SharedPreferences mPreferences;
446 * Used to know when the service is active
448 private boolean mServiceInUse = false;
451 * Used to know if something should be playing or not
453 private boolean mIsSupposedToBePlaying = false;
456 * Gets the last played time to determine whether we still want notifications or not
458 private long mLastPlayedTime;
460 private int mNotifyMode = NOTIFY_MODE_NONE;
461 private long mNotificationPostTime = 0;
463 private static final int NOTIFY_MODE_NONE = 0;
464 private static final int NOTIFY_MODE_FOREGROUND = 1;
465 private static final int NOTIFY_MODE_BACKGROUND = 2;
468 * Used to indicate if the queue can be saved
470 private boolean mQueueIsSaveable = true;
473 * Used to track what type of audio focus loss caused the playback to pause
475 private boolean mPausedByTransientLossOfFocus = false;
478 * Lock screen controls
480 private MediaSession mSession;
482 // We use this to distinguish between different cards when saving/restoring
486 private int mPlayPos = -1;
488 private int mNextPlayPos = -1;
490 private int mOpenFailedCounter = 0;
492 private int mMediaMountedCount = 0;
494 private int mShuffleMode = SHUFFLE_NONE;
496 private int mRepeatMode = REPEAT_NONE;
498 private int mServiceStartId = -1;
500 private String mLyrics;
502 private ArrayList<MusicPlaybackTrack> mPlaylist = new ArrayList<>(100);
504 private long[] mAutoShuffleList = null;
506 private MusicPlayerHandler mPlayerHandler;
507 private HandlerThread mHandlerThread;
509 private BroadcastReceiver mUnmountReceiver = null;
511 // to improve perf, instead of hitting the disk cache or file cache, store the bitmaps in memory
512 private String mCachedKey;
513 private BitmapWithColors[] mCachedBitmapWithColors = new BitmapWithColors[2];
515 private QueueUpdateTask mQueueUpdateTask;
520 private ImageFetcher mImageFetcher;
523 * Recently listened database
525 private RecentStore mRecentsCache;
528 * The song play count database
530 private SongPlayCount mSongPlayCountCache;
533 * Stores the playback state
535 private MusicPlaybackState mPlaybackStateStore;
538 * Shake detector class used for shake to switch song feature
540 private ShakeDetector mShakeDetector;
543 * Switch for displaying album art on lockscreen
545 private boolean mShowAlbumArtOnLockscreen;
547 private boolean mReadGranted = false;
549 private PowerManager.WakeLock mHeadsetHookWakeLock;
551 private ShakeDetector.Listener mShakeDetectorListener=new ShakeDetector.Listener() {
554 public void hearShake() {
556 * on shake detect, play next song
559 Log.d(TAG,"Shake detected!!!");
569 public IBinder onBind(final Intent intent) {
570 if (D) Log.d(TAG, "Service bound, intent = " + intent);
572 mServiceInUse = true;
580 public boolean onUnbind(final Intent intent) {
581 if (D) Log.d(TAG, "Service unbound");
582 mServiceInUse = false;
586 if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) {
587 // Something is currently playing, or will be playing once
588 // an in-progress action requesting audio focus ends, so don't stop
592 // If there is a playlist but playback is paused, then wait a while
593 // before stopping the service, so that pause/resume isn't slow.
594 // Also delay stopping the service if we're transitioning between
596 } else if (mPlaylist.size() > 0 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
597 scheduleDelayedShutdown();
601 stopSelf(mServiceStartId);
610 public void onRebind(final Intent intent) {
612 mServiceInUse = true;
619 public void onCreate() {
620 if (D) Log.d(TAG, "Creating service");
623 if (checkSelfPermission(permission.READ_EXTERNAL_STORAGE) !=
624 PackageManager.PERMISSION_GRANTED) {
631 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
633 // Initialize the favorites and recents databases
634 mRecentsCache = RecentStore.getInstance(this);
636 // gets the song play count cache
637 mSongPlayCountCache = SongPlayCount.getInstance(this);
639 // gets a pointer to the playback state store
640 mPlaybackStateStore = MusicPlaybackState.getInstance(this);
642 // Initialize the image fetcher
643 mImageFetcher = ImageFetcher.getInstance(this);
644 // Initialize the image cache
645 mImageFetcher.setImageCache(ImageCache.getInstance(this));
647 // Start up the thread running the service. Note that we create a
648 // separate thread because the service normally runs in the process's
649 // main thread, which we don't want to block. We also make it
650 // background priority so CPU-intensive work will not disrupt the UI.
651 mHandlerThread = new HandlerThread("MusicPlayerHandler",
652 android.os.Process.THREAD_PRIORITY_BACKGROUND);
653 mHandlerThread.start();
655 // Initialize the handler
656 mPlayerHandler = new MusicPlayerHandler(this, mHandlerThread.getLooper());
658 // Initialize the audio manager and register any headset controls for
660 mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
662 // Use the remote control APIs to set the playback state
665 // Initialize the preferences
666 mPreferences = getSharedPreferences("Service", 0);
667 mCardId = getCardId();
669 mShowAlbumArtOnLockscreen = mPreferences.getBoolean(
670 PreferenceUtils.SHOW_ALBUM_ART_ON_LOCKSCREEN, true);
671 setShakeToPlayEnabled(mPreferences.getBoolean(PreferenceUtils.SHAKE_TO_PLAY, false));
673 mRepeatMode = mPreferences.getInt("repeatmode", REPEAT_NONE);
674 mShuffleMode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
676 registerExternalStorageListener();
678 // Initialize the media player
679 mPlayer = new MultiPlayer(this);
680 mPlayer.setHandler(mPlayerHandler);
682 // Initialize the intent filter and each action
683 final IntentFilter filter = new IntentFilter();
684 filter.addAction(SERVICECMD);
685 filter.addAction(TOGGLEPAUSE_ACTION);
686 filter.addAction(PAUSE_ACTION);
687 filter.addAction(STOP_ACTION);
688 filter.addAction(NEXT_ACTION);
689 filter.addAction(PREVIOUS_ACTION);
690 filter.addAction(PREVIOUS_FORCE_ACTION);
691 filter.addAction(REPEAT_ACTION);
692 filter.addAction(SHUFFLE_ACTION);
693 // Attach the broadcast listener
694 registerReceiver(mIntentReceiver, filter);
696 // Get events when MediaStore content changes
697 mMediaStoreObserver = new MediaStoreObserver(mPlayerHandler);
698 getContentResolver().registerContentObserver(
699 MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mMediaStoreObserver);
700 getContentResolver().registerContentObserver(
701 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mMediaStoreObserver);
703 // Initialize the delayed shutdown intent
704 final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
705 shutdownIntent.setAction(SHUTDOWN);
707 mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
708 mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0);
710 // Listen for the idle state
711 scheduleDelayedShutdown();
713 // Bring the queue back
715 notifyChange(QUEUE_CHANGED);
716 notifyChange(META_CHANGED);
719 private void setUpMediaSession() {
720 mSession = new MediaSession(this, "Eleven");
721 mSession.setCallback(new MediaSession.Callback() {
723 public void onPause() {
725 mPausedByTransientLossOfFocus = false;
728 public void onPlay() {
732 public void onSeekTo(long pos) {
736 public void onSkipToNext() {
740 public void onSkipToPrevious() {
744 public void onStop() {
746 mPausedByTransientLossOfFocus = false;
748 releaseServiceUiAndStop();
751 public void onSkipToQueueItem(long id) {
752 setQueuePosition((int) id);
755 public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
756 if (Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
757 KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
758 if (ke != null && ke.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) {
759 if (ke.getAction() == KeyEvent.ACTION_UP) {
760 handleHeadsetHookClick(ke.getEventTime());
765 return super.onMediaButtonEvent(mediaButtonIntent);
769 PendingIntent pi = PendingIntent.getBroadcast(this, 0,
770 new Intent(this, MediaButtonIntentReceiver.class),
771 PendingIntent.FLAG_UPDATE_CURRENT);
772 mSession.setMediaButtonReceiver(pi);
774 mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
775 | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
782 public void onDestroy() {
783 if (D) Log.d(TAG, "Destroying service");
788 // Remove any sound effects
789 final Intent audioEffectsIntent = new Intent(
790 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
791 audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
792 audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
793 sendBroadcast(audioEffectsIntent);
795 // remove any pending alarms
796 mAlarmManager.cancel(mShutdownIntent);
798 // Remove any callbacks from the handler
799 mPlayerHandler.removeCallbacksAndMessages(null);
800 // quit the thread so that anything that gets posted won't run
801 mHandlerThread.quitSafely();
803 // Release the player
807 // Remove the audio focus listener and lock screen controls
808 mAudioManager.abandonAudioFocus(mAudioFocusListener);
811 // remove the media store observer
812 getContentResolver().unregisterContentObserver(mMediaStoreObserver);
817 // Unregister the mount listener
818 unregisterReceiver(mIntentReceiver);
819 if (mUnmountReceiver != null) {
820 unregisterReceiver(mUnmountReceiver);
821 mUnmountReceiver = null;
824 // deinitialize shake detector
825 stopShakeDetector(true);
832 public int onStartCommand(final Intent intent, final int flags, final int startId) {
833 if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
834 mServiceStartId = startId;
836 if (intent != null) {
837 final String action = intent.getAction();
839 if (SHUTDOWN.equals(action)) {
840 mShutdownScheduled = false;
841 releaseServiceUiAndStop();
842 return START_NOT_STICKY;
845 handleCommandIntent(intent);
848 // Make sure the service will shut down on its own if it was
849 // just started but not bound to and nothing is playing
850 scheduleDelayedShutdown();
852 if (intent != null && intent.getBooleanExtra(FROM_MEDIA_BUTTON, false)) {
853 MediaButtonIntentReceiver.completeWakefulIntent(intent);
856 return START_NOT_STICKY;
859 private void releaseServiceUiAndStop() {
861 || mPausedByTransientLossOfFocus
862 || mPlayerHandler.hasMessages(TRACK_ENDED)) {
866 if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
867 cancelNotification();
868 mAudioManager.abandonAudioFocus(mAudioFocusListener);
869 mSession.setActive(false);
871 if (!mServiceInUse) {
873 stopSelf(mServiceStartId);
877 private void handleCommandIntent(Intent intent) {
878 final String action = intent.getAction();
879 final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
881 if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
883 if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
885 } else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)
886 || PREVIOUS_FORCE_ACTION.equals(action)) {
887 prev(PREVIOUS_FORCE_ACTION.equals(action));
888 } else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
890 } else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
892 mPausedByTransientLossOfFocus = false;
893 } else if (CMDPLAY.equals(command)) {
895 } else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
897 mPausedByTransientLossOfFocus = false;
899 releaseServiceUiAndStop();
900 } else if (REPEAT_ACTION.equals(action)) {
902 } else if (SHUFFLE_ACTION.equals(action)) {
904 } else if (CMDHEADSETHOOK.equals(command)) {
905 long timestamp = intent.getLongExtra(TIMESTAMP, 0);
906 handleHeadsetHookClick(timestamp);
910 private void handleHeadsetHookClick(long timestamp) {
911 if (mHeadsetHookWakeLock == null) {
912 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
913 mHeadsetHookWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
914 "Eleven headset button");
915 mHeadsetHookWakeLock.setReferenceCounted(false);
917 // Make sure we don't indefinitely hold the wake lock under any circumstances
918 mHeadsetHookWakeLock.acquire(10000);
920 Message msg = mPlayerHandler.obtainMessage(HEADSET_HOOK_EVENT, timestamp);
925 * Updates the notification, considering the current play and activity state
927 private void updateNotification() {
928 final int newNotifyMode;
930 newNotifyMode = NOTIFY_MODE_FOREGROUND;
931 } else if (recentlyPlayed()) {
932 newNotifyMode = NOTIFY_MODE_BACKGROUND;
934 newNotifyMode = NOTIFY_MODE_NONE;
937 int notificationId = hashCode();
938 if (mNotifyMode != newNotifyMode) {
939 if (mNotifyMode == NOTIFY_MODE_FOREGROUND) {
940 stopForeground(newNotifyMode == NOTIFY_MODE_NONE);
941 } else if (newNotifyMode == NOTIFY_MODE_NONE) {
942 mNotificationManager.cancel(notificationId);
943 mNotificationPostTime = 0;
947 if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
948 startForeground(notificationId, buildNotification());
949 } else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
950 mNotificationManager.notify(notificationId, buildNotification());
953 mNotifyMode = newNotifyMode;
956 private void cancelNotification() {
957 stopForeground(true);
958 mNotificationManager.cancel(hashCode());
959 mNotificationPostTime = 0;
960 mNotifyMode = NOTIFY_MODE_NONE;
964 * @return A card ID used to save and restore playlists, i.e., the queue.
966 private int getCardId() {
967 final ContentResolver resolver = getContentResolver();
968 Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null,
971 if (cursor != null && cursor.moveToFirst()) {
972 mCardId = cursor.getInt(0);
980 * Called when we receive a ACTION_MEDIA_EJECT notification.
982 * @param storagePath The path to mount point for the removed media
984 public void closeExternalStorageFiles(final String storagePath) {
986 notifyChange(QUEUE_CHANGED);
987 notifyChange(META_CHANGED);
991 * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The
992 * intent will call closeExternalStorageFiles() if the external media is
993 * going to be ejected, so applications can clean up any files they have
996 public void registerExternalStorageListener() {
997 if (mUnmountReceiver == null) {
998 mUnmountReceiver = new BroadcastReceiver() {
1004 public void onReceive(final Context context, final Intent intent) {
1005 final String action = intent.getAction();
1006 if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
1008 mQueueIsSaveable = false;
1009 closeExternalStorageFiles(intent.getData().getPath());
1010 } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
1011 mMediaMountedCount++;
1012 mCardId = getCardId();
1014 mQueueIsSaveable = true;
1015 notifyChange(QUEUE_CHANGED);
1016 notifyChange(META_CHANGED);
1020 final IntentFilter filter = new IntentFilter();
1021 filter.addAction(Intent.ACTION_MEDIA_EJECT);
1022 filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
1023 filter.addDataScheme("file");
1024 registerReceiver(mUnmountReceiver, filter);
1028 private void scheduleDelayedShutdown() {
1029 if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
1030 if (!mReadGranted) {
1033 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
1034 SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
1035 mShutdownScheduled = true;
1038 private void cancelShutdown() {
1039 if (D) Log.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled);
1040 if (mShutdownScheduled) {
1041 mAlarmManager.cancel(mShutdownIntent);
1042 mShutdownScheduled = false;
1049 * @param goToIdle True to go to the idle state, false otherwise
1051 private void stop(final boolean goToIdle) {
1052 if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
1053 if (mPlayer.isInitialized()) {
1059 setIsSupposedToBePlaying(false, false);
1061 stopForeground(false);
1066 * Removes the range of tracks specified from the play list. If a file
1067 * within the range is the file currently being played, playback will move
1068 * to the next file after the range.
1070 * @param first The first file to be removed
1071 * @param last The last file to be removed
1072 * @return the number of tracks deleted
1074 private int removeTracksInternal(int first, int last) {
1075 synchronized (this) {
1078 } else if (first < 0) {
1080 } else if (last >= mPlaylist.size()) {
1081 last = mPlaylist.size() - 1;
1084 boolean gotonext = false;
1085 if (first <= mPlayPos && mPlayPos <= last) {
1088 } else if (mPlayPos > last) {
1089 mPlayPos -= last - first + 1;
1091 final int numToRemove = last - first + 1;
1093 if (first == 0 && last == mPlaylist.size() - 1) {
1099 for (int i = 0; i < numToRemove; i++) {
1100 mPlaylist.remove(first);
1103 // remove the items from the history
1104 // this is not ideal as the history shouldn't be impacted by this
1105 // but since we are removing items from the array, it will throw
1106 // an exception if we keep it around.
1107 ListIterator<Integer> positionIterator = mHistory.listIterator();
1108 while (positionIterator.hasNext()) {
1109 int pos = positionIterator.next();
1110 if (pos >= first && pos <= last) {
1111 positionIterator.remove();
1112 } else if (pos > last) {
1113 positionIterator.set(pos - numToRemove);
1118 if (mPlaylist.size() == 0) {
1123 if (mShuffleMode != SHUFFLE_NONE) {
1124 mPlayPos = getNextPosition(true);
1125 } else if (mPlayPos >= mPlaylist.size()) {
1128 final boolean wasPlaying = isPlaying();
1130 openCurrentAndNext();
1135 notifyChange(META_CHANGED);
1137 return last - first + 1;
1142 * Adds a list to the playlist
1144 * @param list The list to add
1145 * @param position The position to place the tracks
1147 private void addToPlayList(final long[] list, int position, long sourceId, IdType sourceType) {
1148 final int addlen = list.length;
1154 mPlaylist.ensureCapacity(mPlaylist.size() + addlen);
1155 if (position > mPlaylist.size()) {
1156 position = mPlaylist.size();
1159 final ArrayList<MusicPlaybackTrack> arrayList = new ArrayList<>(addlen);
1160 for (int i = 0; i < list.length; i++) {
1161 arrayList.add(new MusicPlaybackTrack(list[i], sourceId, sourceType, i));
1164 mPlaylist.addAll(position, arrayList);
1166 if (mPlaylist.size() == 0) {
1168 notifyChange(META_CHANGED);
1173 * @param trackId The track ID
1175 private void updateCursor(final long trackId) {
1176 updateCursor("_id=" + trackId, null);
1179 private void updateCursor(final String selection, final String[] selectionArgs) {
1180 synchronized (this) {
1182 mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1183 PROJECTION, selection, selectionArgs);
1185 updateAlbumCursor();
1188 private void updateCursor(final Uri uri) {
1189 synchronized (this) {
1191 mCursor = openCursorAndGoToFirst(uri, PROJECTION, null, null);
1193 updateAlbumCursor();
1196 private void updateAlbumCursor() {
1197 long albumId = getAlbumId();
1199 mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
1200 ALBUM_PROJECTION, "_id=" + albumId, null);
1202 mAlbumCursor = null;
1206 private Cursor openCursorAndGoToFirst(Uri uri, String[] projection,
1207 String selection, String[] selectionArgs) {
1208 Cursor c = getContentResolver().query(uri, projection,
1209 selection, selectionArgs, null, null);
1213 if (!c.moveToFirst()) {
1220 private synchronized void closeCursor() {
1221 if (mCursor != null) {
1225 if (mAlbumCursor != null) {
1226 mAlbumCursor.close();
1227 mAlbumCursor = null;
1232 * Called to open a new file as the current track and prepare the next for
1235 private void openCurrentAndNext() {
1236 openCurrentAndMaybeNext(true);
1240 * Called to open a new file as the current track and prepare the next for
1243 * @param openNext True to prepare the next track for playback, false
1246 private void openCurrentAndMaybeNext(final boolean openNext) {
1247 synchronized (this) {
1250 if (mPlaylist.size() == 0) {
1255 boolean shutdown = false;
1257 updateCursor(mPlaylist.get(mPlayPos).mId);
1260 && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
1261 + mCursor.getLong(IDCOLIDX))) {
1265 // if we get here then opening the file failed. We can close the
1266 // cursor now, because
1267 // we're either going to create a new one next, or stop trying
1269 if (mOpenFailedCounter++ < 10 && mPlaylist.size() > 1) {
1270 final int pos = getNextPosition(false);
1278 updateCursor(mPlaylist.get(mPlayPos).mId);
1280 mOpenFailedCounter = 0;
1281 Log.w(TAG, "Failed to open file for playback");
1288 scheduleDelayedShutdown();
1289 if (mIsSupposedToBePlaying) {
1290 mIsSupposedToBePlaying = false;
1291 notifyChange(PLAYSTATE_CHANGED);
1293 } else if (openNext) {
1299 private void sendErrorMessage(final String trackName) {
1300 final Intent i = new Intent(TRACK_ERROR);
1301 i.putExtra(TrackErrorExtra.TRACK_NAME, trackName);
1306 * @param force True to force the player onto the track next, false
1308 * @param saveToHistory True to save the mPlayPos to the history
1309 * @return The next position to play.
1311 private int getNextPosition(final boolean force) {
1312 // as a base case, if the playlist is empty just return -1
1313 if (mPlaylist == null || mPlaylist.isEmpty()) {
1316 // if we're not forced to go to the next track and we are only playing the current track
1317 if (!force && mRepeatMode == REPEAT_CURRENT) {
1322 } else if (mShuffleMode == SHUFFLE_NORMAL) {
1323 final int numTracks = mPlaylist.size();
1325 // count the number of times a track has been played
1326 final int[] trackNumPlays = new int[numTracks];
1327 for (int i = 0; i < numTracks; i++) {
1329 trackNumPlays[i] = 0;
1332 // walk through the history and add up the number of times the track
1334 final int numHistory = mHistory.size();
1335 for (int i = 0; i < numHistory; i++) {
1336 final int idx = mHistory.get(i);
1337 if (idx >= 0 && idx < numTracks) {
1338 trackNumPlays[idx]++;
1342 // also add the currently playing track to the count
1343 if (mPlayPos >= 0 && mPlayPos < numTracks) {
1344 trackNumPlays[mPlayPos]++;
1347 // figure out the least # of times a track has a played as well as
1348 // how many tracks share that count
1349 int minNumPlays = Integer.MAX_VALUE;
1350 int numTracksWithMinNumPlays = 0;
1351 for (final int trackNumPlay : trackNumPlays) {
1352 // if we found a new track that has less number of plays, reset the counters
1353 if (trackNumPlay < minNumPlays) {
1354 minNumPlays = trackNumPlay;
1355 numTracksWithMinNumPlays = 1;
1356 } else if (trackNumPlay == minNumPlays) {
1357 // increment this track shares the # of tracks
1358 numTracksWithMinNumPlays++;
1362 // if we've played each track at least once and all tracks have been played an equal
1363 // # of times and we aren't repeating all and we're not forcing a track, then
1364 // return no more tracks
1365 if (minNumPlays > 0 && numTracksWithMinNumPlays == numTracks
1366 && mRepeatMode != REPEAT_ALL && !force) {
1370 // else pick a track from the least number of played tracks
1371 int skip = mShuffler.nextInt(numTracksWithMinNumPlays);
1372 for (int i = 0; i < trackNumPlays.length; i++) {
1373 if (trackNumPlays[i] == minNumPlays) {
1382 // Unexpected to land here
1383 if (D) Log.e(TAG, "Getting the next position resulted did not get a result when it should have");
1385 } else if (mShuffleMode == SHUFFLE_AUTO) {
1386 doAutoShuffleUpdate();
1387 return mPlayPos + 1;
1389 if (mPlayPos >= mPlaylist.size() - 1) {
1390 if (mRepeatMode == REPEAT_NONE && !force) {
1392 } else if (mRepeatMode == REPEAT_ALL || force) {
1397 return mPlayPos + 1;
1403 * Sets the track to be played
1405 private void setNextTrack() {
1406 setNextTrack(getNextPosition(false));
1410 * Sets the next track to be played
1411 * @param position the target position we want
1413 private void setNextTrack(int position) {
1414 mNextPlayPos = position;
1415 if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
1416 if (mNextPlayPos >= 0 && mPlaylist != null && mNextPlayPos < mPlaylist.size()) {
1417 final long id = mPlaylist.get(mNextPlayPos).mId;
1418 mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id);
1420 mPlayer.setNextDataSource(null);
1425 * Creates a shuffled playlist used for party mode
1427 private boolean makeAutoShuffleList() {
1428 Cursor cursor = null;
1430 cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1432 MediaStore.Audio.Media._ID
1433 }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null);
1434 if (cursor == null || cursor.getCount() == 0) {
1437 final int len = cursor.getCount();
1438 final long[] list = new long[len];
1439 for (int i = 0; i < len; i++) {
1440 cursor.moveToNext();
1441 list[i] = cursor.getLong(0);
1443 mAutoShuffleList = list;
1445 } catch (final RuntimeException e) {
1447 if (cursor != null) {
1456 * Creates the party shuffle playlist
1458 private void doAutoShuffleUpdate() {
1459 boolean notify = false;
1460 if (mPlayPos > 10) {
1461 removeTracks(0, mPlayPos - 9);
1464 final int toAdd = 7 - (mPlaylist.size() - (mPlayPos < 0 ? -1 : mPlayPos));
1465 for (int i = 0; i < toAdd; i++) {
1466 int lookback = mHistory.size();
1469 idx = mShuffler.nextInt(mAutoShuffleList.length);
1470 if (!wasRecentlyUsed(idx, lookback)) {
1476 if (mHistory.size() > MAX_HISTORY_SIZE) {
1479 mPlaylist.add(new MusicPlaybackTrack(mAutoShuffleList[idx], -1, IdType.NA, -1));
1483 notifyChange(QUEUE_CHANGED);
1488 private boolean wasRecentlyUsed(final int idx, int lookbacksize) {
1489 if (lookbacksize == 0) {
1492 final int histsize = mHistory.size();
1493 if (histsize < lookbacksize) {
1494 lookbacksize = histsize;
1496 final int maxidx = histsize - 1;
1497 for (int i = 0; i < lookbacksize; i++) {
1498 final long entry = mHistory.get(maxidx - i);
1507 * Notify the change-receivers that something has changed.
1509 private void notifyChange(final String what) {
1510 if (D) Log.d(TAG, "notifyChange: what = " + what);
1512 // Update the lockscreen controls
1513 updateMediaSession(what);
1515 if (what.equals(POSITION_CHANGED)) {
1519 final Intent intent = new Intent(what);
1520 intent.putExtra("id", getAudioId());
1521 intent.putExtra("artist", getArtistName());
1522 intent.putExtra("album", getAlbumName());
1523 intent.putExtra("track", getTrackName());
1524 intent.putExtra("playing", isPlaying());
1526 if (NEW_LYRICS.equals(what)) {
1527 intent.putExtra("lyrics", mLyrics);
1530 sendStickyBroadcast(intent);
1532 final Intent musicIntent = new Intent(intent);
1533 musicIntent.setAction(what.replace(ELEVEN_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
1534 sendStickyBroadcast(musicIntent);
1538 // Add the track to the recently played list.
1539 mRecentsCache.addSongId(getAudioId());
1541 mSongPlayCountCache.bumpSongCount(getAudioId());
1546 // if we are in shuffle mode and our next track is still valid,
1547 // try to re-use the track
1548 // We need to reimplement the queue to prevent hacky solutions like this
1549 if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size()
1550 && getShuffleMode() != SHUFFLE_NONE) {
1551 setNextTrack(mNextPlayPos);
1562 if (what.equals(PLAYSTATE_CHANGED)) {
1563 updateNotification();
1566 // Update the app-widgets
1567 mAppWidgetSmall.notifyChange(this, what);
1568 mAppWidgetLarge.notifyChange(this, what);
1569 mAppWidgetLargeAlternate.notifyChange(this, what);
1572 private void updateMediaSession(final String what) {
1573 int playState = mIsSupposedToBePlaying
1574 ? PlaybackState.STATE_PLAYING
1575 : PlaybackState.STATE_PAUSED;
1577 long playBackStateActions = PlaybackState.ACTION_PLAY |
1578 PlaybackState.ACTION_PLAY_PAUSE |
1579 PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
1580 PlaybackState.ACTION_PAUSE |
1581 PlaybackState.ACTION_SKIP_TO_NEXT |
1582 PlaybackState.ACTION_SKIP_TO_PREVIOUS |
1583 PlaybackState.ACTION_STOP;
1585 if (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED)) {
1586 mSession.setPlaybackState(new PlaybackState.Builder()
1587 .setActions(playBackStateActions)
1588 .setActiveQueueItemId(getAudioId())
1589 .setState(playState, position(), 1.0f).build());
1590 } else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
1591 Bitmap albumArt = getAlbumArt(false).getBitmap();
1592 if (albumArt != null) {
1593 // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need
1594 // to make sure not to hand out our cache copy
1595 Bitmap.Config config = albumArt.getConfig();
1596 if (config == null) {
1597 config = Bitmap.Config.ARGB_8888;
1599 albumArt = albumArt.copy(config, false);
1602 mSession.setMetadata(new MediaMetadata.Builder()
1603 .putString(MediaMetadata.METADATA_KEY_ARTIST, getArtistName())
1604 .putString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST, getAlbumArtistName())
1605 .putString(MediaMetadata.METADATA_KEY_ALBUM, getAlbumName())
1606 .putString(MediaMetadata.METADATA_KEY_TITLE, getTrackName())
1607 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration())
1608 .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, getQueuePosition() + 1)
1609 .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, getQueue().length)
1610 .putString(MediaMetadata.METADATA_KEY_GENRE, getGenreName())
1611 .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART,
1612 mShowAlbumArtOnLockscreen ? albumArt : null)
1615 if (what.equals(QUEUE_CHANGED)) {
1616 updateMediaSessionQueue();
1619 mSession.setPlaybackState(new PlaybackState.Builder()
1620 .setActions(playBackStateActions)
1621 .setActiveQueueItemId(getAudioId())
1622 .setState(playState, position(), 1.0f).build());
1626 private synchronized void updateMediaSessionQueue() {
1627 if (mQueueUpdateTask != null) {
1628 mQueueUpdateTask.cancel(true);
1630 mQueueUpdateTask = new QueueUpdateTask(getQueue());
1631 mQueueUpdateTask.execute();
1634 private Notification buildNotification() {
1635 final String albumName = getAlbumName();
1636 final String artistName = getArtistName();
1637 final boolean isPlaying = isPlaying();
1638 String text = TextUtils.isEmpty(albumName)
1639 ? artistName : artistName + " - " + albumName;
1641 int playButtonResId = isPlaying
1642 ? R.drawable.btn_playback_pause : R.drawable.btn_playback_play;
1643 int playButtonTitleResId = isPlaying
1644 ? R.string.accessibility_pause : R.string.accessibility_play;
1646 Notification.MediaStyle style = new Notification.MediaStyle()
1647 .setMediaSession(mSession.getSessionToken())
1648 .setShowActionsInCompactView(0, 1, 2);
1650 Intent nowPlayingIntent = new Intent(ACTION_AUDIO_PLAYER)
1651 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1652 PendingIntent clickIntent = PendingIntent.getActivity(this, 0, nowPlayingIntent, 0);
1653 BitmapWithColors artwork = getAlbumArt(false);
1655 if (mNotificationPostTime == 0) {
1656 mNotificationPostTime = System.currentTimeMillis();
1659 Notification.Builder builder = new Notification.Builder(this, CHANNEL_NAME)
1660 .setSmallIcon(R.drawable.ic_notification)
1661 .setLargeIcon(artwork.getBitmap())
1662 .setContentIntent(clickIntent)
1663 .setContentTitle(getTrackName())
1664 .setContentText(text)
1665 .setWhen(mNotificationPostTime)
1668 .setVisibility(Notification.VISIBILITY_PUBLIC)
1669 .addAction(R.drawable.btn_playback_previous,
1670 getString(R.string.accessibility_prev),
1671 retrievePlaybackAction(PREVIOUS_ACTION))
1672 .addAction(playButtonResId, getString(playButtonTitleResId),
1673 retrievePlaybackAction(TOGGLEPAUSE_ACTION))
1674 .addAction(R.drawable.btn_playback_next,
1675 getString(R.string.accessibility_next),
1676 retrievePlaybackAction(NEXT_ACTION));
1678 builder.setColor(artwork.getVibrantDarkColor());
1680 if (BuildCompat.isAtLeastO()) {
1681 NotificationChannel channel = mNotificationManager
1682 .getNotificationChannel(CHANNEL_NAME);
1684 if (channel == null) {
1685 String name = getString(R.string.channel_music);
1687 channel = new NotificationChannel(CHANNEL_NAME, name,
1688 mNotificationManager.IMPORTANCE_DEFAULT);
1689 channel.setShowBadge(false);
1690 channel.enableVibration(false);
1691 channel.setSound(null, null);
1692 mNotificationManager.createNotificationChannel(channel);
1695 builder.setChannelId(channel.getId());
1698 return builder.build();
1701 private final PendingIntent retrievePlaybackAction(final String action) {
1702 final ComponentName serviceName = new ComponentName(this, MusicPlaybackService.class);
1703 Intent intent = new Intent(action);
1704 intent.setComponent(serviceName);
1706 return PendingIntent.getService(this, 0, intent, 0);
1712 * @param full True if the queue is full
1714 private void saveQueue(final boolean full) {
1715 if (!mQueueIsSaveable || mPreferences == null) {
1719 final SharedPreferences.Editor editor = mPreferences.edit();
1721 mPlaybackStateStore.saveState(mPlaylist,
1722 mShuffleMode != SHUFFLE_NONE ? mHistory : null);
1723 editor.putInt("cardid", mCardId);
1725 editor.putInt("curpos", mPlayPos);
1726 if (mPlayer.isInitialized()) {
1727 editor.putLong("seekpos", mPlayer.position());
1729 editor.putInt("repeatmode", mRepeatMode);
1730 editor.putInt("shufflemode", mShuffleMode);
1735 * Reloads the queue as the user left it the last time they stopped using
1738 private void reloadQueue() {
1740 if (mPreferences.contains("cardid")) {
1741 id = mPreferences.getInt("cardid", ~mCardId);
1743 if (id == mCardId) {
1744 mPlaylist = mPlaybackStateStore.getQueue();
1746 if (mPlaylist.size() > 0) {
1747 final int pos = mPreferences.getInt("curpos", 0);
1748 if (pos < 0 || pos >= mPlaylist.size()) {
1753 updateCursor(mPlaylist.get(mPlayPos).mId);
1754 if (mCursor == null) {
1755 SystemClock.sleep(3000);
1756 updateCursor(mPlaylist.get(mPlayPos).mId);
1758 synchronized (this) {
1760 mOpenFailedCounter = 20;
1761 openCurrentAndNext();
1763 if (!mPlayer.isInitialized()) {
1768 final long seekpos = mPreferences.getLong("seekpos", 0);
1769 seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
1772 Log.d(TAG, "restored queue, currently at position "
1773 + position() + "/" + duration()
1774 + " (requested " + seekpos + ")");
1777 int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
1778 if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
1779 repmode = REPEAT_NONE;
1781 mRepeatMode = repmode;
1783 int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
1784 if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
1785 shufmode = SHUFFLE_NONE;
1787 if (shufmode != SHUFFLE_NONE) {
1788 mHistory = mPlaybackStateStore.getHistory(mPlaylist.size());
1790 if (shufmode == SHUFFLE_AUTO) {
1791 if (!makeAutoShuffleList()) {
1792 shufmode = SHUFFLE_NONE;
1795 mShuffleMode = shufmode;
1800 * Opens a file and prepares it for playback
1802 * @param path The path of the file to open
1804 public boolean openFile(final String path) {
1805 if (D) Log.d(TAG, "openFile: path = " + path);
1806 synchronized (this) {
1811 // If mCursor is null, try to associate path with a database cursor
1812 if (mCursor == null) {
1813 Uri uri = Uri.parse(path);
1814 boolean shouldAddToPlaylist = true; // should try adding audio info to playlist
1817 id = Long.valueOf(uri.getLastPathSegment());
1818 } catch (NumberFormatException ex) {
1822 if (id != -1 && path.startsWith(
1823 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
1826 } else if (id != -1 && path.startsWith(
1827 MediaStore.Files.getContentUri("external").toString())) {
1830 // handle downloaded media files
1831 } else if ( path.startsWith("content://downloads/") ) {
1833 // extract MediaProvider(MP) uri , if available
1834 // Downloads.Impl.COLUMN_MEDIAPROVIDER_URI
1835 String mpUri = getValueForDownloadedFile(this, uri, "mediaprovider_uri");
1836 if (D) Log.i(TAG, "Downloaded file's MP uri : " + mpUri);
1837 if ( !TextUtils.isEmpty(mpUri) ) {
1838 // if mpUri is valid, play that URI instead
1839 if (openFile(mpUri)) {
1840 // notify impending change in track
1841 notifyChange(META_CHANGED);
1847 // create phantom cursor with download info, if a MP uri wasn't found
1848 updateCursorForDownloadedFile(this, uri);
1849 shouldAddToPlaylist = false; // song info isn't available in MediaStore
1853 // assuming a "file://" uri by this point ...
1854 String where = MediaStore.Audio.Media.DATA + "=?";
1855 String[] selectionArgs = new String[]{path};
1856 updateCursor(where, selectionArgs);
1859 if (mCursor != null && shouldAddToPlaylist) {
1861 mPlaylist.add(new MusicPlaybackTrack(
1862 mCursor.getLong(IDCOLIDX), -1, IdType.NA, -1));
1863 // propagate the change in playlist state
1864 notifyChange(QUEUE_CHANGED);
1868 } catch (final UnsupportedOperationException ex) {
1874 mPlayer.setDataSource(mFileToPlay);
1875 if (mPlayer.isInitialized()) {
1876 mOpenFailedCounter = 0;
1880 String trackName = getTrackName();
1881 if (TextUtils.isEmpty(trackName)) {
1884 sendErrorMessage(trackName);
1892 Columns for a pseudo cursor we are creating for downloaded songs
1893 Modeled after mCursor to be able to respond to respond to the same queries as it
1895 private static final String[] PROJECTION_MATRIX = new String[] {
1896 "_id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
1897 MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA,
1898 MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID,
1899 MediaStore.Audio.Media.ARTIST_ID
1903 * Creates a pseudo cursor for downloaded audio files with minimal info
1904 * @param context needed to query the download uri
1905 * @param uri the uri of the downloaded file
1907 private void updateCursorForDownloadedFile(Context context, Uri uri) {
1908 synchronized (this) {
1909 closeCursor(); // clear mCursor
1910 MatrixCursor cursor = new MatrixCursor(PROJECTION_MATRIX);
1911 // get title of the downloaded file ; Downloads.Impl.COLUMN_TITLE
1912 String title = getValueForDownloadedFile(this, uri, "title" );
1913 // populating the cursor with bare minimum info
1914 cursor.addRow(new Object[] {
1925 mCursor.moveToFirst();
1930 * Query the DownloadProvider to get the value in the specified column
1932 * @param uri the uri of the downloaded file
1936 private String getValueForDownloadedFile(Context context, Uri uri, String column) {
1938 final String[] projection = {
1941 try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null)) {
1942 if (cursor != null && cursor.moveToFirst()) {
1943 return cursor.getString(0);
1950 * Returns the audio session ID
1952 * @return The current media player audio session ID
1954 public int getAudioSessionId() {
1955 synchronized (this) {
1956 return mPlayer.getAudioSessionId();
1961 * Indicates if the media storeage device has been mounted or not
1963 * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise
1965 public int getMediaMountedCount() {
1966 return mMediaMountedCount;
1970 * Returns the shuffle mode
1972 * @return The current shuffle mode (all, party, none)
1974 public int getShuffleMode() {
1975 return mShuffleMode;
1979 * Returns the repeat mode
1981 * @return The current repeat mode (all, one, none)
1983 public int getRepeatMode() {
1988 * Removes all instances of the track with the given ID from the playlist.
1990 * @param id The id to be removed
1991 * @return how many instances of the track were removed
1993 public int removeTrack(final long id) {
1995 synchronized (this) {
1996 for (int i = 0; i < mPlaylist.size(); i++) {
1997 if (mPlaylist.get(i).mId == id) {
1998 numremoved += removeTracksInternal(i, i);
2003 if (numremoved > 0) {
2004 notifyChange(QUEUE_CHANGED);
2010 * Removes a song from the playlist at the specified position.
2012 * @param id The song id to be removed
2013 * @param position The position of the song in the playlist
2014 * @return true if successful
2016 public boolean removeTrackAtPosition(final long id, final int position) {
2017 synchronized (this) {
2018 if ( position >=0 &&
2019 position < mPlaylist.size() &&
2020 mPlaylist.get(position).mId == id ) {
2022 return removeTracks(position, position) > 0;
2029 * Removes the range of tracks specified from the play list. If a file
2030 * within the range is the file currently being played, playback will move
2031 * to the next file after the range.
2033 * @param first The first file to be removed
2034 * @param last The last file to be removed
2035 * @return the number of tracks deleted
2037 public int removeTracks(final int first, final int last) {
2038 final int numremoved = removeTracksInternal(first, last);
2039 if (numremoved > 0) {
2040 notifyChange(QUEUE_CHANGED);
2046 * Returns the position in the queue
2048 * @return the current position in the queue
2050 public int getQueuePosition() {
2051 synchronized (this) {
2057 * @return the size of the queue history cache
2059 public int getQueueHistorySize() {
2060 synchronized (this) {
2061 return mHistory.size();
2066 * @return the position in the history
2068 public int getQueueHistoryPosition(int position) {
2069 synchronized (this) {
2070 if (position >= 0 && position < mHistory.size()) {
2071 return mHistory.get(position);
2079 * @return the queue of history positions
2081 public int[] getQueueHistoryList() {
2082 synchronized (this) {
2083 int[] history = new int[mHistory.size()];
2084 for (int i = 0; i < mHistory.size(); i++) {
2085 history[i] = mHistory.get(i);
2093 * Returns the path to current song
2095 * @return The path to the current song
2097 public String getPath() {
2098 synchronized (this) {
2099 if (mCursor == null) {
2102 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.DATA));
2107 * Returns the album name
2109 * @return The current song album Name
2111 public String getAlbumName() {
2112 synchronized (this) {
2113 if (mCursor == null) {
2116 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
2121 * Returns the song name
2123 * @return The current song name
2125 public String getTrackName() {
2126 synchronized (this) {
2127 if (mCursor == null) {
2130 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
2135 * Returns the genre name of song
2137 * @return The current song genre name
2139 public String getGenreName() {
2140 synchronized (this) {
2141 if (mCursor == null || mPlayPos < 0 || mPlayPos >= mPlaylist.size()) {
2144 String[] genreProjection = { MediaStore.Audio.Genres.NAME };
2145 Uri genreUri = MediaStore.Audio.Genres.getContentUriForAudioId("external",
2146 (int) mPlaylist.get(mPlayPos).mId);
2147 Cursor genreCursor = getContentResolver().query(genreUri, genreProjection,
2149 if (genreCursor != null) {
2151 if (genreCursor.moveToFirst()) {
2152 return genreCursor.getString(
2153 genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME));
2156 genreCursor.close();
2164 * Returns the artist name
2166 * @return The current song artist name
2168 public String getArtistName() {
2169 synchronized (this) {
2170 if (mCursor == null) {
2173 return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST));
2178 * Returns the artist name
2180 * @return The current song artist name
2182 public String getAlbumArtistName() {
2183 synchronized (this) {
2184 if (mAlbumCursor == null) {
2187 return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(AlbumColumns.ARTIST));
2192 * Returns the album ID
2194 * @return The current song album ID
2196 public long getAlbumId() {
2197 synchronized (this) {
2198 if (mCursor == null) {
2201 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM_ID));
2206 * Returns the artist ID
2208 * @return The current song artist ID
2210 public long getArtistId() {
2211 synchronized (this) {
2212 if (mCursor == null) {
2215 return mCursor.getLong(mCursor.getColumnIndexOrThrow(AudioColumns.ARTIST_ID));
2220 * @return The audio id of the track
2222 public long getAudioId() {
2223 MusicPlaybackTrack track = getCurrentTrack();
2224 if (track != null) {
2232 * Gets the currently playing music track
2234 public MusicPlaybackTrack getCurrentTrack() {
2235 return getTrack(mPlayPos);
2239 * Gets the music track from the queue at the specified index
2240 * @param index position
2241 * @return music track or null
2243 public synchronized MusicPlaybackTrack getTrack(int index) {
2244 if (index >= 0 && index < mPlaylist.size() && mPlayer.isInitialized()) {
2245 return mPlaylist.get(index);
2252 * Returns the next audio ID
2254 * @return The next track ID
2256 public long getNextAudioId() {
2257 synchronized (this) {
2258 if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size() && mPlayer.isInitialized()) {
2259 return mPlaylist.get(mNextPlayPos).mId;
2266 * Returns the previous audio ID
2268 * @return The previous track ID
2270 public long getPreviousAudioId() {
2271 synchronized (this) {
2272 if (mPlayer.isInitialized()) {
2273 int pos = getPreviousPlayPosition(false);
2274 if (pos >= 0 && pos < mPlaylist.size()) {
2275 return mPlaylist.get(pos).mId;
2283 * Seeks the current track to a specific time
2285 * @param position The time to seek to
2286 * @return The time to play the track at
2288 public long seek(long position) {
2289 if (mPlayer.isInitialized()) {
2292 } else if (position > mPlayer.duration()) {
2293 position = mPlayer.duration();
2295 long result = mPlayer.seek(position);
2296 notifyChange(POSITION_CHANGED);
2303 * Seeks the current track to a position relative to its current position
2304 * If the relative position is after or before the track, it will also automatically
2305 * jump to the previous or next track respectively
2307 * @param deltaInMs The delta time to seek to in milliseconds
2309 public void seekRelative(long deltaInMs) {
2310 synchronized (this) {
2311 if (mPlayer.isInitialized()) {
2312 final long newPos = position() + deltaInMs;
2313 final long duration = duration();
2316 // seek to the new duration + the leftover position
2317 seek(duration() + newPos);
2318 } else if (newPos >= duration) {
2320 // seek to the leftover duration
2321 seek(newPos - duration);
2330 * Returns the current position in time of the currenttrack
2332 * @return The current playback position in miliseconds
2334 public long position() {
2335 if (mPlayer.isInitialized()) {
2336 return mPlayer.position();
2342 * Returns the full duration of the current track
2344 * @return The duration of the current track in miliseconds
2346 public long duration() {
2347 if (mPlayer.isInitialized()) {
2348 return mPlayer.duration();
2356 * @return The queue as a long[]
2358 public long[] getQueue() {
2359 synchronized (this) {
2360 final int len = mPlaylist.size();
2361 final long[] list = new long[len];
2362 for (int i = 0; i < len; i++) {
2363 list[i] = mPlaylist.get(i).mId;
2370 * Gets the track id at a given position in the queue
2372 * @return track id in the queue position
2374 public long getQueueItemAtPosition(int position) {
2375 synchronized (this) {
2376 if (position >= 0 && position < mPlaylist.size()) {
2377 return mPlaylist.get(position).mId;
2385 * @return the size of the queue
2387 public int getQueueSize() {
2388 synchronized (this) {
2389 return mPlaylist.size();
2394 * @return True if music is playing, false otherwise
2396 public boolean isPlaying() {
2397 return mIsSupposedToBePlaying;
2401 * Helper function to wrap the logic around mIsSupposedToBePlaying for consistentcy
2402 * @param value to set mIsSupposedToBePlaying to
2403 * @param notify whether we want to fire PLAYSTATE_CHANGED event
2405 private void setIsSupposedToBePlaying(boolean value, boolean notify) {
2406 if (mIsSupposedToBePlaying != value) {
2407 mIsSupposedToBePlaying = value;
2409 // Update mLastPlayed time first and notify afterwards, as
2410 // the notification listener method needs the up-to-date value
2411 // for the recentlyPlayed() method to work
2412 if (!mIsSupposedToBePlaying) {
2413 scheduleDelayedShutdown();
2414 mLastPlayedTime = System.currentTimeMillis();
2418 notifyChange(PLAYSTATE_CHANGED);
2424 * @return true if is playing or has played within the last IDLE_DELAY time
2426 private boolean recentlyPlayed() {
2427 return isPlaying() || System.currentTimeMillis() - mLastPlayedTime < IDLE_DELAY;
2431 * Opens a list for playback
2433 * @param list The list of tracks to open
2434 * @param position The position to start playback at
2436 public void open(final long[] list, final int position, long sourceId, IdType sourceType) {
2437 synchronized (this) {
2438 if (mShuffleMode == SHUFFLE_AUTO) {
2439 mShuffleMode = SHUFFLE_NORMAL;
2441 final long oldId = getAudioId();
2442 final int listlength = list.length;
2443 boolean newlist = true;
2444 if (mPlaylist.size() == listlength) {
2446 for (int i = 0; i < listlength; i++) {
2447 if (list[i] != mPlaylist.get(i).mId) {
2454 addToPlayList(list, -1, sourceId, sourceType);
2455 notifyChange(QUEUE_CHANGED);
2457 if (position >= 0) {
2458 mPlayPos = position;
2460 mPlayPos = mShuffler.nextInt(mPlaylist.size());
2463 openCurrentAndNext();
2464 if (oldId != getAudioId()) {
2465 notifyChange(META_CHANGED);
2473 public void stop() {
2474 stopShakeDetector(false);
2479 * Resumes or starts playback.
2481 public void play() {
2482 startShakeDetector();
2487 * Resumes or starts playback.
2488 * @param createNewNextTrack True if you want to figure out the next track, false
2489 * if you want to re-use the existing next track (used for going back)
2491 public void play(boolean createNewNextTrack) {
2492 int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
2493 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
2495 if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
2497 if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
2501 final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
2502 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2503 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2504 sendBroadcast(intent);
2506 mSession.setActive(true);
2508 if (createNewNextTrack) {
2511 setNextTrack(mNextPlayPos);
2514 if (mPlayer.isInitialized()) {
2515 final long duration = mPlayer.duration();
2516 if (mRepeatMode != REPEAT_CURRENT && duration > 2000
2517 && mPlayer.position() >= duration - 2000) {
2522 mPlayerHandler.removeMessages(FADEDOWN);
2523 mPlayerHandler.sendEmptyMessage(FADEUP);
2525 setIsSupposedToBePlaying(true, true);
2528 updateNotification();
2529 } else if (mPlaylist.size() <= 0) {
2530 setShuffleMode(SHUFFLE_AUTO);
2534 private void togglePlayPause() {
2537 mPausedByTransientLossOfFocus = false;
2544 * Temporarily pauses playback.
2546 public void pause() {
2547 if (mPlayerHandler == null) return;
2548 if (D) Log.d(TAG, "Pausing playback");
2549 synchronized (this) {
2550 if (mPlayerHandler != null) {
2551 mPlayerHandler.removeMessages(FADEUP);
2553 if (mIsSupposedToBePlaying) {
2554 final Intent intent = new Intent(
2555 AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
2556 intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
2557 intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
2558 sendBroadcast(intent);
2560 if (mPlayer != null) {
2563 setIsSupposedToBePlaying(false, true);
2564 stopShakeDetector(false);
2570 * Changes from the current track to the next track
2572 public void gotoNext(final boolean force) {
2573 if (D) Log.d(TAG, "Going to next track");
2574 synchronized (this) {
2575 if (mPlaylist.size() <= 0) {
2576 if (D) Log.d(TAG, "No play queue");
2577 scheduleDelayedShutdown();
2580 int pos = mNextPlayPos;
2582 pos = getNextPosition(force);
2586 setIsSupposedToBePlaying(false, true);
2591 setAndRecordPlayPos(pos);
2592 openCurrentAndNext();
2594 notifyChange(META_CHANGED);
2598 public void setAndRecordPlayPos(int nextPos) {
2599 synchronized (this) {
2600 // save to the history
2601 if (mShuffleMode != SHUFFLE_NONE) {
2602 mHistory.add(mPlayPos);
2603 if (mHistory.size() > MAX_HISTORY_SIZE) {
2613 * Changes from the current track to the previous played track
2615 public void prev(boolean forcePrevious) {
2616 synchronized (this) {
2617 // if we aren't repeating 1, and we are either early in the song
2618 // or we want to force go back, then go to the prevous track
2619 boolean goPrevious = getRepeatMode() != REPEAT_CURRENT &&
2620 (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD || forcePrevious);
2623 if (D) Log.d(TAG, "Going to previous track");
2624 int pos = getPreviousPlayPosition(true);
2625 // if we have no more previous tracks, quit
2629 mNextPlayPos = mPlayPos;
2634 notifyChange(META_CHANGED);
2636 if (D) Log.d(TAG, "Going to beginning of track");
2643 public int getPreviousPlayPosition(boolean removeFromHistory) {
2644 synchronized (this) {
2645 if (mShuffleMode == SHUFFLE_NORMAL) {
2646 // Go to previously-played track and remove it from the history
2647 final int histsize = mHistory.size();
2648 if (histsize == 0) {
2651 final Integer pos = mHistory.get(histsize - 1);
2652 if (removeFromHistory) {
2653 mHistory.remove(histsize - 1);
2658 return mPlayPos - 1;
2660 return mPlaylist.size() - 1;
2667 * We don't want to open the current and next track when the user is using
2668 * the {@code #prev()} method because they won't be able to travel back to
2669 * the previously listened track if they're shuffling.
2671 private void openCurrent() {
2672 openCurrentAndMaybeNext(false);
2676 * Moves an item in the queue from one position to another
2678 * @param from The position the item is currently at
2679 * @param to The position the item is being moved to
2681 public void moveQueueItem(int index1, int index2) {
2682 synchronized (this) {
2683 if (index1 >= mPlaylist.size()) {
2684 index1 = mPlaylist.size() - 1;
2686 if (index2 >= mPlaylist.size()) {
2687 index2 = mPlaylist.size() - 1;
2690 if (index1 == index2) {
2694 final MusicPlaybackTrack track = mPlaylist.remove(index1);
2695 if (index1 < index2) {
2696 mPlaylist.add(index2, track);
2697 if (mPlayPos == index1) {
2699 } else if (mPlayPos >= index1 && mPlayPos <= index2) {
2702 } else if (index2 < index1) {
2703 mPlaylist.add(index2, track);
2704 if (mPlayPos == index1) {
2706 } else if (mPlayPos >= index2 && mPlayPos <= index1) {
2710 notifyChange(QUEUE_CHANGED);
2715 * Sets the repeat mode
2717 * @param repeatmode The repeat mode to use
2719 public void setRepeatMode(final int repeatmode) {
2720 synchronized (this) {
2721 mRepeatMode = repeatmode;
2724 notifyChange(REPEATMODE_CHANGED);
2729 * Sets the shuffle mode
2731 * @param shufflemode The shuffle mode to use
2733 public void setShuffleMode(final int shufflemode) {
2734 synchronized (this) {
2735 if (mShuffleMode == shufflemode && mPlaylist.size() > 0) {
2739 mShuffleMode = shufflemode;
2740 if (mShuffleMode == SHUFFLE_AUTO) {
2741 if (makeAutoShuffleList()) {
2743 doAutoShuffleUpdate();
2745 openCurrentAndNext();
2747 notifyChange(META_CHANGED);
2750 mShuffleMode = SHUFFLE_NONE;
2756 notifyChange(SHUFFLEMODE_CHANGED);
2761 * Sets the position of a track in the queue
2763 * @param index The position to place the track
2765 public void setQueuePosition(final int index) {
2766 synchronized (this) {
2769 openCurrentAndNext();
2771 notifyChange(META_CHANGED);
2772 if (mShuffleMode == SHUFFLE_AUTO) {
2773 doAutoShuffleUpdate();
2779 * Queues a new list for playback
2781 * @param list The list to queue
2782 * @param action The action to take
2784 public void enqueue(final long[] list, final int action, long sourceId, IdType sourceType) {
2785 synchronized (this) {
2786 if (action == NEXT && mPlayPos + 1 < mPlaylist.size()) {
2787 addToPlayList(list, mPlayPos + 1, sourceId, sourceType);
2788 mNextPlayPos = mPlayPos + 1;
2789 notifyChange(QUEUE_CHANGED);
2791 addToPlayList(list, Integer.MAX_VALUE, sourceId, sourceType);
2792 notifyChange(QUEUE_CHANGED);
2797 openCurrentAndNext();
2799 notifyChange(META_CHANGED);
2805 * Cycles through the different repeat modes
2807 private void cycleRepeat() {
2808 if (mRepeatMode == REPEAT_NONE) {
2809 setRepeatMode(REPEAT_ALL);
2810 } else if (mRepeatMode == REPEAT_ALL) {
2811 setRepeatMode(REPEAT_CURRENT);
2812 if (mShuffleMode != SHUFFLE_NONE) {
2813 setShuffleMode(SHUFFLE_NONE);
2816 setRepeatMode(REPEAT_NONE);
2821 * Cycles through the different shuffle modes
2823 private void cycleShuffle() {
2824 if (mShuffleMode == SHUFFLE_NONE) {
2825 setShuffleMode(SHUFFLE_NORMAL);
2826 if (mRepeatMode == REPEAT_CURRENT) {
2827 setRepeatMode(REPEAT_ALL);
2829 } else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
2830 setShuffleMode(SHUFFLE_NONE);
2835 * @param smallBitmap true to return a smaller version of the default artwork image.
2836 * Currently Has no impact on the artwork size if one exists
2837 * @return The album art for the current album.
2839 public BitmapWithColors getAlbumArt(boolean smallBitmap) {
2840 final String albumName = getAlbumName();
2841 final String artistName = getArtistName();
2842 final long albumId = getAlbumId();
2843 final String key = albumName + "_" + artistName + "_" + albumId;
2844 final int targetIndex = smallBitmap ? 0 : 1;
2846 // if the cached key matches and we have the bitmap, return it
2847 if (key.equals(mCachedKey) && mCachedBitmapWithColors[targetIndex] != null) {
2848 return mCachedBitmapWithColors[targetIndex];
2851 // otherwise get the artwork (or defaultartwork if none found)
2852 final BitmapWithColors bitmap = mImageFetcher.getArtwork(albumName,
2853 albumId, artistName, smallBitmap);
2855 // if the key is different, clear the bitmaps first
2856 if (!key.equals(mCachedKey)) {
2857 mCachedBitmapWithColors[0] = null;
2858 mCachedBitmapWithColors[1] = null;
2861 // store the new key and bitmap
2863 mCachedBitmapWithColors[targetIndex] = bitmap;
2868 * Called when one of the lists should refresh or requery.
2870 public void refresh() {
2871 notifyChange(REFRESH);
2875 * Called when one of the playlists have changed (renamed, added/removed tracks)
2877 public void playlistChanged() {
2878 notifyChange(PLAYLIST_CHANGED);
2882 * Called to set the status of shake to play feature
2884 public void setShakeToPlayEnabled(boolean enabled) {
2886 Log.d(TAG, "ShakeToPlay status: " + enabled);
2889 if (mShakeDetector == null) {
2890 mShakeDetector = new ShakeDetector(mShakeDetectorListener);
2892 // if song is already playing, start listening immediately
2894 startShakeDetector();
2898 stopShakeDetector(true);
2903 * Called to set visibility of album art on lockscreen
2905 public void setLockscreenAlbumArt(boolean enabled) {
2906 mShowAlbumArtOnLockscreen = enabled;
2907 notifyChange(META_CHANGED);
2911 * Called to start listening to shakes
2913 private void startShakeDetector() {
2914 if (mShakeDetector != null) {
2915 mShakeDetector.start((SensorManager)getSystemService(SENSOR_SERVICE));
2920 * Called to stop listening to shakes
2922 private void stopShakeDetector(final boolean destroyShakeDetector) {
2923 if (mShakeDetector != null) {
2924 mShakeDetector.stop();
2926 if(destroyShakeDetector){
2927 mShakeDetector = null;
2929 Log.d(TAG, "ShakeToPlay destroyed!!!");
2934 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
2939 public void onReceive(final Context context, final Intent intent) {
2940 final String command = intent.getStringExtra(CMDNAME);
2942 if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
2943 final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2944 mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
2945 } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
2946 final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2947 mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
2948 } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
2949 final int[] largeAlt = intent
2950 .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
2951 mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
2953 handleCommandIntent(intent);
2958 private ContentObserver mMediaStoreObserver;
2960 private class MediaStoreObserver extends ContentObserver implements Runnable {
2961 // milliseconds to delay before calling refresh to aggregate events
2962 private static final long REFRESH_DELAY = 500;
2963 private Handler mHandler;
2965 public MediaStoreObserver(Handler handler) {
2971 public void onChange(boolean selfChange) {
2972 // if a change is detected, remove any scheduled callback
2973 // then post a new one. This is intended to prevent closely
2974 // spaced events from generating multiple refresh calls
2975 mHandler.removeCallbacks(this);
2976 mHandler.postDelayed(this, REFRESH_DELAY);
2981 // actually call refresh when the delayed callback fires
2982 Log.e("ELEVEN", "calling refresh!");
2987 private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
2992 public void onAudioFocusChange(final int focusChange) {
2993 mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
2997 private static final class MusicPlayerHandler extends Handler {
2998 private final WeakReference<MusicPlaybackService> mService;
2999 private float mCurrentVolume = 1.0f;
3001 private static final int DOUBLE_CLICK_TIMEOUT = 800;
3002 private int mHeadsetHookClickCounter = 0;
3005 * Constructor of <code>MusicPlayerHandler</code>
3007 * @param service The service to use.
3008 * @param looper The thread to run on.
3010 public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
3012 mService = new WeakReference<>(service);
3019 public void handleMessage(final Message msg) {
3020 final MusicPlaybackService service = mService.get();
3021 if (service == null) {
3025 synchronized (service) {
3028 mCurrentVolume -= .05f;
3029 if (mCurrentVolume > .2f) {
3030 sendEmptyMessageDelayed(FADEDOWN, 10);
3032 mCurrentVolume = .2f;
3034 service.mPlayer.setVolume(mCurrentVolume);
3037 mCurrentVolume += .01f;
3038 if (mCurrentVolume < 1.0f) {
3039 sendEmptyMessageDelayed(FADEUP, 10);
3041 mCurrentVolume = 1.0f;
3043 service.mPlayer.setVolume(mCurrentVolume);
3046 if (service.isPlaying()) {
3047 final TrackErrorInfo info = (TrackErrorInfo)msg.obj;
3048 service.sendErrorMessage(info.mTrackName);
3050 // since the service isPlaying(), we only need to remove the offending
3051 // audio track, and the code will automatically play the next track
3052 service.removeTrack(info.mId);
3054 service.openCurrentAndNext();
3057 case TRACK_WENT_TO_NEXT:
3058 service.setAndRecordPlayPos(service.mNextPlayPos);
3059 service.setNextTrack();
3060 if (service.mCursor != null) {
3061 service.mCursor.close();
3062 service.mCursor = null;
3064 service.updateCursor(service.mPlaylist.get(service.mPlayPos).mId);
3065 service.notifyChange(META_CHANGED);
3066 service.updateNotification();
3069 if (service.mRepeatMode == REPEAT_CURRENT) {
3073 service.gotoNext(false);
3077 service.mLyrics = (String) msg.obj;
3078 service.notifyChange(NEW_LYRICS);
3081 if (D) Log.d(TAG, "Received audio focus change event " + msg.arg1);
3083 case AudioManager.AUDIOFOCUS_LOSS:
3084 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
3085 if (service.isPlaying()) {
3086 service.mPausedByTransientLossOfFocus =
3087 msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
3091 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
3092 removeMessages(FADEUP);
3093 sendEmptyMessage(FADEDOWN);
3095 case AudioManager.AUDIOFOCUS_GAIN:
3096 if (!service.isPlaying()
3097 && service.mPausedByTransientLossOfFocus) {
3098 service.mPausedByTransientLossOfFocus = false;
3099 mCurrentVolume = 0f;
3100 service.mPlayer.setVolume(mCurrentVolume);
3103 removeMessages(FADEDOWN);
3104 sendEmptyMessage(FADEUP);
3110 case HEADSET_HOOK_EVENT: {
3111 long eventTime = (Long) msg.obj;
3113 mHeadsetHookClickCounter = Math.min(mHeadsetHookClickCounter + 1, 3);
3114 if (D) Log.d(TAG, "Got headset click, count = " + mHeadsetHookClickCounter);
3115 removeMessages(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3117 if (mHeadsetHookClickCounter == 3) {
3118 sendEmptyMessage(HEADSET_HOOK_MULTI_CLICK_TIMEOUT);
3120 sendEmptyMessageAtTime(HEADSET_HOOK_MULTI_CLICK_TIMEOUT,
3121 eventTime + DOUBLE_CLICK_TIMEOUT);
3125 case HEADSET_HOOK_MULTI_CLICK_TIMEOUT:
3126 if (D) Log.d(TAG, "Handling headset click");
3127 switch (mHeadsetHookClickCounter) {
3128 case 1: service.togglePlayPause(); break;
3129 case 2: service.gotoNext(true); break;
3130 case 3: service.prev(false); break;
3132 mHeadsetHookClickCounter = 0;
3133 service.mHeadsetHookWakeLock.release();
3142 private static final class Shuffler {
3144 private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<>();
3146 private final TreeSet<Integer> mPreviousNumbers = new TreeSet<>();
3148 private final Random mRandom = new Random();
3150 private int mPrevious;
3153 * Constructor of <code>Shuffler</code>
3160 * @param interval The length the queue
3161 * @return The position of the next track to play
3163 public int nextInt(final int interval) {
3166 next = mRandom.nextInt(interval);
3167 } while (next == mPrevious && interval > 1 && !mPreviousNumbers.contains(next));
3169 mHistoryOfNumbers.add(mPrevious);
3170 mPreviousNumbers.add(mPrevious);
3176 * Removes old tracks and cleans up the history preparing for new tracks
3177 * to be added to the mapping
3179 private void cleanUpHistory() {
3180 if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
3181 for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
3182 mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
3188 private static final class TrackErrorInfo {
3190 public String mTrackName;
3192 public TrackErrorInfo(long id, String trackName) {
3194 mTrackName = trackName;
3198 private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
3199 MediaPlayer.OnCompletionListener {
3201 private final WeakReference<MusicPlaybackService> mService;
3203 private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
3205 private MediaPlayer mNextMediaPlayer;
3207 private Handler mHandler;
3209 private boolean mIsInitialized = false;
3211 private SrtManager mSrtManager;
3213 private String mNextMediaPath;
3216 * Constructor of <code>MultiPlayer</code>
3218 public MultiPlayer(final MusicPlaybackService service) {
3219 mService = new WeakReference<>(service);
3220 mSrtManager = new SrtManager() {
3222 public void onTimedText(String text) {
3223 mHandler.obtainMessage(LYRICS, text).sendToTarget();
3229 * @param path The path of the file, or the http/rtsp URL of the stream
3232 public void setDataSource(final String path) {
3233 mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
3234 if (mIsInitialized) {
3236 setNextDataSource(null);
3240 private void loadSrt(final String path) {
3241 mSrtManager.reset();
3243 Uri uri = Uri.parse(path);
3244 String filePath = null;
3246 if (path.startsWith("content://")) {
3247 // resolve the content resolver path to a file path
3248 Cursor cursor = null;
3250 final String[] proj = {MediaStore.Audio.Media.DATA};
3251 cursor = mService.get().getContentResolver().query(uri, proj,
3253 if (cursor != null && cursor.moveToFirst()) {
3254 filePath = cursor.getString(0);
3257 if (cursor != null) {
3263 filePath = uri.getPath();
3266 if (!TextUtils.isEmpty(filePath)) {
3267 final int lastIndex = filePath.lastIndexOf('.');
3268 if (lastIndex != -1) {
3269 String newPath = filePath.substring(0, lastIndex) + ".srt";
3270 final File f = new File(newPath);
3272 mSrtManager.initialize(mCurrentMediaPlayer, f);
3278 * @param player The {@link MediaPlayer} to use
3279 * @param path The path of the file, or the http/rtsp URL of the stream
3281 * @return True if the <code>player</code> has been prepared and is
3282 * ready to play, false otherwise
3284 private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
3287 player.setOnPreparedListener(null);
3288 if (path.startsWith("content://")) {
3289 player.setDataSource(mService.get(), Uri.parse(path));
3291 player.setDataSource(path);
3293 player.setAudioStreamType(AudioManager.STREAM_MUSIC);
3296 } catch (final IOException todo) {
3297 // TODO: notify the user why the file couldn't be opened
3299 } catch (final IllegalArgumentException todo) {
3300 // TODO: notify the user why the file couldn't be opened
3303 player.setOnCompletionListener(this);
3304 player.setOnErrorListener(this);
3309 * Set the MediaPlayer to start when this MediaPlayer finishes playback.
3311 * @param path The path of the file, or the http/rtsp URL of the stream
3314 public void setNextDataSource(final String path) {
3315 mNextMediaPath = null;
3317 mCurrentMediaPlayer.setNextMediaPlayer(null);
3318 } catch (IllegalArgumentException e) {
3319 Log.i(TAG, "Next media player is current one, continuing");
3320 } catch (IllegalStateException e) {
3321 Log.e(TAG, "Media player not initialized!");
3324 if (mNextMediaPlayer != null) {
3325 mNextMediaPlayer.release();
3326 mNextMediaPlayer = null;
3331 mNextMediaPlayer = new MediaPlayer();
3332 mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
3333 if (setDataSourceImpl(mNextMediaPlayer, path)) {
3334 mNextMediaPath = path;
3335 mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
3337 if (mNextMediaPlayer != null) {
3338 mNextMediaPlayer.release();
3339 mNextMediaPlayer = null;
3347 * @param handler The handler to use
3349 public void setHandler(final Handler handler) {
3354 * @return True if the player is ready to go, false otherwise
3356 public boolean isInitialized() {
3357 return mIsInitialized;
3361 * Starts or resumes playback.
3363 public void start() {
3364 mCurrentMediaPlayer.start();
3369 * Resets the MediaPlayer to its uninitialized state.
3371 public void stop() {
3372 mCurrentMediaPlayer.reset();
3373 mSrtManager.reset();
3374 mIsInitialized = false;
3378 * Releases resources associated with this MediaPlayer object.
3380 public void release() {
3381 mCurrentMediaPlayer.release();
3382 mSrtManager.release();
3387 * Pauses playback. Call start() to resume.
3389 public void pause() {
3390 mCurrentMediaPlayer.pause();
3391 mSrtManager.pause();
3395 * Gets the duration of the file.
3397 * @return The duration in milliseconds
3399 public long duration() {
3401 return mCurrentMediaPlayer.getDuration();
3402 } catch (IllegalStateException exc) {
3403 Log.e(TAG, "Could not get duration", exc);
3409 * Gets the current playback position.
3411 * @return The current position in milliseconds
3413 public long position() {
3415 return mCurrentMediaPlayer.getCurrentPosition();
3416 } catch (IllegalStateException exc) {
3417 Log.e(TAG, "Could not get current position", exc);
3423 * Gets the current playback position.
3425 * @param whereto The offset in milliseconds from the start to seek to
3426 * @return The offset in milliseconds from the start to seek to
3428 public long seek(final long whereto) {
3429 mCurrentMediaPlayer.seekTo((int)whereto);
3430 mSrtManager.seekTo(whereto);
3435 * Sets the volume on this player.
3437 * @param vol Left and right volume scalar
3439 public void setVolume(final float vol) {
3440 mCurrentMediaPlayer.setVolume(vol, vol);
3444 * Sets the audio session ID.
3446 * @param sessionId The audio session ID
3448 public void setAudioSessionId(final int sessionId) {
3449 mCurrentMediaPlayer.setAudioSessionId(sessionId);
3453 * Returns the audio session ID.
3455 * @return The current audio session ID.
3457 public int getAudioSessionId() {
3458 return mCurrentMediaPlayer.getAudioSessionId();
3465 public boolean onError(final MediaPlayer mp, final int what, final int extra) {
3466 Log.w(TAG, "Music Server Error what: " + what + " extra: " + extra);
3468 case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
3469 final MusicPlaybackService service = mService.get();
3470 if (service == null) {
3473 final TrackErrorInfo errorInfo = new TrackErrorInfo(service.getAudioId(),
3474 service.getTrackName());
3476 mIsInitialized = false;
3477 mCurrentMediaPlayer.release();
3478 mCurrentMediaPlayer = new MediaPlayer();
3479 Message msg = mHandler.obtainMessage(SERVER_DIED, errorInfo);
3480 mHandler.sendMessageDelayed(msg, 2000);
3492 public void onCompletion(final MediaPlayer mp) {
3493 if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
3494 mCurrentMediaPlayer.release();
3495 mCurrentMediaPlayer = mNextMediaPlayer;
3496 loadSrt(mNextMediaPath);
3497 mNextMediaPath = null;
3498 mNextMediaPlayer = null;
3499 mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
3501 mHandler.sendEmptyMessage(TRACK_ENDED);
3506 private static final class ServiceStub extends IElevenService.Stub {
3508 private final WeakReference<MusicPlaybackService> mService;
3510 private ServiceStub(final MusicPlaybackService service) {
3511 mService = new WeakReference<>(service);
3518 public void openFile(final String path) throws RemoteException {
3519 mService.get().openFile(path);
3526 public void open(final long[] list, final int position, long sourceId, int sourceType)
3527 throws RemoteException {
3528 mService.get().open(list, position, sourceId, IdType.getTypeById(sourceType));
3535 public void stop() throws RemoteException {
3536 mService.get().stop();
3543 public void pause() throws RemoteException {
3544 mService.get().pause();
3551 public void play() throws RemoteException {
3552 mService.get().play();
3559 public void prev(boolean forcePrevious) throws RemoteException {
3560 mService.get().prev(forcePrevious);
3567 public void next() throws RemoteException {
3568 mService.get().gotoNext(true);
3575 public void enqueue(final long[] list, final int action, long sourceId, int sourceType)
3576 throws RemoteException {
3577 mService.get().enqueue(list, action, sourceId, IdType.getTypeById(sourceType));
3584 public void setQueuePosition(final int index) throws RemoteException {
3585 mService.get().setQueuePosition(index);
3592 public void setShuffleMode(final int shufflemode) throws RemoteException {
3593 mService.get().setShuffleMode(shufflemode);
3600 public void setRepeatMode(final int repeatmode) throws RemoteException {
3601 mService.get().setRepeatMode(repeatmode);
3608 public void moveQueueItem(final int from, final int to) throws RemoteException {
3609 mService.get().moveQueueItem(from, to);
3616 public void refresh() throws RemoteException {
3617 mService.get().refresh();
3624 public void playlistChanged() throws RemoteException {
3625 mService.get().playlistChanged();
3632 public boolean isPlaying() throws RemoteException {
3633 return mService.get().isPlaying();
3640 public long[] getQueue() throws RemoteException {
3641 return mService.get().getQueue();
3648 public long getQueueItemAtPosition(int position) throws RemoteException {
3649 return mService.get().getQueueItemAtPosition(position);
3656 public int getQueueSize() throws RemoteException {
3657 return mService.get().getQueueSize();
3664 public int getQueueHistoryPosition(int position) throws RemoteException {
3665 return mService.get().getQueueHistoryPosition(position);
3672 public int getQueueHistorySize() throws RemoteException {
3673 return mService.get().getQueueHistorySize();
3680 public int[] getQueueHistoryList() throws RemoteException {
3681 return mService.get().getQueueHistoryList();
3688 public long duration() throws RemoteException {
3689 return mService.get().duration();
3696 public long position() throws RemoteException {
3697 return mService.get().position();
3704 public long seek(final long position) throws RemoteException {
3705 return mService.get().seek(position);
3712 public void seekRelative(final long deltaInMs) throws RemoteException {
3713 mService.get().seekRelative(deltaInMs);
3720 public long getAudioId() throws RemoteException {
3721 return mService.get().getAudioId();
3728 public MusicPlaybackTrack getCurrentTrack() throws RemoteException {
3729 return mService.get().getCurrentTrack();
3736 public MusicPlaybackTrack getTrack(int index) throws RemoteException {
3737 return mService.get().getTrack(index);
3744 public long getNextAudioId() throws RemoteException {
3745 return mService.get().getNextAudioId();
3752 public long getPreviousAudioId() throws RemoteException {
3753 return mService.get().getPreviousAudioId();
3760 public long getArtistId() throws RemoteException {
3761 return mService.get().getArtistId();
3768 public long getAlbumId() throws RemoteException {
3769 return mService.get().getAlbumId();
3776 public String getArtistName() throws RemoteException {
3777 return mService.get().getArtistName();
3784 public String getTrackName() throws RemoteException {
3785 return mService.get().getTrackName();
3792 public String getAlbumName() throws RemoteException {
3793 return mService.get().getAlbumName();
3800 public String getPath() throws RemoteException {
3801 return mService.get().getPath();
3808 public int getQueuePosition() throws RemoteException {
3809 return mService.get().getQueuePosition();
3816 public int getShuffleMode() throws RemoteException {
3817 return mService.get().getShuffleMode();
3824 public int getRepeatMode() throws RemoteException {
3825 return mService.get().getRepeatMode();
3832 public int removeTracks(final int first, final int last) throws RemoteException {
3833 return mService.get().removeTracks(first, last);
3840 public int removeTrack(final long id) throws RemoteException {
3841 return mService.get().removeTrack(id);
3848 public boolean removeTrackAtPosition(final long id, final int position)
3849 throws RemoteException {
3850 return mService.get().removeTrackAtPosition(id, position);
3857 public int getMediaMountedCount() throws RemoteException {
3858 return mService.get().getMediaMountedCount();
3865 public int getAudioSessionId() throws RemoteException {
3866 return mService.get().getAudioSessionId();
3873 public void setShakeToPlayEnabled(boolean enabled) {
3874 mService.get().setShakeToPlayEnabled(enabled);
3881 public void setLockscreenAlbumArt(boolean enabled) {
3882 mService.get().setLockscreenAlbumArt(enabled);
3887 private class QueueUpdateTask extends AsyncTask<Void, Void, List<MediaSession.QueueItem>> {
3888 private long[] mQueue;
3890 public QueueUpdateTask(long[] queue) {
3891 mQueue = queue != null ? Arrays.copyOf(queue, queue.length) : null;
3895 protected List<MediaSession.QueueItem> doInBackground(Void... params) {
3896 if (mQueue == null || mQueue.length == 0) {
3900 final StringBuilder selection = new StringBuilder();
3901 selection.append(MediaStore.Audio.Media._ID).append(" IN (");
3902 for (int i = 0; i < mQueue.length; i++) {
3904 selection.append(",");
3906 selection.append(mQueue[i]);
3908 selection.append(")");
3910 Cursor c = getContentResolver().query(
3911 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
3912 new String[] { AudioColumns._ID, AudioColumns.TITLE, AudioColumns.ARTIST },
3913 selection.toString(), null, null);
3919 LongSparseArray<MediaDescription> descsById = new LongSparseArray<>();
3920 final int idColumnIndex = c.getColumnIndexOrThrow(AudioColumns._ID);
3921 final int titleColumnIndex = c.getColumnIndexOrThrow(AudioColumns.TITLE);
3922 final int artistColumnIndex = c.getColumnIndexOrThrow(AudioColumns.ARTIST);
3924 while (c.moveToNext() && !isCancelled()) {
3925 final MediaDescription desc = new MediaDescription.Builder()
3926 .setTitle(c.getString(titleColumnIndex))
3927 .setSubtitle(c.getString(artistColumnIndex))
3929 final long id = c.getLong(idColumnIndex);
3930 descsById.put(id, desc);
3933 List<MediaSession.QueueItem> items = new ArrayList<>();
3934 for (int i = 0; i < mQueue.length; i++) {
3935 MediaDescription desc = descsById.get(mQueue[i]);
3937 // shouldn't happen except in corner cases like
3938 // music being deleted while we were processing
3939 desc = new MediaDescription.Builder().build();
3941 items.add(new MediaSession.QueueItem(desc, i));
3950 protected void onPostExecute(List<MediaSession.QueueItem> items) {
3951 if (!isCancelled()) {
3952 mSession.setQueue(items);