package org.moire.ultrasonic.service; import android.annotation.SuppressLint; import android.app.PendingIntent; 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.Looper; import android.os.PowerManager; import android.util.Log; import android.widget.SeekBar; import org.jetbrains.annotations.NotNull; import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.audiofx.EqualizerController; import org.moire.ultrasonic.audiofx.VisualizerController; import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.PlayerState; 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.StreamProxy; import org.moire.ultrasonic.util.Util; import java.io.File; import java.net.URLEncoder; import java.util.Locale; 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; /** * Represents a Media Player which uses the mobile's resources for playback */ public class LocalMediaPlayer { private static final String TAG = LocalMediaPlayer.class.getSimpleName(); public Consumer onCurrentPlayingChanged; public Consumer onSongCompleted; public BiConsumer onPlayerStateChanged; public Runnable onPrepared; public Runnable onNextSongRequested; public static boolean equalizerAvailable; public static boolean visualizerAvailable; public PlayerState playerState = IDLE; public DownloadFile currentPlaying; public DownloadFile nextPlaying; private PlayerState nextPlayerState = IDLE; private boolean nextSetup; private CancellableTask nextPlayingTask; private PowerManager.WakeLock wakeLock; private MediaPlayer mediaPlayer; private MediaPlayer nextMediaPlayer; private Looper mediaPlayerLooper; private Handler mediaPlayerHandler; private int cachedPosition; private StreamProxy proxy; private AudioManager audioManager; private RemoteControlClient remoteControlClient; private EqualizerController equalizerController; private VisualizerController visualizerController; private CancellableTask bufferTask; private PositionCache positionCache; private int secondaryProgress = -1; private final AudioFocusHandler audioFocusHandler; private final Context context; static { try { EqualizerController.checkAvailable(); equalizerAvailable = true; } catch (Throwable t) { equalizerAvailable = false; } } static { try { VisualizerController.checkAvailable(); visualizerAvailable = true; } catch (Throwable t) { visualizerAvailable = false; } } public LocalMediaPlayer(AudioFocusHandler audioFocusHandler, Context context) { this.audioFocusHandler = audioFocusHandler; this.context = context; } public void onCreate() { if (mediaPlayer != null) { mediaPlayer.release(); } mediaPlayer = new MediaPlayer(); new Thread(new Runnable() { @Override public void run() { Thread.currentThread().setName("MediaPlayerThread"); Looper.prepare(); mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mediaPlayer, int what, int more) { handleError(new Exception(String.format(Locale.getDefault(), "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, context.getPackageName()); context.sendBroadcast(i); } catch (Throwable e) { // Froyo or lower } mediaPlayerLooper = Looper.myLooper(); mediaPlayerHandler = new Handler(mediaPlayerLooper); Looper.loop(); } }).start(); PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); wakeLock.setReferenceCounted(false); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); Util.registerMediaButtonEventReceiver(context, true); setUpRemoteControlClient(); if (equalizerAvailable) { equalizerController = new EqualizerController(context, mediaPlayer); if (!equalizerController.isAvailable()) { equalizerController = null; } else { equalizerController.loadSettings(); } } if (visualizerAvailable) { visualizerController = new VisualizerController(mediaPlayer); if (!visualizerController.isAvailable()) { visualizerController = null; } } Log.i(TAG, "LocalMediaPlayer created"); } public void onDestroy() { reset(); try { 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, context.getPackageName()); context.sendBroadcast(i); mediaPlayer.release(); if (nextMediaPlayer != null) { nextMediaPlayer.release(); } mediaPlayerLooper.quit(); if (equalizerController != null) { equalizerController.release(); } if (visualizerController != null) { visualizerController.release(); } if (bufferTask != null) { bufferTask.cancel(); } if (nextPlayingTask != null) { nextPlayingTask.cancel(); } audioManager.unregisterRemoteControlClient(remoteControlClient); clearRemoteControl(); Util.unregisterMediaButtonEventReceiver(context, true); wakeLock.release(); } catch (Throwable exception) { Log.w(TAG, "LocalMediaPlayer onDestroy exception: ", exception); } Log.i(TAG, "LocalMediaPlayer destroyed"); } public EqualizerController getEqualizerController() { if (equalizerAvailable && equalizerController == null) { equalizerController = new EqualizerController(context, mediaPlayer); if (!equalizerController.isAvailable()) { equalizerController = null; } else { equalizerController.loadSettings(); } } return equalizerController; } public VisualizerController getVisualizerController() { if (visualizerAvailable && visualizerController == null) { visualizerController = new VisualizerController(mediaPlayer); if (!visualizerController.isAvailable()) { visualizerController = null; } } return visualizerController; } public synchronized void setPlayerState(final PlayerState playerState) { Log.i(TAG, String.format("%s -> %s (%s)", this.playerState.name(), playerState.name(), currentPlaying)); this.playerState = playerState; if (playerState == PlayerState.STARTED) { audioFocusHandler.requestAudioFocus(); } if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) { updateRemoteControl(); } if (onPlayerStateChanged != null) { Handler mainHandler = new Handler(context.getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { onPlayerStateChanged.accept(playerState, currentPlaying); } }; mainHandler.post(myRunnable); } 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; } } public synchronized void setCurrentPlaying(final DownloadFile currentPlaying) { Log.v(TAG, String.format("setCurrentPlaying %s", currentPlaying)); this.currentPlaying = currentPlaying; updateRemoteControl(); if (onCurrentPlayingChanged != null) { Handler mainHandler = new Handler(context.getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { onCurrentPlayingChanged.accept(currentPlaying); } }; mainHandler.post(myRunnable); } } public synchronized void setNextPlaying(DownloadFile nextToPlay) { if (nextToPlay == null) { nextPlaying = null; setNextPlayerState(IDLE); return; } nextPlaying = nextToPlay; nextPlayingTask = new CheckCompletionTask(nextPlaying); nextPlayingTask.start(); } public synchronized void clearNextPlaying() { nextSetup = false; nextPlaying = null; if (nextPlayingTask != null) { nextPlayingTask.cancel(); nextPlayingTask = null; } } public synchronized void setNextPlayerState(PlayerState playerState) { Log.i(TAG, String.format("Next: %s -> %s (%s)", nextPlayerState.name(), playerState.name(), nextPlaying)); nextPlayerState = playerState; } public synchronized void bufferAndPlay() { if (playerState != PREPARED) { reset(); bufferTask = new BufferTask(currentPlaying, 0); bufferTask.start(); } else { doPlay(currentPlaying, 0, true); } } public synchronized void play(DownloadFile fileToPlay) { if (nextPlayingTask != null) { nextPlayingTask.cancel(); nextPlayingTask = null; } setCurrentPlaying(fileToPlay); bufferAndPlay(); } public synchronized void playNext() { MediaPlayer tmp = mediaPlayer; mediaPlayer = nextMediaPlayer; nextMediaPlayer = tmp; setCurrentPlaying(nextPlaying); setPlayerState(PlayerState.STARTED); setupHandlers(currentPlaying, false); if (onNextSongRequested != null) { Handler mainHandler = new Handler(context.getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { onNextSongRequested.run(); } }; mainHandler.post(myRunnable); } // Proxy should not be being used here since the next player was already setup to play if (proxy != null) { proxy.stop(); proxy = null; } } public synchronized void pause() { try { mediaPlayer.pause(); } catch (Exception x) { handleError(x); } } public synchronized void start() { try { mediaPlayer.start(); } catch (Exception x) { handleError(x); } } private void updateRemoteControl() { if (!Util.isLockScreenEnabled(context)) { 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())); if (playerState == 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); } } else { 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); } } if (currentPlaying != null) { MusicDirectory.Entry currentSong = currentPlaying.getSong(); Bitmap lockScreenBitmap = FileUtil.getAlbumArtBitmap(context, currentSong, Util.getMinDisplayMetric(context), 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(); } } public void clearRemoteControl() { if (remoteControlClient != null) { remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); audioManager.unregisterRemoteControlClient(remoteControlClient); remoteControlClient = null; } } private void setUpRemoteControlClient() { if (!Util.isLockScreenEnabled(context)) return; ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); if (remoteControlClient == null) { final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setComponent(componentName); PendingIntent broadcast = PendingIntent.getBroadcast(context, 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); } } public synchronized void seekTo(int position) { try { mediaPlayer.seekTo(position); cachedPosition = position; updateRemoteControl(); } catch (Exception x) { handleError(x); } } public synchronized int getPlayerPosition() { try { if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { return 0; } return cachedPosition; } catch (Exception x) { handleError(x); return 0; } } 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; } public void setVolume(float volume) { if (mediaPlayer != null) { mediaPlayer.setVolume(volume, volume); } } public 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(new Supplier() { @Override public DownloadFile get() { return currentPlaying; } }); proxy.start(); } dataSource = String.format(Locale.getDefault(), "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(context) == 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 (LocalMediaPlayer.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); } } if (onPrepared != null) { Handler mainHandler = new Handler(context.getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { onPrepared.run(); } }; mainHandler.post(myRunnable); } } }); 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(context, 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(context) && 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))) { setPlayerState(COMPLETED); if (Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState == PlayerState.PREPARED) { if (nextSetup) { nextSetup = false; } playNext(); } else { if (onSongCompleted != null) { Handler mainHandler = new Handler(context.getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { onSongCompleted.accept(currentPlaying); } }; mainHandler.post(myRunnable); } } return; } synchronized (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(); } } } }); } 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); } } 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(context); 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() && !ActiveServerProvider.Companion.isOffline(context)) { 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; } @NotNull @Override public String toString() { return String.format("BufferTask (%s)", downloadFile); } } 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); } @NotNull @Override public String toString() { return String.format("CheckCompletionTask (%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(50L); } catch (Exception e) { Log.w(TAG, "Crashed getting current position", e); isRunning = false; positionCache = null; } } } } 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); } } private void handleErrorNext(Exception x) { Log.w(TAG, String.format("Next Media player error: %s", x), x); nextMediaPlayer.reset(); } }