Java tutorial
/* * YAMMP - Yet Another Multi Media Player for android * Copyright (C) 2011-2012 Mariotaku Lee <mariotaku.lee@gmail.com> * * This file is part of YAMMP. * * YAMMP is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * YAMMP is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with YAMMP. If not, see <http://www.gnu.org/licenses/>. */ package org.yammp; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.Random; import java.util.Vector; import org.yammp.util.EqualizerWrapper; import org.yammp.util.LyricsParser; import org.yammp.util.MediaUtils; import org.yammp.util.PreferencesEditor; import org.yammp.util.ShakeListener; import org.yammp.util.ShakeListener.OnShakeListener; 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.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.media.AudioManager; import android.media.AudioManager.OnAudioFocusChangeListener; import android.media.MediaPlayer; import android.net.Uri; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.provider.MediaStore; import android.support.v4.app.NotificationCompat; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.text.format.DateUtils; import android.util.Log; import android.widget.Toast; /** * Provides "background" audio playback capabilities, allowing the user to * switch between activities without stopping playback. */ public class MusicPlaybackService extends Service implements Constants, OnShakeListener { /** * used to specify whether enqueue() should start playing the new list of * files right away, next or once all the currently queued files have been * played */ private static final int TRACK_ENDED = 1; private static final int RELEASE_WAKELOCK = 2; private static final int SERVER_DIED = 3; private static final int FOCUSCHANGE = 4; private static final int FADEDOWN = 5; private static final int FADEUP = 6; private static final int NEW_LYRICS_LOADED = 1; private static final int POSITION_CHANGED = 2; private static final int LYRICS_REFRESHED = 3; private static final int LYRICS_PAUSED = 4; private static final int LYRICS_RESUMED = 5; private static final int START_SLEEP_TIMER = 1; private static final int STOP_SLEEP_TIMER = 2; private MultiPlayer mPlayer; private String mFileToPlay; private NotificationManager mNotificationManager; private int mShuffleMode = SHUFFLE_NONE; private int mRepeatMode = REPEAT_NONE; private int mMediaMountedCount = 0; private long[] mPlayList = null; private int mPlayListLen = 0; private Vector<Integer> mHistory = new Vector<Integer>(); private Cursor mCursor; private int mPlayPos = -1; private Shuffler mShuffler = null; private int mOpenFailedCounter = 0; private String[] mCursorCols = new String[] { "audio._id AS _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.ARTIST_ID, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.IS_PODCAST, MediaStore.Audio.Media.BOOKMARK }; private final static int IDCOLIDX = 0; private final static int PODCASTCOLIDX = 8; private final static int BOOKMARKCOLIDX = 9; private BroadcastReceiver mUnmountReceiver = null; private BroadcastReceiver mA2dpReceiver = null; private WakeLock mWakeLock; private int mServiceStartId = -1; private boolean mServiceInUse = false; private boolean mIsSupposedToBePlaying = false; private boolean mQuietMode = false; private AudioManager mAudioManager; private boolean mQueueIsSaveable = true; // used to track what type of audio focus loss caused the playback to pause private boolean mPausedByTransientLossOfFocus = false; // used to track current volume private float mCurrentVolume = 1.0f; private PreferencesEditor mPrefs; // We use this to distinguish between different cards when saving/restoring // playlists. // This will have to change if we want to support multiple simultaneous // cards. private int mCardId; // interval after which we stop the service when idle private static final int IDLE_DELAY = 60000; private boolean mGentleSleepTimer, mSleepTimerTimedUp; private long mCurrentTimestamp, mStopTimestamp; private EqualizerWrapper mEqualizer = null; private boolean mEqualizerSupported = EqualizerWrapper.isSupported(); private LyricsParser mLyricsParser = new LyricsParser(); private int mLyricsStatus = 0; private int mLyricsId = -1; private String[] mLyrics = new String[] {}; private ShakeListener mShakeDetector; private boolean mShakeEnabled; private String mShakingBehavior; private float mShakingThreshold = DEFAULT_SHAKING_THRESHOLD; private boolean mScrobbleEnabled = false; private boolean mExternalAudioDeviceConnected = false; private MediaUtils mUtils; private YAMMPApplication mApplication; private Intent mPlaybackIntent; private Handler mMediaplayerHandler = new Handler() { @Override public void handleMessage(Message msg) { mUtils.debugLog("mMediaplayerHandler.handleMessage " + msg.what); switch (msg.what) { case FADEDOWN: mCurrentVolume -= 0.05f; if (mCurrentVolume > 0.2f) { mMediaplayerHandler.sendEmptyMessageDelayed(FADEDOWN, 10); } else { mCurrentVolume = 0.2f; } mPlayer.setVolume(mCurrentVolume); break; case FADEUP: mCurrentVolume += 0.01f; if (mCurrentVolume < 1.0f) { mMediaplayerHandler.sendEmptyMessageDelayed(FADEUP, 10); } else { mCurrentVolume = 1.0f; } mPlayer.setVolume(mCurrentVolume); break; case SERVER_DIED: if (mIsSupposedToBePlaying) { next(true); } else { // the server died when we were idle, so just // reopen the same song (it will start again // from the beginning though when the user // restarts) openCurrent(); } break; case TRACK_ENDED: if (mRepeatMode == REPEAT_CURRENT) { seek(0); play(); } else { next(false); } break; case RELEASE_WAKELOCK: mWakeLock.release(); break; case FOCUSCHANGE: // This code is here so we can better synchronize it with // the code that handles fade-in switch (msg.arg1) { case AudioManager.AUDIOFOCUS_LOSS: Log.v(LOGTAG_SERVICE, "AudioFocus: received AUDIOFOCUS_LOSS"); if (isPlaying()) { mPausedByTransientLossOfFocus = false; } pause(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: mMediaplayerHandler.removeMessages(FADEUP); mMediaplayerHandler.sendEmptyMessage(FADEDOWN); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: Log.v(LOGTAG_SERVICE, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT"); if (isPlaying()) { mPlayer.setVolume((float) Math.pow(10.0, -8 / 20.0)); } break; case AudioManager.AUDIOFOCUS_GAIN: Log.v(LOGTAG_SERVICE, "AudioFocus: received AUDIOFOCUS_GAIN"); if (isPlaying() || mPausedByTransientLossOfFocus) { mPausedByTransientLossOfFocus = false; mCurrentVolume = 0f; mPlayer.setVolume(mCurrentVolume); play(); // also queues a fade-in } else { mMediaplayerHandler.removeMessages(FADEDOWN); mMediaplayerHandler.sendEmptyMessage(FADEUP); } break; default: Log.e(LOGTAG_SERVICE, "Unknown audio focus change code"); } break; default: break; } } }; /** * Lyrics Handler * * load lyrics automatically, most accurate and fastest ever.<br> * Usage: send a empty message {@link #NEW_LYRICS_LOADED} when new song * played. * * @author mariotaku */ private Handler mLyricsHandler = new Handler() { private List<String> mLyricsList = new ArrayList<String>(); private int lyrics_id = 0; @Override public void handleMessage(Message msg) { mLyricsHandler.removeMessages(LYRICS_REFRESHED); switch (msg.what) { case NEW_LYRICS_LOADED: mLyricsId = -1; lyrics_id = 0; if (getMediaPath() == null) return; String media_path = getMediaPath(); String lyrics_path = media_path.substring(0, media_path.lastIndexOf(".")) + ".lrc"; mLyricsStatus = mLyricsParser.parseLyrics(lyrics_path); mLyricsList = mLyricsParser.getAllLyrics(); mLyrics = mLyricsList.toArray(new String[mLyricsList.size()]); notifyLyricsChange(BROADCAST_NEW_LYRICS_LOADED); if (mLyricsStatus == LYRICS_STATUS_OK && isPlaying()) { if (mLyricsParser.getTimestamp(lyrics_id) <= position()) { mLyricsHandler.sendEmptyMessage(LYRICS_REFRESHED); } else { mLyricsHandler.sendEmptyMessageDelayed(LYRICS_REFRESHED, mLyricsParser.getTimestamp(lyrics_id) - position()); } } break; case LYRICS_REFRESHED: if (mLyricsParser == null || mLyricsStatus != LYRICS_STATUS_OK) return; if (lyrics_id < mLyricsParser.getAllLyrics().size()) { mLyricsId = lyrics_id; lyrics_id++; notifyLyricsChange(BROADCAST_LYRICS_REFRESHED); if (isPlaying()) { mLyricsHandler.sendEmptyMessageDelayed(LYRICS_REFRESHED, mLyricsParser.getTimestamp(lyrics_id) - position()); } } break; case POSITION_CHANGED: if (mLyricsParser == null || mLyricsStatus != LYRICS_STATUS_OK) return; lyrics_id = mLyricsParser.getId(msg.arg1); mLyricsHandler.sendEmptyMessageDelayed(LYRICS_REFRESHED, mLyricsParser.getTimestamp(lyrics_id) - position()); break; case LYRICS_PAUSED: if (mLyricsParser == null || mLyricsStatus != LYRICS_STATUS_OK) return; break; case LYRICS_RESUMED: if (mLyricsParser == null || mLyricsStatus != LYRICS_STATUS_OK) return; if (lyrics_id < mLyricsParser.getAllLyrics().size()) { lyrics_id = mLyricsParser.getId(position()); mLyricsHandler.sendEmptyMessageDelayed(LYRICS_REFRESHED, mLyricsParser.getTimestamp(lyrics_id) - position()); } break; } } }; private BroadcastReceiver mExternalAudioDeviceStatusReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (Intent.ACTION_HEADSET_PLUG.equals(action)) { int state = intent.getIntExtra("state", 0); mExternalAudioDeviceConnected = state == 1; } } }; private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); String cmd = intent.getStringExtra(CMDNAME); mUtils.debugLog("mIntentReceiver.onReceive " + action + " / " + cmd); if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) { next(true); } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) { prev(); } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) { if (isPlaying()) { pause(); mPausedByTransientLossOfFocus = false; } else { play(); } } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) { pause(); mPausedByTransientLossOfFocus = false; } else if (CMDSTOP.equals(cmd)) { pause(); mPausedByTransientLossOfFocus = false; seek(0); } else if (CMDCYCLEREPEAT.equals(cmd) || CYCLEREPEAT_ACTION.equals(action)) { toggleRepeat(); } else if (CMDTOGGLESHUFFLE.equals(cmd) || TOGGLESHUFFLE_ACTION.equals(action)) { toggleShuffle(); } else if (CMDTOGGLEFAVORITE.equals(cmd)) { if (!isFavorite()) { addToFavorites(); } else { removeFromFavorites(); } } } }; private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { mMediaplayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget(); } }; private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { switch (state) { case TelephonyManager.CALL_STATE_RINGING: Log.v(LOGTAG_SERVICE, "PhoneState: received CALL_STATE_RINGING"); if (isPlaying()) { mPausedByTransientLossOfFocus = true; pause(); } break; case TelephonyManager.CALL_STATE_OFFHOOK: Log.v(LOGTAG_SERVICE, "PhoneState: received CALL_STATE_OFFHOOK"); mPausedByTransientLossOfFocus = false; if (isPlaying()) { pause(); } break; } } }; private final char hexdigits[] = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; private Handler mDelayedStopHandler = new Handler() { @Override public void handleMessage(Message msg) { // Check again to make sure nothing is playing right now if (isPlaying() || mPausedByTransientLossOfFocus || mServiceInUse || mMediaplayerHandler.hasMessages(TRACK_ENDED)) return; // save the queue again, because it might have changed // since the user exited the music app (because of // party-shuffle or because the play-position changed) saveQueue(true); stopSelf(mServiceStartId); } }; private Handler mSleepTimerHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case START_SLEEP_TIMER: mSleepTimerHandler.removeMessages(START_SLEEP_TIMER, null); if (mGentleSleepTimer) { if (isPlaying()) { mSleepTimerTimedUp = true; } else { pause(); mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER); } } else { pause(); mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER); } mStopTimestamp = 0; break; case STOP_SLEEP_TIMER: mStopTimestamp = 0; mSleepTimerHandler.removeMessages(START_SLEEP_TIMER, null); mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER); break; } } }; private final IBinder mBinder = new ServiceStub(this); public MusicPlaybackService() { } public void addToFavorites() { if (getAudioId() >= 0) { addToFavorites(getAudioId()); } } public void addToFavorites(long id) { mUtils.addToFavorites(id); notifyChange(BROADCAST_FAVORITESTATE_CHANGED); } /** * Called when we receive a ACTION_MEDIA_EJECT notification. * * @param storagePath * path to mount point for the removed media */ public void closeExternalStorageFiles(String storagePath) { stop(true); notifyChange(BROADCAST_QUEUE_CHANGED); notifyChange(BROADCAST_META_CHANGED); } /** * Returns the duration of the file in milliseconds. Currently this method * returns -1 for the duration of MIDI files. */ public long duration() { if (mPlayer.isInitialized()) return mPlayer.duration(); return -1; } /** * Appends a list of tracks to the current playlist. If nothing is playing * currently, playback will be started at the first track. If the action is * NOW, playback will switch to the first of the new tracks immediately. * * @param list * The list of tracks to append. * @param action * NOW, NEXT or LAST */ public void enqueue(long[] list, int action) { synchronized (this) { if (action == ACTION_NEXT && mPlayPos + 1 < mPlayListLen) { addToPlayList(list, mPlayPos + 1); notifyChange(BROADCAST_QUEUE_CHANGED); } else { // action == LAST || action == NOW || mPlayPos + 1 == // mPlayListLen addToPlayList(list, Integer.MAX_VALUE); notifyChange(BROADCAST_QUEUE_CHANGED); if (action == ACTION_NOW) { mPlayPos = mPlayListLen - list.length; openCurrent(); play(); notifyChange(BROADCAST_META_CHANGED); return; } } if (mPlayPos < 0) { mPlayPos = 0; openCurrent(); play(); notifyChange(BROADCAST_META_CHANGED); } } } public int eqGetBand(int frequency) { if (!mEqualizerSupported || mEqualizer == null) return 0; return mEqualizer.getBand(frequency); } public int[] eqGetBandFreqRange(short band) { if (!mEqualizerSupported || mEqualizer == null) return null; return mEqualizer.getBandFreqRange(band); } public short eqGetBandLevel(short band) { if (mEqualizerSupported && mEqualizer != null) return mEqualizer.getBandLevel(band); return (short) 0; } public short[] eqGetBandLevelRange() { if (!mEqualizerSupported || mEqualizer == null) return null; return mEqualizer.getBandLevelRange(); } public int eqGetCenterFreq(short band) { if (!mEqualizerSupported || mEqualizer == null) return 0; return mEqualizer.getCenterFreq(band); } public short eqGetCurrentPreset() { if (!mEqualizerSupported || mEqualizer == null) return 0; return mEqualizer.getCurrentPreset(); } public short eqGetNumberOfBands() { if (!mEqualizerSupported || mEqualizer == null) return 0; return mEqualizer.getNumberOfBands(); } public short eqGetNumberOfPresets() { if (!mEqualizerSupported || mEqualizer == null) return 0; return mEqualizer.getNumberOfPresets(); } public String eqGetPresetName(short preset) { if (!mEqualizerSupported || mEqualizer == null) return null; return mEqualizer.getPresetName(preset); } public void eqRelease() { if (!mEqualizerSupported || mEqualizer == null) return; mEqualizer.release(); } public void eqReset() { if (!mEqualizerSupported || mEqualizer == null) return; for (short i = 0; i < eqGetNumberOfBands(); i++) { eqSetBandLevel(i, (short) 0); } } public void eqSetBandLevel(short band, short level) { if (!mEqualizerSupported || mEqualizer == null) return; mEqualizer.setBandLevel(band, level); mPrefs.setEqualizerSetting(band, level); } public int eqSetEnabled(boolean enabled) { if (!mEqualizerSupported || mEqualizer == null) return 0; return mEqualizer.setEnabled(enabled); } public void eqUsePreset(short preset) { if (!mEqualizerSupported || mEqualizer == null) return; mEqualizer.usePreset(preset); for (short i = 0; i < eqGetNumberOfBands(); i++) { mPrefs.setEqualizerSetting(i, eqGetBandLevel(i)); } } public Bitmap getAlbumArt() { synchronized (this) { return mUtils.getArtwork(getAudioId(), getAlbumId()); } } public long getAlbumId() { synchronized (this) { if (mCursor == null) return -1; return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)); } } public String getAlbumName() { synchronized (this) { if (mCursor == null) return null; return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)); } } public long getArtistId() { synchronized (this) { if (mCursor == null) return -1; return mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID)); } } public String getArtistName() { synchronized (this) { if (mCursor == null) return null; return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); } } public Uri getArtworkUri() { synchronized (this) { return mUtils.getArtworkUri(getAudioId(), getAlbumId()); } } /** * Returns the rowid of the currently playing file, or -1 if no file is * currently playing. */ public long getAudioId() { synchronized (this) { if (mPlayPos >= 0 && mPlayer.isInitialized()) return mPlayList[mPlayPos]; } return -1; } /** * Returns the audio session ID. */ public int getAudioSessionId() { synchronized (this) { return mPlayer.getAudioSessionId(); } } public int getCurrentLyricsId() { return mLyricsId; } public String[] getLyrics() { return mLyrics; } public int getLyricsStatus() { return mLyricsStatus; } public int getMediaMountedCount() { return mMediaMountedCount; } /** * Returns the absolute path of the currently playing file, or null if no * file is currently playing. */ public String getMediaPath() { synchronized (this) { if (mCursor == null) return null; return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)); } } /** * Returns the path of the currently playing file, or null if no file is * currently playing. */ public String getPath() { return mFileToPlay; } public long getPositionByLyricsId(int id) { synchronized (this) { if (mLyricsParser != null && id < mLyricsParser.getAllTimestamp().size()) return mLyricsParser.getTimestamp(id); } return position(); } /** * Returns the current play list * * @return An array of integers containing the IDs of the tracks in the play * list */ public long[] getQueue() { synchronized (this) { int len = mPlayListLen; long[] list = new long[len]; for (int i = 0; i < len; i++) { list[i] = mPlayList[i]; } return list; } } /** * Returns the position in the queue * * @return the position in the queue */ public int getQueuePosition() { synchronized (this) { return mPlayPos; } } public int getRepeatMode() { return mRepeatMode; } public int getShuffleMode() { return mShuffleMode; } public String getTrackName() { synchronized (this) { if (mCursor == null) return null; return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); } } public boolean isFavorite() { if (getAudioId() >= 0) return isFavorite(getAudioId()); return false; } public boolean isFavorite(long id) { return mUtils.isFavorite(id); } /** * Returns whether something is currently playing * * @return true if something is playing (or will be playing shortly, in case * we're currently transitioning between tracks), false if not. */ public boolean isPlaying() { return mIsSupposedToBePlaying; } /** * Moves the item at index1 to index2. * * @param from * @param to */ public void moveQueueItem(int from, int to) { synchronized (this) { if (from >= mPlayListLen) { from = mPlayListLen - 1; } if (to >= mPlayListLen) { to = mPlayListLen - 1; } if (from < to) { long tmp = mPlayList[from]; for (int i = from; i < to; i++) { mPlayList[i] = mPlayList[i + 1]; } mPlayList[to] = tmp; if (mPlayPos == from) { mPlayPos = to; } else if (mPlayPos >= from && mPlayPos <= to) { mPlayPos--; } } else if (to < from) { long tmp = mPlayList[from]; for (int i = from; i > to; i--) { mPlayList[i] = mPlayList[i - 1]; } mPlayList[to] = tmp; if (mPlayPos == from) { mPlayPos = to; } else if (mPlayPos >= to && mPlayPos <= from) { mPlayPos++; } } notifyChange(BROADCAST_QUEUE_CHANGED); } } public void next(boolean force) { synchronized (this) { if (mSleepTimerTimedUp) { pause(); mNotificationManager.cancel(ID_NOTIFICATION_SLEEPTIMER); mSleepTimerTimedUp = false; return; } if (mPlayListLen <= 0) { Log.d(LOGTAG_SERVICE, "No play queue"); return; } if (mShuffleMode == SHUFFLE_NORMAL) { if (mPlayPos >= 0) { if (!mHistory.contains(mPlayPos)) { mHistory.add(mPlayPos); } } int numTracks = mPlayListLen; int[] tracks = new int[numTracks]; for (int i = 0; i < numTracks; i++) { tracks[i] = i; } int numHistory = mHistory.size(); int numUnplayed = numTracks; for (int i = 0; i < numHistory; i++) { int idx = mHistory.get(i).intValue(); if (idx < numTracks && tracks[idx] >= 0) { numUnplayed--; tracks[idx] = -1; } } // 'numUnplayed' now indicates how many tracks have not yet // been played, and 'tracks' contains the indices of those // tracks. if (numUnplayed <= 0) { // everything's already been played if (mRepeatMode == REPEAT_ALL || force) { // pick from full set numUnplayed = numTracks; for (int i = 0; i < numTracks; i++) { tracks[i] = i; } } else { // all done gotoIdleState(); if (mIsSupposedToBePlaying) { mIsSupposedToBePlaying = false; notifyChange(BROADCAST_PLAYSTATE_CHANGED); } return; } } int skip = mShuffler.shuffle(numUnplayed); int cnt = -1; while (true) { while (tracks[++cnt] < 0) { ; } skip--; if (skip < 0) { break; } } mPlayPos = cnt; } else { if (mPlayPos >= mPlayListLen - 1) { // we're at the end of the list if (mRepeatMode == REPEAT_NONE && !force) { // all done gotoIdleState(); mIsSupposedToBePlaying = false; notifyChange(BROADCAST_PLAYSTATE_CHANGED); return; } else if (mRepeatMode == REPEAT_ALL || force) { mPlayPos = 0; } } else { mPlayPos++; } } saveBookmarkIfNeeded(); stop(false); openCurrent(); play(); notifyChange(BROADCAST_META_CHANGED); } } public void notifyLyricsChange(String action) { Intent i = new Intent(action); if (BROADCAST_LYRICS_REFRESHED.equals(action)) { i.putExtra(BROADCAST_KEY_LYRICS_ID, mLyricsId); } else if (BROADCAST_NEW_LYRICS_LOADED.equals(action)) { i.putExtra(BROADCAST_KEY_LYRICS_STATUS, mLyricsStatus); i.putExtra(BROADCAST_KEY_LYRICS, mLyrics); } else return; sendBroadcast(i); } @Override public IBinder onBind(Intent intent) { mDelayedStopHandler.removeCallbacksAndMessages(null); mServiceInUse = true; return mBinder; } @Override public void onCreate() { super.onCreate(); mApplication = (YAMMPApplication) getApplication(); mUtils = mApplication.getMediaUtils(); mPlaybackIntent = new Intent(); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mAudioManager.registerMediaButtonEventReceiver( new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName())); mPrefs = new PreferencesEditor(getApplicationContext()); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mShakeDetector = new ShakeListener(this); mCardId = mUtils.getCardId(); registerExternalStorageListener(); registerA2dpServiceListener(); // Needs to be done in this thread, since otherwise // ApplicationContext.getPowerManager() crashes. mPlayer = new MultiPlayer(); mPlayer.setHandler(mMediaplayerHandler); if (mEqualizerSupported) { mEqualizer = new EqualizerWrapper(0, getAudioSessionId()); if (mEqualizer != null) { mEqualizer.setEnabled(true); reloadEqualizer(); } } reloadQueue(); IntentFilter commandFilter = new IntentFilter(); commandFilter.addAction(SERVICECMD); commandFilter.addAction(TOGGLEPAUSE_ACTION); commandFilter.addAction(PAUSE_ACTION); commandFilter.addAction(NEXT_ACTION); commandFilter.addAction(PREVIOUS_ACTION); commandFilter.addAction(CYCLEREPEAT_ACTION); commandFilter.addAction(TOGGLESHUFFLE_ACTION); commandFilter.addAction(BROADCAST_PLAYSTATUS_REQUEST); registerReceiver(mIntentReceiver, commandFilter); registerReceiver(mExternalAudioDeviceStatusReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); mWakeLock.setReferenceCounted(false); // If the service was idle, but got killed before it stopped itself, the // system will relaunch it. Make sure it gets stopped again in that // case. Message msg = mDelayedStopHandler.obtainMessage(); mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); } @Override public void onDestroy() { // Check that we're not being destroyed while something is still // playing. if (isPlaying()) { Log.e(LOGTAG_SERVICE, "Service being destroyed while still playing."); } if (mEqualizer != null) { mEqualizer.setEnabled(false); mEqualizer.release(); } mPlayer.release(); mPlayer = null; mAudioManager.abandonAudioFocus(mAudioFocusListener); TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); // make sure there aren't any other messages coming mDelayedStopHandler.removeCallbacksAndMessages(null); mMediaplayerHandler.removeCallbacksAndMessages(null); if (mCursor != null) { mCursor.close(); mCursor = null; } unregisterReceiver(mIntentReceiver); unregisterReceiver(mA2dpReceiver); unregisterReceiver(mExternalAudioDeviceStatusReceiver); if (mUnmountReceiver != null) { unregisterReceiver(mUnmountReceiver); mUnmountReceiver = null; } mWakeLock.release(); mNotificationManager.cancelAll(); removeStickyBroadcast(mPlaybackIntent); super.onDestroy(); } @Override public void onRebind(Intent intent) { mDelayedStopHandler.removeCallbacksAndMessages(null); mServiceInUse = true; } @Override public void onShake() { mShakingBehavior = mPrefs.getStringPref(KEY_SHAKING_BEHAVIOR, DEFAULT_SHAKING_BEHAVIOR); if (!BEHAVIOR_PLAY_PAUSE.equals(mShakingBehavior) || !BEHAVIOR_NEXT_SONG.equals(mShakingBehavior)) return; if (mPlayer.isInitialized()) { if (BEHAVIOR_PLAY_PAUSE.equals(mShakingBehavior)) { if (isPlaying()) { pause(); Toast.makeText(this, R.string.pause, Toast.LENGTH_SHORT).show(); } else { play(); Toast.makeText(this, R.string.play, Toast.LENGTH_SHORT).show(); } } else if (BEHAVIOR_NEXT_SONG.equals(mShakingBehavior)) { if (isPlaying()) { next(true); Toast.makeText(this, R.string.next, Toast.LENGTH_SHORT).show(); } else { play(); Toast.makeText(this, R.string.play, Toast.LENGTH_SHORT).show(); } } } else { mUtils.shuffleAll(); Toast.makeText(this, R.string.shuffle_all, Toast.LENGTH_SHORT).show(); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { mServiceStartId = startId; mDelayedStopHandler.removeCallbacksAndMessages(null); if (intent != null) { String action = intent.getAction(); String cmd = intent.getStringExtra("command"); mUtils.debugLog("onStartCommand " + action + " / " + cmd); if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) { next(true); } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) { prev(); } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) { if (isPlaying()) { pause(); mPausedByTransientLossOfFocus = false; } else { play(); } } else if (CMDTOGGLEFAVORITE.equals(cmd)) { if (!isFavorite()) { addToFavorites(); } else { removeFromFavorites(); } } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) { pause(); mPausedByTransientLossOfFocus = false; } else if (CMDSTOP.equals(cmd)) { pause(); mPausedByTransientLossOfFocus = false; seek(0); } else if (CMDCYCLEREPEAT.equals(cmd) || CYCLEREPEAT_ACTION.equals(action)) { toggleRepeat(); } else if (CMDTOGGLESHUFFLE.equals(cmd) || TOGGLESHUFFLE_ACTION.equals(action)) { toggleShuffle(); } else if (BROADCAST_PLAYSTATUS_REQUEST.equals(action)) { notifyChange(BROADCAST_PLAYSTATUS_RESPONSE); } } // make sure the service will shut down on its own if it was // just started but not bound to and nothing is playing mDelayedStopHandler.removeCallbacksAndMessages(null); Message msg = mDelayedStopHandler.obtainMessage(); mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); return START_STICKY; } /* * Desired behavior for prev/next/shuffle: * * - NEXT will move to the next track in the list when not shuffling, and to * a track randomly picked from the not-yet-played tracks when shuffling. If * all tracks have already been played, pick from the full set, but avoid * picking the previously played track if possible. - when shuffling, PREV * will go to the previously played track. Hitting PREV again will go to the * track played before that, etc. When the start of the history has been * reached, PREV is a no-op. When not shuffling, PREV will go to the * sequentially previous track (the difference with the shuffle-case is * mainly that when not shuffling, the user can back up to tracks that are * not in the history). * * Example: When playing an album with 10 tracks from the start, and * enabling shuffle while playing track 5, the remaining tracks (6-10) will * be shuffled, e.g. the final play order might be 1-2-3-4-5-8-10-6-9-7. * When hitting 'prev' 8 times while playing track 7 in this example, the * user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next', a * random track will be picked again. If at any time user disables shuffling * the next/previous track will be picked in sequential order again. */ @Override public boolean onUnbind(Intent intent) { mServiceInUse = false; // Take a snapshot of the current playlist saveQueue(true); if (isPlaying() || 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. if (mPlayListLen > 0 || mMediaplayerHandler.hasMessages(TRACK_ENDED)) { Message msg = mDelayedStopHandler.obtainMessage(); mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); return true; } // No active playlist, OK to stop the service right now stopSelf(mServiceStartId); return true; } /** * Replaces the current playlist with a new list, and prepares for starting * playback at the specified position in the list, or a random position if * the specified position is 0. * * @param list * The new list of tracks. */ public void open(long[] list, int position) { synchronized (this) { long oldId = getAudioId(); int listlength = list.length; boolean newlist = true; if (mPlayListLen == listlength) { // possible fast path: list might be the same newlist = false; for (int i = 0; i < listlength; i++) { if (list[i] != mPlayList[i]) { newlist = true; break; } } } if (newlist) { addToPlayList(list, -1); notifyChange(BROADCAST_QUEUE_CHANGED); } if (position >= 0) { mPlayPos = position; } else { mPlayPos = mShuffler.shuffle(mPlayListLen); } mHistory.clear(); saveBookmarkIfNeeded(); openCurrent(); if (oldId != getAudioId()) { notifyChange(BROADCAST_META_CHANGED); } } } /** * Opens the specified file and readies it for playback. * * @param path * The full path of the file to be opened. */ public void open(String path) { synchronized (this) { if (path == null) return; // if mCursor is null, try to associate path with a database cursor if (mCursor == null) { ContentResolver resolver = getContentResolver(); Uri uri; String where; String selectionArgs[]; if (path.startsWith("content://media/")) { uri = Uri.parse(path); where = null; selectionArgs = null; } else { uri = MediaStore.Audio.Media.getContentUriForPath(path); where = MediaStore.Audio.Media.DATA + "=?"; selectionArgs = new String[] { path }; } try { mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null); if (mCursor != null) { if (mCursor.getCount() == 0) { mCursor.close(); mCursor = null; } else { mCursor.moveToNext(); ensurePlayListCapacity(1); mPlayListLen = 1; mPlayList[0] = mCursor.getLong(IDCOLIDX); mPlayPos = 0; } } } catch (UnsupportedOperationException ex) { } } mFileToPlay = path; mPlayer.setDataSource(mFileToPlay); if (!mPlayer.isInitialized()) { stop(true); if (mOpenFailedCounter++ < 10 && mPlayListLen > 1) { // beware: this ends up being recursive because next() calls // open() again. next(false); } if (!mPlayer.isInitialized() && mOpenFailedCounter != 0) { // need to make sure we only shows this once mOpenFailedCounter = 0; if (!mQuietMode) { Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show(); } Log.d(LOGTAG_SERVICE, "Failed to open file for playback"); } } else { mOpenFailedCounter = 0; } } } /** * Pauses playback (call play() to resume) */ public void pause() { synchronized (this) { mMediaplayerHandler.removeMessages(FADEUP); if (isPlaying()) { mPlayer.pause(); gotoIdleState(); mIsSupposedToBePlaying = false; notifyChange(BROADCAST_PLAYSTATE_CHANGED); saveBookmarkIfNeeded(); } } } /** * Starts playback of a previously opened file. */ public void play() { CharSequence contentTitle, contentText = null; TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); if (telephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK) return; mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); mAudioManager.registerMediaButtonEventReceiver( new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName())); telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); if (mPlayer.isInitialized()) { // if we are at the end of the song, go to the next song first long duration = mPlayer.duration(); if (mRepeatMode != REPEAT_CURRENT && duration > 2000 && mPlayer.position() >= duration - 2000) { next(true); } mPlayer.start(); // make sure we fade in, in case a previous fadein was stopped // because // of another focus loss mMediaplayerHandler.removeMessages(FADEDOWN); mMediaplayerHandler.sendEmptyMessage(FADEUP); contentTitle = getTrackName(); String artist = getArtistName(); boolean isUnknownArtist = artist == null || MediaStore.UNKNOWN_STRING.equals(artist); String album = getAlbumName(); boolean isUnknownAlbum = album == null || MediaStore.UNKNOWN_STRING.equals(album); if (!isUnknownArtist && !isUnknownAlbum) { contentText = getString(R.string.notification_artist_album, artist, album); } else if (isUnknownArtist && !isUnknownAlbum) { contentText = album; } else if (!isUnknownArtist && isUnknownAlbum) { contentText = artist; } PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(INTENT_PLAYBACK_VIEWER), 0); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setOngoing(true); builder.setSmallIcon(R.drawable.ic_stat_playback); builder.setContentIntent(contentIntent); builder.setContentTitle(contentTitle); builder.setContentText(contentText); mNotificationManager.notify(ID_NOTIFICATION_PLAYBACK, builder.getNotification()); if (!mIsSupposedToBePlaying) { mIsSupposedToBePlaying = true; notifyChange(BROADCAST_PLAYSTATE_CHANGED); } } else if (mPlayListLen <= 0) { // This is mostly so that if you press 'play' on a bluetooth headset // without every having played anything before, it will still play // something. setShuffleMode(SHUFFLE_NORMAL); } } /** * Returns the current playback position in milliseconds */ public long position() { if (mPlayer.isInitialized()) return mPlayer.position(); return -1; } public void prev() { synchronized (this) { if (mShuffleMode == SHUFFLE_NORMAL) { // go to previously-played track and remove it from the history int histsize = mHistory.size(); if (histsize == 0) // prev is a no-op return; Integer pos = mHistory.remove(histsize - 1); mPlayPos = pos.intValue(); } else { if (mPlayPos > 0) { mPlayPos--; } else { mPlayPos = mPlayListLen - 1; } } saveBookmarkIfNeeded(); stop(false); openCurrent(); play(); notifyChange(BROADCAST_META_CHANGED); } }; public void registerA2dpServiceListener() { mA2dpReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BROADCAST_PLAYSTATUS_REQUEST.equals(action)) { notifyChange(BROADCAST_PLAYSTATUS_RESPONSE); } } }; IntentFilter iFilter = new IntentFilter(); iFilter.addAction(BROADCAST_PLAYSTATUS_REQUEST); registerReceiver(mA2dpReceiver, iFilter); } /** * 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() { @Override public void onReceive(Context context, Intent intent) { 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 = mUtils.getCardId(); reloadQueue(); mQueueIsSaveable = true; notifyChange(BROADCAST_QUEUE_CHANGED); notifyChange(BROADCAST_META_CHANGED); } } }; IntentFilter iFilter = new IntentFilter(); iFilter.addAction(Intent.ACTION_MEDIA_EJECT); iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); iFilter.addDataScheme("file"); registerReceiver(mUnmountReceiver, iFilter); } } public void reloadEqualizer() { if (mEqualizerSupported && mEqualizer != null) { short bands = mEqualizer.getNumberOfBands(); final short minEQLevel = mEqualizer.getBandLevelRange()[0]; final short maxEQLevel = mEqualizer.getBandLevelRange()[1]; for (short i = 0; i < bands; i++) { final short band = i; mEqualizer.setBandLevel(band, mPrefs.getEqualizerSetting(band, (short) ((maxEQLevel + minEQLevel) / 2))); } } } public void reloadLyrics() { mLyricsHandler.removeCallbacksAndMessages(null); mLyricsHandler.sendEmptyMessage(NEW_LYRICS_LOADED); } // TODO reload server settings public void reloadSettings() { mShakeEnabled = mPrefs.getBooleanPref(KEY_SHAKE_ENABLED, false); mShakingThreshold = mPrefs.getFloatPref(KEY_SHAKING_THRESHOLD, DEFAULT_SHAKING_THRESHOLD); if (mShakeDetector != null) { mShakeDetector.stop(); if (mShakeEnabled) { mShakeDetector.start(); } mShakeDetector.setShakeThreshold(mShakingThreshold); } } public void removeFromFavorites() { if (getAudioId() >= 0) { removeFromFavorites(getAudioId()); } } public void removeFromFavorites(long id) { mUtils.removeFromFavorites(id); notifyChange(BROADCAST_FAVORITESTATE_CHANGED); } /** * 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(long id) { int numremoved = 0; synchronized (this) { for (int i = 0; i < mPlayListLen; i++) { if (mPlayList[i] == id) { numremoved += removeTracksInternal(i, i); i--; } } } if (numremoved > 0) { notifyChange(BROADCAST_QUEUE_CHANGED); } return numremoved; } /** * 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(int first, int last) { int numremoved = removeTracksInternal(first, last); if (numremoved > 0) { notifyChange(BROADCAST_QUEUE_CHANGED); } return numremoved; } /** * Seeks to the position specified. * * @param pos * The position to seek to, in milliseconds */ public long seek(long pos) { if (mPlayer.isInitialized()) { if (pos < 0) { pos = 0; } if (pos > mPlayer.duration()) { pos = mPlayer.duration(); } long result = mPlayer.seek(pos); mLyricsHandler.obtainMessage(POSITION_CHANGED, (int) result, 0).sendToTarget(); return result; } return 0; } /** * Starts playing the track at the given id in the queue. * * @param id * The id in the queue of the track that will be played. */ public void setQueueId(long id) { int pos = -1; for (int i = 0; i < mPlayList.length; i++) { if (id == mPlayList[i]) { pos = i; } } if (pos < 0) return; setQueuePosition(pos); } /** * Starts playing the track at the given position in the queue. * * @param pos * The position in the queue of the track that will be played. */ public void setQueuePosition(int pos) { synchronized (this) { stop(false); mPlayPos = pos; openCurrent(); play(); notifyChange(BROADCAST_META_CHANGED); } } public void setRepeatMode(int repeatmode) { synchronized (this) { if (mRepeatMode == repeatmode) return; if (mShuffleMode == SHUFFLE_NORMAL && repeatmode == REPEAT_CURRENT) { setShuffleMode(SHUFFLE_NONE); } mRepeatMode = repeatmode; notifyChange(BROADCAST_REPEATMODE_CHANGED); saveQueue(false); } } public void setShuffleMode(int shufflemode) { synchronized (this) { if (mShuffleMode == shufflemode && mPlayListLen < 1) return; if (mRepeatMode == REPEAT_CURRENT) { mRepeatMode = REPEAT_NONE; } mShuffleMode = shufflemode; notifyChange(BROADCAST_SHUFFLEMODE_CHANGED); saveQueue(false); } } /** * Stops playback. */ public void stop() { stop(true); } public void toggleFavorite() { if (!isFavorite()) { addToFavorites(); } else { removeFromFavorites(); } } public boolean togglePause() { if (isPlaying()) { pause(); } else { play(); } return isPlaying(); } public void toggleRepeat() { if (mRepeatMode == REPEAT_NONE) { setRepeatMode(REPEAT_ALL); } else if (mRepeatMode == REPEAT_ALL) { setRepeatMode(REPEAT_CURRENT); if (mShuffleMode != SHUFFLE_NONE) { setShuffleMode(SHUFFLE_NONE); } } else { setRepeatMode(REPEAT_NONE); } } public void toggleShuffle() { if (mShuffleMode == SHUFFLE_NONE) { setShuffleMode(SHUFFLE_NORMAL); if (mRepeatMode == REPEAT_CURRENT) { setRepeatMode(REPEAT_ALL); } } else if (mShuffleMode == SHUFFLE_NORMAL) { setShuffleMode(SHUFFLE_NONE); } else { setShuffleMode(SHUFFLE_NONE); Log.e("MediaPlaybackService", "Invalid shuffle mode: " + mShuffleMode); } } // insert the list of songs at the specified position in the playlist private void addToPlayList(long[] list, int position) { int addlen = list.length; if (position < 0) { // overwrite mPlayListLen = 0; position = 0; } ensurePlayListCapacity(mPlayListLen + addlen); if (position > mPlayListLen) { position = mPlayListLen; } // move part of list after insertion point int tailsize = mPlayListLen - position; for (int i = tailsize; i > 0; i--) { mPlayList[position + i] = mPlayList[position + i - addlen]; } // copy list into playlist for (int i = 0; i < addlen; i++) { mPlayList[position + i] = list[i]; } mPlayListLen += addlen; if (mPlayListLen == 0) { mCursor.close(); mCursor = null; notifyChange(BROADCAST_META_CHANGED); } } private void ensurePlayListCapacity(int size) { if (mPlayList == null || size > mPlayList.length) { // reallocate at 2x requested size so we don't // need to grow and copy the array for every // insert long[] newlist = new long[size * 2]; int len = mPlayList != null ? mPlayList.length : mPlayListLen; for (int i = 0; i < len; i++) { newlist[i] = mPlayList[i]; } mPlayList = newlist; } } private long getBookmark() { synchronized (this) { if (mCursor == null) return 0; return mCursor.getLong(BOOKMARKCOLIDX); } } private long getSleepTimerRemained() { Calendar now = Calendar.getInstance(); long mCurrentTimestamp = now.getTimeInMillis(); if (mStopTimestamp != 0) return mStopTimestamp - mCurrentTimestamp; else return 0; } private void gotoIdleState() { mDelayedStopHandler.removeCallbacksAndMessages(null); Message msg = mDelayedStopHandler.obtainMessage(); mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY); mNotificationManager.cancel(ID_NOTIFICATION_PLAYBACK); } private boolean isPodcast() { synchronized (this) { if (mCursor == null) return false; return mCursor.getInt(PODCASTCOLIDX) > 0; } } /** * Notify the change-receivers that something has changed. The intent that * is sent contains the following data for the currently playing track: "id" * - Integer: the database row ID "artist" - String: the name of the artist * "album_artist" - String: the name of the album artist "album" - String: * the name of the album "track" - String: the name of the track The intent * has an action that is one of "org.yammp.metachanged" * "org.yammp.queuechanged", "org.yammp.playbackcomplete" * "org.yammp.playstatechanged" respectively indicating that a new track has * started playing, that the playback queue has changed, that playback has * stopped because the last file in the list has been played, or that the * play-state changed (paused/resumed). */ private void notifyChange(String action) { mPlaybackIntent.setAction(action); mPlaybackIntent.putExtra(BROADCAST_KEY_ID, getAudioId()); mPlaybackIntent.putExtra(BROADCAST_KEY_ARTIST, getArtistName()); mPlaybackIntent.putExtra(BROADCAST_KEY_ALBUM, getAlbumName()); mPlaybackIntent.putExtra(BROADCAST_KEY_TRACK, getTrackName()); mPlaybackIntent.putExtra(BROADCAST_KEY_PLAYING, isPlaying()); mPlaybackIntent.putExtra(BROADCAST_KEY_ISFAVORITE, isFavorite()); mPlaybackIntent.putExtra(BROADCAST_KEY_SONGID, getAudioId()); mPlaybackIntent.putExtra(BROADCAST_KEY_ALBUMID, getAlbumId()); mPlaybackIntent.putExtra(BROADCAST_KEY_DURATION, duration()); mPlaybackIntent.putExtra(BROADCAST_KEY_POSITION, position()); mPlaybackIntent.putExtra(BROADCAST_KEY_SHUFFLEMODE, getShuffleMode()); mPlaybackIntent.putExtra(BROADCAST_KEY_REPEATMODE, getRepeatMode()); if (mPlayList != null) { mPlaybackIntent.putExtra(BROADCAST_KEY_LISTSIZE, Long.valueOf(mPlayList.length)); } else { mPlaybackIntent.putExtra(BROADCAST_KEY_LISTSIZE, Long.valueOf(mPlayListLen)); } sendStickyBroadcast(mPlaybackIntent); if (BROADCAST_META_CHANGED.equals(action)) { mLyricsHandler.sendEmptyMessage(NEW_LYRICS_LOADED); if (isPlaying()) { sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_START); } else { sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_COMPLETE); } } if (BROADCAST_PLAYSTATE_CHANGED.equals(action)) { notifyLyricsChange(BROADCAST_LYRICS_REFRESHED); if (isPlaying()) { mLyricsHandler.sendEmptyMessage(LYRICS_RESUMED); sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_RESUME); } else { mLyricsHandler.sendEmptyMessage(LYRICS_PAUSED); sendScrobbleBroadcast(SCROBBLE_PLAYSTATE_PAUSE); } } if (BROADCAST_QUEUE_CHANGED.equals(action)) { saveQueue(true); } else { saveQueue(false); } } private void openCurrent() { synchronized (this) { if (mCursor != null) { mCursor.close(); mCursor = null; } if (mPlayListLen == 0) return; stop(false); String id = String.valueOf(mPlayList[mPlayPos]); mCursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursorCols, "_id=" + id, null, null); if (mCursor != null) { mCursor.moveToFirst(); open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id); // go to bookmark if needed if (isPodcast()) { long bookmark = getBookmark(); // Start playing a little bit before the bookmark, // so it's easier to get back in to the narrative. seek(bookmark - 5000); } } } } private void reloadQueue() { String q = null; int id = mCardId; if (getSharedPreferences(SHAREDPREFS_STATES, Context.MODE_PRIVATE).contains(STATE_KEY_CARDID)) { id = mPrefs.getIntState(STATE_KEY_CARDID, mCardId); } if (id == mCardId) { // Only restore the saved playlist if the card is still // the same one as when the playlist was saved q = mPrefs.getStringState(STATE_KEY_QUEUE, ""); } int qlen = q != null ? q.length() : 0; if (qlen > 1) { // Log.i("@@@@ service", "loaded queue: " + q); int plen = 0; int n = 0; int shift = 0; for (int i = 0; i < qlen; i++) { char c = q.charAt(i); if (c == ';') { ensurePlayListCapacity(plen + 1); mPlayList[plen] = n; plen++; n = 0; shift = 0; } else { if (c >= '0' && c <= '9') { n += c - '0' << shift; } else if (c >= 'a' && c <= 'f') { n += 10 + c - 'a' << shift; } else { // bogus playlist data plen = 0; break; } shift += 4; } } //TODO shuffler. mShuffler = new Shuffler(mPlayList); mPlayListLen = plen; int pos = mPrefs.getIntState(STATE_KEY_CURRPOS, 0); if (pos < 0 || pos >= mPlayListLen) { // The saved playlist is bogus, discard it mPlayListLen = 0; return; } mPlayPos = pos; // When reloadQueue is called in response to a card-insertion, // we might not be able to query the media provider right away. // To deal with this, try querying for the current file, and if // that fails, wait a while and try again. If that too fails, // assume there is a problem and don't restore the state. Cursor cur = mUtils.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[] { "_id" }, "_id=" + mPlayList[mPlayPos], null, null); if (cur == null || cur.getCount() == 0) { // wait a bit and try again SystemClock.sleep(3000); cur = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursorCols, "_id=" + mPlayList[mPlayPos], null, null); } if (cur != null) { cur.close(); } mOpenFailedCounter = 20; mQuietMode = true; openCurrent(); mQuietMode = false; if (!mPlayer.isInitialized()) { // couldn't restore the saved state mPlayListLen = 0; return; } mLyricsHandler.sendEmptyMessage(NEW_LYRICS_LOADED); long seekpos = mPrefs.getLongState(STATE_KEY_SEEKPOS, 0); seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0); Log.d(LOGTAG_SERVICE, "restored queue, currently at position " + position() + "/" + duration() + " (requested " + seekpos + ")"); int repmode = mPrefs.getIntState(STATE_KEY_REPEATMODE, REPEAT_NONE); if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) { repmode = REPEAT_NONE; } mRepeatMode = repmode; int shufmode = mPrefs.getIntState(STATE_KEY_SHUFFLEMODE, SHUFFLE_NONE); if (shufmode != SHUFFLE_NORMAL) { shufmode = SHUFFLE_NONE; } if (shufmode != SHUFFLE_NONE) { // in shuffle mode we need to restore the history too q = mPrefs.getStringState(STATE_KEY_HISTORY, ""); qlen = q != null ? q.length() : 0; if (qlen > 1) { plen = 0; n = 0; shift = 0; mHistory.clear(); for (int i = 0; i < qlen; i++) { char c = q.charAt(i); if (c == ';') { if (n >= mPlayListLen) { // bogus history data mHistory.clear(); break; } if (!mHistory.contains(mPlayPos)) { mHistory.add(mPlayPos); } n = 0; shift = 0; } else { if (c >= '0' && c <= '9') { n += c - '0' << shift; } else if (c >= 'a' && c <= 'f') { n += 10 + c - 'a' << shift; } else { // bogus history data mHistory.clear(); break; } shift += 4; } } } } mShuffleMode = shufmode; } } private int removeTracksInternal(int first, int last) { synchronized (this) { if (last < first) return 0; if (first < 0) { first = 0; } if (last >= mPlayListLen) { last = mPlayListLen - 1; } boolean gotonext = false; if (first <= mPlayPos && mPlayPos <= last) { mPlayPos = first; gotonext = true; } else if (mPlayPos > last) { mPlayPos -= last - first + 1; } int num = mPlayListLen - last - 1; for (int i = 0; i < num; i++) { mPlayList[first + i] = mPlayList[last + 1 + i]; } mPlayListLen -= last - first + 1; if (gotonext) { if (mPlayListLen == 0) { stop(true); mPlayPos = -1; if (mCursor != null) { mCursor.close(); mCursor = null; } } else { if (mPlayPos >= mPlayListLen) { mPlayPos = 0; } boolean wasPlaying = isPlaying(); stop(false); openCurrent(); if (wasPlaying) { play(); } } notifyChange(BROADCAST_META_CHANGED); } return last - first + 1; } } private void saveBookmarkIfNeeded() { try { if (isPodcast()) { long pos = position(); long bookmark = getBookmark(); long duration = duration(); if (pos < bookmark && pos + 10000 > bookmark || pos > bookmark && pos - 10000 < bookmark) // The existing bookmark is // close to the current // position, so don't update it. return; if (pos < 15000 || pos + 10000 > duration) { // if we're near the start or end, clear the bookmark pos = 0; } // write 'pos' to the bookmark field ContentValues values = new ContentValues(); values.put(MediaStore.Audio.Media.BOOKMARK, pos); Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX)); getContentResolver().update(uri, values, null, null); } } catch (SQLiteException ex) { } } private void saveQueue(boolean full) { if (!mQueueIsSaveable) return; // long start = System.currentTimeMillis(); if (full) { StringBuilder q = new StringBuilder(); // The current playlist is saved as a list of "reverse hexadecimal" // numbers, which we can generate faster than normal decimal or // hexadecimal numbers, which in turn allows us to save the playlist // more often without worrying too much about performance. // (saving the full state takes about 40 ms under no-load conditions // on the phone) int len = mPlayListLen; for (int i = 0; i < len; i++) { long n = mPlayList[i]; if (n < 0) { continue; } else if (n == 0) { q.append("0;"); } else { while (n != 0) { int digit = (int) (n & 0xf); n >>>= 4; q.append(hexdigits[digit]); } q.append(";"); } } // Log.i("@@@@ service", "created queue string in " + // (System.currentTimeMillis() - start) + " ms"); mPrefs.setStringState(STATE_KEY_QUEUE, q.toString()); mPrefs.setIntState(STATE_KEY_CARDID, mCardId); if (mShuffleMode != SHUFFLE_NONE) { // In shuffle mode we need to save the history too len = mHistory.size(); q.setLength(0); for (int i = 0; i < len; i++) { int n = mHistory.get(i); if (n == 0) { q.append("0;"); } else { while (n != 0) { int digit = n & 0xf; n >>>= 4; q.append(hexdigits[digit]); } q.append(";"); } } mPrefs.setStringState(STATE_KEY_HISTORY, q.toString()); } } mPrefs.setIntState(STATE_KEY_CURRPOS, mPlayPos); if (mPlayer.isInitialized()) { mPrefs.setLongState(STATE_KEY_SEEKPOS, mPlayer.position()); } mPrefs.setIntState(STATE_KEY_REPEATMODE, mRepeatMode); mPrefs.setIntState(STATE_KEY_SHUFFLEMODE, mShuffleMode); // Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() // - start) + " ms"); } private void sendScrobbleBroadcast(int state) { mScrobbleEnabled = mPrefs.getBooleanPref(KEY_ENABLE_SCROBBLING, false); // check that state is a valid state if (state != SCROBBLE_PLAYSTATE_START && state != SCROBBLE_PLAYSTATE_RESUME && state != SCROBBLE_PLAYSTATE_PAUSE && state != SCROBBLE_PLAYSTATE_COMPLETE) return; if (mScrobbleEnabled) { Intent i = new Intent(SCROBBLE_SLS_API); i.putExtra(BROADCAST_KEY_APP_NAME, getString(R.string.app_name)); i.putExtra(BROADCAST_KEY_APP_PACKAGE, getPackageName()); i.putExtra(BROADCAST_KEY_STATE, state); i.putExtra(BROADCAST_KEY_ARTIST, getArtistName()); i.putExtra(BROADCAST_KEY_ALBUM, getAlbumName()); i.putExtra(BROADCAST_KEY_TRACK, getTrackName()); i.putExtra(BROADCAST_KEY_DURATION, (int) (duration() / 1000)); sendBroadcast(i); } } private void startSleepTimer(long milliseconds, boolean gentle) { Calendar now = Calendar.getInstance(); mCurrentTimestamp = now.getTimeInMillis(); mStopTimestamp = mCurrentTimestamp + milliseconds; int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_CAP_AMPM; format_flags |= DateUtils.FORMAT_SHOW_TIME; String time = DateUtils.formatDateTime(this, mStopTimestamp, format_flags); CharSequence contentTitle = getString(R.string.sleep_timer_enabled); CharSequence contentText = getString(R.string.notification_sleep_timer, time); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(), 0); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setOngoing(true); builder.setSmallIcon(R.drawable.ic_stat_sleeptimer); builder.setContentIntent(contentIntent); builder.setContentTitle(contentTitle); builder.setContentText(contentText); mGentleSleepTimer = gentle; mNotificationManager.notify(ID_NOTIFICATION_SLEEPTIMER, builder.getNotification()); mSleepTimerHandler.sendEmptyMessageDelayed(START_SLEEP_TIMER, milliseconds); Toast.makeText(this, getResources().getQuantityString(R.plurals.NNNminutes_notif, Integer.valueOf((int) milliseconds / 60 / 1000), Integer.valueOf((int) milliseconds / 60 / 1000)), Toast.LENGTH_SHORT).show(); } private void stop(boolean remove_status_icon) { if (mPlayer.isInitialized()) { mPlayer.stop(); } mFileToPlay = null; if (mCursor != null) { mCursor.close(); mCursor = null; } if (remove_status_icon) { gotoIdleState(); } else { mNotificationManager.cancel(ID_NOTIFICATION_PLAYBACK); } if (remove_status_icon) { mIsSupposedToBePlaying = false; } } private void stopSleepTimer() { mSleepTimerHandler.sendEmptyMessage(STOP_SLEEP_TIMER); Toast.makeText(this, R.string.sleep_timer_disabled, Toast.LENGTH_SHORT).show(); } @Override protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { writer.println("" + mPlayListLen + " items in queue, currently at index " + mPlayPos); writer.println("Currently loaded:"); writer.println(getArtistName()); writer.println(getAlbumName()); writer.println(getTrackName()); writer.println(getPath()); writer.println("playing: " + mIsSupposedToBePlaying); writer.println("actual: " + mPlayer.isPlaying()); writer.println("shuffle mode: " + mShuffleMode); mUtils.debugDump(writer); } /** * Provides a unified interface for dealing with midi files and other media * files. */ private class MultiPlayer { private MediaPlayer mMediaPlayer = new MediaPlayer(); private Handler mHandler; private boolean mIsInitialized = false; private MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { // Acquire a temporary wakelock, since when we return from // this callback the MediaPlayer will release its wakelock // and allow the device to go to sleep. // This temporary wakelock is released when the RELEASE_WAKELOCK // message is processed, but just in case, put a timeout on it. mWakeLock.acquire(30000); mHandler.sendEmptyMessage(TRACK_ENDED); mHandler.sendEmptyMessage(RELEASE_WAKELOCK); } }; private MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { switch (what) { case MediaPlayer.MEDIA_ERROR_SERVER_DIED: mIsInitialized = false; mMediaPlayer.release(); // Creating a new MediaPlayer and settings its wakemode // does // not // require the media service, so it's OK to do this now, // while the // service is still being restarted mMediaPlayer = new MediaPlayer(); mMediaPlayer.setWakeMode(MusicPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000); return true; default: Log.d("MultiPlayer", "Error: " + what + "," + extra); break; } return false; } }; public MultiPlayer() { mMediaPlayer.setWakeMode(MusicPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK); } public long duration() { return mMediaPlayer.getDuration(); } public int getAudioSessionId() { try { return (Integer) mMediaPlayer.getClass().getMethod("getAudioSessionId", new Class[] {}) .invoke(mMediaPlayer, new Object[] {}); } catch (Exception e) { return 0; } } public boolean isInitialized() { return mIsInitialized; } public boolean isPlaying() { return mMediaPlayer.isPlaying(); } public void pause() { mMediaPlayer.pause(); } public long position() { return mMediaPlayer.getCurrentPosition(); } /** * You CANNOT use this player anymore after calling release() */ public void release() { stop(); mMediaPlayer.release(); } public long seek(long whereto) { mMediaPlayer.seekTo((int) whereto); return whereto; } public void setDataSource(String path) { try { mMediaPlayer.reset(); mMediaPlayer.setOnPreparedListener(null); if (path.startsWith("content://")) { mMediaPlayer.setDataSource(MusicPlaybackService.this, Uri.parse(path)); } else { mMediaPlayer.setDataSource(path); } mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.prepare(); } catch (IOException ex) { mIsInitialized = false; return; } catch (IllegalArgumentException ex) { mIsInitialized = false; return; } mMediaPlayer.setOnCompletionListener(listener); mMediaPlayer.setOnErrorListener(errorListener); mIsInitialized = true; } public void setHandler(Handler handler) { mHandler = handler; } public void setVolume(float vol) { mMediaPlayer.setVolume(vol, vol); mCurrentVolume = vol; } public void start() { mUtils.debugLog(new Exception("MultiPlayer.start called")); mMediaPlayer.start(); } public void stop() { mMediaPlayer.reset(); mIsInitialized = false; } } // A simple variation of Random that makes sure that the // value it returns is not equal to the value it returned // previously, unless the interval is 1. private class Shuffler { private int mPrevious; private Random mRandom = new Random(); private List<Long> mHistory = new ArrayList<Long>(); private long[] mQueue = new long[] {}; private List<Long> mUnplayed = new ArrayList<Long>(); private Long mPreviousRandom = null, mCurrentRandom = null, mNextRandom = null; private int mPosition = 0; private boolean mTracebackMode = false; public Shuffler(long[] queue) { if (queue == null || queue.length <= 0) { throw new IllegalArgumentException("queue cannot be null or zero-sized!"); } mQueue = queue; mPreviousRandom = null; mCurrentRandom = (long) mRandom.nextInt(mQueue.length); mHistory.add(mCurrentRandom); long tmp_random = mRandom.nextInt(mQueue.length); while (mCurrentRandom == tmp_random) { tmp_random = mRandom.nextInt(mQueue.length); } mNextRandom = tmp_random; } public void reloadQueue(long[] queue) { if (queue == null || queue.length <= 0) { throw new IllegalArgumentException("queue cannot be null or zero-sized!"); } mQueue = queue; List<Long> tmp_list = new ArrayList<Long>(); for (long item : queue) { tmp_list.add(item); } for (Long item : mHistory) { if (!tmp_list.contains(item)) mHistory.remove(item); } } public Long getPrevious() { return mPreviousRandom; } public Long getNext() { return mNextRandom; } public Long getCurrent() { return mCurrentRandom; } public Long doNextShuffle() { if (mHistory.size() > 0) { if (!mTracebackMode) { mPreviousRandom = mCurrentRandom; mCurrentRandom = mNextRandom; mHistory.add(mCurrentRandom); if (mHistory.size() >= 1) { mPosition = mHistory.size() - 1; } else { mPosition = 0; } long tmp_random = mRandom.nextInt(mQueue.length); while (mCurrentRandom == tmp_random) { tmp_random = mRandom.nextInt(mQueue.length); } mNextRandom = tmp_random; } else { mPreviousRandom = mPosition < 0 ? mHistory.get(mPosition) : null; mCurrentRandom = mHistory.get(mPosition); mNextRandom = mPosition < mHistory.size() - 1 ? mHistory.get(mPosition + 1) : null; mPosition++; } } else { mPreviousRandom = null; mCurrentRandom = (long) mRandom.nextInt(mQueue.length); mHistory.add(mCurrentRandom); long tmp_random = mRandom.nextInt(mQueue.length); while (mCurrentRandom == tmp_random) { tmp_random = mRandom.nextInt(mQueue.length); } mNextRandom = tmp_random; } return mCurrentRandom; } public void goToPosition(int position) { if (position >= mHistory.size() - 1) { mTracebackMode = false; return; } else if (position < 0) { mPosition = 0; } else { mPosition = position; } mTracebackMode = true; mPreviousRandom = mPosition < 0 ? mHistory.get(mPosition) : null; mCurrentRandom = mHistory.get(mPosition); mNextRandom = mPosition < mHistory.size() - 1 ? mHistory.get(mPosition + 1) : null; } public void goByPosition(int offset) { goToPosition(mPosition + offset); } } /* * By making this a static class with a WeakReference to the Service, we * ensure that the Service can be GCd even when the system process still has * a remote reference to the stub. */ static class ServiceStub extends IMusicPlaybackService.Stub { WeakReference<MusicPlaybackService> mService; ServiceStub(MusicPlaybackService service) { mService = new WeakReference<MusicPlaybackService>(service); } @Override public void addToFavorites(long id) { mService.get().addToFavorites(id); } @Override public long duration() { return mService.get().duration(); } @Override public void enqueue(long[] list, int action) { mService.get().enqueue(list, action); } @Override public int eqGetBand(int frequency) { return mService.get().eqGetBand(frequency); } @Override public int[] eqGetBandFreqRange(int band) { return mService.get().eqGetBandFreqRange((short) band); } @Override public int eqGetBandLevel(int band) { return mService.get().eqGetBandLevel((short) band); } @Override public int[] eqGetBandLevelRange() { short[] value = mService.get().eqGetBandLevelRange(); if (value == null) return null; int[] result = new int[value.length]; for (short i = 0; i < value.length; i++) { result[i] = value[i]; } return result; } @Override public int eqGetCenterFreq(int band) { return mService.get().eqGetCenterFreq((short) band); } @Override public int eqGetCurrentPreset() { return mService.get().eqGetCurrentPreset(); } @Override public int eqGetNumberOfBands() { return mService.get().eqGetNumberOfBands(); } @Override public int eqGetNumberOfPresets() { return mService.get().eqGetNumberOfPresets(); } @Override public String eqGetPresetName(int preset) { return mService.get().eqGetPresetName((short) preset); } @Override public void eqRelease() { mService.get().eqRelease(); } @Override public void eqReset() { mService.get().eqReset(); } @Override public void eqSetBandLevel(int band, int level) { mService.get().eqSetBandLevel((short) band, (short) level); } @Override public int eqSetEnabled(boolean enabled) { return mService.get().eqSetEnabled(enabled); } @Override public void eqUsePreset(int preset) { mService.get().eqUsePreset((short) preset); } @Override public long getAlbumId() { return mService.get().getAlbumId(); } @Override public String getAlbumName() { return mService.get().getAlbumName(); } @Override public long getArtistId() { return mService.get().getArtistId(); } @Override public String getArtistName() { return mService.get().getArtistName(); } @Override public Uri getArtworkUri() { return mService.get().getArtworkUri(); } @Override public long getAudioId() { return mService.get().getAudioId(); } @Override public int getAudioSessionId() { return mService.get().getAudioSessionId(); } @Override public int getCurrentLyricsId() { return mService.get().getCurrentLyricsId(); } @Override public String[] getLyrics() { return mService.get().getLyrics(); } @Override public int getLyricsStatus() { return mService.get().getLyricsStatus(); } @Override public int getMediaMountedCount() { return mService.get().getMediaMountedCount(); } @Override public String getMediaPath() { return mService.get().getMediaPath(); } @Override public String getPath() { return mService.get().getPath(); } @Override public long getPositionByLyricsId(int id) { return mService.get().getPositionByLyricsId(id); } @Override public long[] getQueue() { return mService.get().getQueue(); } @Override public int getQueuePosition() { return mService.get().getQueuePosition(); } @Override public int getRepeatMode() { return mService.get().getRepeatMode(); } @Override public int getShuffleMode() { return mService.get().getShuffleMode(); } @Override public long getSleepTimerRemained() { return mService.get().getSleepTimerRemained(); } @Override public String getTrackName() { return mService.get().getTrackName(); } @Override public boolean isFavorite(long id) { return mService.get().isFavorite(id); } @Override public boolean isPlaying() { return mService.get().isPlaying(); } @Override public void moveQueueItem(int from, int to) { mService.get().moveQueueItem(from, to); } @Override public void next() { mService.get().next(true); } @Override public void open(long[] list, int position) { mService.get().open(list, position); } @Override public void openFile(String path) { mService.get().open(path); } @Override public void pause() { mService.get().pause(); } @Override public void play() { mService.get().play(); } @Override public long position() { return mService.get().position(); } @Override public void prev() { mService.get().prev(); } @Override public void refreshLyrics() { mService.get().notifyLyricsChange(BROADCAST_LYRICS_REFRESHED); } @Override public void reloadEqualizer() { mService.get().reloadEqualizer(); } @Override public void reloadLyrics() { mService.get().reloadLyrics(); } @Override public void reloadSettings() { mService.get().reloadSettings(); } @Override public void removeFromFavorites(long id) { mService.get().removeFromFavorites(id); } @Override public int removeTrack(long id) { return mService.get().removeTrack(id); } @Override public int removeTracks(int first, int last) { return mService.get().removeTracks(first, last); } @Override public long seek(long pos) { return mService.get().seek(pos); } @Override public void setQueueId(long id) { mService.get().setQueueId(id); } @Override public void setQueuePosition(int index) { mService.get().setQueuePosition(index); } @Override public void setRepeatMode(int repeatmode) { mService.get().setRepeatMode(repeatmode); } @Override public void setShuffleMode(int shufflemode) { mService.get().setShuffleMode(shufflemode); } @Override public void startSleepTimer(long milliseconds, boolean gentle) { mService.get().startSleepTimer(milliseconds, gentle); } @Override public void stop() { mService.get().stop(); } @Override public void stopSleepTimer() { mService.get().stopSleepTimer(); } @Override public void toggleFavorite() { mService.get().toggleFavorite(); } @Override public boolean togglePause() { return mService.get().togglePause(); } @Override public void toggleRepeat() { mService.get().toggleRepeat(); } @Override public void toggleShuffle() { mService.get().toggleShuffle(); } } }