Java tutorial
/* * Copyright (C) 2017 Koma MJ * * Licensed under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law * or agreed to in writing, software distributed under the License is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package com.koma.music.service; import android.Manifest; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Bitmap; import android.hardware.SensorManager; import android.media.AudioManager; import android.media.audiofx.AudioEffect; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.SystemClock; import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.graphics.Palette; import android.text.TextUtils; import android.view.KeyEvent; import com.koma.music.R; import com.koma.music.data.source.local.db.MusicPlaybackState; import com.koma.music.data.source.local.db.RecentlyPlay; import com.koma.music.data.source.local.db.SongPlayCount; import com.koma.music.data.model.MusicPlaybackTrack; import com.koma.music.util.LogUtils; import com.koma.music.util.PreferenceUtils; import com.koma.music.util.Utils; import com.nostra13.universalimageloader.core.ImageLoader; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.LinkedList; import java.util.ListIterator; import static com.koma.music.service.MusicServiceConstants.META_CHANGED; import static com.koma.music.service.MusicServiceConstants.NEW_LYRICS; /** * Created by koma on 3/21/17. */ public class MusicService extends Service { private static final String TAG = MusicService.class.getSimpleName(); /** * Keeps a mapping of the track history */ private static LinkedList<Integer> mHistory = new LinkedList<>(); /** * Used to shuffle the tracks */ private static final Shuffler mShuffler = new Shuffler(); /** * Service stub */ private final IBinder mBinder = new ServiceStub(this); /** * 4x1 widget */ //private final AppWidgetSmall mAppWidgetSmall = AppWidgetSmall.getInstance(); /** * 4x2 widget */ //private final AppWidgetLarge mAppWidgetLarge = AppWidgetLarge.getInstance(); /** * 4x2 alternate widget */ //private final AppWidgetLargeAlternate mAppWidgetLargeAlternate = AppWidgetLargeAlternate.getInstance(); /** * The media player */ private MultiPlayer mPlayer; /** * The path of the current file to play */ private String mFileToPlay; /** * Alarm intent for removing the notification when nothing is playing * for some time */ private AlarmManager mAlarmManager; private PendingIntent mShutdownIntent; private boolean mShutdownScheduled; private NotificationManager mNotificationManager; /** * The cursor used to retrieve info on the current track and run the * necessary queries to play audio files */ private Cursor mCursor; /** * The cursor used to retrieve info on the album the current track is * part of, if any. */ private Cursor mAlbumCursor; /** * Monitors the audio state */ private AudioManager mAudioManager; /** * Settings used to save and retrieve the queue and history */ private SharedPreferences mPreferences; /** * Used to know when the service is active */ private boolean mServiceInUse = false; /** * Used to know if something should be playing or not */ private boolean mIsSupposedToBePlaying = false; /** * Gets the last played time to determine whether we still want notifications or not */ private long mLastPlayedTime; private int mNotifyMode = NOTIFY_MODE_NONE; private long mNotificationPostTime = 0; private static final int NOTIFY_MODE_NONE = 0; private static final int NOTIFY_MODE_FOREGROUND = 1; private static final int NOTIFY_MODE_BACKGROUND = 2; /** * Used to indicate if the queue can be saved */ private boolean mQueueIsSaveable = true; /** * Used to track what type of audio focus loss caused the playback to pause */ private boolean mPausedByTransientLossOfFocus = false; /** * Lock screen controls */ private MediaSession mSession; // We use this to distinguish between different cards when saving/restoring // playlists private int mCardId; private int mPlayPos = -1; private int mNextPlayPos = -1; private int mOpenFailedCounter = 0; private int mMediaMountedCount = 0; private int mShuffleMode = MusicServiceConstants.SHUFFLE_NONE; private int mRepeatMode = MusicServiceConstants.REPEAT_NONE; private int mServiceStartId = -1; private String mLyrics; private ArrayList<MusicPlaybackTrack> mPlaylist = new ArrayList<MusicPlaybackTrack>(100); private long[] mAutoShuffleList = null; private MusicPlayerHandler mPlayerHandler; private HandlerThread mHandlerThread; private BroadcastReceiver mUnmountReceiver = null; private QueueUpdateTask mQueueUpdateTask; private ContentObserver mMediaObserver; /** * Stores the playback state */ private MusicPlaybackState mPlaybackStateStore; /** * Recently played database */ private RecentlyPlay mRecentlyPlayCache; /** * The song play count database */ private SongPlayCount mSongPlayCountCache; /** * Shake detector class used for shake to switch song feature */ private ShakeDetector mShakeDetector; /** * Switch for displaying album art on lockscreen */ private boolean mShowAlbumArtOnLockscreen; private boolean mReadGranted = false; private PowerManager.WakeLock mHeadsetHookWakeLock; private ShakeDetector.Listener mShakeDetectorListener = new ShakeDetector.Listener() { @Override public void hearShake() { /* * on shake detect, play next song */ LogUtils.i(TAG, "Shake detected!!!"); gotoNext(true); } }; @Nullable @Override public IBinder onBind(Intent intent) { LogUtils.i(TAG, "Service bound, intent = " + intent); cancelShutdown(); mServiceInUse = true; return mBinder; } @Override public boolean onUnbind(final Intent intent) { LogUtils.d(TAG, "Service unbound"); mServiceInUse = false; saveQueue(true); if (mReadGranted) { if (mIsSupposedToBePlaying || mPausedByTransientLossOfFocus) { // Something is currently playing, or will be playing once // an in-progress action requesting audio focus ends, so don't stop // the service now. return true; // If there is a playlist but playback is paused, then wait a while // before stopping the service, so that pause/resume isn't slow. // Also delay stopping the service if we're transitioning between // tracks. } else if (mPlaylist.size() > 0 || mPlayerHandler.hasMessages(MusicServiceConstants.TRACK_ENDED)) { scheduleDelayedShutdown(); return true; } } stopSelf(mServiceStartId); return true; } /** * {@inheritDoc} */ @Override public void onRebind(final Intent intent) { cancelShutdown(); mServiceInUse = true; } /** * {@inheritDoc} */ @Override public void onCreate() { LogUtils.d(TAG, "Creating service"); super.onCreate(); if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { stopSelf(); return; } else { mReadGranted = true; } mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // Initialize the favorites and recents databases mRecentlyPlayCache = RecentlyPlay.getInstance(this); // gets the song play count cache mSongPlayCountCache = SongPlayCount.getInstance(this); // gets a pointer to the playback state store mPlaybackStateStore = MusicPlaybackState.getInstance(this); // Initialize the image fetcher // mImageFetcher = ImageFetcher.getInstance(this); // Initialize the image cache // mImageFetcher.setImageCache(ImageCache.getInstance(this)); // Start up the thread running the service. Note that we create a // separate thread because the service normally runs in the process's // main thread, which we don't want to block. We also make it // background priority so CPU-intensive work will not disrupt the UI. mHandlerThread = new HandlerThread("MusicPlayerHandler", android.os.Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); // Initialize the handler mPlayerHandler = new MusicPlayerHandler(this, mHandlerThread.getLooper()); // Initialize the audio manager and register any headset controls for // playback mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); // Use the remote control APIs to set the playback state setUpMediaSession(); // Initialize the preferences mPreferences = getSharedPreferences("MusicService", 0); mCardId = getCardId(); mShowAlbumArtOnLockscreen = mPreferences.getBoolean(PreferenceUtils.SHOW_ALBUM_ART_ON_LOCKSCREEN, false); setShakeToPlayEnabled(mPreferences.getBoolean(PreferenceUtils.SHAKE_TO_PLAY, false)); registerExternalStorageListener(); // Initialize the media player mPlayer = new MultiPlayer(this); mPlayer.setHandler(mPlayerHandler); // Initialize the intent filter and each action final IntentFilter filter = new IntentFilter(); filter.addAction(MusicServiceConstants.SERVICECMD); filter.addAction(MusicServiceConstants.TOGGLEPAUSE_ACTION); filter.addAction(MusicServiceConstants.PAUSE_ACTION); filter.addAction(MusicServiceConstants.STOP_ACTION); filter.addAction(MusicServiceConstants.NEXT_ACTION); filter.addAction(MusicServiceConstants.PREVIOUS_ACTION); filter.addAction(MusicServiceConstants.PREVIOUS_FORCE_ACTION); filter.addAction(MusicServiceConstants.REPEAT_ACTION); filter.addAction(MusicServiceConstants.SHUFFLE_ACTION); // Attach the broadcast listener registerReceiver(mIntentReceiver, filter); // Get events when MediaStore content changes mMediaObserver = new MediaObserver(mPlayerHandler); getContentResolver().registerContentObserver(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mMediaObserver); getContentResolver().registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mMediaObserver); // Initialize the delayed shutdown intent final Intent shutdownIntent = new Intent(this, MusicService.class); shutdownIntent.setAction(MusicServiceConstants.SHUTDOWN); mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); mShutdownIntent = PendingIntent.getService(this, 0, shutdownIntent, 0); // Listen for the idle state scheduleDelayedShutdown(); // Bring the queue back reloadQueue(); notifyChange(MusicServiceConstants.QUEUE_CHANGED); notifyChange(META_CHANGED); } private void setUpMediaSession() { mSession = new MediaSession(this, "KomaMusic"); mSession.setCallback(new MediaSession.Callback() { @Override public void onPause() { pause(); mPausedByTransientLossOfFocus = false; } @Override public void onPlay() { play(); } @Override public void onSeekTo(long pos) { seek(pos); } @Override public void onSkipToNext() { gotoNext(true); } @Override public void onSkipToPrevious() { prev(false); } @Override public void onStop() { pause(); mPausedByTransientLossOfFocus = false; seek(0); releaseServiceUiAndStop(); } @Override public void onSkipToQueueItem(long id) { setQueuePosition((int) id); } @Override public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { if (Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) { KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); if (ke != null && ke.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) { if (ke.getAction() == KeyEvent.ACTION_UP) { handleHeadsetHookClick(ke.getEventTime()); } return true; } } return super.onMediaButtonEvent(mediaButtonIntent); } }); PendingIntent pi = PendingIntent.getBroadcast(this, 0, new Intent(this, MediaButtonIntentReceiver.class), PendingIntent.FLAG_UPDATE_CURRENT); mSession.setMediaButtonReceiver(pi); mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); } /** * {@inheritDoc} */ @Override public void onDestroy() { LogUtils.d(TAG, "Destroying service"); if (!mReadGranted) { return; } super.onDestroy(); // Remove any sound effects final Intent audioEffectsIntent = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(audioEffectsIntent); // remove any pending alarms mAlarmManager.cancel(mShutdownIntent); // Remove any callbacks from the handler mPlayerHandler.removeCallbacksAndMessages(null); // quit the thread so that anything that gets posted won't run mHandlerThread.quitSafely(); // Release the player mPlayer.release(); mPlayer = null; // Remove the audio focus listener and lock screen controls mAudioManager.abandonAudioFocus(mAudioFocusListener); mSession.release(); // remove the media store observer getContentResolver().unregisterContentObserver(mMediaObserver); // Close the cursor closeCursor(); // Unregister the mount listener unregisterReceiver(mIntentReceiver); if (mUnmountReceiver != null) { unregisterReceiver(mUnmountReceiver); mUnmountReceiver = null; } // deinitialize shake detector stopShakeDetector(true); } /** * {@inheritDoc} */ @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { LogUtils.d(TAG, "Got new intent " + intent + ", startId = " + startId); mServiceStartId = startId; if (intent != null) { final String action = intent.getAction(); if (MusicServiceConstants.SHUTDOWN.equals(action)) { mShutdownScheduled = false; releaseServiceUiAndStop(); return START_NOT_STICKY; } handleCommandIntent(intent); } // Make sure the service will shut down on its own if it was // just started but not bound to and nothing is playing scheduleDelayedShutdown(); if (intent != null && intent.getBooleanExtra(MusicServiceConstants.FROM_MEDIA_BUTTON, false)) { MediaButtonIntentReceiver.completeWakefulIntent(intent); } return START_STICKY; } private void releaseServiceUiAndStop() { if (isPlaying() || mPausedByTransientLossOfFocus || mPlayerHandler.hasMessages(MusicServiceConstants.TRACK_ENDED)) { return; } LogUtils.d(TAG, "Nothing is playing anymore, releasing notification"); cancelNotification(); mAudioManager.abandonAudioFocus(mAudioFocusListener); mSession.setActive(false); if (!mServiceInUse) { saveQueue(true); stopSelf(mServiceStartId); } } private void handleCommandIntent(Intent intent) { final String action = intent.getAction(); final String command = MusicServiceConstants.SERVICECMD.equals(action) ? intent.getStringExtra(MusicServiceConstants.CMDNAME) : null; LogUtils.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command); if (MusicServiceConstants.CMDNEXT.equals(command) || MusicServiceConstants.NEXT_ACTION.equals(action)) { gotoNext(true); } else if (MusicServiceConstants.CMDPREVIOUS.equals(command) || MusicServiceConstants.PREVIOUS_ACTION.equals(action) || MusicServiceConstants.PREVIOUS_FORCE_ACTION.equals(action)) { prev(MusicServiceConstants.PREVIOUS_FORCE_ACTION.equals(action)); } else if (MusicServiceConstants.CMDTOGGLEPAUSE.equals(command) || MusicServiceConstants.TOGGLEPAUSE_ACTION.equals(action)) { togglePlayPause(); } else if (MusicServiceConstants.CMDPAUSE.equals(command) || MusicServiceConstants.PAUSE_ACTION.equals(action)) { pause(); mPausedByTransientLossOfFocus = false; } else if (MusicServiceConstants.CMDPLAY.equals(command)) { play(); } else if (MusicServiceConstants.CMDSTOP.equals(command) || MusicServiceConstants.STOP_ACTION.equals(action)) { pause(); mPausedByTransientLossOfFocus = false; seek(0); releaseServiceUiAndStop(); } else if (MusicServiceConstants.REPEAT_ACTION.equals(action)) { cycleRepeat(); } else if (MusicServiceConstants.SHUFFLE_ACTION.equals(action)) { cycleShuffle(); } else if (MusicServiceConstants.CMDHEADSETHOOK.equals(command)) { long timestamp = intent.getLongExtra(MusicServiceConstants.TIMESTAMP, 0); handleHeadsetHookClick(timestamp); } } private void handleHeadsetHookClick(long timestamp) { if (mHeadsetHookWakeLock == null) { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); mHeadsetHookWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "KomaMusic headset button"); mHeadsetHookWakeLock.setReferenceCounted(false); } // Make sure we don't indefinitely hold the wake lock under any circumstances mHeadsetHookWakeLock.acquire(10000); Message msg = mPlayerHandler.obtainMessage(MusicServiceConstants.HEADSET_HOOK_EVENT, Long.valueOf(timestamp)); msg.sendToTarget(); } /** * Updates the notification, considering the current play and activity state */ private void updateNotification() { final int newNotifyMode; if (isPlaying()) { newNotifyMode = NOTIFY_MODE_FOREGROUND; } else if (recentlyPlayed()) { newNotifyMode = NOTIFY_MODE_BACKGROUND; } else { newNotifyMode = NOTIFY_MODE_NONE; } int notificationId = hashCode(); if (mNotifyMode != newNotifyMode) { if (mNotifyMode == NOTIFY_MODE_FOREGROUND) { stopForeground(newNotifyMode == NOTIFY_MODE_NONE); } else if (newNotifyMode == NOTIFY_MODE_NONE) { mNotificationManager.cancel(notificationId); mNotificationPostTime = 0; } } if (newNotifyMode == NOTIFY_MODE_FOREGROUND) { startForeground(notificationId, buildNotification()); } else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) { mNotificationManager.notify(notificationId, buildNotification()); } mNotifyMode = newNotifyMode; } private void cancelNotification() { stopForeground(true); mNotificationManager.cancel(hashCode()); mNotificationPostTime = 0; mNotifyMode = NOTIFY_MODE_NONE; } /** * @return A card ID used to save and restore playlists, i.e., the queue. */ private int getCardId() { final ContentResolver resolver = getContentResolver(); Cursor cursor = resolver.query(Uri.parse("content://media/external/fs_id"), null, null, null, null); int mCardId = -1; if (cursor != null && cursor.moveToFirst()) { mCardId = cursor.getInt(0); cursor.close(); cursor = null; } return mCardId; } /** * Called when we receive a ACTION_MEDIA_EJECT notification. * * @param storagePath The path to mount point for the removed media */ public void closeExternalStorageFiles(final String storagePath) { stop(true); notifyChange(MusicServiceConstants.QUEUE_CHANGED); notifyChange(META_CHANGED); } /** * Registers an intent to listen for ACTION_MEDIA_EJECT notifications. The * intent will call closeExternalStorageFiles() if the external media is * going to be ejected, so applications can clean up any files they have * open. */ public void registerExternalStorageListener() { if (mUnmountReceiver == null) { mUnmountReceiver = new BroadcastReceiver() { /** * {@inheritDoc} */ @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (action.equals(Intent.ACTION_MEDIA_EJECT)) { saveQueue(true); mQueueIsSaveable = false; closeExternalStorageFiles(intent.getData().getPath()); } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { mMediaMountedCount++; mCardId = getCardId(); reloadQueue(); mQueueIsSaveable = true; notifyChange(MusicServiceConstants.QUEUE_CHANGED); notifyChange(META_CHANGED); } } }; final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_MEDIA_EJECT); filter.addAction(Intent.ACTION_MEDIA_MOUNTED); filter.addDataScheme("file"); registerReceiver(mUnmountReceiver, filter); } } private void scheduleDelayedShutdown() { LogUtils.d(TAG, "Scheduling shutdown in " + MusicServiceConstants.IDLE_DELAY + " ms"); if (!mReadGranted) { return; } mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + MusicServiceConstants.IDLE_DELAY, mShutdownIntent); mShutdownScheduled = true; } private void cancelShutdown() { LogUtils.d(TAG, "Cancelling delayed shutdown, scheduled = " + mShutdownScheduled); if (mShutdownScheduled) { mAlarmManager.cancel(mShutdownIntent); mShutdownScheduled = false; } } /** * Stops playback * * @param goToIdle True to go to the idle state, false otherwise */ private void stop(final boolean goToIdle) { LogUtils.d(TAG, "Stopping playback, goToIdle = " + goToIdle); if (mPlayer.isInitialized()) { mPlayer.stop(); } mFileToPlay = null; closeCursor(); if (goToIdle) { setIsSupposedToBePlaying(false, false); } else { stopForeground(false); } } /** * Removes the range of tracks specified from the play list. If a file * within the range is the file currently being played, playback will move * to the next file after the range. * * @param first The first file to be removed * @param last The last file to be removed * @return the number of tracks deleted */ private int removeTracksInternal(int first, int last) { synchronized (this) { if (last < first) { return 0; } else if (first < 0) { first = 0; } else if (last >= mPlaylist.size()) { last = mPlaylist.size() - 1; } boolean gotonext = false; if (first <= mPlayPos && mPlayPos <= last) { mPlayPos = first; gotonext = true; } else if (mPlayPos > last) { mPlayPos -= last - first + 1; } final int numToRemove = last - first + 1; if (first == 0 && last == mPlaylist.size() - 1) { mPlayPos = -1; mNextPlayPos = -1; mPlaylist.clear(); mHistory.clear(); } else { for (int i = 0; i < numToRemove; i++) { mPlaylist.remove(first); } // remove the items from the history // this is not ideal as the history shouldn't be impacted by this // but since we are removing items from the array, it will throw // an exception if we keep it around. Idealistically with the queue // rewrite this should be all be fixed // https://cyanogen.atlassian.net/browse/MUSIC-44 ListIterator<Integer> positionIterator = mHistory.listIterator(); while (positionIterator.hasNext()) { int pos = positionIterator.next(); if (pos >= first && pos <= last) { positionIterator.remove(); } else if (pos > last) { positionIterator.set(pos - numToRemove); } } } if (gotonext) { if (mPlaylist.size() == 0) { stop(true); mPlayPos = -1; closeCursor(); } else { if (mShuffleMode != MusicServiceConstants.SHUFFLE_NONE) { mPlayPos = getNextPosition(true); } else if (mPlayPos >= mPlaylist.size()) { mPlayPos = 0; } final boolean wasPlaying = isPlaying(); stop(false); openCurrentAndNext(); if (wasPlaying) { play(); } } notifyChange(META_CHANGED); } return last - first + 1; } } /** * Adds a list to the playlist * * @param list The list to add * @param position The position to place the tracks */ private void addToPlayList(final long[] list, int position, long sourceId) { final int addlen = list.length; if (position < 0) { mPlaylist.clear(); position = 0; } mPlaylist.ensureCapacity(mPlaylist.size() + addlen); if (position > mPlaylist.size()) { position = mPlaylist.size(); } final ArrayList<MusicPlaybackTrack> arrayList = new ArrayList<MusicPlaybackTrack>(addlen); for (int i = 0; i < list.length; i++) { arrayList.add(new MusicPlaybackTrack(list[i], sourceId, i)); } mPlaylist.addAll(position, arrayList); if (mPlaylist.size() == 0) { closeCursor(); notifyChange(META_CHANGED); } } /** * @param trackId The track ID */ private void updateCursor(final long trackId) { updateCursor("_id=" + trackId, null); } private void updateCursor(final String selection, final String[] selectionArgs) { synchronized (this) { closeCursor(); mCursor = openCursorAndGoToFirst(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MusicServiceConstants.PROJECTION, selection, selectionArgs); } updateAlbumCursor(); } private void updateCursor(final Uri uri) { synchronized (this) { closeCursor(); mCursor = openCursorAndGoToFirst(uri, MusicServiceConstants.PROJECTION, null, null); } updateAlbumCursor(); } private void updateAlbumCursor() { long albumId = getAlbumId(); if (albumId >= 0) { mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, MusicServiceConstants.ALBUM_PROJECTION, "_id=" + albumId, null); } else { mAlbumCursor = null; } } private Cursor openCursorAndGoToFirst(Uri uri, String[] projection, String selection, String[] selectionArgs) { Cursor c = getContentResolver().query(uri, projection, selection, selectionArgs, null, null); if (c == null) { return null; } if (!c.moveToFirst()) { c.close(); return null; } return c; } private synchronized void closeCursor() { if (mCursor != null) { mCursor.close(); mCursor = null; } if (mAlbumCursor != null) { mAlbumCursor.close(); mAlbumCursor = null; } } /** * Called to open a new file as the current track and prepare the next for * playback */ private void openCurrentAndNext() { openCurrentAndMaybeNext(true); } /** * Called to open a new file as the current track and prepare the next for * playback * * @param openNext True to prepare the next track for playback, false * otherwise. */ private void openCurrentAndMaybeNext(final boolean openNext) { synchronized (this) { closeCursor(); if (mPlaylist.size() == 0) { return; } stop(false); boolean shutdown = false; updateCursor(mPlaylist.get(mPlayPos).mId); while (true) { if (mCursor != null && openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + mCursor.getLong(MusicServiceConstants.IDCOLIDX))) { break; } // if we get here then opening the file failed. We can close the // cursor now, because // we're either going to create a new one next, or stop trying closeCursor(); if (mOpenFailedCounter++ < 10 && mPlaylist.size() > 1) { final int pos = getNextPosition(false); if (pos < 0) { shutdown = true; break; } mPlayPos = pos; stop(false); mPlayPos = pos; updateCursor(mPlaylist.get(mPlayPos).mId); } else { mOpenFailedCounter = 0; LogUtils.e(TAG, "Failed to open file for playback"); shutdown = true; break; } } if (shutdown) { scheduleDelayedShutdown(); if (mIsSupposedToBePlaying) { mIsSupposedToBePlaying = false; notifyChange(MusicServiceConstants.PLAYSTATE_CHANGED); } } else if (openNext) { setNextTrack(); } } } private void sendErrorMessage(final String trackName) { final Intent i = new Intent(MusicServiceConstants.TRACK_ERROR); i.putExtra(MusicServiceConstants.TRACK_NAME, trackName); sendBroadcast(i); } /** * @param force True to force the player onto the track next, false * otherwise. * @return The next position to play. */ private int getNextPosition(final boolean force) { // as a base case, if the playlist is empty just return -1 if (mPlaylist == null || mPlaylist.isEmpty()) { return -1; } // if we're not forced to go to the next track and we are only playing the current track if (!force && mRepeatMode == MusicServiceConstants.REPEAT_CURRENT) { if (mPlayPos < 0) { return 0; } return mPlayPos; } else if (mShuffleMode == MusicServiceConstants.SHUFFLE_NORMAL) { final int numTracks = mPlaylist.size(); // count the number of times a track has been played final int[] trackNumPlays = new int[numTracks]; for (int i = 0; i < numTracks; i++) { // set it all to 0 trackNumPlays[i] = 0; } // walk through the history and add up the number of times the track // has been played final int numHistory = mHistory.size(); for (int i = 0; i < numHistory; i++) { final int idx = mHistory.get(i).intValue(); if (idx >= 0 && idx < numTracks) { trackNumPlays[idx]++; } } // also add the currently playing track to the count if (mPlayPos >= 0 && mPlayPos < numTracks) { trackNumPlays[mPlayPos]++; } // figure out the least # of times a track has a played as well as // how many tracks share that count int minNumPlays = Integer.MAX_VALUE; int numTracksWithMinNumPlays = 0; for (int i = 0; i < trackNumPlays.length; i++) { // if we found a new track that has less number of plays, reset the counters if (trackNumPlays[i] < minNumPlays) { minNumPlays = trackNumPlays[i]; numTracksWithMinNumPlays = 1; } else if (trackNumPlays[i] == minNumPlays) { // increment this track shares the # of tracks numTracksWithMinNumPlays++; } } // if we've played each track at least once and all tracks have been played an equal // # of times and we aren't repeating all and we're not forcing a track, then // return no more tracks if (minNumPlays > 0 && numTracksWithMinNumPlays == numTracks && mRepeatMode != MusicServiceConstants.REPEAT_ALL && !force) { return -1; } // else pick a track from the least number of played tracks int skip = mShuffler.nextInt(numTracksWithMinNumPlays); for (int i = 0; i < trackNumPlays.length; i++) { if (trackNumPlays[i] == minNumPlays) { if (skip == 0) { return i; } else { skip--; } } } // Unexpected to land here LogUtils.e(TAG, "Getting the next position resulted did not get a result when it should have"); return -1; } else if (mShuffleMode == MusicServiceConstants.SHUFFLE_AUTO) { doAutoShuffleUpdate(); return mPlayPos + 1; } else { if (mPlayPos >= mPlaylist.size() - 1) { if (mRepeatMode == MusicServiceConstants.REPEAT_NONE && !force) { return -1; } else if (mRepeatMode == MusicServiceConstants.REPEAT_ALL || force) { return 0; } return -1; } else { return mPlayPos + 1; } } } /** * Sets the track to be played */ private void setNextTrack() { setNextTrack(getNextPosition(false)); } /** * Sets the next track to be played * * @param position the target position we want */ private void setNextTrack(int position) { mNextPlayPos = position; LogUtils.d(TAG, "setNextTrack: next play position = " + mNextPlayPos); if (mNextPlayPos >= 0 && mPlaylist != null && mNextPlayPos < mPlaylist.size()) { final long id = mPlaylist.get(mNextPlayPos).mId; mPlayer.setNextDataSource(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id); } else { mPlayer.setNextDataSource(null); } } /** * Creates a shuffled playlist used for party mode */ private boolean makeAutoShuffleList() { Cursor cursor = null; try { cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Audio.Media._ID }, MediaStore.Audio.Media.IS_MUSIC + "=1", null, null); if (cursor == null || cursor.getCount() == 0) { return false; } final int len = cursor.getCount(); final long[] list = new long[len]; for (int i = 0; i < len; i++) { cursor.moveToNext(); list[i] = cursor.getLong(0); } mAutoShuffleList = list; return true; } catch (final RuntimeException e) { } finally { if (cursor != null) { cursor.close(); cursor = null; } } return false; } /** * Creates the party shuffle playlist */ private void doAutoShuffleUpdate() { boolean notify = false; if (mPlayPos > 10) { removeTracks(0, mPlayPos - 9); notify = true; } final int toAdd = 7 - (mPlaylist.size() - (mPlayPos < 0 ? -1 : mPlayPos)); for (int i = 0; i < toAdd; i++) { int lookback = mHistory.size(); int idx = -1; while (true) { idx = mShuffler.nextInt(mAutoShuffleList.length); if (!wasRecentlyUsed(idx, lookback)) { break; } lookback /= 2; } mHistory.add(idx); if (mHistory.size() > MusicServiceConstants.MAX_HISTORY_SIZE) { mHistory.remove(0); } mPlaylist.add(new MusicPlaybackTrack(mAutoShuffleList[idx], -1, -1)); notify = true; } if (notify) { notifyChange(MusicServiceConstants.QUEUE_CHANGED); } } private boolean wasRecentlyUsed(final int idx, int lookbacksize) { if (lookbacksize == 0) { return false; } final int histsize = mHistory.size(); if (histsize < lookbacksize) { lookbacksize = histsize; } final int maxidx = histsize - 1; for (int i = 0; i < lookbacksize; i++) { final long entry = mHistory.get(maxidx - i); if (entry == idx) { return true; } } return false; } /** * Notify the change-receivers that something has changed. */ private void notifyChange(final String what) { LogUtils.d(TAG, "notifyChange: what = " + what); // Update the lockscreen controls updateMediaSession(what); if (what.equals(MusicServiceConstants.POSITION_CHANGED)) { return; } final Intent intent = new Intent(what); intent.putExtra("id", getAudioId()); intent.putExtra("artist", getArtistName()); intent.putExtra("album", getAlbumName()); intent.putExtra("track", getTrackName()); intent.putExtra("playing", isPlaying()); if (NEW_LYRICS.equals(what)) { intent.putExtra("lyrics", mLyrics); } sendStickyBroadcast(intent); final Intent musicIntent = new Intent(intent); musicIntent.setAction(what.replace(MusicServiceConstants.KOMA_MUSIC_PACKAGE_NAME, MusicServiceConstants.MUSIC_PACKAGE_NAME)); sendStickyBroadcast(musicIntent); if (what.equals(META_CHANGED)) { // Add the track to the recently played list. mRecentlyPlayCache.addSongId(getAudioId()); mSongPlayCountCache.bumpSongCount(getAudioId()); } else if (what.equals(MusicServiceConstants.QUEUE_CHANGED)) { saveQueue(true); if (isPlaying()) { // if we are in shuffle mode and our next track is still valid, // try to re-use the track // We need to reimplement the queue to prevent hacky solutions like this // https://cyanogen.atlassian.net/browse/MUSIC-175 // https://cyanogen.atlassian.net/browse/MUSIC-44 if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size() && getShuffleMode() != MusicServiceConstants.SHUFFLE_NONE) { setNextTrack(mNextPlayPos); } else { setNextTrack(); } } } else { saveQueue(false); } if (what.equals(MusicServiceConstants.PLAYSTATE_CHANGED)) { updateNotification(); } // Update the app-widgets /*mAppWidgetSmall.notifyChange(this, what); mAppWidgetLarge.notifyChange(this, what); mAppWidgetLargeAlternate.notifyChange(this, what);*/ LogUtils.i(TAG, "notifyChange finished"); } private void updateMediaSession(final String what) { LogUtils.i(TAG, "updateMediaSession what : " + what); int playState = mIsSupposedToBePlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED; long playBackStateActions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_STOP; if (what.equals(MusicServiceConstants.PLAYSTATE_CHANGED) || what.equals(MusicServiceConstants.POSITION_CHANGED)) { mSession.setPlaybackState(new PlaybackState.Builder().setActions(playBackStateActions) .setActiveQueueItemId(getAudioId()).setState(playState, position(), 1.0f).build()); } else if (what.equals(META_CHANGED) || what.equals(MusicServiceConstants.QUEUE_CHANGED)) { LogUtils.i(TAG, "sadsadsadsad thread id : " + Thread.currentThread().getId() + "name : " + Thread.currentThread().getName()); /*if (albumArt != null) { // RemoteControlClient wants to recycle the bitmaps thrown at it, so we need // to make sure not to hand out our cache copy Bitmap.Config config = albumArt.getConfig(); if (config == null) { config = Bitmap.Config.ARGB_8888; } albumArt = albumArt.copy(config, false); }*/ /*mSession.setMetadata(new MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, getArtistName()) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, getAlbumArtistName()) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, getAlbumName()) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, getTrackName()) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration()) .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, getQueuePosition() + 1) .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getQueue().length) .putString(MediaMetadataCompat.METADATA_KEY_GENRE, getGenreName()) .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, mShowAlbumArtOnLockscreen ? albumArt : null) .build());*/ if (what.equals(MusicServiceConstants.QUEUE_CHANGED)) { updateMediaSessionQueue(); } mSession.setPlaybackState(new PlaybackState.Builder().setActions(playBackStateActions) .setActiveQueueItemId(getAudioId()).setState(playState, position(), 1.0f).build()); } LogUtils.i(TAG, "updateMediaSession finished"); } private synchronized void updateMediaSessionQueue() { if (mQueueUpdateTask != null) { mQueueUpdateTask.cancel(true); } mQueueUpdateTask = new QueueUpdateTask(this, getQueue()); mQueueUpdateTask.execute(); } private Notification buildNotification() { final String albumName = getAlbumName(); final String artistName = getArtistName(); final boolean isPlaying = isPlaying(); String text = TextUtils.isEmpty(albumName) ? artistName : artistName + " - " + albumName; int playButtonResId = isPlaying ? R.drawable.ic_pause_black_36dp : R.drawable.ic_play_arrow_black_36dp; int playButtonTitleResId = isPlaying ? R.string.notification_pause : R.string.notification_play; Notification.MediaStyle style = new Notification.MediaStyle().setMediaSession(mSession.getSessionToken()) .setShowActionsInCompactView(0, 1, 2); Intent nowPlayingIntent = new Intent("com.koma.music.notification.AUDIO_PLAYER") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent clickIntent = PendingIntent.getActivity(this, 0, nowPlayingIntent, 0); Bitmap artwork; artwork = ImageLoader.getInstance().loadImageSync(Utils.getAlbumArtUri(getAlbumId()).toString()); if (artwork == null) { artwork = ImageLoader.getInstance().loadImageSync("drawable://" + R.drawable.ic_album); } if (mNotificationPostTime == 0) { mNotificationPostTime = System.currentTimeMillis(); } Notification.Builder builder = new Notification.Builder(this).setSmallIcon(R.drawable.ic_notification) .setLargeIcon(artwork).setContentIntent(clickIntent).setContentTitle(getTrackName()) .setContentText(text).setWhen(mNotificationPostTime).setShowWhen(false).setStyle(style) .setVisibility(Notification.VISIBILITY_PUBLIC) .addAction(R.drawable.ic_previous_notification, getString(R.string.notification_prev), retrievePlaybackAction(MusicServiceConstants.PREVIOUS_ACTION)) .addAction(playButtonResId, getString(playButtonTitleResId), retrievePlaybackAction(MusicServiceConstants.TOGGLEPAUSE_ACTION)) .addAction(R.drawable.ic_next_notification, getString(R.string.notification_next), retrievePlaybackAction(MusicServiceConstants.NEXT_ACTION)); if (artwork != null) { // builder.setColor(Palette.from(artwork).generate().getVibrantColor(Color.parseColor("#403f4d"))); builder.setColor( Palette.from(artwork).generate().getMutedColor(getResources().getColor(R.color.colorPrimary))); } builder.setVisibility(Notification.VISIBILITY_PUBLIC); return builder.build(); } private final PendingIntent retrievePlaybackAction(final String action) { final ComponentName serviceName = new ComponentName(this, MusicService.class); Intent intent = new Intent(action); intent.setComponent(serviceName); return PendingIntent.getService(this, 0, intent, 0); } /** * Saves the queue * * @param full True if the queue is full */ private void saveQueue(final boolean full) { if (!mQueueIsSaveable || mPreferences == null) { return; } final SharedPreferences.Editor editor = mPreferences.edit(); if (full) { mPlaybackStateStore.saveState(mPlaylist, mShuffleMode != MusicServiceConstants.SHUFFLE_NONE ? mHistory : null); editor.putInt("cardid", mCardId); } editor.putInt("curpos", mPlayPos); if (mPlayer.isInitialized()) { editor.putLong("seekpos", mPlayer.position()); } editor.putInt("repeatmode", mRepeatMode); editor.putInt("shufflemode", mShuffleMode); editor.apply(); } /** * Reloads the queue as the user left it the last time they stopped using * Apollo */ private void reloadQueue() { int id = mCardId; if (mPreferences.contains("cardid")) { id = mPreferences.getInt("cardid", ~mCardId); } if (id == mCardId) { mPlaylist = mPlaybackStateStore.getQueue(); } if (mPlaylist.size() > 0) { final int pos = mPreferences.getInt("curpos", 0); if (pos < 0 || pos >= mPlaylist.size()) { mPlaylist.clear(); return; } mPlayPos = pos; updateCursor(mPlaylist.get(mPlayPos).mId); if (mCursor == null) { SystemClock.sleep(3000); updateCursor(mPlaylist.get(mPlayPos).mId); } synchronized (this) { closeCursor(); mOpenFailedCounter = 20; openCurrentAndNext(); } if (!mPlayer.isInitialized()) { mPlaylist.clear(); return; } final long seekpos = mPreferences.getLong("seekpos", 0); seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0); LogUtils.d(TAG, "restored queue, currently at position " + position() + "/" + duration() + " (requested " + seekpos + ")"); int repmode = mPreferences.getInt("repeatmode", MusicServiceConstants.REPEAT_NONE); if (repmode != MusicServiceConstants.REPEAT_ALL && repmode != MusicServiceConstants.REPEAT_CURRENT) { repmode = MusicServiceConstants.REPEAT_NONE; } mRepeatMode = repmode; int shufmode = mPreferences.getInt("shufflemode", MusicServiceConstants.SHUFFLE_NONE); if (shufmode != MusicServiceConstants.SHUFFLE_AUTO && shufmode != MusicServiceConstants.SHUFFLE_NORMAL) { shufmode = MusicServiceConstants.SHUFFLE_NONE; } if (shufmode != MusicServiceConstants.SHUFFLE_NONE) { mHistory = mPlaybackStateStore.getHistory(mPlaylist.size()); } if (shufmode == MusicServiceConstants.SHUFFLE_AUTO) { if (!makeAutoShuffleList()) { shufmode = MusicServiceConstants.SHUFFLE_NONE; } } mShuffleMode = shufmode; } } /** * Opens a file and prepares it for playback * * @param path The path of the file to open */ public boolean openFile(final String path) { LogUtils.d(TAG, "openFile: path = " + path); synchronized (this) { if (path == null) { return false; } // If mCursor is null, try to associate path with a database cursor if (mCursor == null) { Uri uri = Uri.parse(path); boolean shouldAddToPlaylist = true; // should try adding audio info to playlist long id = -1; try { id = Long.valueOf(uri.getLastPathSegment()); } catch (NumberFormatException ex) { // Ignore } if (id != -1 && path.startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) { updateCursor(uri); } else if (id != -1 && path.startsWith(MediaStore.Files.getContentUri("external").toString())) { updateCursor(id); // handle downloaded media files } else if (path.startsWith("content://downloads/")) { // extract MediaProvider(MP) uri , if available // Downloads.Impl.COLUMN_MEDIAPROVIDER_URI String mpUri = getValueForDownloadedFile(this, uri, "mediaprovider_uri"); LogUtils.i(TAG, "Downloaded file's MP uri : " + mpUri); if (!TextUtils.isEmpty(mpUri)) { // if mpUri is valid, play that URI instead if (openFile(mpUri)) { // notify impending change in track notifyChange(META_CHANGED); return true; } else { return false; } } else { // create phantom cursor with download info, if a MP uri wasn't found updateCursorForDownloadedFile(this, uri); shouldAddToPlaylist = false; // song info isn't available in MediaStore } } else { // assuming a "file://" uri by this point ... String where = MediaStore.Audio.Media.DATA + "=?"; String[] selectionArgs = new String[] { path }; updateCursor(where, selectionArgs); } try { if (mCursor != null && shouldAddToPlaylist) { mPlaylist.clear(); mPlaylist.add( new MusicPlaybackTrack(mCursor.getLong(MusicServiceConstants.IDCOLIDX), -1, -1)); // propagate the change in playlist state notifyChange(MusicServiceConstants.QUEUE_CHANGED); mPlayPos = 0; mHistory.clear(); } } catch (final UnsupportedOperationException ex) { // Ignore } } mFileToPlay = path; mPlayer.setDataSource(mFileToPlay); if (mPlayer.isInitialized()) { mOpenFailedCounter = 0; return true; } String trackName = getTrackName(); if (TextUtils.isEmpty(trackName)) { trackName = path; } sendErrorMessage(trackName); stop(true); return false; } } /* Columns for a pseudo cursor we are creating for downloaded songs Modeled after mCursor to be able to respond to respond to the same queries as it */ private static final String[] PROJECTION_MATRIX = new String[] { "_id", MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.ARTIST_ID }; /** * Creates a pseudo cursor for downloaded audio files with minimal info * * @param context needed to query the download uri * @param uri the uri of the downloaded file */ private void updateCursorForDownloadedFile(Context context, Uri uri) { synchronized (this) { closeCursor(); // clear mCursor MatrixCursor cursor = new MatrixCursor(PROJECTION_MATRIX); // get title of the downloaded file ; Downloads.Impl.COLUMN_TITLE String title = getValueForDownloadedFile(this, uri, "title"); // populating the cursor with bare minimum info cursor.addRow(new Object[] { null, null, null, title, null, null, null, null }); mCursor = cursor; mCursor.moveToFirst(); } } /** * Query the DownloadProvider to get the value in the specified column * * @param context * @param uri the uri of the downloaded file * @param column * @return */ private String getValueForDownloadedFile(Context context, Uri uri, String column) { Cursor cursor = null; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, null, null, null); if (cursor != null && cursor.moveToFirst()) { return cursor.getString(0); } } finally { if (cursor != null) { cursor.close(); } } return null; } /** * Returns the audio session ID * * @return The current media player audio session ID */ public int getAudioSessionId() { synchronized (this) { return mPlayer.getAudioSessionId(); } } /** * Indicates if the media storeage device has been mounted or not * * @return 1 if Intent.ACTION_MEDIA_MOUNTED is called, 0 otherwise */ public int getMediaMountedCount() { return mMediaMountedCount; } /** * Returns the shuffle mode * * @return The current shuffle mode (all, party, none) */ public int getShuffleMode() { return mShuffleMode; } /** * Returns the repeat mode * * @return The current repeat mode (all, one, none) */ public int getRepeatMode() { return mRepeatMode; } /** * Removes all instances of the track with the given ID from the playlist. * * @param id The id to be removed * @return how many instances of the track were removed */ public int removeTrack(final long id) { int numremoved = 0; synchronized (this) { for (int i = 0; i < mPlaylist.size(); i++) { if (mPlaylist.get(i).mId == id) { numremoved += removeTracksInternal(i, i); i--; } } } if (numremoved > 0) { notifyChange(MusicServiceConstants.QUEUE_CHANGED); } return numremoved; } /** * Removes a song from the playlist at the specified position. * * @param id The song id to be removed * @param position The position of the song in the playlist * @return true if successful */ public boolean removeTrackAtPosition(final long id, final int position) { synchronized (this) { if (position >= 0 && position < mPlaylist.size() && mPlaylist.get(position).mId == id) { return removeTracks(position, position) > 0; } } return false; } /** * Removes the range of tracks specified from the play list. If a file * within the range is the file currently being played, playback will move * to the next file after the range. * * @param first The first file to be removed * @param last The last file to be removed * @return the number of tracks deleted */ public int removeTracks(final int first, final int last) { final int numremoved = removeTracksInternal(first, last); if (numremoved > 0) { notifyChange(MusicServiceConstants.QUEUE_CHANGED); } return numremoved; } /** * Returns the position in the queue * * @return the current position in the queue */ public int getQueuePosition() { synchronized (this) { return mPlayPos; } } /** * @return the size of the queue history cache */ public int getQueueHistorySize() { synchronized (this) { return mHistory.size(); } } /** * @return the position in the history */ public int getQueueHistoryPosition(int position) { synchronized (this) { if (position >= 0 && position < mHistory.size()) { return mHistory.get(position); } } return -1; } /** * @return the queue of history positions */ public int[] getQueueHistoryList() { synchronized (this) { int[] history = new int[mHistory.size()]; for (int i = 0; i < mHistory.size(); i++) { history[i] = mHistory.get(i); } return history; } } /** * Returns the path to current song * * @return The path to the current song */ public String getPath() { synchronized (this) { if (mCursor == null) { return null; } return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)); } } /** * Returns the album name * * @return The current song album Name */ public String getAlbumName() { synchronized (this) { if (mCursor == null) { return null; } return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)); } } /** * Returns the song name * * @return The current song name */ public String getTrackName() { synchronized (this) { if (mCursor == null) { return null; } return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)); } } /** * Returns the genre name of song * * @return The current song genre name */ public String getGenreName() { synchronized (this) { if (mCursor == null || mPlayPos < 0 || mPlayPos >= mPlaylist.size()) { return null; } String[] genreProjection = { MediaStore.Audio.Genres.NAME }; Uri genreUri = MediaStore.Audio.Genres.getContentUriForAudioId("external", (int) mPlaylist.get(mPlayPos).mId); Cursor genreCursor = getContentResolver().query(genreUri, genreProjection, null, null, null); if (genreCursor != null) { try { if (genreCursor.moveToFirst()) { return genreCursor .getString(genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)); } } finally { genreCursor.close(); } } return null; } } /** * Returns the artist name * * @return The current song artist name */ public String getArtistName() { synchronized (this) { if (mCursor == null) { return null; } return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)); } } /** * Returns the artist name * * @return The current song artist name */ public String getAlbumArtistName() { synchronized (this) { if (mAlbumCursor == null) { return null; } return mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.AlbumColumns.ARTIST)); } } /** * Returns the album ID * * @return The current song album ID */ public long getAlbumId() { synchronized (this) { if (mCursor == null) { return -1; } return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)); } } /** * Returns the artist ID * * @return The current song artist ID */ public long getArtistId() { synchronized (this) { if (mCursor == null) { return -1; } return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST_ID)); } } /** * @return The audio id of the track */ public long getAudioId() { MusicPlaybackTrack track = getCurrentTrack(); if (track != null) { return track.mId; } return -1; } /** * Gets the currently playing music track */ public MusicPlaybackTrack getCurrentTrack() { return getTrack(mPlayPos); } /** * Gets the music track from the queue at the specified index * * @param index position * @return music track or null */ public synchronized MusicPlaybackTrack getTrack(int index) { if (index >= 0 && index < mPlaylist.size() && mPlayer.isInitialized()) { return mPlaylist.get(index); } return null; } /** * Returns the next audio ID * * @return The next track ID */ public long getNextAudioId() { synchronized (this) { if (mNextPlayPos >= 0 && mNextPlayPos < mPlaylist.size() && mPlayer.isInitialized()) { return mPlaylist.get(mNextPlayPos).mId; } } return -1; } /** * Returns the previous audio ID * * @return The previous track ID */ public long getPreviousAudioId() { synchronized (this) { if (mPlayer.isInitialized()) { int pos = getPreviousPlayPosition(false); if (pos >= 0 && pos < mPlaylist.size()) { return mPlaylist.get(pos).mId; } } } return -1; } /** * Seeks the current track to a specific time * * @param position The time to seek to * @return The time to play the track at */ public long seek(long position) { if (mPlayer.isInitialized()) { if (position < 0) { position = 0; } else if (position > mPlayer.duration()) { position = mPlayer.duration(); } long result = mPlayer.seek(position); notifyChange(MusicServiceConstants.POSITION_CHANGED); return result; } return -1; } /** * Seeks the current track to a position relative to its current position * If the relative position is after or before the track, it will also automatically * jump to the previous or next track respectively * * @param deltaInMs The delta time to seek to in milliseconds */ public void seekRelative(long deltaInMs) { synchronized (this) { if (mPlayer.isInitialized()) { final long newPos = position() + deltaInMs; final long duration = duration(); if (newPos < 0) { prev(true); // seek to the new duration + the leftover position seek(duration() + newPos); } else if (newPos >= duration) { gotoNext(true); // seek to the leftover duration seek(newPos - duration); } else { seek(newPos); } } } } /** * Returns the current position in time of the currenttrack * * @return The current playback position in miliseconds */ public long position() { if (mPlayer.isInitialized()) { return mPlayer.position(); } return -1; } /** * Returns the full duration of the current track * * @return The duration of the current track in miliseconds */ public long duration() { if (mPlayer.isInitialized()) { return mPlayer.duration(); } return -1; } /** * Returns the queue * * @return The queue as a long[] */ public long[] getQueue() { synchronized (this) { final int len = mPlaylist.size(); final long[] list = new long[len]; for (int i = 0; i < len; i++) { list[i] = mPlaylist.get(i).mId; } return list; } } /** * Gets the track id at a given position in the queue * * @param position * @return track id in the queue position */ public long getQueueItemAtPosition(int position) { synchronized (this) { if (position >= 0 && position < mPlaylist.size()) { return mPlaylist.get(position).mId; } } return -1; } /** * @return the size of the queue */ public int getQueueSize() { synchronized (this) { return mPlaylist.size(); } } /** * @return True if music is playing, false otherwise */ public boolean isPlaying() { return mIsSupposedToBePlaying; } /** * Helper function to wrap the logic around mIsSupposedToBePlaying for consistentcy * * @param value to set mIsSupposedToBePlaying to * @param notify whether we want to fire PLAYSTATE_CHANGED event */ private void setIsSupposedToBePlaying(boolean value, boolean notify) { if (mIsSupposedToBePlaying != value) { mIsSupposedToBePlaying = value; // Update mLastPlayed time first and notify afterwards, as // the notification listener method needs the up-to-date value // for the recentlyPlayed() method to work if (!mIsSupposedToBePlaying) { scheduleDelayedShutdown(); mLastPlayedTime = System.currentTimeMillis(); } if (notify) { notifyChange(MusicServiceConstants.PLAYSTATE_CHANGED); } } } /** * @return true if is playing or has played within the last IDLE_DELAY time */ private boolean recentlyPlayed() { return isPlaying() || System.currentTimeMillis() - mLastPlayedTime < MusicServiceConstants.IDLE_DELAY; } /** * Opens a list for playback * * @param list The list of tracks to open * @param position The position to start playback at */ public void open(final long[] list, final int position, long sourceId) { synchronized (this) { if (mShuffleMode == MusicServiceConstants.SHUFFLE_AUTO) { mShuffleMode = MusicServiceConstants.SHUFFLE_NORMAL; } final long oldId = getAudioId(); final int listlength = list.length; boolean newlist = true; if (mPlaylist.size() == listlength) { newlist = false; for (int i = 0; i < listlength; i++) { if (list[i] != mPlaylist.get(i).mId) { newlist = true; break; } } } if (newlist) { addToPlayList(list, -1, sourceId); notifyChange(MusicServiceConstants.QUEUE_CHANGED); } if (position >= 0) { mPlayPos = position; } else { mPlayPos = mShuffler.nextInt(mPlaylist.size()); } mHistory.clear(); openCurrentAndNext(); if (oldId != getAudioId()) { notifyChange(META_CHANGED); } LogUtils.i(TAG, "open finished"); } } /** * Stops playback. */ public void stop() { stopShakeDetector(false); stop(true); } /** * Resumes or starts playback. */ public void play() { startShakeDetector(); play(true); } /** * Resumes or starts playback. * * @param createNewNextTrack True if you want to figure out the next track, false * if you want to re-use the existing next track (used for going back) */ public void play(boolean createNewNextTrack) { int status = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); LogUtils.d(TAG, "Starting playback: audio focus request status = " + status); if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { return; } final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(intent); mSession.setActive(true); if (createNewNextTrack) { setNextTrack(); } else { setNextTrack(mNextPlayPos); } if (mPlayer.isInitialized()) { final long duration = mPlayer.duration(); if (mRepeatMode != MusicServiceConstants.REPEAT_CURRENT && duration > 2000 && mPlayer.position() >= duration - 2000) { gotoNext(true); } mPlayer.start(); mPlayerHandler.removeMessages(MusicServiceConstants.FADEDOWN); mPlayerHandler.sendEmptyMessage(MusicServiceConstants.FADEUP); setIsSupposedToBePlaying(true, true); cancelShutdown(); updateNotification(); } else if (mPlaylist.size() <= 0) { setShuffleMode(MusicServiceConstants.SHUFFLE_AUTO); } } private void togglePlayPause() { if (isPlaying()) { pause(); mPausedByTransientLossOfFocus = false; } else { play(); } } /** * Temporarily pauses playback. */ public void pause() { if (mPlayerHandler == null) return; LogUtils.d(TAG, "Pausing playback"); synchronized (this) { if (mPlayerHandler != null) { mPlayerHandler.removeMessages(MusicServiceConstants.FADEUP); } if (mIsSupposedToBePlaying) { final Intent intent = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(intent); if (mPlayer != null) { mPlayer.pause(); } setIsSupposedToBePlaying(false, true); stopShakeDetector(false); } } } /** * Changes from the current track to the next track */ public void gotoNext(final boolean force) { LogUtils.d(TAG, "Going to next track"); synchronized (this) { if (mPlaylist.size() <= 0) { LogUtils.d(TAG, "No play queue"); scheduleDelayedShutdown(); return; } int pos = mNextPlayPos; if (pos < 0) { pos = getNextPosition(force); } if (pos < 0) { setIsSupposedToBePlaying(false, true); return; } stop(false); setAndRecordPlayPos(pos); openCurrentAndNext(); play(); notifyChange(META_CHANGED); } } public void setAndRecordPlayPos(int nextPos) { synchronized (this) { // save to the history if (mShuffleMode != MusicServiceConstants.SHUFFLE_NONE) { mHistory.add(mPlayPos); if (mHistory.size() > MusicServiceConstants.MAX_HISTORY_SIZE) { mHistory.remove(0); } } mPlayPos = nextPos; } } /** * Changes from the current track to the previous played track */ public void prev(boolean forcePrevious) { synchronized (this) { // if we aren't repeating 1, and we are either early in the song // or we want to force go back, then go to the prevous track boolean goPrevious = getRepeatMode() != MusicServiceConstants.REPEAT_CURRENT && (position() < MusicServiceConstants.REWIND_INSTEAD_PREVIOUS_THRESHOLD || forcePrevious); if (goPrevious) { LogUtils.d(TAG, "Going to previous track"); int pos = getPreviousPlayPosition(true); // if we have no more previous tracks, quit if (pos < 0) { return; } mNextPlayPos = mPlayPos; mPlayPos = pos; stop(false); openCurrent(); play(false); notifyChange(META_CHANGED); } else { LogUtils.d(TAG, "Going to beginning of track"); seek(0); play(false); } } } public int getPreviousPlayPosition(boolean removeFromHistory) { synchronized (this) { if (mShuffleMode == MusicServiceConstants.SHUFFLE_NORMAL) { // Go to previously-played track and remove it from the history final int histsize = mHistory.size(); if (histsize == 0) { return -1; } final Integer pos = mHistory.get(histsize - 1); if (removeFromHistory) { mHistory.remove(histsize - 1); } return pos.intValue(); } else { if (mPlayPos > 0) { return mPlayPos - 1; } else { return mPlaylist.size() - 1; } } } } /** * We don't want to open the current and next track when the user is using * the {@code #prev()} method because they won't be able to travel back to * the previously listened track if they're shuffling. */ private void openCurrent() { openCurrentAndMaybeNext(false); } /** * Moves an item in the queue from one position to another * * @param index1 The position the item is currently at * @param index2 The position the item is being moved to */ public void moveQueueItem(int index1, int index2) { synchronized (this) { if (index1 >= mPlaylist.size()) { index1 = mPlaylist.size() - 1; } if (index2 >= mPlaylist.size()) { index2 = mPlaylist.size() - 1; } if (index1 == index2) { return; } final MusicPlaybackTrack track = mPlaylist.remove(index1); if (index1 < index2) { mPlaylist.add(index2, track); if (mPlayPos == index1) { mPlayPos = index2; } else if (mPlayPos >= index1 && mPlayPos <= index2) { mPlayPos--; } } else if (index2 < index1) { mPlaylist.add(index2, track); if (mPlayPos == index1) { mPlayPos = index2; } else if (mPlayPos >= index2 && mPlayPos <= index1) { mPlayPos++; } } notifyChange(MusicServiceConstants.QUEUE_CHANGED); } } /** * Sets the repeat mode * * @param repeatmode The repeat mode to use */ public void setRepeatMode(final int repeatmode) { synchronized (this) { mRepeatMode = repeatmode; setNextTrack(); saveQueue(false); notifyChange(MusicServiceConstants.REPEATMODE_CHANGED); } } /** * Sets the shuffle mode * * @param shufflemode The shuffle mode to use */ public void setShuffleMode(final int shufflemode) { synchronized (this) { if (mShuffleMode == shufflemode && mPlaylist.size() > 0) { return; } mShuffleMode = shufflemode; if (mShuffleMode == MusicServiceConstants.SHUFFLE_AUTO) { if (makeAutoShuffleList()) { mPlaylist.clear(); doAutoShuffleUpdate(); mPlayPos = 0; openCurrentAndNext(); play(); notifyChange(META_CHANGED); return; } else { mShuffleMode = MusicServiceConstants.SHUFFLE_NONE; } } else { setNextTrack(); } saveQueue(false); notifyChange(MusicServiceConstants.SHUFFLEMODE_CHANGED); } } /** * Sets the position of a track in the queue * * @param index The position to place the track */ public void setQueuePosition(final int index) { synchronized (this) { stop(false); mPlayPos = index; openCurrentAndNext(); play(); notifyChange(META_CHANGED); if (mShuffleMode == MusicServiceConstants.SHUFFLE_AUTO) { doAutoShuffleUpdate(); } } } /** * Queues a new list for playback * * @param list The list to queue * @param action The action to take */ public void enqueue(final long[] list, final int action, long sourceId) { synchronized (this) { if (action == MusicServiceConstants.NEXT && mPlayPos + 1 < mPlaylist.size()) { addToPlayList(list, mPlayPos + 1, sourceId); mNextPlayPos = mPlayPos + 1; notifyChange(MusicServiceConstants.QUEUE_CHANGED); } else { addToPlayList(list, Integer.MAX_VALUE, sourceId); notifyChange(MusicServiceConstants.QUEUE_CHANGED); } if (mPlayPos < 0) { mPlayPos = 0; openCurrentAndNext(); play(); notifyChange(META_CHANGED); } } } /** * Cycles through the different repeat modes */ private void cycleRepeat() { if (mRepeatMode == MusicServiceConstants.REPEAT_NONE) { setRepeatMode(MusicServiceConstants.REPEAT_ALL); } else if (mRepeatMode == MusicServiceConstants.REPEAT_ALL) { setRepeatMode(MusicServiceConstants.REPEAT_CURRENT); if (mShuffleMode != MusicServiceConstants.SHUFFLE_NONE) { setShuffleMode(MusicServiceConstants.SHUFFLE_NONE); } } else { setRepeatMode(MusicServiceConstants.REPEAT_NONE); } } /** * Cycles through the different shuffle modes */ private void cycleShuffle() { if (mShuffleMode == MusicServiceConstants.SHUFFLE_NONE) { setShuffleMode(MusicServiceConstants.SHUFFLE_NORMAL); if (mRepeatMode == MusicServiceConstants.REPEAT_CURRENT) { setRepeatMode(MusicServiceConstants.REPEAT_ALL); } } else if (mShuffleMode == MusicServiceConstants.SHUFFLE_NORMAL || mShuffleMode == MusicServiceConstants.SHUFFLE_AUTO) { setShuffleMode(MusicServiceConstants.SHUFFLE_NONE); } } /** * Called when one of the lists should refresh or requery. */ public void refresh() { notifyChange(MusicServiceConstants.REFRESH); } /** * Called when one of the playlists have changed (renamed, added/removed tracks) */ public void playlistChanged() { notifyChange(MusicServiceConstants.PLAYLIST_CHANGED); } /** * Called to set the status of shake to play feature */ public void setShakeToPlayEnabled(boolean enabled) { LogUtils.d(TAG, "ShakeToPlay status: " + enabled); if (enabled) { if (mShakeDetector == null) { mShakeDetector = new ShakeDetector(mShakeDetectorListener); } // if song is already playing, start listening immediately if (isPlaying()) { startShakeDetector(); } } else { stopShakeDetector(true); } } /** * Called to set visibility of album art on lockscreen */ public void setLockscreenAlbumArt(boolean enabled) { mShowAlbumArtOnLockscreen = enabled; notifyChange(META_CHANGED); } /** * Called to start listening to shakes */ private void startShakeDetector() { if (mShakeDetector != null) { mShakeDetector.start((SensorManager) getSystemService(SENSOR_SERVICE)); } } /** * Called to stop listening to shakes */ private void stopShakeDetector(final boolean destroyShakeDetector) { if (mShakeDetector != null) { mShakeDetector.stop(); } if (destroyShakeDetector) { mShakeDetector = null; LogUtils.d(TAG, "ShakeToPlay destroyed!!!"); } } private final AudioManager.OnAudioFocusChangeListener mAudioFocusListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(final int focusChange) { mPlayerHandler.obtainMessage(MusicServiceConstants.FOCUSCHANGE, focusChange, 0).sendToTarget(); } }; private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { /** * {@inheritDoc} */ @Override public void onReceive(final Context context, final Intent intent) { final String command = intent.getStringExtra(MusicServiceConstants.CMDNAME); /* if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) { final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); mAppWidgetSmall.performUpdate(MusicService.this, small); } else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) { final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); mAppWidgetLarge.performUpdate(MusicService.this, large); } else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) { final int[] largeAlt = intent .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); mAppWidgetLargeAlternate.performUpdate(MusicService.this, largeAlt); } else {*/ handleCommandIntent(intent); // } } }; private static class MusicPlayerHandler extends Handler { private static final String TAG = MusicPlayerHandler.class.getSimpleName(); private final WeakReference<MusicService> mService; private float mCurrentVolume = 1.0f; private int mHeadsetHookClickCounter = 0; /** * Constructor of <code>MusicPlayerHandler</code> * * @param service The service to use. * @param looper The thread to run on. */ public MusicPlayerHandler(final MusicService service, final Looper looper) { super(looper); mService = new WeakReference<MusicService>(service); } /** * {@inheritDoc} */ @Override public void handleMessage(final Message msg) { final MusicService service = mService.get(); if (service == null) { return; } synchronized (service) { switch (msg.what) { case MusicServiceConstants.FADEDOWN: mCurrentVolume -= .05f; if (mCurrentVolume > .2f) { sendEmptyMessageDelayed(MusicServiceConstants.FADEDOWN, 10); } else { mCurrentVolume = .2f; } service.mPlayer.setVolume(mCurrentVolume); break; case MusicServiceConstants.FADEUP: mCurrentVolume += .01f; if (mCurrentVolume < 1.0f) { sendEmptyMessageDelayed(MusicServiceConstants.FADEUP, 10); } else { mCurrentVolume = 1.0f; } service.mPlayer.setVolume(mCurrentVolume); break; case MusicServiceConstants.SERVER_DIED: if (service.isPlaying()) { final TrackErrorInfo info = (TrackErrorInfo) msg.obj; service.sendErrorMessage(info.getTrackName()); // since the service isPlaying(), we only need to remove the offending // audio track, and the code will automatically play the next track service.removeTrack(info.getId()); } else { service.openCurrentAndNext(); } break; case MusicServiceConstants.TRACK_WENT_TO_NEXT: service.setAndRecordPlayPos(service.mNextPlayPos); service.setNextTrack(); if (service.mCursor != null) { service.mCursor.close(); service.mCursor = null; } service.updateCursor(service.mPlaylist.get(service.mPlayPos).mId); service.notifyChange(META_CHANGED); service.updateNotification(); break; case MusicServiceConstants.TRACK_ENDED: if (service.mRepeatMode == MusicServiceConstants.REPEAT_CURRENT) { service.seek(0); service.play(); } else { service.gotoNext(false); } break; case MusicServiceConstants.LYRICS: service.mLyrics = (String) msg.obj; service.notifyChange(NEW_LYRICS); break; case MusicServiceConstants.FOCUSCHANGE: LogUtils.i(TAG, "Received audio focus change event " + msg.arg1); switch (msg.arg1) { case AudioManager.AUDIOFOCUS_LOSS: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: if (service.isPlaying()) { service.mPausedByTransientLossOfFocus = msg.arg1 == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT; } service.pause(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: removeMessages(MusicServiceConstants.FADEUP); sendEmptyMessage(MusicServiceConstants.FADEDOWN); break; case AudioManager.AUDIOFOCUS_GAIN: if (!service.isPlaying() && service.mPausedByTransientLossOfFocus) { service.mPausedByTransientLossOfFocus = false; mCurrentVolume = 0f; service.mPlayer.setVolume(mCurrentVolume); service.play(); } else { removeMessages(MusicServiceConstants.FADEDOWN); sendEmptyMessage(MusicServiceConstants.FADEUP); } break; default: } break; case MusicServiceConstants.HEADSET_HOOK_EVENT: { long eventTime = (Long) msg.obj; mHeadsetHookClickCounter = Math.min(mHeadsetHookClickCounter + 1, 3); LogUtils.i(TAG, "Got headset click, count = " + mHeadsetHookClickCounter); removeMessages(MusicServiceConstants.HEADSET_HOOK_MULTI_CLICK_TIMEOUT); if (mHeadsetHookClickCounter == 3) { sendEmptyMessage(MusicServiceConstants.HEADSET_HOOK_MULTI_CLICK_TIMEOUT); } else { sendEmptyMessageAtTime(MusicServiceConstants.HEADSET_HOOK_MULTI_CLICK_TIMEOUT, eventTime + MusicServiceConstants.DOUBLE_CLICK_TIMEOUT); } break; } case MusicServiceConstants.HEADSET_HOOK_MULTI_CLICK_TIMEOUT: LogUtils.i(TAG, "Handling headset click"); switch (mHeadsetHookClickCounter) { case 1: service.togglePlayPause(); break; case 2: service.gotoNext(true); break; case 3: service.prev(false); break; } mHeadsetHookClickCounter = 0; service.mHeadsetHookWakeLock.release(); break; default: break; } } } } private static final class ServiceStub extends IMusicService.Stub { private WeakReference<MusicService> mService; public ServiceStub(MusicService musicService) { mService = new WeakReference<>(musicService); } @Override public void openFile(final String path) throws RemoteException { mService.get().openFile(path); } @Override public void open(final long[] list, final int position, long sourceId) throws RemoteException { mService.get().open(list, position, sourceId); } @Override public void stop() throws RemoteException { mService.get().stop(); } @Override public void pause() throws RemoteException { mService.get().pause(); } @Override public void play() throws RemoteException { mService.get().play(); } @Override public void prev(boolean forcePrevious) throws RemoteException { mService.get().prev(forcePrevious); } @Override public void next() throws RemoteException { mService.get().gotoNext(true); } @Override public void enqueue(final long[] list, final int action, long sourceId) throws RemoteException { mService.get().enqueue(list, action, sourceId); } @Override public void setQueuePosition(final int index) throws RemoteException { mService.get().setQueuePosition(index); } @Override public void setShuffleMode(final int shufflemode) throws RemoteException { mService.get().setShuffleMode(shufflemode); } @Override public void setRepeatMode(final int repeatmode) throws RemoteException { mService.get().setRepeatMode(repeatmode); } @Override public void moveQueueItem(final int from, final int to) throws RemoteException { mService.get().moveQueueItem(from, to); } @Override public void refresh() throws RemoteException { mService.get().refresh(); } @Override public void playlistChanged() throws RemoteException { mService.get().playlistChanged(); } @Override public boolean isPlaying() throws RemoteException { return mService.get().isPlaying(); } @Override public long[] getQueue() throws RemoteException { return mService.get().getQueue(); } @Override public long getQueueItemAtPosition(int position) throws RemoteException { return mService.get().getQueueItemAtPosition(position); } @Override public int getQueueSize() throws RemoteException { return mService.get().getQueueSize(); } @Override public int getQueueHistoryPosition(int position) throws RemoteException { return mService.get().getQueueHistoryPosition(position); } @Override public int getQueueHistorySize() throws RemoteException { return mService.get().getQueueHistorySize(); } @Override public int[] getQueueHistoryList() throws RemoteException { return mService.get().getQueueHistoryList(); } @Override public long duration() throws RemoteException { return mService.get().duration(); } @Override public long position() throws RemoteException { return mService.get().position(); } @Override public long seek(final long position) throws RemoteException { return mService.get().seek(position); } @Override public void seekRelative(final long deltaInMs) throws RemoteException { mService.get().seekRelative(deltaInMs); } @Override public long getAudioId() throws RemoteException { return mService.get().getAudioId(); } @Override public MusicPlaybackTrack getCurrentTrack() throws RemoteException { return mService.get().getCurrentTrack(); } @Override public MusicPlaybackTrack getTrack(int index) throws RemoteException { return mService.get().getTrack(index); } @Override public long getNextAudioId() throws RemoteException { return mService.get().getNextAudioId(); } @Override public long getPreviousAudioId() throws RemoteException { return mService.get().getPreviousAudioId(); } @Override public long getArtistId() throws RemoteException { return mService.get().getArtistId(); } @Override public long getAlbumId() throws RemoteException { return mService.get().getAlbumId(); } @Override public String getArtistName() throws RemoteException { return mService.get().getArtistName(); } @Override public String getTrackName() throws RemoteException { return mService.get().getTrackName(); } @Override public String getAlbumName() throws RemoteException { return mService.get().getAlbumName(); } @Override public String getPath() throws RemoteException { return mService.get().getPath(); } @Override public int getQueuePosition() throws RemoteException { return mService.get().getQueuePosition(); } @Override public int getShuffleMode() throws RemoteException { return mService.get().getShuffleMode(); } @Override public int getRepeatMode() throws RemoteException { return mService.get().getRepeatMode(); } @Override public int removeTracks(final int first, final int last) throws RemoteException { return mService.get().removeTracks(first, last); } @Override public int removeTrack(final long id) throws RemoteException { return mService.get().removeTrack(id); } @Override public boolean removeTrackAtPosition(final long id, final int position) throws RemoteException { return mService.get().removeTrackAtPosition(id, position); } @Override public int getMediaMountedCount() throws RemoteException { return mService.get().getMediaMountedCount(); } @Override public int getAudioSessionId() throws RemoteException { return mService.get().getAudioSessionId(); } @Override public void setShakeToPlayEnabled(boolean enabled) { mService.get().setShakeToPlayEnabled(enabled); } @Override public void setLockscreenAlbumArt(boolean enabled) { mService.get().setLockscreenAlbumArt(enabled); } } }