org.moire.ultrasonic.service.DownloadServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.moire.ultrasonic.service.DownloadServiceImpl.java

Source

/*
 This file is part of Subsonic.
    
 Subsonic 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.
    
 Subsonic 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 Subsonic.  If not, see <http://www.gnu.org/licenses/>.
    
 Copyright 2009 (C) Sindre Mehus
 */
package org.moire.ultrasonic.service;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.media.RemoteControlClient;
import android.media.audiofx.AudioEffect;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.SeekBar;

import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.audiofx.VisualizerController;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.RepeatMode;
import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x1;
import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x2;
import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x3;
import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x4;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.LRUCache;
import org.moire.ultrasonic.util.ShufflePlayBuffer;
import org.moire.ultrasonic.util.SimpleServiceBinder;
import org.moire.ultrasonic.util.StreamProxy;
import org.moire.ultrasonic.util.Util;

import java.io.File;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import static org.moire.ultrasonic.domain.PlayerState.COMPLETED;
import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING;
import static org.moire.ultrasonic.domain.PlayerState.IDLE;
import static org.moire.ultrasonic.domain.PlayerState.PAUSED;
import static org.moire.ultrasonic.domain.PlayerState.PREPARED;
import static org.moire.ultrasonic.domain.PlayerState.PREPARING;
import static org.moire.ultrasonic.domain.PlayerState.STARTED;
import static org.moire.ultrasonic.domain.PlayerState.STOPPED;

/**
 * @author Sindre Mehus, Joshua Bahnsen
 * @version $Id$
 */
public class DownloadServiceImpl extends Service implements DownloadService {
    private static final String TAG = DownloadServiceImpl.class.getSimpleName();

    public static final String CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY";
    public static final String CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE";
    public static final String CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE";
    public static final String CMD_STOP = "org.moire.ultrasonic.CMD_STOP";
    public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS";
    public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT";

    private static final int NOTIFICATION_ID = 3033;

    private final IBinder binder = new SimpleServiceBinder<DownloadService>(this);
    private Looper mediaPlayerLooper;
    private MediaPlayer mediaPlayer;
    private MediaPlayer nextMediaPlayer;
    private boolean nextSetup;
    private final List<DownloadFile> downloadList = new ArrayList<DownloadFile>();
    private final List<DownloadFile> backgroundDownloadList = new ArrayList<DownloadFile>();
    private final Handler handler = new Handler();
    private Handler mediaPlayerHandler;
    private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this);
    private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this);

    private final LRUCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LRUCache<MusicDirectory.Entry, DownloadFile>(
            100);
    private final List<DownloadFile> cleanupCandidates = new ArrayList<DownloadFile>();
    private final Scrobbler scrobbler = new Scrobbler();
    private final JukeboxService jukeboxService = new JukeboxService(this);

    private DownloadFile currentPlaying;
    private DownloadFile nextPlaying;
    private DownloadFile currentDownloading;
    private CancellableTask bufferTask;
    private CancellableTask nextPlayingTask;
    private PlayerState playerState = IDLE;
    private PlayerState nextPlayerState = IDLE;
    private boolean shufflePlay;
    private long revision;
    private static DownloadService instance;
    private String suggestedPlaylistName;
    private PowerManager.WakeLock wakeLock;
    private boolean keepScreenOn;
    private int cachedPosition;

    private static boolean equalizerAvailable;
    private static boolean visualizerAvailable;
    private EqualizerController equalizerController;
    private VisualizerController visualizerController;
    private boolean showVisualization;
    private boolean jukeboxEnabled;
    private PositionCache positionCache;
    private StreamProxy proxy;
    public RemoteControlClient remoteControlClient;
    private AudioManager audioManager;
    private int secondaryProgress = -1;
    private boolean autoPlayStart;
    private final static int lockScreenBitmapSize = 500;

    static {
        try {
            EqualizerController.checkAvailable();
            equalizerAvailable = true;
        } catch (Throwable t) {
            equalizerAvailable = false;
        }
    }

    static {
        try {
            VisualizerController.checkAvailable();
            visualizerAvailable = true;
        } catch (Throwable t) {
            visualizerAvailable = false;
        }
    }

    @SuppressLint("NewApi")
    @Override
    public void onCreate() {
        super.onCreate();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread.currentThread().setName("DownloadServiceImpl");

                Looper.prepare();

                if (mediaPlayer != null) {
                    mediaPlayer.release();
                }

                mediaPlayer = new MediaPlayer();
                mediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK);

                mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
                    @Override
                    public boolean onError(MediaPlayer mediaPlayer, int what, int more) {
                        handleError(new Exception(String.format("MediaPlayer error: %d (%d)", what, more)));
                        return false;
                    }
                });

                try {
                    Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
                    i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId());
                    i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
                    sendBroadcast(i);
                } catch (Throwable e) {
                    // Froyo or lower
                }

                mediaPlayerLooper = Looper.myLooper();
                mediaPlayerHandler = new Handler(mediaPlayerLooper);
                Looper.loop();
            }
        }).start();

        audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
        setUpRemoteControlClient();

        if (equalizerAvailable) {
            equalizerController = new EqualizerController(this, mediaPlayer);
            if (!equalizerController.isAvailable()) {
                equalizerController = null;
            } else {
                equalizerController.loadSettings();
            }
        }
        if (visualizerAvailable) {
            visualizerController = new VisualizerController(mediaPlayer);
            if (!visualizerController.isAvailable()) {
                visualizerController = null;
            }
        }

        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
        wakeLock.setReferenceCounted(false);

        instance = this;
        lifecycleSupport.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);
        lifecycleSupport.onStart(intent);
        return START_NOT_STICKY;
    }

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

        try {
            instance = null;
            lifecycleSupport.onDestroy();
            mediaPlayer.release();

            if (nextMediaPlayer != null) {
                nextMediaPlayer.release();
            }

            mediaPlayerLooper.quit();
            shufflePlayBuffer.shutdown();

            if (equalizerController != null) {
                equalizerController.release();
            }

            if (visualizerController != null) {
                visualizerController.release();
            }

            if (bufferTask != null) {
                bufferTask.cancel();
            }

            if (nextPlayingTask != null) {
                nextPlayingTask.cancel();
            }

            Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
            i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId());
            i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
            sendBroadcast(i);

            audioManager.unregisterRemoteControlClient(remoteControlClient);
            clearRemoteControl();

            wakeLock.release();
        } catch (Throwable ignored) {
        }
    }

    public static DownloadService getInstance() {
        return instance;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay,
            boolean playNext, boolean shuffle, boolean newPlaylist) {
        shufflePlay = false;
        int offset = 1;

        if (songs.isEmpty()) {
            return;
        }

        if (newPlaylist) {
            downloadList.clear();
        }

        if (playNext) {
            if (autoplay && getCurrentPlayingIndex() >= 0) {
                offset = 0;
            }

            for (MusicDirectory.Entry song : songs) {
                DownloadFile downloadFile = new DownloadFile(this, song, save);
                downloadList.add(getCurrentPlayingIndex() + offset, downloadFile);
                offset++;
            }

            revision++;
        } else {
            int size = size();
            int index = getCurrentPlayingIndex();

            for (MusicDirectory.Entry song : songs) {
                DownloadFile downloadFile = new DownloadFile(this, song, save);
                downloadList.add(downloadFile);
            }

            if (!autoplay && (size - 1) == index) {
                setNextPlaying();
            }

            revision++;
        }

        updateJukeboxPlaylist();

        if (shuffle)
            shuffle();

        if (autoplay) {
            play(0);
        } else {
            if (currentPlaying == null) {
                currentPlaying = downloadList.get(0);
                currentPlaying.setPlaying(true);
            }

            checkDownloads();
        }

        lifecycleSupport.serializeDownloadQueue();
    }

    @Override
    public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save) {
        for (MusicDirectory.Entry song : songs) {
            DownloadFile downloadFile = new DownloadFile(this, song, save);
            backgroundDownloadList.add(downloadFile);
        }

        revision++;

        checkDownloads();
        lifecycleSupport.serializeDownloadQueue();
    }

    private void updateJukeboxPlaylist() {
        if (jukeboxEnabled) {
            jukeboxService.updatePlaylist();
        }
    }

    @Override
    public void restore(List<MusicDirectory.Entry> songs, int currentPlayingIndex, int currentPlayingPosition,
            boolean autoPlay, boolean newPlaylist) {
        download(songs, false, false, false, false, newPlaylist);

        if (currentPlayingIndex != -1) {
            while (mediaPlayer == null) {
                Util.sleepQuietly(50L);
            }

            play(currentPlayingIndex, autoPlayStart);

            if (currentPlaying != null) {
                if (autoPlay && jukeboxEnabled) {
                    jukeboxService.skip(getCurrentPlayingIndex(), currentPlayingPosition / 1000);
                } else {
                    if (currentPlaying.isCompleteFileAvailable()) {
                        doPlay(currentPlaying, currentPlayingPosition, autoPlay);
                    }
                }
            }

            autoPlayStart = false;
        }
    }

    @Override
    public void stopJukeboxService() {
        jukeboxService.stopJukeboxService();
    }

    @Override
    public void startJukeboxService() {
        jukeboxService.startJukeboxService();
    }

    @Override
    public synchronized void setShufflePlayEnabled(boolean enabled) {
        shufflePlay = enabled;
        if (shufflePlay) {
            clear();
            checkDownloads();
        }
    }

    @Override
    public boolean isShufflePlayEnabled() {
        return shufflePlay;
    }

    @Override
    public synchronized void shuffle() {
        Collections.shuffle(downloadList);
        if (currentPlaying != null) {
            downloadList.remove(getCurrentPlayingIndex());
            downloadList.add(0, currentPlaying);
        }
        revision++;
        lifecycleSupport.serializeDownloadQueue();
        updateJukeboxPlaylist();
        setNextPlaying();
    }

    @Override
    public RepeatMode getRepeatMode() {
        return Util.getRepeatMode(this);
    }

    @Override
    public void setRepeatMode(RepeatMode repeatMode) {
        Util.setRepeatMode(this, repeatMode);
        setNextPlaying();
    }

    @Override
    public boolean getKeepScreenOn() {
        return keepScreenOn;
    }

    @Override
    public void setKeepScreenOn(boolean keepScreenOn) {
        this.keepScreenOn = keepScreenOn;
    }

    @Override
    public boolean getShowVisualization() {
        return showVisualization;
    }

    @Override
    public void setShowVisualization(boolean showVisualization) {
        this.showVisualization = showVisualization;
    }

    @Override
    public synchronized DownloadFile forSong(MusicDirectory.Entry song) {
        for (DownloadFile downloadFile : downloadList) {
            if (downloadFile.getSong().equals(song)
                    && ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled()
                            && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone())) {
                return downloadFile;
            }
        }
        for (DownloadFile downloadFile : backgroundDownloadList) {
            if (downloadFile.getSong().equals(song)) {
                return downloadFile;
            }
        }

        DownloadFile downloadFile = downloadFileCache.get(song);
        if (downloadFile == null) {
            downloadFile = new DownloadFile(this, song, false);
            downloadFileCache.put(song, downloadFile);
        }
        return downloadFile;
    }

    @Override
    public synchronized void clear() {
        clear(true);
    }

    @Override
    public synchronized void clearBackground() {
        if (currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) {
            currentDownloading.cancelDownload();
            currentDownloading = null;
        }
        backgroundDownloadList.clear();
    }

    @Override
    public synchronized void clearIncomplete() {
        reset();
        Iterator<DownloadFile> iterator = downloadList.iterator();

        while (iterator.hasNext()) {
            DownloadFile downloadFile = iterator.next();
            if (!downloadFile.isCompleteFileAvailable()) {
                iterator.remove();
            }
        }

        lifecycleSupport.serializeDownloadQueue();
        updateJukeboxPlaylist();
    }

    @Override
    public synchronized int size() {
        return downloadList.size();
    }

    public synchronized void clear(boolean serialize) {
        reset();
        downloadList.clear();
        revision++;
        if (currentDownloading != null) {
            currentDownloading.cancelDownload();
            currentDownloading = null;
        }
        setCurrentPlaying(null);

        if (serialize) {
            lifecycleSupport.serializeDownloadQueue();
        }
        updateJukeboxPlaylist();
        setNextPlaying();
    }

    @Override
    public synchronized void remove(int which) {
        downloadList.remove(which);
    }

    @Override
    public synchronized void remove(DownloadFile downloadFile) {
        if (downloadFile == currentDownloading) {
            currentDownloading.cancelDownload();
            currentDownloading = null;
        }
        if (downloadFile == currentPlaying) {
            reset();
            setCurrentPlaying(null);
        }
        downloadList.remove(downloadFile);
        backgroundDownloadList.remove(downloadFile);
        revision++;
        lifecycleSupport.serializeDownloadQueue();
        updateJukeboxPlaylist();
        if (downloadFile == nextPlaying) {
            setNextPlaying();
        }
    }

    @Override
    public synchronized void delete(List<MusicDirectory.Entry> songs) {
        for (MusicDirectory.Entry song : songs) {
            forSong(song).delete();
        }
    }

    @Override
    public synchronized void unpin(List<MusicDirectory.Entry> songs) {
        for (MusicDirectory.Entry song : songs) {
            forSong(song).unpin();
        }
    }

    synchronized void setCurrentPlaying(int currentPlayingIndex) {
        try {
            setCurrentPlaying(downloadList.get(currentPlayingIndex));
        } catch (IndexOutOfBoundsException x) {
            // Ignored
        }
    }

    synchronized void setCurrentPlaying(DownloadFile currentPlaying) {
        this.currentPlaying = currentPlaying;

        if (currentPlaying != null) {
            Util.broadcastNewTrackInfo(this, currentPlaying.getSong());
            Util.broadcastA2dpMetaDataChange(this, instance);
        } else {
            Util.broadcastNewTrackInfo(this, null);
            Util.broadcastA2dpMetaDataChange(this, null);
        }

        updateRemoteControl();

        // Update widget
        UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED,
                false);
        UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED,
                true);
        UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED,
                false);
        UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED,
                false);
        SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance();

        if (currentPlaying != null) {
            if (tabInstance != null) {
                if (Util.isNotificationEnabled(this)) {
                    startForeground(NOTIFICATION_ID, buildForegroundNotification());
                }
                tabInstance.showNowPlaying();
            }
        } else {
            if (tabInstance != null) {
                stopForeground(true);
                tabInstance.hideNowPlaying();
            }
        }
    }

    synchronized void setNextPlaying() {
        boolean gaplessPlayback = Util.getGaplessPlaybackPreference(DownloadServiceImpl.this);

        if (!gaplessPlayback) {
            nextPlaying = null;
            nextPlayerState = IDLE;
            return;
        }

        int index = getCurrentPlayingIndex();

        if (index != -1) {
            switch (getRepeatMode()) {
            case OFF:
                index += 1;
                break;
            case ALL:
                index = (index + 1) % size();
                break;
            case SINGLE:
                break;
            default:
                break;
            }
        }

        nextSetup = false;
        if (nextPlayingTask != null) {
            nextPlayingTask.cancel();
            nextPlayingTask = null;
        }

        if (index < size() && index != -1) {
            nextPlaying = downloadList.get(index);
            nextPlayingTask = new CheckCompletionTask(nextPlaying);
            nextPlayingTask.start();
        } else {
            nextPlaying = null;
            setNextPlayerState(IDLE);
        }
    }

    @Override
    public synchronized int getCurrentPlayingIndex() {
        return downloadList.indexOf(currentPlaying);
    }

    @Override
    public DownloadFile getCurrentPlaying() {
        return currentPlaying;
    }

    @Override
    public DownloadFile getCurrentDownloading() {
        return currentDownloading;
    }

    @Override
    public List<DownloadFile> getSongs() {
        return downloadList;
    }

    @Override
    public long getDownloadListDuration() {
        long totalDuration = 0;

        for (DownloadFile downloadFile : downloadList) {
            Entry entry = downloadFile.getSong();

            if (!entry.isDirectory()) {
                if (entry.getArtist() != null) {
                    Integer duration = entry.getDuration();

                    if (duration != null) {
                        totalDuration += duration;
                    }
                }
            }
        }

        return totalDuration;
    }

    @Override
    public synchronized List<DownloadFile> getDownloads() {
        List<DownloadFile> temp = new ArrayList<DownloadFile>();
        temp.addAll(downloadList);
        temp.addAll(backgroundDownloadList);
        return temp;
    }

    @Override
    public List<DownloadFile> getBackgroundDownloads() {
        return backgroundDownloadList;
    }

    /**
     * Plays either the current song (resume) or the first/next one in queue.
     */
    public synchronized void play() {
        int current = getCurrentPlayingIndex();
        if (current == -1) {
            play(0);
        } else {
            play(current);
        }
    }

    @Override
    public synchronized void play(int index) {
        play(index, true);
    }

    private synchronized void play(int index, boolean start) {
        updateRemoteControl();

        if (index < 0 || index >= size()) {
            resetPlayback();
        } else {
            if (nextPlayingTask != null) {
                nextPlayingTask.cancel();
                nextPlayingTask = null;
            }

            setCurrentPlaying(index);

            if (start) {
                if (jukeboxEnabled) {
                    jukeboxService.skip(getCurrentPlayingIndex(), 0);
                    setPlayerState(STARTED);
                } else {
                    bufferAndPlay();
                }
            }

            checkDownloads();
            setNextPlaying();
        }
    }

    private synchronized void resetPlayback() {
        reset();
        setCurrentPlaying(null);
        lifecycleSupport.serializeDownloadQueue();
    }

    private synchronized void playNext() {
        MediaPlayer tmp = mediaPlayer;
        mediaPlayer = nextMediaPlayer;
        nextMediaPlayer = tmp;
        setCurrentPlaying(nextPlaying);
        setPlayerState(PlayerState.STARTED);
        setupHandlers(currentPlaying, false);
        setNextPlaying();

        // Proxy should not be being used here since the next player was already setup to play
        if (proxy != null) {
            proxy.stop();
            proxy = null;
        }
    }

    /**
     * Plays or resumes the playback, depending on the current player state.
     */
    @Override
    public synchronized void togglePlayPause() {
        if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) {
            start();
        } else if (playerState == IDLE) {
            autoPlayStart = true;
            play();
        } else if (playerState == STARTED) {
            pause();
        }
    }

    @Override
    public synchronized void seekTo(int position) {
        try {
            if (jukeboxEnabled) {
                jukeboxService.skip(getCurrentPlayingIndex(), position / 1000);
            } else {
                mediaPlayer.seekTo(position);
                cachedPosition = position;

                updateRemoteControl();
            }
        } catch (Exception x) {
            handleError(x);
        }
    }

    @Override
    public synchronized void previous() {
        int index = getCurrentPlayingIndex();
        if (index == -1) {
            return;
        }

        // Restart song if played more than five seconds.
        if (getPlayerPosition() > 5000 || index == 0) {
            play(index);
        } else {
            play(index - 1);
        }
    }

    @Override
    public synchronized void next() {
        int index = getCurrentPlayingIndex();
        if (index != -1) {
            play(index + 1);
        }
    }

    private void onSongCompleted() {
        int index = getCurrentPlayingIndex();

        if (currentPlaying != null) {
            final Entry song = currentPlaying.getSong();

            if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(this)) {
                MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this);
                try {
                    musicService.deleteBookmark(song.getId(), DownloadServiceImpl.this, null);
                } catch (Exception ignored) {

                }
            }
        }

        if (index != -1) {
            switch (getRepeatMode()) {
            case OFF:
                if (index + 1 < 0 || index + 1 >= size()) {
                    if (Util.getShouldClearPlaylist(this)) {
                        clear();
                    }

                    resetPlayback();
                    break;
                }

                play(index + 1);
                break;
            case ALL:
                play((index + 1) % size());
                break;
            case SINGLE:
                play(index);
                break;
            default:
                break;
            }
        }
    }

    @Override
    public synchronized void pause() {
        try {
            if (playerState == STARTED) {
                if (jukeboxEnabled) {
                    jukeboxService.stop();
                } else {
                    mediaPlayer.pause();
                }
                setPlayerState(PAUSED);
            }
        } catch (Exception x) {
            handleError(x);
        }
    }

    @Override
    public synchronized void stop() {
        try {
            if (playerState == STARTED) {
                if (jukeboxEnabled) {
                    jukeboxService.stop();
                } else {
                    mediaPlayer.pause();
                }
                setPlayerState(STOPPED);
            } else if (playerState == PAUSED) {
                setPlayerState(STOPPED);
            }
        } catch (Exception x) {
            handleError(x);
        }
    }

    @Override
    public synchronized void start() {
        try {
            if (jukeboxEnabled) {
                jukeboxService.start();
            } else {
                mediaPlayer.start();
            }
            setPlayerState(STARTED);
        } catch (Exception x) {
            handleError(x);
        }
    }

    @Override
    public synchronized void reset() {
        if (bufferTask != null) {
            bufferTask.cancel();
        }
        try {
            setPlayerState(IDLE);
            mediaPlayer.setOnErrorListener(null);
            mediaPlayer.setOnCompletionListener(null);
            mediaPlayer.reset();
        } catch (Exception x) {
            handleError(x);
        }
    }

    @Override
    public synchronized int getPlayerPosition() {
        try {
            if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) {
                return 0;
            }

            return jukeboxEnabled ? jukeboxService.getPositionSeconds() * 1000 : cachedPosition;
        } catch (Exception x) {
            handleError(x);
            return 0;
        }
    }

    @Override
    public synchronized int getPlayerDuration() {
        if (currentPlaying != null) {
            Integer duration = currentPlaying.getSong().getDuration();
            if (duration != null) {
                return duration * 1000;
            }
        }
        if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) {
            try {
                return mediaPlayer.getDuration();
            } catch (Exception x) {
                handleError(x);
            }
        }
        return 0;
    }

    @Override
    public PlayerState getPlayerState() {
        return playerState;
    }

    synchronized void setPlayerState(PlayerState playerState) {
        Log.i(TAG, String.format("%s -> %s (%s)", this.playerState.name(), playerState.name(), currentPlaying));

        this.playerState = playerState;

        if (this.playerState == PAUSED) {
            lifecycleSupport.serializeDownloadQueue();
        }

        if (this.playerState == PlayerState.STARTED) {
            Util.requestAudioFocus(this);
        }

        boolean showWhenPaused = (this.playerState != PlayerState.STOPPED
                && Util.isNotificationAlwaysEnabled(this));
        boolean show = this.playerState == PlayerState.STARTED || showWhenPaused;

        Util.broadcastPlaybackStatusChange(this, this.playerState);
        Util.broadcastA2dpPlayStatusChange(this, this.playerState, instance);

        if (this.playerState == PlayerState.STARTED || this.playerState == PlayerState.PAUSED) {
            // Set remote control
            updateRemoteControl();
        }

        // Update widget
        UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, this,
                this.playerState == PlayerState.STARTED, false);
        UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, this,
                this.playerState == PlayerState.STARTED, true);
        UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, this,
                this.playerState == PlayerState.STARTED, false);
        UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, this,
                this.playerState == PlayerState.STARTED, false);
        SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance();

        if (show) {
            if (tabInstance != null) {
                // Only update notification is player state is one that will change the icon
                if (this.playerState == PlayerState.STARTED || this.playerState == PlayerState.PAUSED) {
                    if (Util.isNotificationEnabled(this)) {
                        final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
                        notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification());
                    }
                    tabInstance.showNowPlaying();
                }
            }
        } else {
            if (tabInstance != null) {
                stopForeground(true);
                tabInstance.hideNowPlaying();
            }
        }

        if (this.playerState == STARTED) {
            scrobbler.scrobble(this, currentPlaying, false);
        } else if (this.playerState == COMPLETED) {
            scrobbler.scrobble(this, currentPlaying, true);
        }

        if (playerState == STARTED && positionCache == null) {
            positionCache = new PositionCache();
            Thread thread = new Thread(positionCache);
            thread.start();
        } else if (playerState != STARTED && positionCache != null) {
            positionCache.stop();
            positionCache = null;
        }
    }

    private void setPlayerStateCompleted() {
        Log.i(TAG, String.format("%s -> %s (%s)", this.playerState.name(), PlayerState.COMPLETED, currentPlaying));
        this.playerState = PlayerState.COMPLETED;

        if (positionCache != null) {
            positionCache.stop();
            positionCache = null;
        }

        scrobbler.scrobble(this, currentPlaying, true);
    }

    private synchronized void setNextPlayerState(PlayerState playerState) {
        Log.i(TAG,
                String.format("Next: %s -> %s (%s)", this.nextPlayerState.name(), playerState.name(), nextPlaying));
        this.nextPlayerState = playerState;
    }

    @Override
    public void setSuggestedPlaylistName(String name) {
        this.suggestedPlaylistName = name;
    }

    @Override
    public String getSuggestedPlaylistName() {
        return suggestedPlaylistName;
    }

    @Override
    public boolean getEqualizerAvailable() {
        return equalizerAvailable;
    }

    @Override
    public boolean getVisualizerAvailable() {
        return visualizerAvailable;
    }

    @Override
    public EqualizerController getEqualizerController() {
        if (equalizerAvailable && equalizerController == null) {
            equalizerController = new EqualizerController(this, mediaPlayer);
            if (!equalizerController.isAvailable()) {
                equalizerController = null;
            } else {
                equalizerController.loadSettings();
            }
        }
        return equalizerController;
    }

    @Override
    public VisualizerController getVisualizerController() {
        if (visualizerAvailable && visualizerController == null) {
            visualizerController = new VisualizerController(mediaPlayer);
            if (!visualizerController.isAvailable()) {
                visualizerController = null;
            }
        }
        return visualizerController;
    }

    @Override
    public boolean isJukeboxEnabled() {
        return jukeboxEnabled;
    }

    @Override
    public boolean isJukeboxAvailable() {
        MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this);

        try {
            String username = Util.getUserName(DownloadServiceImpl.this,
                    Util.getActiveServer(DownloadServiceImpl.this));
            UserInfo user = musicService.getUser(username, DownloadServiceImpl.this, null);
            return user.getJukeboxRole();
        } catch (Exception e) {
            Log.w(TAG, "Error getting user information", e);
        }

        return false;
    }

    @Override
    public boolean isSharingAvailable() {
        MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this);

        try {
            String username = Util.getUserName(DownloadServiceImpl.this,
                    Util.getActiveServer(DownloadServiceImpl.this));
            UserInfo user = musicService.getUser(username, DownloadServiceImpl.this, null);
            return user.getShareRole();
        } catch (Exception e) {
            Log.w(TAG, "Error getting user information", e);
        }

        return false;
    }

    @Override
    public void setJukeboxEnabled(boolean jukeboxEnabled) {
        this.jukeboxEnabled = jukeboxEnabled;
        jukeboxService.setEnabled(jukeboxEnabled);

        if (jukeboxEnabled) {
            jukeboxService.startJukeboxService();

            reset();

            // Cancel current download, if necessary.
            if (currentDownloading != null) {
                currentDownloading.cancelDownload();
            }
        } else {
            jukeboxService.stopJukeboxService();
        }
    }

    @Override
    public void adjustJukeboxVolume(boolean up) {
        jukeboxService.adjustVolume(up);
    }

    @SuppressLint("NewApi")
    public void setUpRemoteControlClient() {
        if (!Util.isLockScreenEnabled(this))
            return;

        ComponentName componentName = new ComponentName(getPackageName(),
                MediaButtonIntentReceiver.class.getName());

        if (remoteControlClient == null) {
            final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
            mediaButtonIntent.setComponent(componentName);
            PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, mediaButtonIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            remoteControlClient = new RemoteControlClient(broadcast);
            audioManager.registerRemoteControlClient(remoteControlClient);

            // Flags for the media transport control that this client supports.
            int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | RemoteControlClient.FLAG_KEY_MEDIA_NEXT
                    | RemoteControlClient.FLAG_KEY_MEDIA_PLAY | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE
                    | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | RemoteControlClient.FLAG_KEY_MEDIA_STOP;

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;

                remoteControlClient
                        .setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() {
                            @Override
                            public long onGetPlaybackPosition() {
                                return mediaPlayer.getCurrentPosition();
                            }
                        });

                remoteControlClient.setPlaybackPositionUpdateListener(
                        new RemoteControlClient.OnPlaybackPositionUpdateListener() {
                            @Override
                            public void onPlaybackPositionUpdate(long newPositionMs) {
                                seekTo((int) newPositionMs);
                            }
                        });
            }

            remoteControlClient.setTransportControlFlags(flags);
        }
    }

    private void clearRemoteControl() {
        if (remoteControlClient != null) {
            remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
            audioManager.unregisterRemoteControlClient(remoteControlClient);
            remoteControlClient = null;
        }
    }

    private void updateRemoteControl() {
        if (!Util.isLockScreenEnabled(this)) {
            clearRemoteControl();
            return;
        }

        if (remoteControlClient != null) {
            audioManager.unregisterRemoteControlClient(remoteControlClient);
            audioManager.registerRemoteControlClient(remoteControlClient);
        } else {
            setUpRemoteControlClient();
        }

        Log.i(TAG, String.format("In updateRemoteControl, playerState: %s [%d]", playerState, getPlayerPosition()));

        switch (playerState) {
        case STARTED:
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
                remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
            } else {
                remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, getPlayerPosition(),
                        1.0f);
            }
            break;
        default:
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
                remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED);
            } else {
                remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, getPlayerPosition(),
                        1.0f);
            }
            break;
        }

        if (currentPlaying != null) {
            MusicDirectory.Entry currentSong = currentPlaying.getSong();

            Bitmap lockScreenBitmap = FileUtil.getAlbumArtBitmap(this, currentSong, Util.getMinDisplayMetric(this),
                    true);

            String artist = currentSong.getArtist();
            String album = currentSong.getAlbum();
            String title = currentSong.getTitle();
            Integer currentSongDuration = currentSong.getDuration();
            Long duration = 0L;

            if (currentSongDuration != null)
                duration = (long) currentSongDuration * 1000;

            remoteControlClient.editMetadata(true).putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist)
                    .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist)
                    .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album)
                    .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title)
                    .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration)
                    .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap).apply();
        }
    }

    private synchronized void bufferAndPlay() {
        if (playerState != PREPARED) {
            reset();

            bufferTask = new BufferTask(currentPlaying, 0);
            bufferTask.start();
        } else {
            doPlay(currentPlaying, 0, true);
        }
    }

    private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) {
        try {
            downloadFile.setPlaying(false);
            //downloadFile.setPlaying(true);
            final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile()
                    : downloadFile.getPartialFile();
            boolean partial = file.equals(downloadFile.getPartialFile());
            downloadFile.updateModificationDate();

            mediaPlayer.setOnCompletionListener(null);
            secondaryProgress = -1; // Ensure seeking in non StreamProxy playback works
            mediaPlayer.reset();
            setPlayerState(IDLE);
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            String dataSource = file.getPath();

            if (partial) {
                if (proxy == null) {
                    proxy = new StreamProxy(this);
                    proxy.start();
                }

                dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(),
                        URLEncoder.encode(dataSource, Constants.UTF_8));
                Log.i(TAG, String.format("Data Source: %s", dataSource));
            } else if (proxy != null) {
                proxy.stop();
                proxy = null;
            }

            Log.i(TAG, "Preparing media player");
            mediaPlayer.setDataSource(dataSource);
            setPlayerState(PREPARING);

            mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
                @Override
                public void onBufferingUpdate(MediaPlayer mp, int percent) {
                    SeekBar progressBar = DownloadActivity.getProgressBar();
                    MusicDirectory.Entry song = downloadFile.getSong();

                    if (percent == 100) {
                        if (progressBar != null) {
                            progressBar.setSecondaryProgress(100 * progressBar.getMax());
                        }

                        mp.setOnBufferingUpdateListener(null);
                    } else if (progressBar != null && song.getTranscodedContentType() == null
                            && Util.getMaxBitRate(DownloadServiceImpl.this) == 0) {
                        secondaryProgress = (int) (((double) percent / (double) 100) * progressBar.getMax());
                        progressBar.setSecondaryProgress(secondaryProgress);
                    }
                }
            });

            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    Log.i(TAG, "Media player prepared");

                    setPlayerState(PREPARED);

                    SeekBar progressBar = DownloadActivity.getProgressBar();

                    if (progressBar != null && downloadFile.isWorkDone()) {
                        // Populate seek bar secondary progress if we have a complete file for consistency
                        DownloadActivity.getProgressBar().setSecondaryProgress(100 * progressBar.getMax());
                    }

                    synchronized (DownloadServiceImpl.this) {
                        if (position != 0) {
                            Log.i(TAG, String.format("Restarting player from position %d", position));
                            seekTo(position);
                        }
                        cachedPosition = position;

                        if (start) {
                            mediaPlayer.start();
                            setPlayerState(STARTED);
                        } else {
                            setPlayerState(PAUSED);
                        }
                    }

                    lifecycleSupport.serializeDownloadQueue();
                }
            });

            setupHandlers(downloadFile, partial);

            mediaPlayer.prepareAsync();
        } catch (Exception x) {
            handleError(x);
        }
    }

    private synchronized void setupNext(final DownloadFile downloadFile) {
        try {
            final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile()
                    : downloadFile.getPartialFile();

            if (nextMediaPlayer != null) {
                nextMediaPlayer.setOnCompletionListener(null);
                nextMediaPlayer.release();
                nextMediaPlayer = null;
            }

            nextMediaPlayer = new MediaPlayer();
            nextMediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK);

            try {
                nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId());
            } catch (Throwable e) {
                nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            }

            nextMediaPlayer.setDataSource(file.getPath());
            setNextPlayerState(PREPARING);

            nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                @SuppressLint("NewApi")
                public void onPrepared(MediaPlayer mp) {
                    try {
                        setNextPlayerState(PREPARED);

                        if (Util.getGaplessPlaybackPreference(DownloadServiceImpl.this)
                                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
                                && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) {
                            mediaPlayer.setNextMediaPlayer(nextMediaPlayer);
                            nextSetup = true;
                        }
                    } catch (Exception x) {
                        handleErrorNext(x);
                    }
                }
            });

            nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
                @Override
                public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
                    Log.w(TAG, String.format("Error on playing next (%d, %d): %s", what, extra, downloadFile));
                    return true;
                }
            });

            nextMediaPlayer.prepareAsync();
        } catch (Exception x) {
            handleErrorNext(x);
        }
    }

    private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) {
        mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
                Log.w(TAG, String.format("Error on playing file (%d, %d): %s", what, extra, downloadFile));
                int pos = cachedPosition;
                reset();
                downloadFile.setPlaying(false);
                doPlay(downloadFile, pos, true);
                downloadFile.setPlaying(true);
                return true;
            }
        });

        final int duration = downloadFile.getSong().getDuration() == null ? 0
                : downloadFile.getSong().getDuration() * 1000;

        mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mediaPlayer) {
                // Acquire a temporary wakelock, since when we return from
                // this callback the MediaPlayer will release its wakelock
                // and allow the device to go to sleep.
                wakeLock.acquire(60000);

                int pos = cachedPosition;
                Log.i(TAG, String.format("Ending position %d of %d", pos, duration));

                if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 1000))) {
                    setPlayerStateCompleted();

                    if (Util.getGaplessPlaybackPreference(DownloadServiceImpl.this) && nextPlaying != null
                            && nextPlayerState == PlayerState.PREPARED) {
                        if (!nextSetup) {
                            playNext();
                        } else {
                            nextSetup = false;
                            playNext();
                        }
                    } else {
                        onSongCompleted();
                    }

                    return;
                }

                synchronized (DownloadServiceImpl.this) {
                    if (downloadFile.isWorkDone()) {
                        // Complete was called early even though file is fully buffered
                        Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration));
                        reset();
                        downloadFile.setPlaying(false);
                        doPlay(downloadFile, pos, true);
                        downloadFile.setPlaying(true);
                    } else {
                        Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration));
                        reset();
                        bufferTask = new BufferTask(downloadFile, pos);
                        bufferTask.start();
                    }
                }
            }
        });
    }

    @Override
    public void setVolume(float volume) {
        if (mediaPlayer != null) {
            mediaPlayer.setVolume(volume, volume);
        }
    }

    @Override
    public synchronized void swap(boolean mainList, int from, int to) {
        List<DownloadFile> list = mainList ? downloadList : backgroundDownloadList;
        int max = list.size();

        if (to >= max) {
            to = max - 1;
        } else if (to < 0) {
            to = 0;
        }

        int currentPlayingIndex = getCurrentPlayingIndex();
        DownloadFile movedSong = list.remove(from);
        list.add(to, movedSong);

        if (jukeboxEnabled && mainList) {
            updateJukeboxPlaylist();
        } else if (mainList && (movedSong == nextPlaying || (currentPlayingIndex + 1) == to)) {
            // Moving next playing or moving a song to be next playing
            setNextPlaying();
        }
    }

    private void handleError(Exception x) {
        Log.w(TAG, String.format("Media player error: %s", x), x);

        try {
            mediaPlayer.reset();
        } catch (Exception ex) {
            Log.w(TAG, String.format("Exception encountered when resetting media player: %s", ex), ex);
        }

        setPlayerState(IDLE);
    }

    private void handleErrorNext(Exception x) {
        Log.w(TAG, String.format("Next Media player error: %s", x), x);
        nextMediaPlayer.reset();
        setNextPlayerState(IDLE);
    }

    protected synchronized void checkDownloads() {
        if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) {
            return;
        }

        if (shufflePlay) {
            checkShufflePlay();
        }

        if (jukeboxEnabled || !Util.isNetworkConnected(this)) {
            return;
        }

        if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) {
            return;
        }

        // Need to download current playing?
        if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) {
            // Cancel current download, if necessary.
            if (currentDownloading != null) {
                currentDownloading.cancelDownload();
            }

            currentDownloading = currentPlaying;
            currentDownloading.download();
            cleanupCandidates.add(currentDownloading);
        }

        // Find a suitable target for download.
        else {
            if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed()
                    && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) {
                currentDownloading = null;
                int n = size();

                int preloaded = 0;

                if (n != 0) {
                    int start = currentPlaying == null ? 0 : getCurrentPlayingIndex();
                    if (start == -1) {
                        start = 0;
                    }
                    int i = start;
                    do {
                        DownloadFile downloadFile = downloadList.get(i);
                        if (!downloadFile.isWorkDone()) {
                            if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) {
                                currentDownloading = downloadFile;
                                currentDownloading.download();
                                cleanupCandidates.add(currentDownloading);
                                if (i == (start + 1)) {
                                    setNextPlayerState(DOWNLOADING);
                                }
                                break;
                            }
                        } else if (currentPlaying != downloadFile) {
                            preloaded++;
                        }

                        i = (i + 1) % n;
                    } while (i != start);
                }

                if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty())
                        && !backgroundDownloadList.isEmpty()) {
                    for (int i = 0; i < backgroundDownloadList.size(); i++) {
                        DownloadFile downloadFile = backgroundDownloadList.get(i);
                        if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) {
                            if (Util.getShouldScanMedia(this)) {
                                Util.scanMedia(this, downloadFile.getCompleteFile());
                            }

                            // Don't need to keep list like active song list
                            backgroundDownloadList.remove(i);
                            revision++;
                            i--;
                        } else {
                            currentDownloading = downloadFile;
                            currentDownloading.download();
                            cleanupCandidates.add(currentDownloading);
                            break;
                        }
                    }
                }
            }
        }

        // Delete obsolete .partial and .complete files.
        cleanup();
    }

    private synchronized void checkShufflePlay() {
        // Get users desired random playlist size
        int listSize = Util.getMaxSongs(this);
        boolean wasEmpty = downloadList.isEmpty();

        long revisionBefore = revision;

        // First, ensure that list is at least 20 songs long.
        int size = size();
        if (size < listSize) {
            for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) {
                DownloadFile downloadFile = new DownloadFile(this, song, false);
                downloadList.add(downloadFile);
                revision++;
            }
        }

        int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex();

        // Only shift playlist if playing song #5 or later.
        if (currIndex > 4) {
            int songsToShift = currIndex - 2;
            for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) {
                downloadList.add(new DownloadFile(this, song, false));
                downloadList.get(0).cancelDownload();
                downloadList.remove(0);
                revision++;
            }
        }

        if (revisionBefore != revision) {
            updateJukeboxPlaylist();
        }

        if (wasEmpty && !downloadList.isEmpty()) {
            play(0);
        }
    }

    @Override
    public long getDownloadListUpdateRevision() {
        return revision;
    }

    private synchronized void cleanup() {
        Iterator<DownloadFile> iterator = cleanupCandidates.iterator();
        while (iterator.hasNext()) {
            DownloadFile downloadFile = iterator.next();
            if (downloadFile != currentPlaying && downloadFile != currentDownloading) {
                if (downloadFile.cleanup()) {
                    iterator.remove();
                }
            }
        }
    }

    @SuppressWarnings("IconColors")
    private Notification buildForegroundNotification() {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.drawable.ic_stat_ultrasonic);

        builder.setAutoCancel(false);
        builder.setOngoing(true);
        builder.setWhen(System.currentTimeMillis());

        RemoteViews contentView = new RemoteViews(this.getPackageName(), R.layout.notification);
        Util.linkButtons(this, contentView, false);
        RemoteViews bigView = new RemoteViews(this.getPackageName(), R.layout.notification_large);
        Util.linkButtons(this, bigView, false);

        builder.setContent(contentView);

        Intent notificationIntent = new Intent(this, DownloadActivity.class);
        builder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0));

        if (playerState == PlayerState.PAUSED || playerState == PlayerState.IDLE) {
            contentView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark);
            bigView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark);
        } else if (playerState == PlayerState.STARTED) {
            contentView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark);
            bigView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark);
        }

        final Entry song = currentPlaying.getSong();
        final String title = song.getTitle();
        final String text = song.getArtist();
        final String album = song.getAlbum();
        final int imageSize = Util.getNotificationImageSize(this);

        try {
            final Bitmap nowPlayingImage = FileUtil.getAlbumArtBitmap(this, currentPlaying.getSong(), imageSize,
                    true);
            if (nowPlayingImage == null) {
                contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
                bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
            } else {
                contentView.setImageViewBitmap(R.id.notification_image, nowPlayingImage);
                bigView.setImageViewBitmap(R.id.notification_image, nowPlayingImage);
            }
        } catch (Exception x) {
            Log.w(TAG, "Failed to get notification cover art", x);
            contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
            bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
        }

        contentView.setTextViewText(R.id.trackname, title);
        bigView.setTextViewText(R.id.trackname, title);
        contentView.setTextViewText(R.id.artist, text);
        bigView.setTextViewText(R.id.artist, text);
        contentView.setTextViewText(R.id.album, album);
        bigView.setTextViewText(R.id.album, album);

        Notification notification = builder.build();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            notification.bigContentView = bigView;
        }

        return notification;
    }

    private class BufferTask extends CancellableTask {
        private final DownloadFile downloadFile;
        private final int position;
        private final long expectedFileSize;
        private final File partialFile;

        public BufferTask(DownloadFile downloadFile, int position) {
            this.downloadFile = downloadFile;
            this.position = position;
            partialFile = downloadFile.getPartialFile();

            long bufferLength = Util.getBufferLength(DownloadServiceImpl.this);

            if (bufferLength == 0) {
                // Set to seconds in a day, basically infinity
                bufferLength = 86400L;
            }

            // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to.
            int bitRate = downloadFile.getBitRate();
            long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength);

            // Find out how large the file should grow before resuming playback.
            Log.i(TAG, String.format("Buffering from position %d and bitrate %d", position, bitRate));
            expectedFileSize = (position * bitRate / 8) + byteCount;
        }

        @Override
        public void execute() {
            setPlayerState(DOWNLOADING);

            while (!bufferComplete() && !Util.isOffline(DownloadServiceImpl.this)) {
                Util.sleepQuietly(1000L);
                if (isCancelled()) {
                    return;
                }
            }
            doPlay(downloadFile, position, true);
        }

        private boolean bufferComplete() {
            boolean completeFileAvailable = downloadFile.isWorkDone();
            long size = partialFile.length();

            Log.i(TAG, String.format("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize,
                    completeFileAvailable));
            return completeFileAvailable || size >= expectedFileSize;
        }

        @Override
        public String toString() {
            return String.format("BufferTask (%s)", downloadFile);
        }
    }

    private class PositionCache implements Runnable {
        boolean isRunning = true;

        public void stop() {
            isRunning = false;
        }

        @Override
        public void run() {
            Thread.currentThread().setName("PositionCache");

            // Stop checking position before the song reaches completion
            while (isRunning) {
                try {
                    if (mediaPlayer != null && playerState == STARTED) {
                        cachedPosition = mediaPlayer.getCurrentPosition();
                    }

                    Util.sleepQuietly(25L);
                } catch (Exception e) {
                    Log.w(TAG, "Crashed getting current position", e);
                    isRunning = false;
                    positionCache = null;
                }
            }
        }
    }

    private class CheckCompletionTask extends CancellableTask {
        private final DownloadFile downloadFile;
        private final File partialFile;

        public CheckCompletionTask(DownloadFile downloadFile) {
            super();
            setNextPlayerState(PlayerState.IDLE);

            this.downloadFile = downloadFile;

            partialFile = downloadFile != null ? downloadFile.getPartialFile() : null;
        }

        @Override
        public void execute() {
            Thread.currentThread().setName("CheckCompletionTask");

            if (downloadFile == null) {
                return;
            }

            // Do an initial sleep so this prepare can't compete with main prepare
            Util.sleepQuietly(5000L);

            while (!bufferComplete()) {
                Util.sleepQuietly(5000L);

                if (isCancelled()) {
                    return;
                }
            }

            // Start the setup of the next media player
            mediaPlayerHandler.post(new Runnable() {
                @Override
                public void run() {
                    setupNext(downloadFile);
                }
            });
        }

        private boolean bufferComplete() {
            boolean completeFileAvailable = downloadFile.isWorkDone();
            Log.i(TAG, String.format("Buffering next %s (%d)", partialFile, partialFile.length()));
            return completeFileAvailable
                    && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED);
        }

        @Override
        public String toString() {
            return String.format("CheckCompletionTask (%s)", downloadFile);
        }
    }
}