/*
* Copyright (C) 2014 Fastboot Mobile, LLC.
*
* This program 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.
*
* This program 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 this program;
* if not, see <http://www.gnu.org/licenses>.
*/
package com.fastbootmobile.encore.service;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.drawable.BitmapDrawable;
import android.media.AudioManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import com.echonest.api.v4.EchoNestException;
import com.fastbootmobile.encore.api.echonest.AutoMixManager;
import com.fastbootmobile.encore.app.OmniMusic;
import com.fastbootmobile.encore.framework.ListenLogger;
import com.fastbootmobile.encore.framework.PluginsLookup;
import com.fastbootmobile.encore.model.Album;
import com.fastbootmobile.encore.model.Artist;
import com.fastbootmobile.encore.model.Playlist;
import com.fastbootmobile.encore.model.SearchResult;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.AbstractProviderConnection;
import com.fastbootmobile.encore.providers.BaseProviderCallback;
import com.fastbootmobile.encore.providers.ILocalCallback;
import com.fastbootmobile.encore.providers.IMusicProvider;
import com.fastbootmobile.encore.providers.ProviderAggregator;
import com.fastbootmobile.encore.providers.ProviderConnection;
import com.fastbootmobile.encore.providers.ProviderIdentifier;
import com.fastbootmobile.encore.receivers.PacManReceiver;
import com.fastbootmobile.encore.receivers.RemoteControlReceiver;
import com.fastbootmobile.encore.utils.SettingsKeys;
import com.fastbootmobile.encore.utils.Utils;
import com.squareup.leakcanary.RefWatcher;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Service handling the playback of the audio and the play notification
*/
public class PlaybackService extends Service
implements PluginsLookup.ConnectionListener, ILocalCallback,
AudioManager.OnAudioFocusChangeListener, NativeHub.OnSampleWrittenListener {
private static final String TAG = "PlaybackService";
public static final int STATE_STOPPED = 0;
public static final int STATE_PLAYING = 1;
public static final int STATE_PAUSED = 2;
public static final int STATE_BUFFERING = 3;
public static final int STATE_PAUSING = 4;
private static final String SERVICE_SHARED_PREFS = "PlaybackServicePrefs";
private static final String QUEUE_SHARED_PREFS = "PlaybackQueueMemory";
private static final String PREF_KEY_REPEAT = "repeatMode";
private static final String PREF_KEY_SHUFFLE = "shuffleMode";
public static final String ACTION_COMMAND = "command";
public static final String EXTRA_COMMAND_NAME = "command_name";
public static final int COMMAND_NEXT = 1;
public static final int COMMAND_PREVIOUS = 2;
public static final int COMMAND_PAUSE = 3;
public static final int COMMAND_STOP = 4;
private Runnable mNotifyQueueChangedRunnable = new Runnable() {
@Override
public void run() {
mNotification.setHasNext(mPlaybackQueue.size() > 1 || (mPlaybackQueue.size() > 0 && mRepeatMode));
for (IPlaybackCallback cb : mCallbacks) {
try {
cb.onPlaybackQueueChanged();
} catch (RemoteException e) {
Log.e(TAG, "Cannot notify playback queue changed", e);
}
}
// Save the queue as well
savePlaybackQueue();
}
};
private BroadcastReceiver mAudioNoisyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
try {
mBinder.stop();
} catch (RemoteException e) {
Log.e(TAG, "Cannot stop playback during AUDIO_BECOMING_NOISY", e);
}
}
}
};
private Handler mHandler;
private NativeAudioSink mNativeSink;
private NativeHub mNativeHub;
private DSPProcessor mDSPProcessor;
private final PlaybackQueue mPlaybackQueue;
private List<IPlaybackCallback> mCallbacks;
private ServiceNotification mNotification;
private int mCurrentTrack = -1;
private long mCurrentTrackElapsedMs;
private int mState = STATE_STOPPED;
private boolean mIsResuming;
private boolean mIsStopping;
private boolean mCurrentTrackWaitLoading;
private ProviderIdentifier mCurrentPlayingProvider;
private boolean mHasAudioFocus;
private boolean mRepeatMode;
private boolean mShuffleMode;
private Prefetcher mPrefetcher;
private IRemoteMetadataManager mRemoteMetadata;
private PowerManager.WakeLock mWakeLock;
private boolean mIsForeground;
private ListenLogger mListenLogger;
private PacManReceiver mPacManReceiver;
private boolean mCurrentTrackLoaded;
private HandlerThread mCommandsHandlerThread;
private CommandHandler mCommandsHandler;
private long mSleepTimerUptime = -1;
private boolean mPausedByFocusLoss = false;
private PlaybackServiceBinder mBinder = new PlaybackServiceBinder(new WeakReference<>(this));
private PlaybackProviderCallback mProviderCallback = new PlaybackProviderCallback(new WeakReference<>(this));
private boolean mShouldFlushBuffers = false;
private static class CommandHandler extends Handler {
private WeakReference<PlaybackService> mService;
private static final int MSG_START_PLAYBACK = 1;
private static final int MSG_PAUSE_PROVIDER = 2;
private static final int MSG_RESUME_PLAYBACK = 3;
private static final int MSG_FLUSH_BUFFERS = 4;
private static final int MSG_STOP_SERVICE = 5;
public CommandHandler(PlaybackService service, HandlerThread looper) {
super(looper.getLooper());
mService = new WeakReference<>(service);
}
@Override
public void handleMessage(Message msg) {
final PlaybackService service = mService.get();
if (service == null) {
Log.w(TAG, "Service reference is null, dropping handler message");
return;
}
final PluginsLookup plugins = PluginsLookup.getDefault();
IMusicProvider provider;
switch (msg.what) {
case MSG_START_PLAYBACK:
service.startPlayingQueue();
break;
case MSG_PAUSE_PROVIDER:
provider = plugins.getProvider((ProviderIdentifier.fromSerialized((String) msg.obj))).getBinder();
try {
if (provider != null) {
provider.pause();
} else {
Log.e(TAG, "Provider is null! Has it crashed?");
}
} catch (Exception e) {
Log.e(TAG, "Cannot pause the track!", e);
}
break;
case MSG_RESUME_PLAYBACK:
service.playImplLocked();
break;
case MSG_FLUSH_BUFFERS:
service.mNativeSink.flushSamples();
break;
case MSG_STOP_SERVICE:
service.stopImpl();
break;
}
}
}
public PlaybackService() {
mPlaybackQueue = new PlaybackQueue();
mCallbacks = new ArrayList<>();
mHandler = new Handler();
}
/**
* Called when the service is created
*/
@Override
public void onCreate() {
super.onCreate();
mListenLogger = new ListenLogger(this);
mPrefetcher = new Prefetcher(this);
mCommandsHandlerThread = new HandlerThread("PlaybackServiceCommandsHandler");
mCommandsHandlerThread.start();
mCommandsHandler = new CommandHandler(this, mCommandsHandlerThread);
// Register package manager to receive updates
mPacManReceiver = new PacManReceiver();
IntentFilter pacManFilter = new IntentFilter();
pacManFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
pacManFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
pacManFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
pacManFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
pacManFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
pacManFilter.addDataScheme("package");
registerReceiver(mPacManReceiver, pacManFilter);
// Really Google, I'd love to use your new APIs... But they're not working. If you use
// the new Lollipop metadata system, you lose Bluetooth AVRCP since the Bluetooth
// package still use the old RemoteController system.
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mRemoteMetadata = new RemoteMetadataManagerv21(this);
} else*/ {
mRemoteMetadata = new RemoteMetadataManager(this);
}
ProviderAggregator.getDefault().addUpdateCallback(this);
// Native playback initialization
mNativeHub = new NativeHub(getApplicationContext());
mNativeSink = new NativeAudioSink();
mNativeHub.setSinkPointer(mNativeSink.getPlayer().getHandle());
mNativeHub.setOnAudioWrittenListener(this);
mNativeHub.onStart();
mDSPProcessor = new DSPProcessor(this);
mDSPProcessor.restoreChain(this);
// Plugins initialization
PluginsLookup.getDefault().initialize(getApplicationContext());
PluginsLookup.getDefault().registerProviderListener(this);
List<ProviderConnection> connections = PluginsLookup.getDefault().getAvailableProviders();
for (ProviderConnection conn : connections) {
if (conn.getBinder(false) != null) {
assignProviderAudioSocket(conn);
} else {
Log.w(TAG, "Cannot assign audio socket to " + conn.getIdentifier() + ", binder is null");
}
}
// Setup
mIsStopping = false;
// Bind to all provider
List<ProviderConnection> providers = PluginsLookup.getDefault().getAvailableProviders();
for (ProviderConnection pc : providers) {
try {
IMusicProvider binder = pc.getBinder(false);
if (binder != null) {
binder.registerCallback(mProviderCallback);
}
} catch (RemoteException e) {
Log.e(TAG, "Cannot register callback", e);
}
}
// Register AutoMix manager
mCallbacks.add(AutoMixManager.getDefault());
// Setup notification system
mNotification = new ServiceNotification(this);
mNotification.setOnNotificationChangedListener(new ServiceNotification.NotificationChangedListener() {
@Override
public void onNotificationChanged(ServiceNotification notification) {
NotificationManagerCompat nmc = NotificationManagerCompat.from(PlaybackService.this);
if (mIsForeground) {
notification.notify(nmc);
mIsForeground = true;
} else {
notification.notify(PlaybackService.this);
}
BitmapDrawable albumArt = notification.getAlbumArt();
mRemoteMetadata.setAlbumArt(albumArt);
}
});
// Setup lockscreen remote controls
mRemoteMetadata.setup();
// Setup playback wakelock (but don't acquire it yet)
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "OmniMusicPlayback");
// Restore preferences
SharedPreferences prefs = getSharedPreferences(SERVICE_SHARED_PREFS, MODE_PRIVATE);
mRepeatMode = prefs.getBoolean(PREF_KEY_REPEAT, false);
mShuffleMode = prefs.getBoolean(PREF_KEY_SHUFFLE, false);
// TODO: Use callbacks
// Restore playback queue after one second - we have multiple things to wait here:
// - The callbacks of the main app's UI
// - The providers connecting
// - The providers ready to send us data
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
SharedPreferences queuePrefs = getSharedPreferences(QUEUE_SHARED_PREFS, MODE_PRIVATE);
mPlaybackQueue.restore(queuePrefs);
mCurrentTrack = queuePrefs.getInt("current", -1);
mCurrentTrackLoaded = false;
mNotification.setHasNext(mPlaybackQueue.size() > 1 || (mPlaybackQueue.size() > 0 && mRepeatMode));
}
}, 1000);
}
/**
* Called when the service is destroyed
*/
@Override
public void onDestroy() {
unregisterReceiver(mPacManReceiver);
PluginsLookup.getDefault().removeProviderListener(this);
ProviderAggregator.getDefault().removeUpdateCallback(this);
mRemoteMetadata.release();
// Cancel prefetching
mHandler.removeCallbacks(mPrefetcher);
mPrefetcher.cancel();
mPrefetcher = null;
if (mHasAudioFocus) {
abandonAudioFocus();
}
mIsForeground = false;
// Remove audio hosts from providers
List<ProviderConnection> connections = PluginsLookup.getDefault().getAvailableProviders();
for (ProviderConnection pc : connections) {
IMusicProvider provider = pc.getBinder();
try {
if (provider != null) {
provider.unregisterCallback(mProviderCallback);
} else {
Log.e(TAG, "Cannot unregister callback: provider binder is null");
}
} catch (RemoteException e) {
Log.e(TAG, "Cannot unregister callback", e);
}
}
PluginsLookup.getDefault().tearDown(mNativeHub);
// Store the playback queue
savePlaybackQueue();
// Shutdown DSP chain
mNativeHub.onStop();
mNativeHub.setOnAudioWrittenListener(null);
mNativeSink.release();
mCommandsHandlerThread.interrupt();
RefWatcher watcher = OmniMusic.getRefWatcher(this);
watcher.watch(this);
super.onDestroy();
}
/**
* Called when the main app is calling startService on this service.
*
* @param intent The intent attached, not used
* @param flags The flags, not used
* @param startId The start id, not used
* @return a status integer
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mIsStopping = false;
if (intent != null && intent.getAction() != null
&& intent.getAction().equals(ACTION_COMMAND)) {
switch (intent.getIntExtra(EXTRA_COMMAND_NAME, -1)) {
case COMMAND_NEXT:
nextImpl();
break;
case COMMAND_PREVIOUS:
previousImpl();
break;
case COMMAND_PAUSE:
if (mState == STATE_STOPPED || mState == STATE_PAUSED || mState == STATE_PAUSING) {
playImpl();
} else {
pauseImpl();
}
break;
case COMMAND_STOP:
stopImpl();
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
/**
* Called when the main app binds on this service
*
* @param intent The intent attached, not used
* @return The binder, in our case an IPlaybackService
*/
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "Client bound");
return mBinder;
}
@Override
public void onRebind(Intent intent) {
Log.i(TAG, "Client rebound");
}
/**
* Called when all clients unbound from this service
*
* @param intent The intent attached, not used
* @return true
*/
@Override
public boolean onUnbind(Intent intent) {
Log.i(TAG, "Clients unbound");
return true;
}
/**
* ProviderConnection listener: Called when a provider is bound
*
* @param connection The provider connection
*/
@Override
public void onServiceConnected(AbstractProviderConnection connection) {
Log.i(TAG, "Service connected: " + connection.getIdentifier());
assignProviderAudioSocket(connection);
if (connection instanceof ProviderConnection) {
try {
final IMusicProvider binder = ((ProviderConnection) connection).getBinder(false);
if (binder != null) {
binder.registerCallback(mProviderCallback);
} else {
Log.e(TAG, "Cannot register callback in onServiceConnected, binder is null");
}
// If we were playing from this provider and it just connected, it means it crashed.
// We try to restore its state.
if (binder != null && mState == STATE_PLAYING) {
final Song currentSong = getCurrentSong();
if (currentSong != null
&& connection.getIdentifier().equals(currentSong.getProvider())) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
int restorePosition = getCurrentTrackPositionImpl();
if (restorePosition < 0 || restorePosition > currentSong.getDuration()) {
restorePosition = 0;
}
Log.d(TAG, "Provider crashed, restoring playback of "
+ currentSong.getRef()
+ " to " + restorePosition + "ms");
try {
binder.playSong(currentSong.getRef());
if (restorePosition > 0) {
binder.seek(getCurrentTrackPositionImpl());
}
} catch (Exception e) {
Log.e(TAG, "Cannot restore state", e);
}
}
}, 2000);
}
}
} catch (RemoteException e) {
Log.e(TAG, "Cannot register callback on connected service");
}
}
}
/**
* ProviderConnection listener: Called when a provider has disconnected
*
* @param connection The provider connected
*/
@Override
public void onServiceDisconnected(AbstractProviderConnection connection) {
Log.w(TAG, "Provider disconnected, rebinding before it's too late");
connection.bindService();
}
public NativeHub getNativeHub() {
return mNativeHub;
}
public PlaybackQueue getQueue() {
return mPlaybackQueue;
}
/**
* Saves the playback queue in the local storage
*/
private void savePlaybackQueue() {
SharedPreferences queuePrefs = getSharedPreferences(QUEUE_SHARED_PREFS, MODE_PRIVATE);
mPlaybackQueue.save(queuePrefs.edit());
queuePrefs.edit().putInt("current", mCurrentTrack).apply();
}
/**
* Assigns the provided provider an audio client socket
*
* @param connection The provider
*/
public String assignProviderAudioSocket(AbstractProviderConnection connection) {
String socket = connection.getAudioSocketName();
if (socket == null) {
// Assign the providers an audio socket
socket = "com.fastbootmobile.encore.AUDIO_SOCKET_" + connection.getProviderName()
+ "_" + System.currentTimeMillis();
if (connection.createAudioSocket(mNativeHub, socket)) {
Log.i(TAG, "Provider connected and socket set: " + connection.getProviderName());
} else {
Log.w(TAG, "Error while creating audio socket for " + connection.getProviderName());
}
}
return socket;
}
/**
* Request the audio focus and registers the remote media controller
*/
private synchronized void requestAudioFocus() {
if (!mHasAudioFocus) {
final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// Request audio focus for music playback
int result = am.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
am.registerMediaButtonEventReceiver(RemoteControlReceiver.getComponentName(this));
// Notify the remote metadata that we're getting active
mRemoteMetadata.setActive(true);
// Register AUDIO_BECOMING_NOISY to stop playback when earbuds are pulled
registerReceiver(mAudioNoisyReceiver,
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
// Add a WakeLock to avoid CPU going to sleep while music is playing
mWakeLock.acquire();
mHasAudioFocus = true;
} else {
Log.e(TAG, "Audio focus request denied: " + result);
}
}
}
/**
* Release the audio focus and unregisters the media controls
*/
private synchronized void abandonAudioFocus() {
if (mHasAudioFocus) {
// Release the audio focus
final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.abandonAudioFocus(this);
unregisterReceiver(mAudioNoisyReceiver);
// Release our CPU wakelock
mWakeLock.release();
// Notify the remote metadata that we're getting down
mRemoteMetadata.setActive(false);
mHasAudioFocus = false;
}
}
/**
* Starts playing the current playback queue
*/
private void startPlayingQueue() {
// Check sleep timer
if (mSleepTimerUptime > 0 && SystemClock.uptimeMillis() >= mSleepTimerUptime) {
Log.d(TAG, "Stopping playback because of sleep timer");
mSleepTimerUptime = -1;
stopImpl();
return;
}
if (mPlaybackQueue.size() > 0) {
// mCurrentTrack in this context is the track that is going to be played
if (mCurrentTrack < 0) {
mCurrentTrack = 0;
} else if (mCurrentTrack >= mPlaybackQueue.size()) {
mCurrentTrack = mPlaybackQueue.size() - 1;
}
final Song next = mPlaybackQueue.get(mCurrentTrack);
if (next == null) {
// Song got unavailable, retry the next one
boolean shouldFlush = mShouldFlushBuffers;
nextImpl();
// Keep the flush status
mShouldFlushBuffers = shouldFlush;
return;
}
final ProviderIdentifier providerId = next.getProvider();
if (mCurrentPlayingProvider != null && !next.getProvider().equals(mCurrentPlayingProvider)) {
// Pause the previously playing track to avoid overlap if it's not the same provider
ProviderConnection prevConn = PluginsLookup.getDefault().getProvider(mCurrentPlayingProvider);
if (prevConn != null) {
IMusicProvider prevProv = prevConn.getBinder();
if (prevProv != null) {
try {
prevProv.pause();
} catch (RemoteException e) {
Log.e(TAG, "Unable to pause previously playing provider", e);
}
}
}
mCurrentPlayingProvider = null;
}
// Clear up the sink buffers if we were paused (otherwise we'll cut the end of the
// previous song)
if (mState == STATE_PAUSED || mState == STATE_PAUSING || mState == STATE_STOPPED) {
mCommandsHandler.sendEmptyMessage(CommandHandler.MSG_FLUSH_BUFFERS);
}
if (providerId != null) {
ProviderConnection connection = PluginsLookup.getDefault().getProvider(providerId);
if (connection != null) {
// Reset playbar status
getSharedPreferences(SettingsKeys.PREF_SETTINGS, 0)
.edit().putBoolean(SettingsKeys.KEY_PLAYBAR_HIDDEN, false).apply();
// Start playback
IMusicProvider provider = connection.getBinder();
if (provider != null) {
mState = STATE_BUFFERING;
for (IPlaybackCallback cb : mCallbacks) {
try {
cb.onSongStarted(true, next);
} catch (RemoteException e) {
Log.e(TAG, "Cannot call playback callback for song start event", e);
}
}
Log.d(TAG, "onSongStarted: Buffering...");
if (!next.isLoaded()) {
// Track not loaded yet, delay until track info arrived
mCurrentTrackWaitLoading = true;
mCurrentTrackLoaded = false;
Log.w(TAG, "Track not yet loaded: " + next.getRef() + ", delaying");
} else if (!next.isAvailable()) {
// Track is not available, skip to the next one
boolean shouldFlush = mShouldFlushBuffers;
nextImpl();
// Keep the flush status
mShouldFlushBuffers = shouldFlush;
} else {
mCurrentTrackWaitLoading = false;
mCurrentPlayingProvider = providerId;
requestAudioFocus();
try {
provider.playSong(next.getRef());
} catch (RemoteException e) {
Log.e(TAG, "Unable to play song", e);
} catch (NullPointerException e) {
Log.e(TAG, "No provider attached", e);
} catch (IllegalStateException e) {
Log.e(TAG, "Illegal State from provider", e);
}
mCurrentTrackLoaded = true;
mListenLogger.addEntry(next);
// The notification system takes care of calling startForeground
mHandler.post(new Runnable() {
@Override
public void run() {
mNotification.setCurrentSong(next);
}
});
mRemoteMetadata.setCurrentSong(next, getNextTrack() != null);
mRemoteMetadata.notifyBuffering();
}
}
}
} else {
Log.e(TAG, "Cannot play the first song of the queue because the Song's " +
"ProviderIdentifier is null!");
}
}
}
/**
* Notifies the listeners that the playback queue contents changed. Note that we don't call
* this when the first item of the queue is removed because of playback moving on to the next
* track of the queue, but only when a manual/non-logical operation is done.
*/
private void notifyQueueChanged() {
mHandler.removeCallbacks(mNotifyQueueChangedRunnable);
mHandler.post(mNotifyQueueChangedRunnable);
}
/**
* If a song is currently playing, returns the Song in the playback queue at the index
* corresponding to mCurrentTrack
* @return A Song if a song is playing, null otherwise
*/
private Song getCurrentSong() {
synchronized (mPlaybackQueue) {
try {
if (mCurrentTrack >= 0 && mPlaybackQueue.size() > mCurrentTrack) {
return mPlaybackQueue.get(mCurrentTrack);
} else {
return null;
}
} catch (Exception e) {
return null;
}
}
}
/**
* Requests the playback to start, if a request hasn't been posted already
*/
private void requestStartPlayback() {
if (!mCommandsHandler.hasMessages(CommandHandler.MSG_START_PLAYBACK)) {
mCommandsHandler.sendEmptyMessage(CommandHandler.MSG_START_PLAYBACK);
}
}
/**
* Moves to the next track
*/
void nextImpl() {
boolean hasNext = mCurrentTrack < mPlaybackQueue.size() - 1;
if (mPlaybackQueue.size() > 1 && mShuffleMode) {
// Shuffle mode is enabled, play any track but not the one we just played
int previousTrack = mCurrentTrack;
while (previousTrack == mCurrentTrack) {
mCurrentTrack = Utils.getRandom(mPlaybackQueue.size());
}
mNativeSink.setPaused(true);
mShouldFlushBuffers = true;
requestStartPlayback();
mNotification.setHasNext(true);
} else if (mPlaybackQueue.size() > 0 && hasNext) {
mCurrentTrack++;
mNativeSink.flushSamples();
requestStartPlayback();
hasNext = mCurrentTrack < mPlaybackQueue.size() - 1;
hasNext = hasNext || (mPlaybackQueue.size() > 0 && mRepeatMode);
mNotification.setHasNext(hasNext);
} else if (mRepeatMode && mPlaybackQueue.size() > 0) {
mCurrentTrack = 0;
mNativeSink.setPaused(true);
mShouldFlushBuffers = true;
requestStartPlayback();
mNotification.setHasNext(true);
}
final AutoMixManager mixManager = AutoMixManager.getDefault();
if (mixManager.getCurrentPlayingBucket() != null) {
try {
mixManager.getCurrentPlayingBucket().notifySkip();
} catch (EchoNestException e) {
Log.e(TAG, "Cannot notify EchoNest of skip event", e);
}
}
}
/**
* Restarts the current song or goes to the previous one
*/
void previousImpl() {
boolean shouldRestart = (getCurrentTrackPositionImpl() > 4000 || (!mRepeatMode && mCurrentTrack == 0))
&& mCurrentTrackLoaded;
if (shouldRestart) {
// Restart playback
mNativeSink.setPaused(true);
mShouldFlushBuffers = true;
seekImpl(0);
} else {
boolean retry = true;
while (retry) {
// Go to the previous track
mCurrentTrack--;
if (mCurrentTrack < 0) {
if (mRepeatMode) {
mCurrentTrack = mPlaybackQueue.size() - 1;
} else {
mCurrentTrack = 0;
}
}
if (mCurrentTrack < 0 || mCurrentTrack >= mPlaybackQueue.size()) {
retry = true;
continue;
}
Song songToPlay = mPlaybackQueue.get(mCurrentTrack);
// If song is unavailable, try the next one
retry = songToPlay == null || (songToPlay.isLoaded() && !songToPlay.isAvailable());
}
mNativeSink.setPaused(true);
mShouldFlushBuffers = true;
requestStartPlayback();
}
}
/**
* Pauses the playback
*/
void pauseImpl() {
final Song currentSong = getCurrentSong();
if (currentSong != null) {
Log.d(TAG, "onSongPaused: Pausing...");
boolean wasBuffering = (mState == STATE_BUFFERING);
mState = STATE_PAUSING;
final ProviderIdentifier identifier = currentSong.getProvider();
mCommandsHandler.obtainMessage(CommandHandler.MSG_PAUSE_PROVIDER, identifier.serialize()).sendToTarget();
mNativeSink.setPaused(true);
if (wasBuffering) {
// We were buffering, which means the provider might not be playing the song,
// and might not send the playback paused callback. We assume we're paused.
mState = STATE_PAUSED;
for (IPlaybackCallback cb : mCallbacks) {
try {
cb.onPlaybackPause();
} catch (RemoteException e) {
Log.e(TAG, "Cannot call playback callback for playback pause event", e);
} catch (Exception e) {
Log.e(TAG, "BIG EXCEPTION DURING REMOTE PLAYBACK PAUSE: ", e);
Log.e(TAG, "Callback: " + cb);
}
}
Log.d(TAG, "onSongPaused: Paused (was buffering)...");
mNotification.setPlayPauseAction(true);
mRemoteMetadata.notifyPaused(getCurrentTrackPositionImpl());
}
}
}
/**
* Stops the playback and the service, release the audio focus
*/
void stopImpl() {
if ((mState == STATE_PLAYING || mState == STATE_BUFFERING) && mPlaybackQueue.size() > 0
&& mCurrentTrack >= 0) {
pauseImpl();
}
mRemoteMetadata.notifyStopped();
abandonAudioFocus();
for (IPlaybackCallback cb : mCallbacks) {
try {
cb.onPlaybackPause();
} catch (RemoteException e) {
Log.e(TAG, "Cannot call playback callback for playback pause event", e);
}
}
mState = STATE_STOPPED;
mIsStopping = true;
stopForeground(true);
mIsForeground = false;
stopSelf();
}
synchronized void playImpl() {
if (mState == STATE_PLAYING || mState == STATE_BUFFERING) {
// We are already playing, don't do anything
return;
}
mCommandsHandler.sendEmptyMessage(CommandHandler.MSG_RESUME_PLAYBACK);
}
private void playImplLocked() {
final Song currentSong = getCurrentSong();
if (currentSong != null && mCurrentTrackLoaded) {
ProviderConnection conn = PluginsLookup.getDefault().getProvider(currentSong.getProvider());
mNativeSink.setPaused(false);
if (conn != null) {
IMusicProvider provider = conn.getBinder();
if (provider != null) {
try {
provider.resume();
} catch (RemoteException e) {
Log.e(TAG, "Cannot resume", e);
} catch (IllegalStateException e) {
Log.e(TAG, "Illegal state while resuming", e);
}
mIsResuming = true;
mState = STATE_BUFFERING;
for (IPlaybackCallback cb : mCallbacks) {
try {
cb.onSongStarted(true, currentSong);
} catch (RemoteException e) {
Log.e(TAG, "Cannot call playback callback for song start event", e);
}
}
requestAudioFocus();
mNotification.setPlayPauseAction(false);
} else {
Log.e(TAG, "Provider is null! Can't resume.");
}
}
} else if (mPlaybackQueue.size() > 0) {
requestStartPlayback();
mIsResuming = false;
}
}
/**
* Plays the song in the queue at the specified index
* @param index The index to play, 0-based
*/
private void playAtIndexImpl(int index) {
Log.d(TAG, "Playing track " + (index + 1) + "/" + mPlaybackQueue.size());
mCurrentTrack = index;
mNativeSink.setPaused(true);
mShouldFlushBuffers = true;
requestStartPlayback();
}
/**
* @return The reference to the next track in the queue
*/
public Song getNextTrack() {
if (mCurrentTrack < mPlaybackQueue.size() - 1) {
return mPlaybackQueue.get(mCurrentTrack + 1);
} else {
// No more tracks
return null;
}
}
public int getCurrentTrackPositionImpl() {
return (int) mCurrentTrackElapsedMs;
}
void seekImpl(final long timeMs) {
// First, unpause if paused
mNativeSink.setPaused(false);
// Then seek on the provider
final Song currentSong = getCurrentSong();
boolean success = false;
if (currentSong != null && mCurrentTrackLoaded) {
ProviderIdentifier id = currentSong.getProvider();
ProviderConnection conn = PluginsLookup.getDefault().getProvider(id);
if (conn != null) {
final IMusicProvider provider = conn.getBinder();
if (provider != null) {
try {
provider.seek(timeMs);
success = true;
mCurrentTrackElapsedMs = timeMs;
} catch (RemoteException e) {
Log.e(TAG, "Cannot seek to time", e);
} catch (Exception e) {
Log.e(TAG, "Provider thrown exception while seeking", e);
}
}
}
}
if (success) {
mHandler.post(new Runnable() {
@Override
public void run() {
for (IPlaybackCallback cb : mCallbacks) {
try {
cb.onSongScrobble((int) timeMs);
} catch (RemoteException e) {
Log.e(TAG, "Cannot notify scrobbling", e);
}
}
}
});
}
}
private static class PlaybackServiceBinder extends IPlaybackService.Stub {
private WeakReference<PlaybackService> mParent;
PlaybackServiceBinder(WeakReference<PlaybackService> parent) {
mParent = parent;
}
@Override
public void addCallback(IPlaybackCallback cb) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
// Check if we don't have it already
for (IPlaybackCallback callback : service.mCallbacks) {
if (cb.getIdentifier() == callback.getIdentifier()) {
return;
}
}
// Then we add it
service.mCallbacks.add(cb);
}
}
@Override
public void removeCallback(IPlaybackCallback cb) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
for (IPlaybackCallback callback : service.mCallbacks) {
if (cb.getIdentifier() == callback.getIdentifier()) {
service.mCallbacks.remove(cb);
break;
}
}
}
}
@Override
public void playPlaylist(Playlist p) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
Log.i(TAG, "Play playlist: " + p.getRef());
service.mCurrentTrack = 0;
synchronized (service.mPlaybackQueue) {
service.mPlaybackQueue.clear();
queuePlaylist(p, false);
}
service. requestStartPlayback();
}
}
@Override
public void playSong(Song s) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
Log.i(TAG, "Play song: " + s.getRef());
service.mCurrentTrack = 0;
synchronized (service.mPlaybackQueue) {
service.mPlaybackQueue.clear();
queueSong(s, true);
}
service.requestStartPlayback();
}
}
@Override
public void playAlbum(Album a) throws RemoteException {
PlaybackService service = mParent.get();
if (a != null) {
if (service != null) {
Log.i(TAG, "Play album: " + a.getRef() + " (this=" + this + ")");
service.mCurrentTrack = 0;
synchronized (service.mPlaybackQueue) {
service.mPlaybackQueue.clear();
queueAlbum(a, false);
}
service.requestStartPlayback();
}
} else {
Log.e(TAG, "Cannot play album: Null");
}
}
@Override
public void queuePlaylist(Playlist p, boolean top) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
Iterator<String> songsIt = p.songs();
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
while (songsIt.hasNext()) {
String ref = songsIt.next();
service.mPlaybackQueue.addSong(aggregator.retrieveSong(ref, p.getProvider()), top);
}
service.notifyQueueChanged();
}
}
@Override
public void queueSong(Song s, boolean top) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
service.mPlaybackQueue.addSong(aggregator.retrieveSong(s.getRef(), null), top);
service.notifyQueueChanged();
}
}
@Override
public void queueAlbum(Album p, boolean top) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
Iterator<String> songsIt = p.songs();
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
while (songsIt.hasNext()) {
String ref = songsIt.next();
service.mPlaybackQueue.addSong(aggregator.retrieveSong(ref, p.getProvider()), top);
}
service.notifyQueueChanged();
}
}
@Override
public void playNext(Song s) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
if (service.mCurrentTrack >= 0) {
service.mPlaybackQueue.add(service.mCurrentTrack + 1, s);
} else {
service.mPlaybackQueue.add(0, s);
}
}
}
@Override
public void pause() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
// User-requested pause, so abandon audio focus to avoid resume if something else
// grabs and release playback while we were just paused.
service.pauseImpl();
service.abandonAudioFocus();
}
}
@Override
public boolean play() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.playImpl();
return true;
} else {
return false;
}
}
@Override
public int getState() {
PlaybackService service = mParent.get();
if (service != null) {
return service.mState;
} else {
return STATE_STOPPED;
}
}
@Override
public int getCurrentTrackLength() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
final Song currentSong = service.getCurrentSong();
if (currentSong != null) {
return currentSong.getDuration();
}
}
return -1;
}
@Override
public int getCurrentTrackPosition() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
return service.getCurrentTrackPositionImpl();
} else {
return -1;
}
}
@Override
public Song getCurrentTrack() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
return service.getCurrentSong();
} else {
return null;
}
}
@Override
public int getCurrentTrackIndex() {
PlaybackService service = mParent.get();
if (service != null) {
return service.mCurrentTrack;
} else {
return -1;
}
}
@Override
public List<Song> getCurrentPlaybackQueue() {
PlaybackService service = mParent.get();
if (service != null) {
return service.mPlaybackQueue;
} else {
return new ArrayList<Song>();
}
}
@Override
public int getCurrentRms() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
return service.mDSPProcessor.getRms();
} else {
return 0;
}
}
@Override
public List<ProviderIdentifier> getDSPChain() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
return service.mDSPProcessor.getActiveChain();
} else {
return new ArrayList<>();
}
}
@Override
public void setDSPChain(List<ProviderIdentifier> chain) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.mDSPProcessor.setActiveChain(service, chain, service.mNativeHub);
}
}
@Override
public void seek(final long timeMs) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.seekImpl(timeMs);
}
}
@Override
public void next() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.nextImpl();
}
}
@Override
public void previous() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.previousImpl();
}
}
@Override
public void playAtQueueIndex(int index) {
PlaybackService service = mParent.get();
if (service != null) {
service.playAtIndexImpl(index);
}
}
@Override
public void stop() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.stopImpl();
}
}
@Override
public void setRepeatMode(boolean repeat) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.mRepeatMode = repeat;
SharedPreferences prefs = service.getSharedPreferences(SERVICE_SHARED_PREFS, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PREF_KEY_REPEAT, repeat);
editor.apply();
}
}
@Override
public boolean isRepeatMode() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
return service.mRepeatMode;
} else {
return false;
}
}
@Override
public void setShuffleMode(boolean shuffle) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.mShuffleMode = shuffle;
SharedPreferences prefs = service.getSharedPreferences(SERVICE_SHARED_PREFS, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PREF_KEY_SHUFFLE, shuffle);
editor.apply();
}
}
@Override
public boolean isShuffleMode() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
return service.mShuffleMode;
} else {
return false;
}
}
@Override
public void clearPlaybackQueue() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
synchronized (service.mPlaybackQueue) {
service.mPlaybackQueue.clear();
}
}
}
@Override
public void setSleepTimer(long uptime) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
service.mSleepTimerUptime = uptime;
}
}
@Override
public long getSleepTimerEndTime() throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
return service.mSleepTimerUptime;
} else {
return 0;
}
}
@Override
public void setPlayerMuted(boolean muted) {
PlaybackService service = mParent.get();
if (service != null) {
service.mNativeSink.setMuted(muted);
}
}
}
@Override
public void onSongUpdate(List<Song> s) {
final Song currentSong = getCurrentSong();
if (currentSong != null && s.contains(currentSong) && currentSong.isLoaded()) {
if (mCurrentTrackWaitLoading) {
requestStartPlayback();
}
if (mCurrentTrackLoaded) {
mRemoteMetadata.setCurrentSong(currentSong, getNextTrack() != null);
mHandler.post(new Runnable() {
@Override
public void run() {
mNotification.setCurrentSong(getCurrentSong());
}
});
}
}
}
@Override
public void onAlbumUpdate(List<Album> a) {
// ignore
}
@Override
public void onPlaylistUpdate(List<Playlist> p) {
// TODO: Update playback queue if it's the playlist we're playing
}
@Override
public void onPlaylistRemoved(String ref) {
}
@Override
public void onArtistUpdate(List<Artist> a) {
}
@Override
public void onProviderConnected(IMusicProvider provider) {
}
@Override
public void onSearchResult(List<SearchResult> searchResult) {
}
private static class PlaybackProviderCallback extends BaseProviderCallback {
private WeakReference<PlaybackService> mParent;
PlaybackProviderCallback(WeakReference<PlaybackService> parent) {
mParent = parent;
}
@Override
public void onSongPlaying(ProviderIdentifier provider) {
PlaybackService service = mParent.get();
if (service != null) {
boolean wasPaused = service.mIsResuming;
if (wasPaused) {
service.mIsResuming = false;
} else {
service.mCurrentTrackElapsedMs = 0;
// Flush and unpause the sink to clear previous track data (if from user action)
if (service.mShouldFlushBuffers) {
service.mNativeSink.flushSamples();
}
service.mNativeSink.setPaused(false);
}
service.mState = STATE_PLAYING;
final Song currentSong = service.getCurrentSong();
if (currentSong == null) {
throw new IllegalStateException("Current song is null on callback! Queue size=" + service.mPlaybackQueue.size() +
" and index=" + service.mCurrentTrack + " and this=" + this);
}
for (IPlaybackCallback cb : service.mCallbacks) {
try {
if (!wasPaused) {
cb.onSongStarted(false, currentSong);
} else {
cb.onPlaybackResume();
}
} catch (RemoteException e) {
Log.e(TAG, "Cannot call playback callback for song start event", e);
}
}
Log.d(TAG, "onSongPlaying: Playing...");
service.mNotification.setPlayPauseAction(false);
service.mRemoteMetadata.notifyPlaying(0);
// Prepare pre-fetching the next song
// Note: We don't take care of the delay being too early when it's paused, as long
// as it matches the next track.
ProviderConnection conn = PluginsLookup.getDefault().getProvider(provider);
if (conn != null) {
IMusicProvider binder = conn.getBinder();
if (binder != null) {
try {
service.mHandler.removeCallbacks(service.mPrefetcher);
service.mHandler.postDelayed(service.mPrefetcher,
currentSong.getDuration() - binder.getPrefetchDelay());
} catch (RemoteException e) {
Log.e(TAG, "Cannot get prefetch delay from provider", e);
}
}
}
// Save the queue as we started playing a new song (maybe)
service.savePlaybackQueue();
}
}
@Override
public void onSongPaused(ProviderIdentifier provider) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
final Song currentSong = service.getCurrentSong();
if (currentSong != null && currentSong.getProvider().equals(provider) && !service.mIsStopping) {
service.mState = STATE_PAUSED;
for (IPlaybackCallback cb : service.mCallbacks) {
try {
cb.onPlaybackPause();
} catch (RemoteException e) {
Log.e(TAG, "Cannot call playback callback for playback pause event", e);
} catch (Exception e) {
Log.e(TAG, "BIG EXCEPTION DURING REMOTE PLAYBACK PAUSE: ", e);
Log.e(TAG, "Callback: " + cb);
}
}
Log.d(TAG, "onSongPaused: Paused...");
// stopImpl() calls pauseImpl(), which may cause the notification to come back
// after the current notification went away due to this callback
if (!service.mIsStopping) {
service.mNotification.setPlayPauseAction(true);
service.mRemoteMetadata.notifyPaused(service.getCurrentTrackPositionImpl());
}
}
}
}
@Override
public void onTrackEnded(ProviderIdentifier provider) throws RemoteException {
PlaybackService service = mParent.get();
if (service != null) {
// We restart the queue in an handler. In the case of the Spotify provider, the
// endOfTrack callback locks the main API thread, leading to a dead lock if we
// try to play a track here while still being in the callstack of the endOfTrack
// callback.
if (service.mPlaybackQueue.size() > 1 && service.mShuffleMode) {
// Shuffle mode is enabled, play any track but not the one we just played
int previousTrack = service.mCurrentTrack;
while (previousTrack == service.mCurrentTrack) {
service.mCurrentTrack = Utils.getRandom(service.mPlaybackQueue.size());
}
service.mShouldFlushBuffers = false;
service.requestStartPlayback();
} else if (service.mPlaybackQueue.size() > 0 && service.mCurrentTrack < service.mPlaybackQueue.size() - 1) {
// Regular sequential mode, not at the end, move to the next track
service.mCurrentTrack++;
service.mShouldFlushBuffers = false;
service.requestStartPlayback();
} else if (service.mPlaybackQueue.size() > 0 && service.mCurrentTrack == service.mPlaybackQueue.size() - 1) {
// Regular sequential mode, at the end of the queue
if (service.mRepeatMode) {
// We're repeating, go back to the first track and play it
service.mCurrentTrack = 0;
service.mShouldFlushBuffers = false;
service.requestStartPlayback();
} else {
// Not repeating and at the end of the playlist, stop after a little while
// to allow the buffers to empty
service.mHandler.sendEmptyMessageDelayed(CommandHandler.MSG_STOP_SERVICE,
2000);
}
}
}
}
};
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// You have gained the audio focus.
mNativeHub.setDucking(false);
if (mState != STATE_PLAYING && mPausedByFocusLoss) {
playImpl();
mPausedByFocusLoss = false;
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
// You have lost the audio focus for a presumably long time. You must stop all audio
// playback.
pauseImpl();
mPausedByFocusLoss = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// You have temporarily lost audio focus, but should receive it back shortly. You
// must stop all audio playback, but you can keep your resources because you will
// probably get focus back shortly.
pauseImpl();
mPausedByFocusLoss = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// You have temporarily lost audio focus, but you are allowed to continue to play
// audio quietly (at a low volume) instead of killing audio completely.
mNativeHub.setDucking(true);
break;
}
}
@Override
public void onSampleWritten(byte[] bytes, int len, int sampleRate, int channels) {
len = len / 2; // first, we want the number of samples, and we assume 16 bits audio
len = len / channels; // then, we count "mono"
mCurrentTrackElapsedMs += len * 1000 / sampleRate;
}
}