public class ApplicationConstants {

    private ApplicationConstants() {}

    /* Request codes */
    public static final int LOGIN_INTENT_REQUEST_CODE = 5473;
    public static final int SEARCH_INTENT_REQUEST_CODE = 3125;

    /* Time and timing-related */
    public static final int DEFER_SNACKBAR_DELAY = 1000;
    public static final int REQUEST_RETRY_DELAY_SEC = 3;
    public static final int SHORT_VIBRATION_DURATION_MS = 30;
    public static final int SHORT_ACTION_DELAY_SEC = 1;
    public static final int MEDIUM_ACTION_DELAY_SEC = 2;
    public static final int LONG_ACTION_DELAY_SEC = 3;
    public static final int LONG_TOAST_DURATION_SEC = 5;

    /* Image-related */
    //Placeholders
    public static final String ALBUM_ART_PLACEHOLDER_URL = "https://www.zinokader.se/img/spotiq-album-placeholder.png";
    public static final String PLAYLIST_IMAGE_PLACEHOLDER_URL = "https://www.zinokader.se/img/spotiq-playlist-placeholder.png";
    public static final String PROFILE_IMAGE_PLACEHOLDER_URL = "https://www.zinokader.se/img/spotiq-profile-placeholder.png";
    //Dimensions
    public static final int DEFAULT_TRACKLIST_CROP_WIDTH = 600;
    public static final int DEFAULT_TRACKLIST_CROP_HEIGHT = 200;
    public static final int DEFAULT_TRACKLIST_BLUR_RADIUS = 100;
    public static final int LOW_QUALITY_ALBUM_ART_DIMENSION = 100;
    public static final int MEDIUM_QUALITY_ALBUM_ART_DIMENSION = 200;

    /* Notification-related */
    public static final String MEDIA_NOTIFICATION_CHANNEL_ID = "media_notification_channel";
    public static final String MEDIA_SESSSION_TAG = "spotiq_media";

    /* App shortcut-related */
    public static final String SEARCH_SHORTCUT_ID = "spotiq_search_shortcut";

    /* Activity & Lifecycle-related */

    //Splash-related
    public static final float ANIMATION_LOGO_SCALE_DOWN = 0.7f;
    public static final float ANIMATION_LOGO_SCALE_UP = 100f;
    public static final int ANIMATION_LOGO_SCALE_DOWN_DURATION_MS = 750;
    public static final int ANIMATION_LOGO_SCALE_UP_DURATION_MS = 200;

    //Lobby-related
    public static final int DIALOG_ANIMATION_DURATION = 650;

    //Party-related
    public static final String PARTY_NAME_EXTRA = "party_name_extra";
    public static final int PLAY_PAUSE_BUTTON_SYNCHRONIZATION_DELAY_MS = 800;
    public static final int DEFAULT_NEW_ITEM_DELAY_MS = 0;
    public static final int DEFAULT_LIST_ANIMATION_DURATION_MS = 400;
    public static final int DEFAULT_LIST_ANIMATION_ITEM_POSITION_START = 6;
    public static final int DEFAULT_ITEM_ADD_DURATION_MS = 1200;
    public static final int DEFAULT_ITEM_REMOVE_DURATION_MS = 350;
    public static final int DEFAULT_ITEM_MOVE_DURATION_MS = 900;

    /* Search-related */
    public static final String SONG_REQUESTS_EXTRA = "song_requests_extra";
    public static final int DEFAULT_DEBOUNCE_MS = 600;
    public static final int MAX_SONG_SUGGESTIONS = 50;

} public final class LogTag {

    private LogTag() {}

    public static final String LOG_TOKEN_SERVICE = "service";
    public static final String LOG_LOGIN = "login";
    public static final String LOG_LOBBY = "lobby";
    public static final String LOG_PARTY = "party";
    public static final String LOG_SEARCH = "search";
    public static final String LOG_TRACKLIST_REPOSITORY = "tracklist_repo";
    public static final String LOG_PREVIEW_PLAYER = "preview_player";
    public static final String LOG_HOST_SERVICE ="host_service";

} public class ServiceConstants {

    private ServiceConstants() {}

    /* Authentication-service */
    public static final long TOKEN_EXPIRY_CUTOFF = TimeUnit.MINUTES.toSeconds(20);

    /* Player-service */
    public static final int NOTIFICATION_SEND_DELAY_MS = 2000;
    public static final String ACTION_INIT = "spotiq.ACTION_INIT";
    public static final String ACTION_PLAY_PAUSE = "spotiq.ACTION_PLAY_PAUSE";
    public static final String PLAYING_STATUS_BROADCAST_NAME = "spotiq.PLAYING_STATUS_BROADCAST";
    public static final String PLAYING_STATUS_ISPLAYING_EXTRA = "playing_status_isplaying_extra";

} public class ChildEvent {

    private DataSnapshot dataSnapshot;
    private String previousChildName;
    private Type changeType;

    public enum Type {
        ADDED, CHANGED, REMOVED, MOVED
    }

    public ChildEvent(DataSnapshot dataSnapshot, Type changeType) {
        this.dataSnapshot = dataSnapshot;
        this.changeType = changeType;
    }

    public ChildEvent(DataSnapshot dataSnapshot, Type changeType, String previousChildName) {
        this.dataSnapshot = dataSnapshot;
        this.changeType = changeType;
        this.previousChildName = previousChildName;
    }

    public DataSnapshot getDataSnapshot() {
        return this.dataSnapshot;
    }

    @Nullable
    public String getPreviousChildName() {
        return this.previousChildName;
    }

    public Type getChangeType() {
        return this.changeType;
    }


} public class TracklistRepository {

    private DatabaseReference databaseReference;

    public TracklistRepository(DatabaseReference databaseReference) {
        this.databaseReference = databaseReference;
    }

    public Observable<Pair<Song, Boolean>> checkSongInDbPlaylist(Song song, String partyTitle) {
        return Observable.create(subscriber -> databaseReference
            .child(partyTitle)
            .child(FirebaseConstants.CHILD_TRACKLIST)
            .addListenerForSingleValueEvent(new ValueEventListener() {
                @Override
                public void onDataChange(DataSnapshot dataSnapshot) {
                    for (DataSnapshot songSnapshot : dataSnapshot.getChildren()) {
                        Song dbSong = songSnapshot.getValue(Song.class);
                        if (dbSong == null) continue;
                        if (SongComparator.isEqual(song, dbSong)) {
                            subscriber.onNext(new Pair<>(song, true));
                            subscriber.onComplete();
                            break;
                        }
                    }
                    subscriber.onNext(new Pair<>(song, false));
                    subscriber.onComplete();
                }

                @Override
                public void onCancelled(DatabaseError databaseError) {
                    Log.d(LogTag.LOG_TRACKLIST_REPOSITORY, "Something went wrong when checking " +
                        "if song exists in db: " + databaseError.getMessage());
                    databaseError.toException().printStackTrace();
                }
            }));
    }

    public Observable<Boolean> addSong(Song song, String partyTitle) {
        return Observable.create(subscriber -> databaseReference
            .child(partyTitle)
            .child(FirebaseConstants.CHILD_TRACKLIST)
            .push() //push is timestamp-based, items are chronologically ordered
            .setValue(song)
            .addOnCompleteListener(task -> subscriber.onNext(task.isSuccessful()))
            .addOnFailureListener(subscriber::onError));
    }

    public Single<Song> getFirstSong(String partyTitle) {
        return Single.create(subscriber -> databaseReference
            .child(partyTitle)
            .child(FirebaseConstants.CHILD_TRACKLIST)
            .orderByKey()
            .addListenerForSingleValueEvent(new ValueEventListener() {
                @Override
                public void onDataChange(DataSnapshot dataSnapshot) {
                    if (dataSnapshot.getChildren().iterator().hasNext()) {
                        subscriber.onSuccess(dataSnapshot.getChildren().iterator().next().getValue(Song.class));
                    }
                    else {
                        subscriber.onError(new EmptyTracklistException());
                    }
                }

                @Override
                public void onCancelled(DatabaseError databaseError) {
                    subscriber.onError(databaseError.toException());
                }
            }));
    }

    public Single<Boolean> removeFirstSong(String partyTitle) {
        return Single.create(subscriber -> getFirstSongKey(partyTitle)
            .subscribe(songKey -> databaseReference
                    .child(partyTitle)
                    .child(FirebaseConstants.CHILD_TRACKLIST)
                    .child(songKey)
                    .removeValue()
                    .addOnSuccessListener(removeSuccess -> subscriber.onSuccess(true))
                    .addOnFailureListener(failedRemoveException -> subscriber.onSuccess(false)),
                subscriber::onError));
    }

    private Single<String> getFirstSongKey(String partyTitle) {
        return Single.create(subscriber -> databaseReference
            .child(partyTitle)
            .child(FirebaseConstants.CHILD_TRACKLIST)
            .orderByKey()
            .addListenerForSingleValueEvent(new ValueEventListener() {
                @Override
                public void onDataChange(DataSnapshot dataSnapshot) {
                    if (dataSnapshot.getChildren().iterator().hasNext()) {
                        subscriber.onSuccess(dataSnapshot.getChildren().iterator().next().getKey());
                    }
                    else {
                        subscriber.onError(new EmptyTracklistException());
                    }
                }

                @Override
                public void onCancelled(DatabaseError databaseError) {
                    subscriber.onError(databaseError.toException());
                }
            }));
    }

    public Observable<ChildEvent> listenToTracklistChanges(String partyTitle) {
        return Observable.create(subscriber -> databaseReference
            .child(partyTitle)
            .child(FirebaseConstants.CHILD_TRACKLIST)
            .addChildEventListener(new ChildEventListener() {
                @Override
                public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
                    subscriber.onNext(new ChildEvent(dataSnapshot, ChildEvent.Type.ADDED, previousChildName));
                }

                @Override
                public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
                    subscriber.onNext(new ChildEvent(dataSnapshot, ChildEvent.Type.CHANGED, previousChildName));
                }

                @Override
                public void onChildRemoved(DataSnapshot dataSnapshot) {
                    subscriber.onNext(new ChildEvent(dataSnapshot, ChildEvent.Type.REMOVED));
                }

                @Override
                public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
                    subscriber.onNext(new ChildEvent(dataSnapshot, ChildEvent.Type.MOVED, previousChildName));
                }

                @Override
                public void onCancelled(DatabaseError databaseError) {
                    subscriber.onError(databaseError.toException());
                }
            }));
    }

} @Singleton
public class SpotifyAuthenticationService extends Service {

    private static final SpotifyAuthenticator spotifyAuthenticator = new SpotifyAuthenticator();
    private static final SpotifyApi spotifyWebApi = new SpotifyApi();
    private static Disposable tokenRenewalJob;

    @Override
    public void onCreate() {
        super.onCreate();
        startJob();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopJob();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    /**
     * We are implementing our service this way because the only way to refresh the token is opening
     * a new activity, which is annoying when the user is navigating other apps while using SpotiQ.
     * To resolve this, we are registering an observable interval that will mimic a foreground service.
     * This job should be run on resume from all presenters which care about token renewal, and paused
     * on pause. The job will be run immidiately on call, and then every 5 minutes thereafter.
     */

    private void startJob() {
        tokenRenewalJob = Observable.interval(0, 5, TimeUnit.MINUTES)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(interval -> {
                if (spotifyAuthenticator.getExpiresIn() <= ServiceConstants.TOKEN_EXPIRY_CUTOFF) {
                    Intent loginIntent = new Intent(this, SpotifyAuthenticationActivity.class);
                    loginIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(loginIntent);
                    Log.d(LogTag.LOG_TOKEN_SERVICE, "Token was updated");
                }
                else {
                    Log.d(LogTag.LOG_TOKEN_SERVICE, "Token wasn't updated - existing token still valid");
                }
            });
    }

    private void stopJob() {
        tokenRenewalJob.dispose();
    }

    public SpotifyAuthenticator getAuthenticator() {
        return spotifyAuthenticator;
    }

    public SpotifyService getWebApi() {
        return spotifyWebApi.setAccessToken(getAuthenticator().getAccessToken()).getService();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
} public class NetworkUtil {

    public static boolean isConnected(Context context) {
        ConnectivityManager cm =
            (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
    }

} public class NotificationUtil {

    /**
     * Create notification channels required for displayal of notifications on >=API O
     */
    @RequiresApi(api = Build.VERSION_CODES.O)
    public static void createNotificationChannels(Context context) {
        NotificationManager mNotificationManager =
            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        String channelId = ApplicationConstants.MEDIA_NOTIFICATION_CHANNEL_ID;
        CharSequence userVisibleChannelName = context.getString(R.string.media_notification_channel_name);
        String userVisibleDescription = context.getString(R.string.media_notification_channel_description);

        int notificationImportance = NotificationManager.IMPORTANCE_DEFAULT;
        AudioAttributes audioAttributes =
            new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build();

        NotificationChannel mediaNotificationChannel = new NotificationChannel(channelId, userVisibleChannelName, notificationImportance);
        mediaNotificationChannel.setDescription(userVisibleDescription);
        mediaNotificationChannel.enableVibration(false);
        mediaNotificationChannel.enableLights(false);
        mediaNotificationChannel.setSound(Uri.EMPTY, audioAttributes);

        mNotificationManager.createNotificationChannel(mediaNotificationChannel);
    }


    @RequiresApi(api = Build.VERSION_CODES.O)
    public static Notification buildPlayerNotification(Context context, MediaSession mediaSession,
                                                       String title, String description, Bitmap largeIcon) {

        PendingIntent openPartyIntent = PendingIntent.getActivity(context, 0,
            new Intent(context, PartyActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);

        Intent playPauseActionIntent = new Intent(context, SpotiqHostService.class);
        playPauseActionIntent.setAction(ServiceConstants.ACTION_PLAY_PAUSE);

        PendingIntent playPauseButtonIntent = PendingIntent.getForegroundService(context, 1,
            playPauseActionIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        return new Notification.Builder(context, ApplicationConstants.MEDIA_NOTIFICATION_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification_logo)
            .setLargeIcon(largeIcon)
            .setActions(new Notification.Action(R.drawable.ic_notification_play_pause, "Play/Pause", playPauseButtonIntent))
            .setStyle(new Notification.MediaStyle()
                .setMediaSession(mediaSession.getSessionToken())
                .setShowActionsInCompactView(0))
            .setColorized(true)
            .setContentTitle(title)
            .setContentText(description)
            .setContentIntent(openPartyIntent)
            .setOngoing(true)
            .build();

    }

    public static Notification buildPlayerNotificationCompat(Context context, MediaSessionCompat mediaSessionCompat,
                                                             String title, String description, Bitmap largeIcon) {

        PendingIntent openPartyIntent = PendingIntent.getActivity(context, 0,
            new Intent(context, PartyActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);

        Intent playPauseActionIntent = new Intent(context, SpotiqHostService.class);
        playPauseActionIntent.setAction(ServiceConstants.ACTION_PLAY_PAUSE);

        PendingIntent playPauseIntent = PendingIntent.getService(context, 1,
            playPauseActionIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        int largeIconWidth = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
        int largeIconHeight = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height);

        largeIcon = Bitmap.createScaledBitmap(largeIcon, largeIconWidth, largeIconHeight, false);

        return new NotificationCompat.Builder(context, ApplicationConstants.MEDIA_NOTIFICATION_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification_logo)
            .setLargeIcon(largeIcon)
            .addAction(new NotificationCompat.Action(R.drawable.ic_notification_play_pause, "Play/Pause", playPauseIntent))
            .setStyle(new MediaStyle()
                .setMediaSession(mediaSessionCompat.getSessionToken())
                .setShowActionsInCompactView(0))
            .setColorized(true)
            .setContentTitle(title)
            .setContentText(description)
            .setContentIntent(openPartyIntent)
            .setOngoing(true)
            .setDefaults(4)
            .build();
    }

    /**
     * Create meta data for notifications that allow the system to send track information to
     * bluetooth systems, show artwork in lock screens among other uses
     */
    @RequiresApi(api = Build.VERSION_CODES.O)
    public static MediaMetadata buildMediaMetadata(Metadata.Track trackMetadata, Bitmap albumArt) {
        return new MediaMetadata.Builder()
            .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, trackMetadata.name) //song title for display
            .putString(MediaMetadata.METADATA_KEY_TITLE, trackMetadata.name) //song title for info
            .putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, trackMetadata.artistName) //artist name for display
            .putString(MediaMetadata.METADATA_KEY_ARTIST, trackMetadata.artistName) //artist name for info
            .putString(MediaMetadata.METADATA_KEY_DISPLAY_DESCRIPTION, trackMetadata.albumName) //album name for display
            .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, albumArt) //album art for display
            .build();
    }

    public static MediaMetadataCompat buildMediaMetadataCompat(Metadata.Track trackMetadata, Bitmap albumArt) {
        return new MediaMetadataCompat.Builder()
            .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, trackMetadata.name) //song title for display
            .putString(MediaMetadataCompat.METADATA_KEY_TITLE, trackMetadata.name) //song title for info
            .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, trackMetadata.artistName) //artist name for display
            .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, trackMetadata.artistName) //artist name for info
            .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, trackMetadata.albumName) //album name for display
            .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt) //album art for display
            .build();
    }

} public class PreferenceUtil {

    public static boolean isAutoPlayEnabled(Context context) {
        SharedPreferences prefs = getPrefs(context, PreferenceConstants.PLAYER_PREFERENCES);
        if (prefs.contains(PreferenceConstants.PREFERENCE_AUTOPLAY)) {
            return prefs.getBoolean(PreferenceConstants.PREFERENCE_AUTOPLAY, PreferenceConstants.PREFERENCE_AUTOPLAY_DEFAULT);
        }
        else {
            prefs.edit()
                .putBoolean(PreferenceConstants.PREFERENCE_AUTOPLAY, PreferenceConstants.PREFERENCE_AUTOPLAY_DEFAULT)
                .apply();
            return true;
        }
    }

    private static SharedPreferences getPrefs(Context context, String preferenceName) {
        return context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
    }

} public interface Injector {
    void inject(Object target);
} public class EmptyTracklistException extends Throwable {
    public EmptyTracklistException() {
        super("Tracklist is empty");
    }
}
import android.app.Notification;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.view.LayoutInflater;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.github.andrewlord1990.snackbarbuilder.toastbuilder.ToastBuilder;
import com.spotify.sdk.android.player.Config;
import com.spotify.sdk.android.player.ConnectionStateCallback;
import com.spotify.sdk.android.player.Error;
import com.spotify.sdk.android.player.Metadata;
import com.spotify.sdk.android.player.PlaybackBitrate;
import com.spotify.sdk.android.player.Player;
import com.spotify.sdk.android.player.PlayerEvent;
import com.spotify.sdk.android.player.PlayerInitializationException;
import com.spotify.sdk.android.player.Spotify;
import com.spotify.sdk.android.player.SpotifyPlayer;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import se.zinokader.spotiq.BuildConfig;
import se.zinokader.spotiq.R;
import se.zinokader.spotiq.constant.ApplicationConstants;
import se.zinokader.spotiq.constant.LogTag;
import se.zinokader.spotiq.constant.ServiceConstants;
import se.zinokader.spotiq.model.ChildEvent;
import se.zinokader.spotiq.repository.TracklistRepository;
import se.zinokader.spotiq.service.authentication.SpotifyAuthenticationService;
import se.zinokader.spotiq.util.NetworkUtil;
import se.zinokader.spotiq.util.NotificationUtil;
import se.zinokader.spotiq.util.PreferenceUtil;
import se.zinokader.spotiq.util.di.Injector;
import se.zinokader.spotiq.util.exception.EmptyTracklistException;
        playingStatusIntent.putExtra(ServiceConstants.PLAYING_STATUS_ISPLAYING_EXTRA, isPlaying);
        LocalBroadcastManager.getInstance(this).sendBroadcast(playingStatusIntent);
    }

    private Single<Boolean> initPlayer() {
        return Single.create(subscriber -> {

            playerConfig = new Config(SpotiqHostService.this,
                spotifyCommunicatorService.getAuthenticator().getAccessToken(),
                BuildConfig.SPOTIFY_CLIENT_ID);
            playerConfig.useCache(false);

            spotifyPlayer = Spotify.getPlayer(playerConfig, SpotiqHostService.this, new SpotifyPlayer.InitializationObserver() {
                @Override
                public void onInitialized(SpotifyPlayer player) {
                    spotifyPlayer.addConnectionStateCallback(SpotiqHostService.this);
                    spotifyPlayer.addNotificationCallback(SpotiqHostService.this);
                    spotifyPlayer.setPlaybackBitrate(new Player.OperationCallback() {
                        @Override
                        public void onSuccess() {
                            subscriber.onSuccess(true);
                            Log.d(LogTag.LOG_HOST_SERVICE, "Set Spotify Player custom playback bitrate successfully");
                            sendPlayingStatusBroadcast(false); //make sure all listeners are up to sync with an inititally paused status
                            mediaSessionHandler.setSessionActive();
                        }

                        @Override
                        public void onError(Error error) {
                            subscriber.onError(new PlayerInitializationException(error.name()));
                            Log.d(LogTag.LOG_HOST_SERVICE, "Failed to set Spotify Player playback bitrate. Cause: " + error.toString());

                        }
                    }, PlaybackBitrate.BITRATE_HIGH);

                }

                @Override
                public void onError(Throwable throwable) {
                    subscriber.onError(throwable);
                    Log.d(LogTag.LOG_HOST_SERVICE, "Could not initialize Spotify Player: " + throwable.getMessage());
                    stopSelf();
                }

            });
        });
    }

    public boolean isPlaying() {
        return !(spotifyPlayer == null || spotifyPlayer.getPlaybackState() == null) && spotifyPlayer.getPlaybackState().isPlaying;
    }

    public void musicAction() {
        if (isPlaying()) {
            pause();
        }
        else {
            play();
        }
    }

    private void play() {
        if (!NetworkUtil.isConnected(this)) {
            handleLostConnection();
            return;
        }
        if (!isTracklistEmpty && spotifyPlayer.getMetadata().currentTrack != null) {
            resume();
        }
        else {
            playNext();
        }
    }

    private void pause() {
        spotifyPlayer.pause(new Player.OperationCallback() {
            @Override
            public void onSuccess() {
                Log.d(LogTag.LOG_HOST_SERVICE, "Paused music successfully");
            }

            @Override
            public void onError(Error error) {
                Log.d(LogTag.LOG_HOST_SERVICE, "Failed to pause music");
            }
        });
    }

    private void resume() {
        spotifyPlayer.resume(new Player.OperationCallback() {
            @Override
            public void onSuccess() {
                Log.d(LogTag.LOG_HOST_SERVICE, "Resumed music successfully");
            }

            @Override
            public void onError(Error error) {
                Log.d(LogTag.LOG_HOST_SERVICE, "Failed to resume music");
            }
        });
    }

    private void playNext() {
        tracklistRepository.getFirstSong(partyTitle)
            .subscribe(song -> {
                spotifyPlayer.playUri(new Player.OperationCallback() {
                    @Override
                    public void onSuccess() {
                        Log.d(LogTag.LOG_HOST_SERVICE, "Playing next song in tracklist");
                        isTracklistEmpty = false;
                    }

                    @Override
                    public void onError(Error error) {
                        Log.d(LogTag.LOG_HOST_SERVICE, "Failed to play next song in tracklist: " + error.name());
                        sendPlayingStatusBroadcast(false);
                    }
                }, song.getSongUri(), 0, 0);
            }, throwable -> {
                Log.d(LogTag.LOG_HOST_SERVICE, "Could not play next song: " + throwable.getMessage());
                sendPlayingStatusBroadcast(false);
                if (throwable instanceof EmptyTracklistException) {