/* * 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; } }