/*
* Copyright (C) 2012 Andrew Neal 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.andrew.apollo;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.Random;
import java.util.TreeSet;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
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.database.Cursor;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.RemoteControlClient;
import android.media.audiofx.AudioEffect;
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.PowerManager.WakeLock;
import android.os.RemoteException;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AlbumColumns;
import android.provider.MediaStore.Audio.AudioColumns;
import android.util.Log;
import com.andrew.apollo.appwidgets.AppWidgetLarge;
import com.andrew.apollo.appwidgets.AppWidgetLargeAlternate;
import com.andrew.apollo.appwidgets.AppWidgetSmall;
import com.andrew.apollo.appwidgets.RecentWidgetProvider;
import com.andrew.apollo.cache.ImageFetcher;
import com.andrew.apollo.provider.FavoritesStore;
import com.andrew.apollo.provider.RecentStore;
import com.andrew.apollo.utils.Lists;
import com.andrew.apollo.utils.MusicUtils;
import com.bt.download.android.gui.activities.AudioPlayerActivity;
import com.bt.download.android.util.SystemUtils;
import com.frostwire.logging.Logger;
import com.frostwire.util.Ref;
/**
* A backbround {@link Service} used to keep music playing between activities
* and when the user moves Apollo into the background.
*/
public class MusicPlaybackService extends Service {
private static final Logger LOG = Logger.getLogger(MusicPlaybackService.class);
private static final String TAG = "MusicPlaybackService";
private static final boolean D = false;
/**
* Indicates that the music has paused or resumed
*/
public static final String PLAYSTATE_CHANGED = "com.andrew.apollo.playstatechanged";
/**
* Indicates that music playback position within
* a title was changed
*/
public static final String POSITION_CHANGED = "com.android.apollo.positionchanged";
/**
* Indicates the meta data has changed in some way, like a track change
*/
public static final String META_CHANGED = "com.andrew.apollo.metachanged";
/**
* Indicates the queue has been updated
*/
public static final String QUEUE_CHANGED = "com.andrew.apollo.queuechanged";
/**
* Indicates the repeat mode chaned
*/
public static final String REPEATMODE_CHANGED = "com.andrew.apollo.repeatmodechanged";
/**
* Indicates the shuffle mode chaned
*/
public static final String SHUFFLEMODE_CHANGED = "com.andrew.apollo.shufflemodechanged";
/**
* For backwards compatibility reasons, also provide sticky
* broadcasts under the music package
*/
public static final String APOLLO_PACKAGE_NAME = "com.andrew.apollo";
public static final String MUSIC_PACKAGE_NAME = "com.android.music";
/**
* Called to indicate a general service commmand. Used in
* {@link MediaButtonIntentReceiver}
*/
public static final String SERVICECMD = "com.andrew.apollo.musicservicecommand";
/**
* Called to go toggle between pausing and playing the music
*/
public static final String TOGGLEPAUSE_ACTION = "com.andrew.apollo.togglepause";
/**
* Called to go to pause the playback
*/
public static final String PAUSE_ACTION = "com.andrew.apollo.pause";
/**
* Called to go to stop the playback
*/
public static final String STOP_ACTION = "com.andrew.apollo.stop";
/**
* Called to go to the previous track
*/
public static final String PREVIOUS_ACTION = "com.andrew.apollo.previous";
/**
* Called to go to the next track
*/
public static final String NEXT_ACTION = "com.andrew.apollo.next";
/**
* Called to change the repeat mode
*/
public static final String REPEAT_ACTION = "com.andrew.apollo.repeat";
/**
* Called to change the shuffle mode
*/
public static final String SHUFFLE_ACTION = "com.andrew.apollo.shuffle";
/**
* Called to update the service about the foreground state of Apollo's activities
*/
public static final String FOREGROUND_STATE_CHANGED = "com.andrew.apollo.fgstatechanged";
public static final String NOW_IN_FOREGROUND = "nowinforeground";
public static final String FROM_MEDIA_BUTTON = "frommediabutton";
/**
* Used to easily notify a list that it should refresh. i.e. A playlist
* changes
*/
public static final String REFRESH = "com.andrew.apollo.refresh";
/**
* Used by the alarm intent to shutdown the service after being idle
*/
private static final String SHUTDOWN = "com.andrew.apollo.shutdown";
/**
* Called to update the remote control client
*/
public static final String UPDATE_LOCKSCREEN = "com.andrew.apollo.updatelockscreen";
public static final String CMDNAME = "command";
public static final String CMDTOGGLEPAUSE = "togglepause";
public static final String CMDSTOP = "stop";
public static final String CMDPAUSE = "pause";
public static final String CMDPLAY = "play";
public static final String CMDPREVIOUS = "previous";
public static final String CMDNEXT = "next";
public static final String CMDNOTIF = "buttonId";
private static final int IDCOLIDX = 0;
/**
* Moves a list to the front of the queue
*/
public static final int NOW = 1;
/**
* Moves a list to the next position in the queue
*/
public static final int NEXT = 2;
/**
* Moves a list to the last position in the queue
*/
public static final int LAST = 3;
/**
* Shuffles no songs, turns shuffling off
*/
public static final int SHUFFLE_NONE = 0;
/**
* Shuffles all songs
*/
public static final int SHUFFLE_NORMAL = 1;
/**
* Party shuffle
*/
public static final int SHUFFLE_AUTO = 2;
/**
* Turns repeat off
*/
public static final int REPEAT_NONE = 0;
/**
* Repeats the current track in a list
*/
public static final int REPEAT_CURRENT = 1;
/**
* Repeats all the tracks in a list
*/
public static final int REPEAT_ALL = 2;
/**
* Indicates when the track ends
*/
private static final int TRACK_ENDED = 1;
/**
* Indicates that the current track was changed the next track
*/
private static final int TRACK_WENT_TO_NEXT = 2;
/**
* Indicates when the release the wake lock
*/
private static final int RELEASE_WAKELOCK = 3;
/**
* Indicates the player died
*/
private static final int SERVER_DIED = 4;
/**
* Indicates some sort of focus change, maybe a phone call
*/
private static final int FOCUSCHANGE = 5;
/**
* Indicates to fade the volume down
*/
private static final int FADEDOWN = 6;
/**
* Indicates to fade the volume back up
*/
private static final int FADEUP = 7;
/**
* Idle time before stopping the foreground notfication (1 minute)
*/
private static final int IDLE_DELAY = 60000;
/**
* Song play time used as threshold for rewinding to the beginning of the
* track instead of skipping to the previous track when getting the PREVIOUS
* command
*/
private static final long REWIND_INSTEAD_PREVIOUS_THRESHOLD = 3000;
/**
* The max size allowed for the track history
*/
private static final int MAX_HISTORY_SIZE = 100;
/**
* The columns used to retrieve any info from the current track
*/
private static final String[] PROJECTION = 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.ALBUM_ID,
MediaStore.Audio.Media.ARTIST_ID
};
/**
* The columns used to retrieve any info from the current album
*/
private static final String[] ALBUM_PROJECTION = new String[] {
MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST,
MediaStore.Audio.Albums.LAST_YEAR
};
/**
* Keeps a mapping of the track history
*/
private static final LinkedList<Integer> mHistory = Lists.newLinkedList();
/**
* Used to shuffle the tracks
*/
private static final Shuffler mShuffler = new Shuffler();
/**
* Used to save the queue as 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
*/
private static final char HEX_DIGITS[] = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
/**
* 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();
/**
* Recently listened widget
*/
private final RecentWidgetProvider mRecentWidgetProvider = RecentWidgetProvider.getInstance();
/**
* The media player
*/
private MultiPlayer mPlayer;
/**
* The path of the current file to play
*/
private String mFileToPlay;
/**
* Keeps the service running when the screen is off
*/
private WakeLock mWakeLock;
/**
* Alarm intent for removing the notification when nothing is playing
* for some time
*/
private AlarmManager mAlarmManager;
private PendingIntent mShutdownIntent;
private boolean mShutdownScheduled;
/**
* 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;
/**
* 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;
/**
* Used to track whether any of Apollo's activities is in the foreground
*/
private boolean mAnyActivityInForeground = false;
/**
* Lock screen controls
*/
private RemoteControlClient mRemoteControlClient;
private ComponentName mMediaButtonReceiverComponent;
// We use this to distinguish between different cards when saving/restoring
// playlists
private int mCardId;
private int mPlayListLen = 0;
private int mPlayPos = -1;
private int mNextPlayPos = -1;
private int mOpenFailedCounter = 0;
private int mMediaMountedCount = 0;
private int mShuffleMode = SHUFFLE_NONE;
private int mRepeatMode = REPEAT_NONE;
private int mServiceStartId = -1;
private long[] mPlayList = null;
private long[] mAutoShuffleList = null;
private MusicPlayerHandler mPlayerHandler;
private BroadcastReceiver mUnmountReceiver = null;
/**
* Image cache
*/
private ImageFetcher mImageFetcher;
/**
* Used to build the notification
*/
private NotificationHelper mNotificationHelper;
/**
* Recently listened database
*/
private RecentStore mRecentsCache;
/**
* Favorites database
*/
private FavoritesStore mFavoritesCache;
private boolean launchPlayerActivity;
/**
* {@inheritDoc}
*/
@Override
public IBinder onBind(final Intent intent) {
if (D) Log.d(TAG, "Service bound, intent = " + intent);
cancelShutdown();
mServiceInUse = true;
return mBinder;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onUnbind(final Intent intent) {
if (D) Log.d(TAG, "Service unbound");
mServiceInUse = false;
saveQueue(true);
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 (mPlayListLen > 0 || mPlayerHandler.hasMessages(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() {
if (D) Log.d(TAG, "Creating service");
super.onCreate();
// Initialize the favorites and recents databases
mRecentsCache = RecentStore.getInstance(this);
mFavoritesCache = FavoritesStore.getInstance(this);
// Initialize the notification helper
mNotificationHelper = new NotificationHelper(this);
// Initialize the image fetcher
mImageFetcher = ImageFetcher.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.
final HandlerThread thread = new HandlerThread("MusicPlayerHandler",
android.os.Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
// Initialize the handler
mPlayerHandler = new MusicPlayerHandler(this, thread.getLooper());
// Initialize the audio manager and register any headset controls for
// playback
mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
mMediaButtonReceiverComponent = new ComponentName(getPackageName(),
MediaButtonIntentReceiver.class.getName());
mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent);
// Use the remote control APIs to set the playback state
setUpRemoteControlClient();
// Initialize the preferences
mPreferences = getSharedPreferences("Service", 0);
mCardId = getCardId();
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(SERVICECMD);
filter.addAction(TOGGLEPAUSE_ACTION);
filter.addAction(PAUSE_ACTION);
filter.addAction(STOP_ACTION);
filter.addAction(NEXT_ACTION);
filter.addAction(PREVIOUS_ACTION);
filter.addAction(REPEAT_ACTION);
filter.addAction(SHUFFLE_ACTION);
// Attach the broadcast listener
registerReceiver(mIntentReceiver, filter);
// Initialize the wake lock
final PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
mWakeLock.setReferenceCounted(false);
// Initialize the delayed shutdown intent
final Intent shutdownIntent = new Intent(this, MusicPlaybackService.class);
shutdownIntent.setAction(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(QUEUE_CHANGED);
notifyChange(META_CHANGED);
}
/**
* Initializes the remote control client
*/
private void setUpRemoteControlClient() {
final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setComponent(mMediaButtonReceiverComponent);
mRemoteControlClient = new RemoteControlClient(
PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent,
PendingIntent.FLAG_UPDATE_CURRENT));
mAudioManager.registerRemoteControlClient(mRemoteControlClient);
// Flags for the media transport control that this client supports.
int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS
| RemoteControlClient.FLAG_KEY_MEDIA_NEXT
| RemoteControlClient.FLAG_KEY_MEDIA_PLAY
| RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
| RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
| RemoteControlClient.FLAG_KEY_MEDIA_STOP;
if (SystemUtils.hasJellyBeanMR2()) {
// flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
//
// mRemoteControlClient.setOnGetPlaybackPositionListener(
// new RemoteControlClient.OnGetPlaybackPositionListener() {
// @Override
// public long onGetPlaybackPosition() {
// return position();
// }
// });
// mRemoteControlClient.setPlaybackPositionUpdateListener(
// new RemoteControlClient.OnPlaybackPositionUpdateListener() {
// @Override
// public void onPlaybackPositionUpdate(long newPositionMs) {
// seek(newPositionMs);
// }
// });
}
mRemoteControlClient.setTransportControlFlags(flags);
}
/**
* {@inheritDoc}
*/
@Override
public void onDestroy() {
if (D) Log.d(TAG, "Destroying service");
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);
// Release the player
if (mPlayer != null) {
mPlayer.release();
mPlayer = null;
}
// Remove the audio focus listener and lock screen controls
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
// Remove any callbacks from the handler
mPlayerHandler.removeCallbacksAndMessages(null);
mPlayerHandler.getLooper().quit();
// Close the cursor
closeCursor();
// Unregister the mount listener
unregisterReceiver(mIntentReceiver);
if (mUnmountReceiver != null) {
unregisterReceiver(mUnmountReceiver);
mUnmountReceiver = null;
}
// Release the wake lock
mWakeLock.release();
}
/**
* {@inheritDoc}
*/
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (D) Log.d(TAG, "Got new intent " + intent + ", startId = " + startId);
mServiceStartId = startId;
if (intent != null) {
final String action = intent.getAction();
if (intent.hasExtra(NOW_IN_FOREGROUND)) {
mAnyActivityInForeground = intent.getBooleanExtra(NOW_IN_FOREGROUND, false);
updateNotification();
}
if (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(FROM_MEDIA_BUTTON, false)) {
MediaButtonIntentReceiver.completeWakefulIntent(intent);
}
return START_STICKY;
}
private void releaseServiceUiAndStop() {
if (isPlaying()
|| mPausedByTransientLossOfFocus
|| mPlayerHandler.hasMessages(TRACK_ENDED)) {
return;
}
if (D) Log.d(TAG, "Nothing is playing anymore, releasing notification");
mNotificationHelper.killNotification();
mAudioManager.abandonAudioFocus(mAudioFocusListener);
if (!mServiceInUse) {
saveQueue(true);
stopSelf(mServiceStartId);
}
}
private void handleCommandIntent(Intent intent) {
final String action = intent.getAction();
final String command = SERVICECMD.equals(action) ? intent.getStringExtra(CMDNAME) : null;
if (D) Log.d(TAG, "handleCommandIntent: action = " + action + ", command = " + command);
if (CMDNEXT.equals(command) || NEXT_ACTION.equals(action)) {
gotoNext(true);
} else if (CMDPREVIOUS.equals(command) || PREVIOUS_ACTION.equals(action)) {
if (position() < REWIND_INSTEAD_PREVIOUS_THRESHOLD) {
prev();
} else {
seek(0);
play();
}
} else if (CMDTOGGLEPAUSE.equals(command) || TOGGLEPAUSE_ACTION.equals(action)) {
if (isPlaying()) {
pause();
mPausedByTransientLossOfFocus = false;
} else {
play();
}
} else if (CMDPAUSE.equals(command) || PAUSE_ACTION.equals(action)) {
pause();
mPausedByTransientLossOfFocus = false;
} else if (CMDPLAY.equals(command)) {
play();
} else if (CMDSTOP.equals(command) || STOP_ACTION.equals(action)) {
pause();
mPausedByTransientLossOfFocus = false;
seek(0);
releaseServiceUiAndStop();
} else if (REPEAT_ACTION.equals(action)) {
cycleRepeat();
} else if (SHUFFLE_ACTION.equals(action)) {
cycleShuffle();
}
}
/**
* Updates the notification, considering the current play and activity state
*/
private void updateNotification() {
if (!mAnyActivityInForeground && isPlaying()) {
mNotificationHelper.buildNotification(getAlbumName(), getArtistName(),
getTrackName(), getAlbumId(), getAlbumArt(), isPlaying());
} else if (mAnyActivityInForeground) {
mNotificationHelper.killNotification();
}
}
/**
* @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(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(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() {
if (D) Log.v(TAG, "Scheduling shutdown in " + IDLE_DELAY + " ms");
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + IDLE_DELAY, mShutdownIntent);
mShutdownScheduled = true;
}
private void cancelShutdown() {
if (D) Log.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) {
if (D) Log.d(TAG, "Stopping playback, goToIdle = " + goToIdle);
if (mPlayer!=null && mPlayer.isInitialized()) {
mPlayer.stop();
}
mFileToPlay = null;
closeCursor();
if (goToIdle) {
scheduleDelayedShutdown();
mIsSupposedToBePlaying = false;
} else {
stopForeground(false);
}
}
public void shutdown() {
stopForeground(true);
stopSelf();
}
/**
* 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 >= mPlayListLen) {
last = mPlayListLen - 1;
}
boolean gotonext = false;
if (first <= mPlayPos && mPlayPos <= last) {
mPlayPos = first;
gotonext = true;
} else if (mPlayPos > last) {
mPlayPos -= last - first + 1;
}
final 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;
closeCursor();
} else {
if (mShuffleMode != SHUFFLE_NONE) {
mPlayPos = getNextPosition(true);
} else if (mPlayPos >= mPlayListLen) {
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) {
final int addlen = list.length;
if (position < 0) {
mPlayListLen = 0;
position = 0;
}
ensurePlayListCapacity(mPlayListLen + addlen);
if (position > mPlayListLen) {
position = mPlayListLen;
}
final int tailsize = mPlayListLen - position;
for (int i = tailsize; i > 0; i--) {
mPlayList[position + i] = mPlayList[position + i - addlen];
}
for (int i = 0; i < addlen; i++) {
mPlayList[position + i] = list[i];
}
mPlayListLen += addlen;
if (mPlayListLen == 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,
PROJECTION, selection, selectionArgs);
}
long albumId = getAlbumId();
if (albumId >= 0) {
mAlbumCursor = openCursorAndGoToFirst(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
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);
if (c == null) {
return null;
}
if (!c.moveToFirst()) {
c.close();
return null;
}
return c;
}
private 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 (mPlayListLen == 0) {
return;
}
stop(false);
updateCursor(mPlayList[mPlayPos]);
while (true) {
if (mCursor != null
&& openFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/"
+ mCursor.getLong(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 && mPlayListLen > 1) {
final int pos = getNextPosition(false);
if (pos < 0) {
scheduleDelayedShutdown();
if (mIsSupposedToBePlaying) {
mIsSupposedToBePlaying = false;
notifyChange(PLAYSTATE_CHANGED);
}
return;
}
mPlayPos = pos;
stop(false);
mPlayPos = pos;
updateCursor(mPlayList[mPlayPos]);
} else {
mOpenFailedCounter = 0;
Log.w(TAG, "Failed to open file for playback");
scheduleDelayedShutdown();
if (mIsSupposedToBePlaying) {
mIsSupposedToBePlaying = false;
notifyChange(PLAYSTATE_CHANGED);
}
return;
}
}
if (openNext) {
setNextTrack();
}
}
}
/**
* @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) {
if (!force && mRepeatMode == REPEAT_CURRENT) {
if (mPlayPos < 0) {
return 0;
}
return mPlayPos;
} else if (mShuffleMode == SHUFFLE_NORMAL) {
if (mPlayPos >= 0) {
mHistory.add(mPlayPos);
}
if (mHistory.size() > MAX_HISTORY_SIZE) {
mHistory.remove(0);
}
final int numTracks = mPlayListLen;
final int[] tracks = new int[numTracks];
for (int i = 0; i < numTracks; i++) {
tracks[i] = i;
}
final int numHistory = mHistory.size();
int numUnplayed = numTracks;
for (int i = 0; i < numHistory; i++) {
final int idx = mHistory.get(i).intValue();
if (idx < numTracks && tracks[idx] >= 0) {
numUnplayed--;
tracks[idx] = -1;
}
}
if (numUnplayed <= 0) {
if (mRepeatMode == REPEAT_ALL || force) {
numUnplayed = numTracks;
for (int i = 0; i < numTracks; i++) {
tracks[i] = i;
}
} else {
return -1;
}
}
int skip = 0;
if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
skip = mShuffler.nextInt(numUnplayed);
}
int cnt = -1;
while (true) {
while (tracks[++cnt] < 0) {
;
}
skip--;
if (skip < 0) {
break;
}
}
return cnt;
} else if (mShuffleMode == SHUFFLE_AUTO) {
doAutoShuffleUpdate();
return mPlayPos + 1;
} else {
if (mPlayPos >= mPlayListLen - 1) {
if (mRepeatMode == REPEAT_NONE && !force) {
return -1;
} else if (mRepeatMode == REPEAT_ALL || force) {
return 0;
}
return -1;
} else {
return mPlayPos + 1;
}
}
}
/**
* Sets the track track to be played
*/
private void setNextTrack() {
mNextPlayPos = getNextPosition(false);
if (D) Log.d(TAG, "setNextTrack: next play position = " + mNextPlayPos);
if (mNextPlayPos >= 0 && mPlayList != null) {
final long id = mPlayList[mNextPlayPos];
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 - (mPlayListLen - (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() > MAX_HISTORY_SIZE) {
mHistory.remove(0);
}
ensurePlayListCapacity(mPlayListLen + 1);
mPlayList[mPlayListLen++] = mAutoShuffleList[idx];
notify = true;
}
if (notify) {
notifyChange(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;
}
/**
* Makes sure the playlist has enough space to hold all of the songs
*
* @param size The size of the playlist
*/
private void ensurePlayListCapacity(final 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
final long[] newlist = new long[size * 2];
final int len = mPlayList != null ? mPlayList.length : mPlayListLen;
for (int i = 0; i < len; i++) {
newlist[i] = mPlayList[i];
}
mPlayList = newlist;
}
// FIXME: shrink the array when the needed size is much smaller
// than the allocated size
}
/**
* Notify the change-receivers that something has changed.
*/
private void notifyChange(final String what) {
if (D) Log.d(TAG, "notifyChange: what = " + what);
// Update the lockscreen controls
updateRemoteControlClient(what);
if (what.equals(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());
intent.putExtra("isfavorite", isFavorite());
sendStickyBroadcast(intent);
final Intent musicIntent = new Intent(intent);
musicIntent.setAction(what.replace(APOLLO_PACKAGE_NAME, MUSIC_PACKAGE_NAME));
sendStickyBroadcast(musicIntent);
if (what.equals(META_CHANGED)) {
// Increase the play count for favorite songs.
if (mFavoritesCache.getSongId(getAudioId()) != null) {
mFavoritesCache.addSongId(getAudioId(), getTrackName(), getAlbumName(),
getArtistName());
}
// Add the track to the recently played list.
mRecentsCache.addAlbumId(getAlbumId(), getAlbumName(), getArtistName(),
MusicUtils.getSongCountForAlbum(this, getAlbumId()),
MusicUtils.getReleaseDateForAlbum(this, getAlbumId()));
} else if (what.equals(QUEUE_CHANGED)) {
saveQueue(true);
if (isPlaying()) {
setNextTrack();
}
} else {
saveQueue(false);
}
if (what.equals(PLAYSTATE_CHANGED)) {
mNotificationHelper.updatePlayState(isPlaying());
}
// Update the app-widgets
mAppWidgetSmall.notifyChange(this, what);
mAppWidgetLarge.notifyChange(this, what);
mAppWidgetLargeAlternate.notifyChange(this, what);
mRecentWidgetProvider.notifyChange(this, what);
}
/**
* Updates the lockscreen controls.
*
* @param what The broadcast
*/
private void updateRemoteControlClient(final String what) {
int playState = mIsSupposedToBePlaying
? RemoteControlClient.PLAYSTATE_PLAYING
: RemoteControlClient.PLAYSTATE_PAUSED;
if (SystemUtils.hasJellyBeanMR2()
&& (what.equals(PLAYSTATE_CHANGED) || what.equals(POSITION_CHANGED))) {
//mRemoteControlClient.setPlaybackState(playState, position(), 1.0f);
mRemoteControlClient.setPlaybackState(playState);
} else if (what.equals(PLAYSTATE_CHANGED)) {
mRemoteControlClient.setPlaybackState(playState);
} else if (what.equals(META_CHANGED) || what.equals(QUEUE_CHANGED)) {
Bitmap albumArt = getAlbumArt();
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);
}
mRemoteControlClient
.editMetadata(true)
.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, getArtistName())
.putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST,
getAlbumArtistName())
.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, getAlbumName())
.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, getTrackName())
.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration())
.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, albumArt)
.apply();
if (SystemUtils.hasJellyBeanMR2()) {
//mRemoteControlClient.setPlaybackState(playState, position(), 1.0f);
mRemoteControlClient.setPlaybackState(playState);
}
}
}
/**
* Saves the queue
*
* @param full True if the queue is full
*/
private void saveQueue(final boolean full) {
if (!mQueueIsSaveable) {
return;
}
final SharedPreferences.Editor editor = mPreferences.edit();
if (full) {
final StringBuilder q = new StringBuilder();
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) {
final int digit = (int)(n & 0xf);
n >>>= 4;
q.append(HEX_DIGITS[digit]);
}
q.append(";");
}
}
editor.putString("queue", q.toString());
editor.putInt("cardid", mCardId);
if (mShuffleMode != SHUFFLE_NONE) {
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) {
final int digit = n & 0xf;
n >>>= 4;
q.append(HEX_DIGITS[digit]);
}
q.append(";");
}
}
editor.putString("history", q.toString());
}
}
editor.putInt("curpos", mPlayPos);
if (mPlayer!=null && 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() {
String q = null;
int id = mCardId;
if (mPreferences.contains("cardid")) {
id = mPreferences.getInt("cardid", ~mCardId);
}
if (id == mCardId) {
q = mPreferences.getString("queue", "");
}
int qlen = q != null ? q.length() : 0;
if (qlen > 1) {
int plen = 0;
int n = 0;
int shift = 0;
for (int i = 0; i < qlen; i++) {
final 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 {
plen = 0;
break;
}
shift += 4;
}
}
mPlayListLen = plen;
final int pos = mPreferences.getInt("curpos", 0);
if (pos < 0 || pos >= mPlayListLen) {
mPlayListLen = 0;
return;
}
mPlayPos = pos;
updateCursor(mPlayList[mPlayPos]);
if (mCursor == null) {
SystemClock.sleep(3000);
updateCursor(mPlayList[mPlayPos]);
}
synchronized (this) {
closeCursor();
mOpenFailedCounter = 20;
openCurrentAndNext();
}
if (mPlayer==null || !mPlayer.isInitialized()) {
mPlayListLen = 0;
return;
}
final long seekpos = mPreferences.getLong("seekpos", 0);
seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
if (D) {
Log.d(TAG, "restored queue, currently at position "
+ position() + "/" + duration()
+ " (requested " + seekpos + ")");
}
int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
repmode = REPEAT_NONE;
}
mRepeatMode = repmode;
int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
shufmode = SHUFFLE_NONE;
}
if (shufmode != SHUFFLE_NONE) {
q = mPreferences.getString("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++) {
final char c = q.charAt(i);
if (c == ';') {
if (n >= mPlayListLen) {
mHistory.clear();
break;
}
mHistory.add(n);
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 {
mHistory.clear();
break;
}
shift += 4;
}
}
}
}
if (shufmode == SHUFFLE_AUTO) {
if (!makeAutoShuffleList()) {
shufmode = 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) {
if (D) Log.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) {
//final 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 {
updateCursor(where, selectionArgs);
if (mCursor != null) {
ensurePlayListCapacity(1);
mPlayListLen = 1;
mPlayList[0] = mCursor.getLong(IDCOLIDX);
mPlayPos = 0;
}
} catch (final UnsupportedOperationException ex) {
}
}
mFileToPlay = path;
mPlayer.setDataSource(mFileToPlay);
if (mPlayer.isInitialized()) {
mOpenFailedCounter = 0;
return true;
}
stop(true);
return false;
}
}
/**
* 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 < mPlayListLen; i++) {
if (mPlayList[i] == id) {
numremoved += removeTracksInternal(i, i);
i--;
}
}
}
if (numremoved > 0) {
notifyChange(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(final int first, final int last) {
final int numremoved = removeTracksInternal(first, last);
if (numremoved > 0) {
notifyChange(QUEUE_CHANGED);
}
return numremoved;
}
/**
* Returns the position in the queue
*
* @return the current position in the queue
*/
public int getQueuePosition() {
synchronized (this) {
return mPlayPos;
}
}
/**
* 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(AudioColumns.DATA));
}
}
/**
* Returns the album name
*
* @return The current song album Name
*/
public String getAlbumName() {
synchronized (this) {
if (mCursor == null) {
return null;
}
try {
return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.ALBUM));
} catch (Throwable e) {
LOG.error("Error getting album name", e);
return "--";
}
}
}
/**
* Returns the song name
*
* @return The current song name
*/
public String getTrackName() {
synchronized (this) {
if (mCursor == null) {
return null;
}
try {
return mCursor.getString(mCursor.getColumnIndexOrThrow(AudioColumns.TITLE));
} catch (Throwable e) {
LOG.error("Error getting track name", e);
return "--";
}
}
}
/**
* 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(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(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(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(AudioColumns.ARTIST_ID));
}
}
/**
* Returns the current audio ID
*
* @return The current track ID
*/
public long getAudioId() {
synchronized (this) {
if (mPlayPos >= 0 && mPlayer != null && mPlayer.isInitialized()) {
return mPlayList[mPlayPos];
}
}
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!= null && mPlayer.isInitialized()) {
if (position < 0) {
position = 0;
} else if (position > mPlayer.duration()) {
position = mPlayer.duration();
}
long result = mPlayer.seek(position);
notifyChange(POSITION_CHANGED);
return result;
}
return -1;
}
/**
* Returns the current position in time of the currenttrack
*
* @return The current playback position in miliseconds
*/
public long position() {
if (mPlayer!=null && 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 != null && mPlayer.isInitialized()) {
return mPlayer.duration();
}
return -1;
}
/**
* Returns the queue
*
* @return The queue as a long[]
*/
public long[] getQueue() {
synchronized (this) {
final int len = mPlayListLen;
final long[] list = new long[len];
for (int i = 0; i < len; i++) {
list[i] = mPlayList[i];
}
return list;
}
}
/**
* @return True if music is playing, false otherwise
*/
public boolean isPlaying() {
return mIsSupposedToBePlaying;
}
/**
* True if the current track is a "favorite", false otherwise
*/
public boolean isFavorite() {
if (mFavoritesCache != null) {
synchronized (this) {
final Long id = mFavoritesCache.getSongId(getAudioId());
return id != null ? true : false;
}
}
return false;
}
/**
* 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) {
launchPlayerActivity = true;
synchronized (this) {
if (mShuffleMode == SHUFFLE_AUTO) {
mShuffleMode = SHUFFLE_NORMAL;
}
final long oldId = getAudioId();
final int listlength = list.length;
boolean newlist = true;
if (mPlayListLen == listlength) {
newlist = false;
for (int i = 0; i < listlength; i++) {
if (list[i] != mPlayList[i]) {
newlist = true;
break;
}
}
}
if (newlist) {
addToPlayList(list, -1);
notifyChange(QUEUE_CHANGED);
}
if (position >= 0) {
mPlayPos = position;
} else {
mPlayPos = mShuffler.nextInt(mPlayListLen);
}
mHistory.clear();
openCurrentAndNext();
if (oldId != getAudioId()) {
notifyChange(META_CHANGED);
}
}
}
/**
* Stops playback.
*/
public void stop() {
stop(true);
}
/**
* Resumes or starts playback.
*/
public void play() {
int status = mAudioManager.requestAudioFocus(mAudioFocusListener,
AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
if (D) Log.d(TAG, "Starting playback: audio focus request status = " + status);
if (status != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
return;
}
mAudioManager.registerMediaButtonEventReceiver(new ComponentName(getPackageName(),
MediaButtonIntentReceiver.class.getName()));
setNextTrack();
if (mPlayer!=null && mPlayer.isInitialized()) {
final long duration = mPlayer.duration();
if (mRepeatMode != REPEAT_CURRENT && duration > 2000
&& mPlayer.position() >= duration - 2000) {
gotoNext(true);
}
mPlayer.start();
mPlayerHandler.removeMessages(FADEDOWN);
mPlayerHandler.sendEmptyMessage(FADEUP);
if (!mIsSupposedToBePlaying) {
mIsSupposedToBePlaying = true;
notifyChange(PLAYSTATE_CHANGED);
}
cancelShutdown();
updateNotification();
} else if (mPlayListLen <= 0) {
setShuffleMode(SHUFFLE_AUTO);
}
}
/**
* Temporarily pauses playback.
*/
public void pause() {
if (D) Log.d(TAG, "Pausing playback");
synchronized (this) {
mPlayerHandler.removeMessages(FADEUP);
if (mIsSupposedToBePlaying) {
mPlayer.pause();
scheduleDelayedShutdown();
mIsSupposedToBePlaying = false;
notifyChange(PLAYSTATE_CHANGED);
}
}
}
/**
* Changes from the current track to the next track
*/
public void gotoNext(final boolean force) {
if (D) Log.d(TAG, "Going to next track");
synchronized (this) {
if (mPlayListLen <= 0) {
if (D) Log.d(TAG, "No play queue");
scheduleDelayedShutdown();
return;
}
final int pos = getNextPosition(force);
if (pos < 0) {
scheduleDelayedShutdown();
if (mIsSupposedToBePlaying) {
mIsSupposedToBePlaying = false;
notifyChange(PLAYSTATE_CHANGED);
}
return;
}
mPlayPos = pos;
stop(false);
mPlayPos = pos;
openCurrentAndNext();
play();
notifyChange(META_CHANGED);
}
}
/**
* Changes from the current track to the previous played track
*/
public void prev() {
if (D) Log.d(TAG, "Going to previous track");
synchronized (this) {
if (mShuffleMode == SHUFFLE_NORMAL) {
// Go to previously-played track and remove it from the history
final int histsize = mHistory.size();
if (histsize == 0) {
return;
}
final Integer pos = mHistory.remove(histsize - 1);
mPlayPos = pos.intValue();
} else {
if (mPlayPos > 0) {
mPlayPos--;
} else {
mPlayPos = mPlayListLen - 1;
}
}
stop(false);
openCurrent();
play();
notifyChange(META_CHANGED);
}
}
/**
* 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);
}
/**
* Toggles the current song as a favorite.
*/
public void toggleFavorite() {
if (mFavoritesCache != null) {
synchronized (this) {
mFavoritesCache.toggleSong(getAudioId(), getTrackName(), getAlbumName(),
getArtistName());
}
}
}
/**
* Moves an item in the queue from one position to another
*
* @param from The position the item is currently at
* @param to The position the item is being moved to
*/
public void moveQueueItem(int index1, int index2) {
synchronized (this) {
if (index1 >= mPlayListLen) {
index1 = mPlayListLen - 1;
}
if (index2 >= mPlayListLen) {
index2 = mPlayListLen - 1;
}
if (index1 < index2) {
final long tmp = mPlayList[index1];
for (int i = index1; i < index2; i++) {
mPlayList[i] = mPlayList[i + 1];
}
mPlayList[index2] = tmp;
if (mPlayPos == index1) {
mPlayPos = index2;
} else if (mPlayPos >= index1 && mPlayPos <= index2) {
mPlayPos--;
}
} else if (index2 < index1) {
final long tmp = mPlayList[index1];
for (int i = index1; i > index2; i--) {
mPlayList[i] = mPlayList[i - 1];
}
mPlayList[index2] = tmp;
if (mPlayPos == index1) {
mPlayPos = index2;
} else if (mPlayPos >= index2 && mPlayPos <= index1) {
mPlayPos++;
}
}
notifyChange(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(REPEATMODE_CHANGED);
}
}
/**
* Sets the shuffle mode
*
* @param shufflemode The shuffle mode to use
*/
public void setShuffleMode(final int shufflemode) {
synchronized (this) {
if (mShuffleMode == shufflemode && mPlayListLen > 0) {
return;
}
mShuffleMode = shufflemode;
if (mShuffleMode == SHUFFLE_AUTO) {
if (makeAutoShuffleList()) {
mPlayListLen = 0;
doAutoShuffleUpdate();
mPlayPos = 0;
openCurrentAndNext();
play();
notifyChange(META_CHANGED);
return;
} else {
mShuffleMode = SHUFFLE_NONE;
}
}
saveQueue(false);
notifyChange(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 == 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) {
synchronized (this) {
if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
addToPlayList(list, mPlayPos + 1);
notifyChange(QUEUE_CHANGED);
} else {
addToPlayList(list, Integer.MAX_VALUE);
notifyChange(QUEUE_CHANGED);
if (action == NOW) {
mPlayPos = mPlayListLen - list.length;
openCurrentAndNext();
play();
notifyChange(META_CHANGED);
return;
}
}
if (mPlayPos < 0) {
mPlayPos = 0;
openCurrentAndNext();
play();
notifyChange(META_CHANGED);
}
}
}
/**
* Cycles through the different repeat modes
*/
private void cycleRepeat() {
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);
}
}
/**
* Cycles through the different shuffle modes
*/
private void cycleShuffle() {
if (mShuffleMode == SHUFFLE_NONE) {
setShuffleMode(SHUFFLE_NORMAL);
if (mRepeatMode == REPEAT_CURRENT) {
setRepeatMode(REPEAT_ALL);
}
} else if (mShuffleMode == SHUFFLE_NORMAL || mShuffleMode == SHUFFLE_AUTO) {
setShuffleMode(SHUFFLE_NONE);
}
}
/**
* @return The album art for the current album.
*/
public Bitmap getAlbumArt() {
// Return the cached artwork
final Bitmap bitmap = mImageFetcher.getArtwork(getAlbumName(),
getAlbumId(), getArtistName());
return bitmap;
}
/**
* Called when one of the lists should refresh or requery.
*/
public void refresh() {
notifyChange(REFRESH);
}
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
/**
* {@inheritDoc}
*/
@Override
public void onReceive(final Context context, final Intent intent) {
final String command = intent.getStringExtra(CMDNAME);
if (AppWidgetSmall.CMDAPPWIDGETUPDATE.equals(command)) {
final int[] small = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
mAppWidgetSmall.performUpdate(MusicPlaybackService.this, small);
} else if (AppWidgetLarge.CMDAPPWIDGETUPDATE.equals(command)) {
final int[] large = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
mAppWidgetLarge.performUpdate(MusicPlaybackService.this, large);
} else if (AppWidgetLargeAlternate.CMDAPPWIDGETUPDATE.equals(command)) {
final int[] largeAlt = intent
.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
mAppWidgetLargeAlternate.performUpdate(MusicPlaybackService.this, largeAlt);
} else if (RecentWidgetProvider.CMDAPPWIDGETUPDATE.equals(command)) {
final int[] recent = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
mRecentWidgetProvider.performUpdate(MusicPlaybackService.this, recent);
} else {
handleCommandIntent(intent);
}
}
};
private final OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
/**
* {@inheritDoc}
*/
@Override
public void onAudioFocusChange(final int focusChange) {
mPlayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();
}
};
private static final class MusicPlayerHandler extends Handler {
private final WeakReference<MusicPlaybackService> mService;
private float mCurrentVolume = 1.0f;
/**
* Constructor of <code>MusicPlayerHandler</code>
*
* @param service The service to use.
* @param looper The thread to run on.
*/
public MusicPlayerHandler(final MusicPlaybackService service, final Looper looper) {
super(looper);
mService = new WeakReference<MusicPlaybackService>(service);
}
/**
* {@inheritDoc}
*/
@Override
public void handleMessage(final Message msg) {
final MusicPlaybackService service = mService.get();
if (service == null) {
return;
}
switch (msg.what) {
case FADEDOWN:
mCurrentVolume -= .05f;
if (mCurrentVolume > .2f) {
sendEmptyMessageDelayed(FADEDOWN, 10);
} else {
mCurrentVolume = .2f;
}
service.mPlayer.setVolume(mCurrentVolume);
break;
case FADEUP:
mCurrentVolume += .01f;
if (mCurrentVolume < 1.0f) {
sendEmptyMessageDelayed(FADEUP, 10);
} else {
mCurrentVolume = 1.0f;
}
service.mPlayer.setVolume(mCurrentVolume);
break;
case SERVER_DIED:
if (service.isPlaying()) {
service.gotoNext(true);
} else {
service.openCurrentAndNext();
}
break;
case TRACK_WENT_TO_NEXT:
service.mPlayPos = service.mNextPlayPos;
if (service.mCursor != null) {
service.mCursor.close();
}
service.updateCursor(service.mPlayList[service.mPlayPos]);
service.notifyChange(META_CHANGED);
service.updateNotification();
service.setNextTrack();
break;
case TRACK_ENDED:
if (service.mRepeatMode == REPEAT_CURRENT) {
service.seek(0);
service.play();
} else {
service.gotoNext(false);
}
break;
case RELEASE_WAKELOCK:
service.mWakeLock.release();
break;
case FOCUSCHANGE:
if (D) Log.d(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(FADEUP);
sendEmptyMessage(FADEDOWN);
break;
case AudioManager.AUDIOFOCUS_GAIN:
if (!service.isPlaying()
&& service.mPausedByTransientLossOfFocus) {
service.mPausedByTransientLossOfFocus = false;
mCurrentVolume = 0f;
service.mPlayer.setVolume(mCurrentVolume);
service.play();
} else {
removeMessages(FADEDOWN);
sendEmptyMessage(FADEUP);
}
break;
default:
}
break;
default:
break;
}
}
}
private static final class AudioOnPreparedListener implements OnPreparedListener {
private WeakReference<MusicPlaybackService> serviceRef;
public AudioOnPreparedListener(WeakReference<MusicPlaybackService> serviceRef) {
this.serviceRef = serviceRef;
}
@Override
public void onPrepared(MediaPlayer mp) {
if (Ref.alive(serviceRef) && serviceRef.get().launchPlayerActivity) {
serviceRef.get().launchPlayerActivity = false;
Intent i = new Intent(serviceRef.get(), AudioPlayerActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
serviceRef.get().startActivity(i);
}
}
}
private static final class Shuffler {
private final LinkedList<Integer> mHistoryOfNumbers = new LinkedList<Integer>();
private final TreeSet<Integer> mPreviousNumbers = new TreeSet<Integer>();
private final Random mRandom = new Random();
private int mPrevious;
/**
* Constructor of <code>Shuffler</code>
*/
public Shuffler() {
super();
}
/**
* @param interval The length the queue
* @return The position of the next track to play
*/
public int nextInt(final int interval) {
if (interval <= 0) {
return 0;
}
int next;
do {
next = mRandom.nextInt(interval);
} while (next == mPrevious && interval > 1
&& !mPreviousNumbers.contains(Integer.valueOf(next)));
mPrevious = next;
mHistoryOfNumbers.add(mPrevious);
mPreviousNumbers.add(mPrevious);
cleanUpHistory();
return next;
}
/**
* Removes old tracks and cleans up the history preparing for new tracks
* to be added to the mapping
*/
private void cleanUpHistory() {
if (!mHistoryOfNumbers.isEmpty() && mHistoryOfNumbers.size() >= MAX_HISTORY_SIZE) {
for (int i = 0; i < Math.max(1, MAX_HISTORY_SIZE / 2); i++) {
mPreviousNumbers.remove(mHistoryOfNumbers.removeFirst());
}
}
}
};
private static final class MultiPlayer implements MediaPlayer.OnErrorListener,
MediaPlayer.OnCompletionListener {
private final WeakReference<MusicPlaybackService> mService;
private CompatMediaPlayer mCurrentMediaPlayer = new CompatMediaPlayer();
private CompatMediaPlayer mNextMediaPlayer;
private Handler mHandler;
private boolean mIsInitialized = false;
/**
* Constructor of <code>MultiPlayer</code>
*/
public MultiPlayer(final MusicPlaybackService service) {
mService = new WeakReference<MusicPlaybackService>(service);
mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
}
/**
* @param path The path of the file, or the http/rtsp URL of the stream
* you want to play
*/
public void setDataSource(final String path) {
mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
if (mIsInitialized) {
setNextDataSource(null);
}
}
/**
* @param player The {@link MediaPlayer} to use
* @param path The path of the file, or the http/rtsp URL of the stream
* you want to play
* @return True if the <code>player</code> has been prepared and is
* ready to play, false otherwise
*/
private boolean setDataSourceImpl(final MediaPlayer player, final String path) {
try {
player.reset();
if (Ref.alive(mService)&& mService.get().launchPlayerActivity) {
player.setOnPreparedListener(new AudioOnPreparedListener(mService));
}
if (path.startsWith("content://")) {
player.setDataSource(mService.get(), Uri.parse(path));
} else {
player.setDataSource(path);
}
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.prepare();
} catch (final IOException todo) {
// TODO: notify the user why the file couldn't be opened
return false;
} catch (final IllegalArgumentException todo) {
// TODO: notify the user why the file couldn't be opened
return false;
} catch (Throwable e) {
// TODO: notify the user why the file couldn't be opened due to an unknown error
return false;
}
player.setOnCompletionListener(this);
player.setOnErrorListener(this);
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, mService.get().getPackageName());
mService.get().sendBroadcast(intent);
return true;
}
/**
* Set the MediaPlayer to start when this MediaPlayer finishes playback.
*
* @param path The path of the file, or the http/rtsp URL of the stream
* you want to play
*/
public void setNextDataSource(final String path) {
try {
mCurrentMediaPlayer.setNextMediaPlayerSupport(null);
} catch (IllegalArgumentException e) {
Log.i(TAG, "Next media player is current one, continuing");
} catch (IllegalStateException e) {
Log.e(TAG, "Media player not initialized!");
return;
} catch (Throwable e) {
Log.e(TAG, "Media player fatal error", e);
return;
}
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
if (path == null) {
return;
}
mNextMediaPlayer = new CompatMediaPlayer();
mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
if (setDataSourceImpl(mNextMediaPlayer, path)) {
try {
mCurrentMediaPlayer.setNextMediaPlayerSupport(mNextMediaPlayer);
} catch (Throwable e) {
Log.e(TAG, "Media player fatal error", e);
return;
}
} else {
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
}
}
/**
* Sets the handler
*
* @param handler The handler to use
*/
public void setHandler(final Handler handler) {
mHandler = handler;
}
/**
* @return True if the player is ready to go, false otherwise
*/
public boolean isInitialized() {
return mIsInitialized;
}
/**
* Starts or resumes playback.
*/
public void start() {
mCurrentMediaPlayer.start();
}
/**
* Resets the MediaPlayer to its uninitialized state.
*/
public void stop() {
mCurrentMediaPlayer.reset();
mIsInitialized = false;
}
/**
* Releases resources associated with this MediaPlayer object.
*/
public void release() {
stop();
mCurrentMediaPlayer.release();
}
/**
* Pauses playback. Call start() to resume.
*/
public void pause() {
mCurrentMediaPlayer.pause();
}
/**
* Gets the duration of the file.
*
* @return The duration in milliseconds
*/
public long duration() {
return mCurrentMediaPlayer.getDuration();
}
/**
* Gets the current playback position.
*
* @return The current position in milliseconds
*/
public long position() {
return mCurrentMediaPlayer.getCurrentPosition();
}
/**
* Gets the current playback position.
*
* @param whereto The offset in milliseconds from the start to seek to
* @return The offset in milliseconds from the start to seek to
*/
public long seek(final long whereto) {
mCurrentMediaPlayer.seekTo((int)whereto);
return whereto;
}
/**
* Sets the volume on this player.
*
* @param vol Left and right volume scalar
*/
public void setVolume(final float vol) {
mCurrentMediaPlayer.setVolume(vol, vol);
}
/**
* Returns the audio session ID.
*
* @return The current audio session ID.
*/
public int getAudioSessionId() {
return mCurrentMediaPlayer.getAudioSessionId();
}
/**
* {@inheritDoc}
*/
@Override
public boolean onError(final MediaPlayer mp, final int what, final int extra) {
switch (what) {
case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
mIsInitialized = false;
mCurrentMediaPlayer.release();
mCurrentMediaPlayer = new CompatMediaPlayer();
mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK);
mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
return true;
default:
break;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void onCompletion(final MediaPlayer mp) {
if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
mCurrentMediaPlayer.release();
mCurrentMediaPlayer = mNextMediaPlayer;
mNextMediaPlayer = null;
mHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT);
} else {
mService.get().mWakeLock.acquire(30000);
mHandler.sendEmptyMessage(TRACK_ENDED);
mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
}
}
}
private static final class ServiceStub extends IApolloService.Stub {
private final WeakReference<MusicPlaybackService> mService;
private ServiceStub(final MusicPlaybackService service) {
mService = new WeakReference<MusicPlaybackService>(service);
}
/**
* {@inheritDoc}
*/
@Override
public void openFile(final String path) throws RemoteException {
mService.get().openFile(path);
}
/**
* {@inheritDoc}
*/
@Override
public void open(final long[] list, final int position) throws RemoteException {
mService.get().open(list, position);
}
/**
* {@inheritDoc}
*/
@Override
public void stop() throws RemoteException {
mService.get().stop();
}
/**
* {@inheritDoc}
*/
@Override
public void pause() throws RemoteException {
mService.get().pause();
}
/**
* {@inheritDoc}
*/
@Override
public void play() throws RemoteException {
mService.get().play();
}
/**
* {@inheritDoc}
*/
@Override
public void prev() throws RemoteException {
mService.get().prev();
}
/**
* {@inheritDoc}
*/
@Override
public void next() throws RemoteException {
mService.get().gotoNext(true);
}
/**
* {@inheritDoc}
*/
@Override
public void enqueue(final long[] list, final int action) throws RemoteException {
mService.get().enqueue(list, action);
}
/**
* {@inheritDoc}
*/
@Override
public void setQueuePosition(final int index) throws RemoteException {
mService.get().setQueuePosition(index);
}
/**
* {@inheritDoc}
*/
@Override
public void setShuffleMode(final int shufflemode) throws RemoteException {
mService.get().setShuffleMode(shufflemode);
}
/**
* {@inheritDoc}
*/
@Override
public void setRepeatMode(final int repeatmode) throws RemoteException {
mService.get().setRepeatMode(repeatmode);
}
/**
* {@inheritDoc}
*/
@Override
public void moveQueueItem(final int from, final int to) throws RemoteException {
mService.get().moveQueueItem(from, to);
}
/**
* {@inheritDoc}
*/
@Override
public void toggleFavorite() throws RemoteException {
mService.get().toggleFavorite();
}
/**
* {@inheritDoc}
*/
@Override
public void refresh() throws RemoteException {
mService.get().refresh();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isFavorite() throws RemoteException {
return mService.get().isFavorite();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPlaying() throws RemoteException {
return mService.get().isPlaying();
}
/**
* {@inheritDoc}
*/
@Override
public long[] getQueue() throws RemoteException {
return mService.get().getQueue();
}
/**
* {@inheritDoc}
*/
@Override
public long duration() throws RemoteException {
return mService.get().duration();
}
/**
* {@inheritDoc}
*/
@Override
public long position() throws RemoteException {
return mService.get().position();
}
/**
* {@inheritDoc}
*/
@Override
public long seek(final long position) throws RemoteException {
return mService.get().seek(position);
}
/**
* {@inheritDoc}
*/
@Override
public long getAudioId() throws RemoteException {
return mService.get().getAudioId();
}
/**
* {@inheritDoc}
*/
@Override
public long getArtistId() throws RemoteException {
return mService.get().getArtistId();
}
/**
* {@inheritDoc}
*/
@Override
public long getAlbumId() throws RemoteException {
return mService.get().getAlbumId();
}
/**
* {@inheritDoc}
*/
@Override
public String getArtistName() throws RemoteException {
return mService.get().getArtistName();
}
/**
* {@inheritDoc}
*/
@Override
public String getTrackName() throws RemoteException {
return mService.get().getTrackName();
}
/**
* {@inheritDoc}
*/
@Override
public String getAlbumName() throws RemoteException {
return mService.get().getAlbumName();
}
/**
* {@inheritDoc}
*/
@Override
public String getPath() throws RemoteException {
return mService.get().getPath();
}
/**
* {@inheritDoc}
*/
@Override
public int getQueuePosition() throws RemoteException {
return mService.get().getQueuePosition();
}
/**
* {@inheritDoc}
*/
@Override
public int getShuffleMode() throws RemoteException {
return mService.get().getShuffleMode();
}
/**
* {@inheritDoc}
*/
@Override
public int getRepeatMode() throws RemoteException {
return mService.get().getRepeatMode();
}
/**
* {@inheritDoc}
*/
@Override
public int removeTracks(final int first, final int last) throws RemoteException {
return mService.get().removeTracks(first, last);
}
/**
* {@inheritDoc}
*/
@Override
public int removeTrack(final long id) throws RemoteException {
return mService.get().removeTrack(id);
}
/**
* {@inheritDoc}
*/
@Override
public int getMediaMountedCount() throws RemoteException {
return mService.get().getMediaMountedCount();
}
/**
* {@inheritDoc}
*/
@Override
public int getAudioSessionId() throws RemoteException {
return mService.get().getAudioSessionId();
}
@Override
public void shutdown() throws RemoteException {
mService.get().shutdown();
}
}
}