Java tutorial
/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> === * * Copyright 2012, Christopher Reichert <creichert07@gmail.com> * Copyright 2013, Enno Gottschalk <mrmaffen@googlemail.com> * * Tomahawk 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. * * Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>. */ package org.runbuddy.tomahawk.services; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaBrowserServiceCompat; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.RatingCompat; import android.support.v4.media.session.MediaButtonReceiver; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.util.SparseArrayCompat; import android.util.Log; import android.util.LruCache; import android.widget.Toast; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import org.jdeferred.DoneCallback; import org.jdeferred.FailCallback; import org.jdeferred.Promise; import org.runbuddy.libtomahawk.authentication.AuthenticatorManager; import org.runbuddy.libtomahawk.collection.CollectionManager; import org.runbuddy.libtomahawk.collection.Image; import org.runbuddy.libtomahawk.collection.Playlist; import org.runbuddy.libtomahawk.collection.PlaylistEntry; import org.runbuddy.libtomahawk.collection.StationPlaylist; import org.runbuddy.libtomahawk.database.DatabaseHelper; import org.runbuddy.libtomahawk.infosystem.InfoSystem; import org.runbuddy.libtomahawk.resolver.PipeLine; import org.runbuddy.libtomahawk.resolver.Query; import org.runbuddy.libtomahawk.utils.ImageUtils; import org.runbuddy.tomahawk.R; import org.runbuddy.tomahawk.activities.TomahawkMainActivity; import org.runbuddy.tomahawk.app.TomahawkApp; import org.runbuddy.tomahawk.mediaplayers.AndroidMediaPlayer; import org.runbuddy.tomahawk.mediaplayers.DeezerMediaPlayer; import org.runbuddy.tomahawk.mediaplayers.PluginMediaPlayer; import org.runbuddy.tomahawk.mediaplayers.SpotifyMediaPlayer; import org.runbuddy.tomahawk.mediaplayers.TomahawkMediaPlayer; import org.runbuddy.tomahawk.mediaplayers.TomahawkMediaPlayerCallback; import org.runbuddy.tomahawk.mediaplayers.VLCMediaPlayer; import org.runbuddy.tomahawk.ui.fragments.TomahawkFragment; import org.runbuddy.tomahawk.utils.DelayedHandler; import org.runbuddy.tomahawk.utils.IdGenerator; import org.runbuddy.tomahawk.utils.MediaBrowserHelper; import org.runbuddy.tomahawk.utils.MediaNotification; import org.runbuddy.tomahawk.utils.MediaPlayIntentHandler; import org.runbuddy.tomahawk.utils.PlaybackManager; import org.runbuddy.tomahawk.utils.ThreadManager; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import de.greenrobot.event.EventBus; /** * This {@link Service} handles all playback related processes. */ public class PlaybackService extends MediaBrowserServiceCompat { private static final String TAG = PlaybackService.class.getSimpleName(); public static final String ACTION_PLAY = "org.tomahawk.tomahawk_android.ACTION_PLAY"; public static final String ACTION_STOP_NOTIFICATION = "org.tomahawk.tomahawk_android.STOP_NOTIFICATION"; public static final String ACTION_DELETE_ENTRY_IN_QUEUE = "org.tomahawk.tomahawk_android.DELETE_ENTRY_IN_QUEUE"; public static final String ACTION_ADD_QUERY_TO_QUEUE = "org.tomahawk.tomahawk_android.ADD_QUERY_TO_QUEUE"; public static final String ACTION_ADD_QUERIES_TO_QUEUE = "org.tomahawk.tomahawk_android.ADD_QUERIES_TO_QUEUE"; public static final String ACTION_SET_SHUFFLE_MODE = "org.tomahawk.tomahawk_android.SET_SHUFFLE_MODE"; public static final String ACTION_SET_REPEAT_MODE = "org.tomahawk.tomahawk_android.SET_REPEAT_MODE"; public static final String EXTRAS_KEY_PLAYBACKMANAGER = "org.tomahawk.tomahawk_android.PLAYBACKMANAGER"; public static final String EXTRAS_KEY_REPEAT_MODE = "org.tomahawk.tomahawk_android.REPEAT_MODE"; public static final String EXTRAS_KEY_SHUFFLE_MODE = "org.tomahawk.tomahawk_android.SHUFFLE_MODE"; // we don't have audio focus, and can't duck (play at a low volume) private static final int AUDIO_NO_FOCUS_NO_DUCK = 0; // we don't have focus, but can duck (play at a low volume) private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1; // we have full audio focus private static final int AUDIO_FOCUSED = 2; private static final int MAX_ALBUM_ART_CACHE_SIZE = 5 * 1024 * 1024; private int mPlayState = PlaybackStateCompat.STATE_NONE; private boolean mIsPreparing = false; private static final int DELAY_SCROBBLE = 15000; private static final int DELAY_UNBIND_PLUGINSERVICES = 1800000; private static final int DELAY_SUICIDE = 1800000; private final Set<Query> mCorrespondingQueries = Collections .newSetFromMap(new ConcurrentHashMap<Query, Boolean>()); private final ConcurrentHashMap<String, String> mCorrespondingRequestIds = new ConcurrentHashMap<>(); private final Map<StationPlaylist, Set<Query>> mStationQueries = new ConcurrentHashMap<>(); private PlaybackManager mPlaybackManager; private TomahawkMediaPlayer mCurrentMediaPlayer; private final Map<Class, TomahawkMediaPlayer> mMediaPlayers = new HashMap<>(); private MediaNotification mNotification; private MediaSessionCompat mMediaSession; private Handler mCallbackHandler; private MediaBrowserHelper mMediaBrowserHelper; private SparseArrayCompat<PlaylistEntry> mQueueMap = new SparseArrayCompat<>(); private boolean mPlayOnFocusGain; private int mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK; private AudioManager mAudioManager; private AudioManager.OnAudioFocusChangeListener mFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { Log.d(TAG, "onAudioFocusChange. focusChange= " + focusChange); if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { // We have gained focus mAudioFocus = AUDIO_FOCUSED; if (mPlayState == PlaybackStateCompat.STATE_PAUSED) { play(true); } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { // We have lost focus mAudioFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK; if (mPlayState == PlaybackStateCompat.STATE_PLAYING) { pause(true); } } else { Log.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange); } } }; private MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() { /** * Override to handle requests to begin playback. */ @Override public void onPlay() { play(false); } /** * Override to handle requests to pause playback. */ @Override public void onPause() { pause(false); } /** * Override to handle requests to begin playback from a search query. An * empty query indicates that the app may play any music. The * implementation should attempt to make a smart choice about what to * play. */ public void onPlayFromSearch(String query, Bundle extras) { Log.d(TAG, "onPlayFromSearch: " + query + ", " + extras); MediaPlayIntentHandler intentHandler = new MediaPlayIntentHandler( mMediaSession.getController().getTransportControls(), mPlaybackManager); intentHandler.mediaPlayFromSearch(extras); } /** * Override to handle requests to play a specific mediaId that was * provided by your app. */ @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { if (mMediaSession == null) { Log.e(TAG, "onPlayFromMediaId failed - mMediaSession == null!"); return; } mMediaBrowserHelper.onPlayFromMediaId(mMediaSession, mPlaybackManager, mediaId, extras); } /** * Override to handle requests to play an item with a given id from the * play queue. */ public void onSkipToQueueItem(long id) { Log.d(TAG, "Skipping to queue item with id " + id); PlaylistEntry entry = mQueueMap.get((int) id); mPlaybackManager.setCurrentEntry(entry); } /** * Override to handle requests to skip to the next media item. */ @Override public void onSkipToNext() { Log.d(TAG, "next"); int counter = 0; PlaylistEntry entry = mPlaybackManager.getCurrentEntry(); while ((entry = mPlaybackManager.getNextEntry(entry)) != null && counter++ < mPlaybackManager.getPlaybackListSize()) { if (entry.getQuery().isPlayable()) { mPlaybackManager.setCurrentEntry(entry); break; } } } /** * Override to handle requests to skip to the previous media item. */ @Override public void onSkipToPrevious() { Log.d(TAG, "previous"); int counter = 0; PlaylistEntry entry = mPlaybackManager.getCurrentEntry(); while ((entry = mPlaybackManager.getPreviousEntry(entry)) != null && counter++ < mPlaybackManager.getPlaybackListSize()) { if (entry.getQuery().isPlayable()) { mPlaybackManager.setCurrentEntry(entry); break; } } } /** * Override to handle requests to fast forward. */ @Override public void onFastForward() { Log.d(TAG, "fastForward"); long duration = mPlaybackManager.getCurrentTrack().getDuration(); long newPos = Math.min(duration, Math.max(0, getPlaybackPosition() + 10000)); onSeekTo(newPos); } /** * Override to handle requests to rewind. */ @Override public void onRewind() { Log.d(TAG, "rewind"); long duration = mPlaybackManager.getCurrentTrack().getDuration(); long newPos = Math.min(duration, Math.max(0, getPlaybackPosition() - 10000)); onSeekTo(newPos); } /** * Override to handle requests to stop playback. */ @Override public void onStop() { onPause(); } /** * Override to handle requests to seek to a specific position in ms. * * @param pos New position to move to, in milliseconds. */ @Override public void onSeekTo(final long pos) { Log.d(TAG, "seekTo " + pos); final Query currentQuery = mPlaybackManager.getCurrentQuery(); if (currentQuery != null && currentQuery.getMediaPlayerClass() != null) { final TomahawkMediaPlayer mp = mMediaPlayers.get(currentQuery.getMediaPlayerClass()); Runnable r = new Runnable() { @Override public void run() { if (mp.isPrepared(currentQuery)) { mp.seekTo(pos); updateMediaPlayState(); } } }; ThreadManager.get().executePlayback(mp, r); } } /** * Override to handle the item being rated. */ @Override public void onSetRating(RatingCompat rating) { if (rating.getRatingStyle() == RatingCompat.RATING_HEART && mPlaybackManager.getCurrentQuery() != null) { CollectionManager.get().setLovedItem(mPlaybackManager.getCurrentQuery(), rating.hasHeart()); mPlaybackManagerCallback.onCurrentEntryChanged(); } else if (rating.getRatingStyle() == RatingCompat.RATING_THUMB_UP_DOWN && mPlaybackManager.getCurrentQuery() != null) { CollectionManager.get().setLovedItem(mPlaybackManager.getCurrentQuery(), rating.isThumbUp()); mPlaybackManagerCallback.onCurrentEntryChanged(); } } @Override public void onCustomAction(String action, Bundle extras) { if (ACTION_STOP_NOTIFICATION.equals(action)) { mNotification.stopNotification(); } else if (ACTION_DELETE_ENTRY_IN_QUEUE.equals(action)) { PlaylistEntry entry = PlaylistEntry.getByKey(extras.getString(TomahawkFragment.PLAYLISTENTRY)); mPlaybackManager.deleteFromQueue(entry); } else if (ACTION_ADD_QUERY_TO_QUEUE.equals(action)) { Query query = Query.getByKey(extras.getString(TomahawkFragment.QUERY)); mPlaybackManager.addToQueue(query); } else if (ACTION_ADD_QUERIES_TO_QUEUE.equals(action)) { List<String> queryKeys = extras.getStringArrayList(TomahawkFragment.QUERYARRAY); List<Query> queries = new ArrayList<>(); for (String queryKey : queryKeys) { queries.add(Query.getByKey(queryKey)); } mPlaybackManager.addToQueue(queries); } else if (ACTION_SET_SHUFFLE_MODE.equals(action)) { int shuffleMode = extras.getInt(EXTRAS_KEY_SHUFFLE_MODE); Log.d(TAG, "setShuffleMode to " + shuffleMode); mPlaybackManager.setShuffleMode(shuffleMode); } else if (ACTION_SET_REPEAT_MODE.equals(action)) { int repeatMode = extras.getInt(EXTRAS_KEY_REPEAT_MODE); Log.d(TAG, "setRepeatMode to " + repeatMode); mPlaybackManager.setRepeatMode(repeatMode); } } }; public void play(boolean onAudioFocusGain) { Log.d(TAG, "play"); if (onAudioFocusGain && !mPlayOnFocusGain) { return; } if (mPlaybackManager.getCurrentQuery() != null) { if (mAudioBecomingNoisyReceiver == null) { mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(); registerReceiver(mAudioBecomingNoisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); } mSuicideHandler.stop(); mSuicideHandler.reset(); mPluginServiceKillHandler.stop(); mPluginServiceKillHandler.reset(); mScrobbleHandler.start(); mPlayState = PlaybackStateCompat.STATE_PLAYING; handlePlayState(); tryToGetAudioFocus(); updateMediaPlayState(); mNotification.startNotification(); } } public void pause(boolean onAudioFocusLost) { Log.d(TAG, "pause"); mPlayOnFocusGain = onAudioFocusLost; if (mAudioBecomingNoisyReceiver != null) { unregisterReceiver(mAudioBecomingNoisyReceiver); mAudioBecomingNoisyReceiver = null; } mSuicideHandler.start(); mPluginServiceKillHandler.start(); mScrobbleHandler.stop(); mPlayState = PlaybackStateCompat.STATE_PAUSED; handlePlayState(); updateMediaPlayState(); } private PlaybackManager.Callback mPlaybackManagerCallback = new PlaybackManager.Callback() { @Override public void onPlaylistChanged() { Playlist playlist = mPlaybackManager.getPlaylist(); Log.d(TAG, "Playlist has changed to: " + playlist); if (playlist instanceof StationPlaylist) { StationPlaylist stationPlaylist = (StationPlaylist) playlist; stationPlaylist.setPlayedTimeStamp(System.currentTimeMillis()); if (stationPlaylist.getPlaylist() == null) { DatabaseHelper.get().storeStation(stationPlaylist); } if (!mPlaybackManager.hasNextEntry(mPlaybackManager.getNextEntry())) { // there's no track after the next one, // so we should fill the station with some new tracks if (mPlaybackManager.getCurrentEntry() == null) { mIsPreparing = true; } updateMediaPlayState(); fillStation(stationPlaylist); } } onCurrentEntryChanged(); } @Override public void onCurrentEntryChanged() { Log.d(TAG, "Current entry has changed to: " + mPlaybackManager.getCurrentEntry()); if (mPlaybackManager.getCurrentEntry() == null) { mNotification.stopNotification(); } handlePlayState(); Playlist playlist = mPlaybackManager.getPlaylist(); if (playlist instanceof StationPlaylist) { if (!mPlaybackManager.hasNextEntry(mPlaybackManager.getNextEntry())) { // there's no track after the next one, // so we should fill the station with some new tracks fillStation((StationPlaylist) playlist); } } resolveProximalQueries(); updateMediaMetadata(); updateMediaQueue(); } @Override public void onShuffleModeChanged() { updateMediaQueue(); } @Override public void onRepeatModeChanged() { updateMediaQueue(); } }; private void fillStation(final StationPlaylist stationPlaylist) { Promise<List<Query>, Throwable, Void> promise = stationPlaylist.fillPlaylist(10); if (promise != null) { Log.d(TAG, "filling " + stationPlaylist); promise.done(new DoneCallback<List<Query>>() { @Override public void onDone(List<Query> result) { Log.d(TAG, "found " + result.size() + " candidates to fill " + stationPlaylist); for (Query query : result) { mCorrespondingQueries.add(query); if (!mStationQueries.containsKey(stationPlaylist)) { Set<Query> querySet = Collections .newSetFromMap(new ConcurrentHashMap<Query, Boolean>()); mStationQueries.put(stationPlaylist, querySet); } mStationQueries.get(stationPlaylist).add(query); PipeLine.get().resolve(query); } } }); promise.fail(new FailCallback<Throwable>() { @Override public void onFail(final Throwable result) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { Toast.makeText(TomahawkApp.getContext(), result.getMessage(), Toast.LENGTH_LONG).show(); } }); } }); } } private AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver; private class AudioBecomingNoisyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { // AudioManager tells us that the sound will be played through the speaker Log.d(TAG, "Action audio becoming noisy, pausing ..."); // So we stop playback, if needed mMediaSession.getController().getTransportControls().pause(); } } } private PowerManager.WakeLock mWakeLock; private Bitmap mCachedPlaceHolder; private final LruCache<Image, Bitmap> mMediaImageCache = new LruCache<Image, Bitmap>(MAX_ALBUM_ART_CACHE_SIZE) { @Override protected int sizeOf(Image key, Bitmap value) { return value.getByteCount(); } }; private MediaImageTarget mMediaImageTarget; private class MediaImageTarget implements Target { private Image mImageToLoad; public MediaImageTarget(Image imageToLoad) { mImageToLoad = imageToLoad; } @Override public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { new Runnable() { @Override public void run() { if (mMediaSession == null) { Log.e(TAG, "updateAlbumArt failed - mMediaSession == null!"); return; } Bitmap copy = bitmap.copy(bitmap.getConfig(), false); if (mImageToLoad == null) { // Has to the placeHolder bitmap then mCachedPlaceHolder = copy; } else { mMediaImageCache.put(mImageToLoad, copy); } mMediaSession.setMetadata(buildMetadata()); Log.d(TAG, "Setting lockscreen bitmap"); } }.run(); } @Override public void onBitmapFailed(Drawable drawable) { } @Override public void onPrepareLoad(Drawable drawable) { } } private RemoteControllerConnection mRemoteControllerConnection; private static class RemoteControllerConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "Connected to RemoteControllerService!"); } @Override public void onServiceDisconnected(ComponentName name) { Log.e(TAG, "RemoteControllerService has crashed :("); } } private SuicideHandler mSuicideHandler = new SuicideHandler(this); // Stops this service if it doesn't have any bound services private static class SuicideHandler extends DelayedHandler<PlaybackService> { public SuicideHandler(PlaybackService service) { super(service, DELAY_SUICIDE); } @Override public void handleMessage(Message msg) { if (getReferencedObject() != null) { Log.d(TAG, "Killtimer called stopSelf() on me"); getReferencedObject().stopSelf(); } } } private PluginServiceKillHandler mPluginServiceKillHandler = new PluginServiceKillHandler(this); private static class PluginServiceKillHandler extends DelayedHandler<PlaybackService> { public PluginServiceKillHandler(PlaybackService service) { super(service, DELAY_UNBIND_PLUGINSERVICES); } @Override public void handleMessage(Message msg) { if (getReferencedObject() != null) { getReferencedObject().unbindPluginServices(); } } } private ScrobbleHandler mScrobbleHandler = new ScrobbleHandler(this); private static class ScrobbleHandler extends DelayedHandler<PlaybackService> { public ScrobbleHandler(PlaybackService service) { super(service, DELAY_SCROBBLE); } @Override public void handleMessage(Message msg) { if (getReferencedObject() != null) { Log.d(TAG, "Scrobbling delay has passed. Scrobbling..."); if (getReferencedObject().mPlaybackManager.getCurrentQuery() != null) { InfoSystem.get().sendNowPlayingPostStruct( AuthenticatorManager.get().getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET), getReferencedObject().mPlaybackManager.getCurrentQuery()); } } } } private TomahawkMediaPlayerCallback mMediaPlayerCallback = new TomahawkMediaPlayerCallback() { @Override public void onPrepared(TomahawkMediaPlayer mediaPlayer, Query query) { if (mediaPlayer != mCurrentMediaPlayer) { Log.d(TAG, "Ignoring onPrepared call, because it hasn't been invoked by mCurrentMediaPlayer"); return; } if (query != null && query == mPlaybackManager.getCurrentQuery()) { Log.d(TAG, mediaPlayer + " successfully prepared the track " + mPlaybackManager.getCurrentQuery() + " resolved by " + mPlaybackManager.getCurrentQuery().getPreferredTrackResult().getResolvedBy().getId()); mIsPreparing = false; updateMediaPlayState(); mScrobbleHandler.reset(); handlePlayState(); } else { String queryInfo; if (query != null) { queryInfo = mPlaybackManager.getCurrentQuery() + " resolved by " + mPlaybackManager.getCurrentQuery().getPreferredTrackResult().getResolvedBy().getId(); } else { queryInfo = "null"; } Log.e(TAG, "onPrepared received for an unexpected Query: " + queryInfo); } } @Override public void onCompletion(TomahawkMediaPlayer mediaPlayer, Query query) { if (mediaPlayer != mCurrentMediaPlayer) { Log.d(TAG, "Ignoring onCompletion call, because it hasn't been invoked by mCurrentMediaPlayer"); return; } if (mMediaSession == null) { Log.e(TAG, "onCompletion failed - mMediaSession == null!"); return; } if (query != null && query == mPlaybackManager.getCurrentQuery()) { Log.d(TAG, "onCompletion - mediaPlayer: " + mediaPlayer + ", query: " + query); if (mPlaybackManager.hasNextEntry()) { mMediaSession.getController().getTransportControls().skipToNext(); } else { mMediaSession.getController().getTransportControls().pause(); } } } @Override public void onError(TomahawkMediaPlayer mediaPlayer, final String message) { Log.d(TAG, "onError - mediaPlayer: " + mediaPlayer + ", message: " + message); if (mediaPlayer != mCurrentMediaPlayer) { Log.d(TAG, "Ignoring onError call, because it hasn't been invoked by mCurrentMediaPlayer"); return; } new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { Toast.makeText(TomahawkApp.getContext(), message, Toast.LENGTH_LONG).show(); } }); giveUpAudioFocus(); if (mMediaSession == null) { Log.e(TAG, "onError failed - mMediaSession == null!"); return; } if (mPlaybackManager.hasNextEntry()) { mMediaSession.getController().getTransportControls().skipToNext(); } else { mMediaSession.getController().getTransportControls().pause(); } } }; @SuppressWarnings("unused") public void onEventAsync(PipeLine.ResultsEvent event) { Playlist playlist = mPlaybackManager.getPlaylist(); if (playlist instanceof StationPlaylist && event.mQuery.isPlayable() && mStationQueries.containsKey(playlist) && mStationQueries.get(playlist).remove(event.mQuery)) { boolean wasNull = mPlaybackManager.getCurrentEntry() == null; mPlaybackManager.addToPlaylist(event.mQuery); if (wasNull) { if (mMediaSession == null) { Log.e(TAG, "onEventAsync(PipeLine.ResultsEvent event) failed - mMediaSession == null!"); } else { mMediaSession.getController().getTransportControls().play(); } } } final Query currentQuery = mPlaybackManager.getCurrentQuery(); if (currentQuery != null && currentQuery == event.mQuery) { mPlaybackManagerCallback.onCurrentEntryChanged(); Runnable r = new Runnable() { @Override public void run() { if (mCurrentMediaPlayer == null || !(mCurrentMediaPlayer.isPrepared(currentQuery) || mCurrentMediaPlayer.isPreparing(currentQuery))) { handlePlayState(); } } }; ThreadManager.get().executePlayback(mCurrentMediaPlayer, r); } } @SuppressWarnings("unused") public void onEventAsync(InfoSystem.ResultsEvent event) { Query currentQuery = mPlaybackManager.getCurrentQuery(); if (currentQuery != null && currentQuery.getCacheKey() .equals(mCorrespondingRequestIds.get(event.mInfoRequestData.getRequestId()))) { mPlaybackManagerCallback.onCurrentEntryChanged(); } } @SuppressWarnings("unused") public void onEventAsync(CollectionManager.UpdatedEvent event) { Query currentQuery = mPlaybackManager.getCurrentQuery(); if (event.mUpdatedItemIds != null && currentQuery != null && event.mUpdatedItemIds.contains(currentQuery.getCacheKey())) { mPlaybackManagerCallback.onCurrentEntryChanged(); } } public static class RequestServiceBindingEvent { private ServiceConnection mConnection; private String mServicePackageName; public RequestServiceBindingEvent(ServiceConnection connection, String servicePackageName) { mConnection = connection; mServicePackageName = servicePackageName; } } @SuppressWarnings("unused") public void onEvent(RequestServiceBindingEvent event) { Intent intent = new Intent(event.mServicePackageName + ".BindToService"); intent.setPackage(event.mServicePackageName); bindService(intent, event.mConnection, Context.BIND_AUTO_CREATE); } @Override public void onCreate() { super.onCreate(); EventBus.getDefault().register(this); PipeLine.get(); mMediaBrowserHelper = new MediaBrowserHelper(this); mMediaPlayers.put(AndroidMediaPlayer.class, new AndroidMediaPlayer()); mMediaPlayers.put(VLCMediaPlayer.class, new VLCMediaPlayer()); mMediaPlayers.put(DeezerMediaPlayer.class, new DeezerMediaPlayer()); mMediaPlayers.put(SpotifyMediaPlayer.class, new SpotifyMediaPlayer()); startService(new Intent(this, MicroService.class)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mRemoteControllerConnection = new RemoteControllerConnection(); bindService(new Intent(this, RemoteControllerService.class), mRemoteControllerConnection, Context.BIND_AUTO_CREATE); } // Initialize WakeLock PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); mPlaybackManager = PlaybackManager.get(IdGenerator.getSessionUniqueStringId()); mPlaybackManager.setCallback(mPlaybackManagerCallback); initMediaSession(); try { mNotification = new MediaNotification(this); } catch (RemoteException e) { Log.e(TAG, "Could not connect to media controller: ", e); } Log.d(TAG, "PlaybackService has been created"); } private void initMediaSession() { ComponentName componentName = new ComponentName(this, MediaButtonReceiver.class); mMediaSession = new MediaSessionCompat(getApplicationContext(), "Tomahawk", componentName, null); mMediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); Intent intent = new Intent(PlaybackService.this, TomahawkMainActivity.class); intent.setAction(TomahawkMainActivity.SHOW_PLAYBACKFRAGMENT_ON_STARTUP); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(PlaybackService.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); mMediaSession.setSessionActivity(pendingIntent); HandlerThread thread = new HandlerThread("playbackservice_callback"); thread.start(); mCallbackHandler = new Handler(thread.getLooper()); mMediaSession.setCallback(mMediaSessionCallback, mCallbackHandler); mMediaSession.setRatingType(RatingCompat.RATING_HEART); Bundle extras = new Bundle(); extras.putString(EXTRAS_KEY_PLAYBACKMANAGER, mPlaybackManager.getId()); mMediaSession.setExtras(extras); updateMediaPlayState(); setSessionToken(mMediaSession.getSessionToken()); } public Handler getCallbackHandler() { return mCallbackHandler; } @Override public int onStartCommand(Intent intent, int flags, int startId) { MediaButtonReceiver.handleIntent(mMediaSession, intent); return START_STICKY; } @Override public IBinder onBind(Intent intent) { Log.d(TAG, "Client has been bound to PlaybackService"); return super.onBind(intent); } @Nullable @Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) { return mMediaBrowserHelper.onGetRoot(clientPackageName, clientUid, rootHints); } @Override public void onLoadChildren(@NonNull String parentId, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) { mMediaBrowserHelper.onLoadChildren(parentId, result); } @Override public boolean onUnbind(Intent intent) { Log.d(TAG, "Client has been unbound from PlaybackService"); return super.onUnbind(intent); } @Override public void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); giveUpAudioFocus(); mPlaybackManager.setCallback(null); if (mAudioBecomingNoisyReceiver != null) { unregisterReceiver(mAudioBecomingNoisyReceiver); mAudioBecomingNoisyReceiver = null; } mScrobbleHandler.stop(); mPlayState = PlaybackStateCompat.STATE_PAUSED; handlePlayState(); mNotification.stopNotification(); releaseAllPlayers(); if (mWakeLock.isHeld()) { mWakeLock.release(); } mWakeLock = null; mSuicideHandler.stop(); mSuicideHandler = null; mPluginServiceKillHandler.stop(); mPluginServiceKillHandler = null; if (mMediaSession != null) { mMediaSession.setCallback(null); mMediaSession.release(); mMediaSession = null; } if (mRemoteControllerConnection != null) { unbindService(mRemoteControllerConnection); } unbindPluginServices(); Log.d(TAG, "PlaybackService has been destroyed"); } private void unbindPluginServices() { Log.d(TAG, "Unbinding all PluginServices..."); for (TomahawkMediaPlayer mp : mMediaPlayers.values()) { if (mp instanceof PluginMediaPlayer) { PluginMediaPlayer pmp = (PluginMediaPlayer) mp; if (pmp.isBound()) { pmp.setService(null); unbindService(pmp.getServiceConnection()); } } } } /** * Update the TomahawkMediaPlayer so that it reflects the current playState */ private void handlePlayState() { Log.d(TAG, "handlePlayState"); final Query currentQuery = mPlaybackManager.getCurrentQuery(); if (currentQuery != null && currentQuery.getMediaPlayerClass() != null) { final TomahawkMediaPlayer mp = mMediaPlayers.get(currentQuery.getMediaPlayerClass()); Runnable r = new Runnable() { @Override public void run() { switch (mPlayState) { case PlaybackStateCompat.STATE_PLAYING: // The service needs to continue running even after the bound client // (usually a MediaController) disconnects, otherwise the music playback // will stop. Calling startService(Intent) will keep the service running // until it is explicitly killed. startService(new Intent(getApplicationContext(), PlaybackService.class)); if (mWakeLock != null && mWakeLock.isHeld()) { mWakeLock.acquire(); } if (mp.isPreparing(currentQuery) || mp.isPrepared(currentQuery)) { if (!mp.isPlaying(currentQuery)) { mp.play(); } } else { prepareCurrentQuery(); } break; case PlaybackStateCompat.STATE_PAUSED: if (mp.isPlaying(currentQuery) && (mp.isPreparing(currentQuery) || mp.isPrepared(currentQuery))) { InfoSystem.get().sendPlaybackEntryPostStruct(AuthenticatorManager.get() .getAuthenticatorUtils(TomahawkApp.PLUGINNAME_HATCHET)); mp.pause(); } if (mWakeLock != null && mWakeLock.isHeld()) { mWakeLock.release(); } break; } } }; ThreadManager.get().executePlayback(mp, r); } else { releaseAllPlayers(); if (mWakeLock != null && mWakeLock.isHeld()) { mWakeLock.release(); } Log.d(TAG, "handlePlayState couldn't do anything, isPreparing: " + mIsPreparing); } } /** * This method sets the current track and prepares it for playback. */ private void prepareCurrentQuery() { if (mMediaSession == null) { Log.e(TAG, "prepareCurrentQuery failed - mMediaSession == null!"); return; } Log.d(TAG, "prepareCurrentQuery"); final Query currentQuery = mPlaybackManager.getCurrentQuery(); if (currentQuery != null) { if (!currentQuery.isPlayable() || currentQuery.getMediaPlayerClass() == null) { Log.e(TAG, currentQuery + " isn't playable. Skipping to next track"); mMediaSession.getController().getTransportControls().skipToNext(); } else { // Resolve images for current query if (currentQuery.getImage() == null) { String requestId = InfoSystem.get().resolve(currentQuery.getArtist(), false); if (requestId != null) { mCorrespondingRequestIds.put(requestId, currentQuery.getCacheKey()); } requestId = InfoSystem.get().resolve(currentQuery.getAlbum()); if (requestId != null) { mCorrespondingRequestIds.put(requestId, currentQuery.getCacheKey()); } } mIsPreparing = true; updateMediaPlayState(); TomahawkMediaPlayer mp = mMediaPlayers.get(currentQuery.getMediaPlayerClass()); if (mCurrentMediaPlayer != null && mCurrentMediaPlayer != mp) { mCurrentMediaPlayer.release(); } mCurrentMediaPlayer = mp; mp.prepare(currentQuery, mMediaPlayerCallback); } } } /** * Returns the position of playback in the current Track. */ private long getPlaybackPosition() { long position = 0; final Query currentQuery = mPlaybackManager.getCurrentQuery(); if (currentQuery != null && currentQuery.getMediaPlayerClass() != null) { position = mMediaPlayers.get(currentQuery.getMediaPlayerClass()).getPosition(); } return position; } /** * Update the playback controls/views which are being shown on the lockscreen */ private void updateMediaMetadata() { Log.d(TAG, "updateMediaMetadata()"); if (mMediaSession == null) { Log.e(TAG, "updateMediaMetadata failed - mMediaSession == null!"); return; } updateMediaPlayState(); mMediaSession.setActive(true); mMediaSession.setMetadata(buildMetadata()); if (mPlaybackManager.getCurrentQuery() != null) { Log.d(TAG, "Setting media metadata to: " + mPlaybackManager.getCurrentQuery()); } else if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { Log.d(TAG, "Setting media metadata to: " + getString(R.string.loading_station) + " " + mPlaybackManager.getPlaylist().getName()); } else { Log.e(TAG, "Wasn't able to set media metadata"); } } private MediaMetadataCompat buildMetadata() { final Query currentQuery = mPlaybackManager.getCurrentQuery(); MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); if (currentQuery != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mPlaybackManager.getCurrentEntry().getCacheKey()) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, currentQuery.getArtist().getPrettyName()) .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentQuery.getArtist().getPrettyName()) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentQuery.getPrettyName()) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, currentQuery.getPreferredTrack().getDuration()) .putRating(MediaMetadataCompat.METADATA_KEY_USER_RATING, RatingCompat.newHeartRating(DatabaseHelper.get().isItemLoved(currentQuery))); if (!currentQuery.getAlbum().getName().isEmpty()) { builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, currentQuery.getAlbum().getPrettyName()); } Bitmap bitmap; if (currentQuery.getImage() != null) { bitmap = mMediaImageCache.get(currentQuery.getImage()); } else { bitmap = mCachedPlaceHolder; } if (bitmap == null) { // Image is not in cache yet. We have to fetch it... new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { if (mMediaImageTarget == null || mMediaImageTarget.mImageToLoad != currentQuery.getImage()) { mMediaImageTarget = new MediaImageTarget(currentQuery.getImage()); ImageUtils.loadImageIntoBitmap(TomahawkApp.getContext(), currentQuery.getImage(), mMediaImageTarget, Image.getLargeImageSize(), false); } } }); } if (bitmap != null) { builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap); } } else if (mPlaybackManager.getPlaylist() instanceof StationPlaylist) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, getString(R.string.loading_station) + " " + mPlaybackManager.getPlaylist().getName()); } return builder.build(); } private void updateMediaPlayState() { if (mMediaSession == null) { Log.e(TAG, "updateMediaPlayState failed - mMediaSession == null!"); return; } long actions = 0L; if (mPlaybackManager.getCurrentQuery() != null) { actions |= PlaybackStateCompat.ACTION_SET_RATING; } if (mPlayState == PlaybackStateCompat.STATE_PLAYING) { actions |= PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND; } else { actions |= PlaybackStateCompat.ACTION_PLAY; } if (mPlaybackManager.hasNextEntry()) { actions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; } if (mPlaybackManager.hasPreviousEntry()) { actions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; } Log.d(TAG, "updateMediaPlayState()"); Bundle extras = new Bundle(); extras.putInt(EXTRAS_KEY_REPEAT_MODE, mPlaybackManager.getRepeatMode()); extras.putInt(EXTRAS_KEY_SHUFFLE_MODE, mPlaybackManager.getShuffleMode()); int playState = mIsPreparing ? PlaybackStateCompat.STATE_BUFFERING : mPlayState; PlaybackStateCompat playbackStateCompat = new PlaybackStateCompat.Builder().setActions(actions) .setState(playState, getPlaybackPosition(), 1f, SystemClock.elapsedRealtime()).setExtras(extras) .build(); mMediaSession.setPlaybackState(playbackStateCompat); } private void updateMediaQueue() { if (mMediaSession == null) { Log.e(TAG, "updateMediaQueue failed - mMediaSession == null!"); return; } updateMediaMetadata(); mMediaSession.setQueue(buildQueue()); mMediaSession.setQueueTitle(getString(R.string.mediabrowser_queue_title)); } private List<MediaSessionCompat.QueueItem> buildQueue() { List<MediaSessionCompat.QueueItem> queue = null; if (mPlaybackManager.getPlaylist() != null) { queue = new ArrayList<>(); int currentIndex = mPlaybackManager.getCurrentIndex(); for (int i = Math.max(0, currentIndex - 1); i < Math.min(mPlaybackManager.getPlaybackListSize(), currentIndex + 40); i++) { PlaylistEntry entry = mPlaybackManager.getPlaybackListEntry(i); MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder(); descBuilder.setMediaId(entry.getCacheKey()); descBuilder.setTitle(entry.getQuery().getPrettyName()); descBuilder.setSubtitle(entry.getArtist().getPrettyName()); MediaSessionCompat.QueueItem item = new MediaSessionCompat.QueueItem(descBuilder.build(), i); queue.add(item); mQueueMap.put(i, entry); } } return queue; } private void resolveProximalQueries() { Set<Query> qs = new HashSet<>(); int start = Math.max(0, mPlaybackManager.getCurrentIndex() - 2); int end = Math.min(mPlaybackManager.getPlaybackListSize(), mPlaybackManager.getCurrentIndex() + 10); for (int i = start; i < end; i++) { Query q = mPlaybackManager.getPlaybackListEntry(i).getQuery(); if (!mCorrespondingQueries.contains(q)) { qs.add(q); } } if (!qs.isEmpty()) { HashSet<Query> queries = PipeLine.get().resolve(qs); mCorrespondingQueries.addAll(queries); } } private void setBitrate(final int mode) { for (final TomahawkMediaPlayer mp : mMediaPlayers.values()) { Runnable r = new Runnable() { @Override public void run() { mp.setBitrate(mode); } }; ThreadManager.get().executePlayback(mp, r); } } private void releaseAllPlayers() { for (final TomahawkMediaPlayer mp : mMediaPlayers.values()) { Runnable r = new Runnable() { @Override public void run() { mp.release(); } }; ThreadManager.get().executePlayback(mp, r); } } /** * Try to get the system audio focus. */ private void tryToGetAudioFocus() { Log.d(TAG, "tryToGetAudioFocus"); if (mAudioFocus != AUDIO_FOCUSED) { int result = mAudioManager.requestAudioFocus(mFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { mAudioFocus = AUDIO_FOCUSED; } } } /** * Give up the audio focus. */ private void giveUpAudioFocus() { Log.d(TAG, "giveUpAudioFocus"); if (mAudioFocus == AUDIO_FOCUSED) { if (mAudioManager.abandonAudioFocus(mFocusChangeListener) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK; } } } }