-Fixed deferred media source from releasing reused resources.
-Fixed external play queue to load more than once. -Fixed wrong item removal due to player error. -Added new event to indicate error to play queue. -Changed player error to skip item instead of removing. -Modified play queue adapter to update changed items only. -Removed headers from play queue adapter. -Merged event broadcast on play queue. -Changed toast on player error. -Modified remove event to no longer indicate current index status. -Modified move event to no longer indicate randomization status. -Added shuffle check to play queue.
This commit is contained in:
parent
a9aee21e58
commit
eebd83d6ac
|
@ -36,6 +36,7 @@ import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.RemoteViews;
|
import android.widget.RemoteViews;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
@ -402,6 +403,7 @@ public final class BackgroundPlayer extends Service {
|
||||||
@Override
|
@Override
|
||||||
public void onError(Exception exception) {
|
public void onError(Exception exception) {
|
||||||
exception.printStackTrace();
|
exception.printStackTrace();
|
||||||
|
Toast.makeText(context, "Failed to play this audio", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -33,12 +33,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
private static final String TAG = "BGPlayerActivity";
|
private static final String TAG = "BGPlayerActivity";
|
||||||
|
|
||||||
private boolean isServiceBound;
|
private boolean serviceBound;
|
||||||
private ServiceConnection serviceConnection;
|
private ServiceConnection serviceConnection;
|
||||||
|
|
||||||
private BackgroundPlayer.BasePlayerImpl player;
|
private BackgroundPlayer.BasePlayerImpl player;
|
||||||
|
|
||||||
private boolean isSeeking;
|
private boolean seeking;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
|
@ -104,9 +104,9 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
if(isServiceBound) {
|
if(serviceBound) {
|
||||||
unbindService(serviceConnection);
|
unbindService(serviceConnection);
|
||||||
isServiceBound = false;
|
serviceBound = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
Log.d(TAG, "Background player service is disconnected");
|
Log.d(TAG, "Background player service is disconnected");
|
||||||
isServiceBound = false;
|
serviceBound = false;
|
||||||
player = null;
|
player = null;
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
isServiceBound = true;
|
serviceBound = true;
|
||||||
buildComponents();
|
buildComponents();
|
||||||
|
|
||||||
player.setActivityListener(BackgroundPlayerActivity.this);
|
player.setActivityListener(BackgroundPlayerActivity.this);
|
||||||
|
@ -220,13 +220,13 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||||
isSeeking = true;
|
seeking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
player.simpleExoPlayer.seekTo(seekBar.getProgress());
|
player.simpleExoPlayer.seekTo(seekBar.getProgress());
|
||||||
isSeeking = false;
|
seeking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -284,7 +284,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
progressEndTime.setText(Localization.getDurationString(duration / 1000));
|
progressEndTime.setText(Localization.getDurationString(duration / 1000));
|
||||||
|
|
||||||
// Set current time if not seeking
|
// Set current time if not seeking
|
||||||
if (!isSeeking) {
|
if (!seeking) {
|
||||||
progressSeekBar.setProgress(currentProgress);
|
progressSeekBar.setProgress(currentProgress);
|
||||||
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
||||||
}
|
}
|
||||||
|
|
|
@ -664,8 +664,22 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(ExoPlaybackException error) {
|
public void onPlayerError(ExoPlaybackException error) {
|
||||||
if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]");
|
if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]");
|
||||||
playQueue.remove(playQueue.getIndex());
|
|
||||||
onError(error);
|
// If the current window is seekable, then the error is produced by transitioning into
|
||||||
|
// bad window, therefore we simply increment the current index.
|
||||||
|
// This is done because ExoPlayer reports the exception before window is
|
||||||
|
// transitioned due to seamless playback.
|
||||||
|
if (!simpleExoPlayer.isCurrentWindowSeekable()) {
|
||||||
|
playQueue.error();
|
||||||
|
onError(error);
|
||||||
|
} else {
|
||||||
|
playQueue.offsetIndex(+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player error causes ExoPlayer to go back to IDLE state, which requires resetting
|
||||||
|
// preparing a new media source.
|
||||||
|
playbackManager.reset();
|
||||||
|
playbackManager.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -30,27 +30,37 @@ import io.reactivex.schedulers.Schedulers;
|
||||||
public final class DeferredMediaSource implements MediaSource {
|
public final class DeferredMediaSource implements MediaSource {
|
||||||
private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode());
|
private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode());
|
||||||
|
|
||||||
private int state = -1;
|
/**
|
||||||
|
* This state indicates the {@link DeferredMediaSource} has just been initialized or reset.
|
||||||
|
* The source must be prepared and loaded again before playback.
|
||||||
|
* */
|
||||||
public final static int STATE_INIT = 0;
|
public final static int STATE_INIT = 0;
|
||||||
|
/**
|
||||||
|
* This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load.
|
||||||
|
* */
|
||||||
public final static int STATE_PREPARED = 1;
|
public final static int STATE_PREPARED = 1;
|
||||||
|
/**
|
||||||
|
* This state indicates the {@link DeferredMediaSource} has been loaded without errors and
|
||||||
|
* is ready for playback.
|
||||||
|
* */
|
||||||
public final static int STATE_LOADED = 2;
|
public final static int STATE_LOADED = 2;
|
||||||
public final static int STATE_DISPOSED = 3;
|
|
||||||
|
|
||||||
public interface Callback {
|
public interface Callback {
|
||||||
/**
|
/**
|
||||||
* Player-specific MediaSource resolution from given StreamInfo.
|
* Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution
|
||||||
|
* from a given StreamInfo.
|
||||||
* */
|
* */
|
||||||
MediaSource sourceOf(final StreamInfo info);
|
MediaSource sourceOf(final StreamInfo info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueueItem stream;
|
private PlayQueueItem stream;
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
|
private int state;
|
||||||
|
|
||||||
private MediaSource mediaSource;
|
private MediaSource mediaSource;
|
||||||
|
|
||||||
|
/* Custom internal objects */
|
||||||
private Disposable loader;
|
private Disposable loader;
|
||||||
|
|
||||||
private ExoPlayer exoPlayer;
|
private ExoPlayer exoPlayer;
|
||||||
private Listener listener;
|
private Listener listener;
|
||||||
private Throwable error;
|
private Throwable error;
|
||||||
|
@ -62,6 +72,17 @@ public final class DeferredMediaSource implements MediaSource {
|
||||||
this.state = STATE_INIT;
|
this.state = STATE_INIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current state of the {@link DeferredMediaSource}.
|
||||||
|
*
|
||||||
|
* @see DeferredMediaSource#STATE_INIT
|
||||||
|
* @see DeferredMediaSource#STATE_PREPARED
|
||||||
|
* @see DeferredMediaSource#STATE_LOADED
|
||||||
|
* */
|
||||||
|
public int state() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameters are kept in the class for delayed preparation.
|
* Parameters are kept in the class for delayed preparation.
|
||||||
* */
|
* */
|
||||||
|
@ -72,54 +93,37 @@ public final class DeferredMediaSource implements MediaSource {
|
||||||
this.state = STATE_PREPARED;
|
this.state = STATE_PREPARED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int state() {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Externally controlled loading. This method fully prepares the source to be used
|
* Externally controlled loading. This method fully prepares the source to be used
|
||||||
* like any other native MediaSource.
|
* like any other native {@link com.google.android.exoplayer2.source.MediaSource}.
|
||||||
*
|
*
|
||||||
* Ideally, this should be called after this source has entered PREPARED state and
|
* Ideally, this should be called after this source has entered PREPARED state and
|
||||||
* called once only.
|
* called once only.
|
||||||
*
|
*
|
||||||
* If loading fails here, an error will be propagated out and result in a
|
* If loading fails here, an error will be propagated out and result in an
|
||||||
* {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated
|
* {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, which is delegated
|
||||||
* to the player.
|
* to the player.
|
||||||
* */
|
* */
|
||||||
public synchronized void load() {
|
public synchronized void load() {
|
||||||
if (state != STATE_PREPARED || stream == null || loader != null) return;
|
if (stream == null) {
|
||||||
|
Log.e(TAG, "Stream Info missing, media source loading terminated.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state != STATE_PREPARED || loader != null) return;
|
||||||
|
|
||||||
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
||||||
|
|
||||||
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
|
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(StreamInfo streamInfo) throws Exception {
|
public void accept(StreamInfo streamInfo) throws Exception {
|
||||||
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
onStreamInfoReceived(streamInfo);
|
||||||
state = STATE_LOADED;
|
|
||||||
|
|
||||||
if (exoPlayer == null || listener == null || streamInfo == null) {
|
|
||||||
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSource = callback.sourceOf(streamInfo);
|
|
||||||
if (mediaSource == null) {
|
|
||||||
error = new Throwable("Unable to resolve source from stream info. URL: " + stream.getUrl() +
|
|
||||||
", audio count: " + streamInfo.audio_streams.size() +
|
|
||||||
", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSource.prepareSource(exoPlayer, false, listener);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(Throwable throwable) throws Exception {
|
public void accept(Throwable throwable) throws Exception {
|
||||||
Log.e(TAG, "Loading error:", throwable);
|
onStreamInfoError(throwable);
|
||||||
error = throwable;
|
|
||||||
state = STATE_LOADED;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -129,6 +133,38 @@ public final class DeferredMediaSource implements MediaSource {
|
||||||
.subscribe(onSuccess, onError);
|
.subscribe(onSuccess, onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onStreamInfoReceived(final StreamInfo streamInfo) {
|
||||||
|
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
||||||
|
state = STATE_LOADED;
|
||||||
|
|
||||||
|
if (exoPlayer == null || listener == null || streamInfo == null) {
|
||||||
|
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSource = callback.sourceOf(streamInfo);
|
||||||
|
if (mediaSource == null) {
|
||||||
|
error = new Throwable("Unable to resolve source from stream info. URL: " + stream.getUrl() +
|
||||||
|
", audio count: " + streamInfo.audio_streams.size() +
|
||||||
|
", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSource.prepareSource(exoPlayer, false, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onStreamInfoError(final Throwable throwable) {
|
||||||
|
Log.e(TAG, "Loading error:", throwable);
|
||||||
|
error = throwable;
|
||||||
|
state = STATE_LOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate all errors to the player after {@link #load() load} is complete.
|
||||||
|
*
|
||||||
|
* Specifically, this method is called after an exception has occurred during loading or
|
||||||
|
* {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}.
|
||||||
|
* */
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
|
@ -145,19 +181,27 @@ public final class DeferredMediaSource implements MediaSource {
|
||||||
return mediaSource.createPeriod(mediaPeriodId, allocator);
|
return mediaSource.createPeriod(mediaPeriodId, allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the media period (buffers).
|
||||||
|
*
|
||||||
|
* This may be called after {@link #releaseSource releaseSource}.
|
||||||
|
* */
|
||||||
@Override
|
@Override
|
||||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||||
if (mediaSource == null) {
|
mediaSource.releasePeriod(mediaPeriod);
|
||||||
Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred.");
|
|
||||||
} else {
|
|
||||||
mediaSource.releasePeriod(mediaPeriod);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up all internal custom objects creating during loading.
|
||||||
|
*
|
||||||
|
* This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource}
|
||||||
|
* is released or when the player is stopped.
|
||||||
|
*
|
||||||
|
* This method should not release or set null the resources passed in through the constructor.
|
||||||
|
* This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}.
|
||||||
|
* */
|
||||||
@Override
|
@Override
|
||||||
public void releaseSource() {
|
public void releaseSource() {
|
||||||
state = STATE_DISPOSED;
|
|
||||||
|
|
||||||
if (mediaSource != null) {
|
if (mediaSource != null) {
|
||||||
mediaSource.releaseSource();
|
mediaSource.releaseSource();
|
||||||
}
|
}
|
||||||
|
@ -166,9 +210,11 @@ public final class DeferredMediaSource implements MediaSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Do not set mediaSource as null here as it may be called through releasePeriod */
|
/* Do not set mediaSource as null here as it may be called through releasePeriod */
|
||||||
stream = null;
|
loader = null;
|
||||||
callback = null;
|
|
||||||
exoPlayer = null;
|
exoPlayer = null;
|
||||||
listener = null;
|
listener = null;
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
state = STATE_INIT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,11 +107,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the current playing stream and the streams within its WINDOW_SIZE bound.
|
* Loads the current playing stream and the streams within its WINDOW_SIZE bound.
|
||||||
|
*
|
||||||
|
* Unblocks the player once the item at the current index is loaded.
|
||||||
* */
|
* */
|
||||||
public void load() {
|
public void load() {
|
||||||
// The current item has higher priority
|
// The current item has higher priority
|
||||||
final int currentIndex = playQueue.getIndex();
|
final int currentIndex = playQueue.getIndex();
|
||||||
final PlayQueueItem currentItem = playQueue.get(currentIndex);
|
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
||||||
if (currentItem == null) return;
|
if (currentItem == null) return;
|
||||||
load(currentItem);
|
load(currentItem);
|
||||||
|
|
||||||
|
@ -121,12 +123,24 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
final int rightBound = Math.min(playQueue.size(), rightLimit);
|
final int rightBound = Math.min(playQueue.size(), rightLimit);
|
||||||
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
|
final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound));
|
||||||
|
|
||||||
|
// Do a round robin
|
||||||
final int excess = rightLimit - playQueue.size();
|
final int excess = rightLimit - playQueue.size();
|
||||||
if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
|
if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
|
||||||
|
|
||||||
for (final PlayQueueItem item: items) load(item);
|
for (final PlayQueueItem item: items) load(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks the player and repopulate the sources.
|
||||||
|
*
|
||||||
|
* Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}.
|
||||||
|
* */
|
||||||
|
public void reset() {
|
||||||
|
tryBlock();
|
||||||
|
resetSources();
|
||||||
|
populateSources();
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Event Reactor
|
// Event Reactor
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -141,44 +155,8 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(@NonNull PlayQueueMessage event) {
|
public void onNext(@NonNull PlayQueueMessage playQueueMessage) {
|
||||||
// why no pattern matching in Java =(
|
onPlayQueueChanged(playQueueMessage);
|
||||||
switch (event.type()) {
|
|
||||||
case APPEND:
|
|
||||||
populateSources();
|
|
||||||
break;
|
|
||||||
case SELECT:
|
|
||||||
if (isCurrentIndexLoaded()) {
|
|
||||||
sync();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case REMOVE:
|
|
||||||
final RemoveEvent removeEvent = (RemoveEvent) event;
|
|
||||||
if (!removeEvent.isCurrent()) {
|
|
||||||
remove(removeEvent.index());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Reset the sources if the index to remove is the current playing index
|
|
||||||
case INIT:
|
|
||||||
case REORDER:
|
|
||||||
tryBlock();
|
|
||||||
resetSources();
|
|
||||||
populateSources();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPlayQueueReady()) {
|
|
||||||
tryBlock();
|
|
||||||
playQueue.fetch();
|
|
||||||
} else if (playQueue.isEmpty()) {
|
|
||||||
playbackListener.shutdown();
|
|
||||||
} else {
|
|
||||||
load(); // All event warrants a load
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playQueueReactor != null) playQueueReactor.request(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -189,6 +167,45 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onPlayQueueChanged(final PlayQueueMessage event) {
|
||||||
|
// why no pattern matching in Java =(
|
||||||
|
switch (event.type()) {
|
||||||
|
case APPEND:
|
||||||
|
populateSources();
|
||||||
|
break;
|
||||||
|
case SELECT:
|
||||||
|
if (isCurrentIndexLoaded()) {
|
||||||
|
sync();
|
||||||
|
} else {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case REMOVE:
|
||||||
|
final RemoveEvent removeEvent = (RemoveEvent) event;
|
||||||
|
remove(removeEvent.index());
|
||||||
|
break;
|
||||||
|
case INIT:
|
||||||
|
case REORDER:
|
||||||
|
reset();
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
|
case MOVE:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlayQueueReady()) {
|
||||||
|
tryBlock();
|
||||||
|
playQueue.fetch();
|
||||||
|
} else if (playQueue.isEmpty()) {
|
||||||
|
playbackListener.shutdown();
|
||||||
|
} else {
|
||||||
|
load(); // All event warrants a load
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playQueueReactor != null) playQueueReactor.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Internal Helpers
|
// Internal Helpers
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -220,7 +237,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sync() {
|
private void sync() {
|
||||||
final PlayQueueItem currentItem = playQueue.getCurrent();
|
final PlayQueueItem currentItem = playQueue.getItem();
|
||||||
|
|
||||||
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
|
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -20,8 +20,6 @@ import io.reactivex.schedulers.Schedulers;
|
||||||
public final class ExternalPlayQueue extends PlayQueue {
|
public final class ExternalPlayQueue extends PlayQueue {
|
||||||
private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode());
|
private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode());
|
||||||
|
|
||||||
private static final int RETRY_COUNT = 2;
|
|
||||||
|
|
||||||
private boolean isComplete;
|
private boolean isComplete;
|
||||||
|
|
||||||
private int serviceId;
|
private int serviceId;
|
||||||
|
@ -54,7 +52,6 @@ public final class ExternalPlayQueue extends PlayQueue {
|
||||||
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl)
|
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.retry(RETRY_COUNT)
|
|
||||||
.subscribe(getPlaylistObserver());
|
.subscribe(getPlaylistObserver());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +72,9 @@ public final class ExternalPlayQueue extends PlayQueue {
|
||||||
nextUrl = result.nextItemsUrl;
|
nextUrl = result.nextItemsUrl;
|
||||||
|
|
||||||
append(extractPlaylistItems(result.nextItemsList));
|
append(extractPlaylistItems(result.nextItemsList));
|
||||||
|
|
||||||
|
fetchReactor.dispose();
|
||||||
|
fetchReactor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.util.Log;
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.playlist.events.AppendEvent;
|
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||||
|
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
||||||
import org.schabi.newpipe.playlist.events.InitEvent;
|
import org.schabi.newpipe.playlist.events.InitEvent;
|
||||||
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
||||||
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||||
|
@ -44,8 +45,7 @@ public abstract class PlayQueue implements Serializable {
|
||||||
private ArrayList<PlayQueueItem> streams;
|
private ArrayList<PlayQueueItem> streams;
|
||||||
private final AtomicInteger queueIndex;
|
private final AtomicInteger queueIndex;
|
||||||
|
|
||||||
private transient BehaviorSubject<PlayQueueMessage> streamsEventBroadcast;
|
private transient BehaviorSubject<PlayQueueMessage> eventBroadcast;
|
||||||
private transient BehaviorSubject<PlayQueueMessage> indexEventBroadcast;
|
|
||||||
private transient Flowable<PlayQueueMessage> broadcastReceiver;
|
private transient Flowable<PlayQueueMessage> broadcastReceiver;
|
||||||
private transient Subscription reportingReactor;
|
private transient Subscription reportingReactor;
|
||||||
|
|
||||||
|
@ -70,13 +70,11 @@ public abstract class PlayQueue implements Serializable {
|
||||||
* Also starts a self reporter for logging if debug mode is enabled.
|
* Also starts a self reporter for logging if debug mode is enabled.
|
||||||
* */
|
* */
|
||||||
public void init() {
|
public void init() {
|
||||||
streamsEventBroadcast = BehaviorSubject.create();
|
eventBroadcast = BehaviorSubject.create();
|
||||||
indexEventBroadcast = BehaviorSubject.create();
|
|
||||||
|
|
||||||
broadcastReceiver = Flowable.merge(
|
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
|
||||||
streamsEventBroadcast.toFlowable(BackpressureStrategy.BUFFER),
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
indexEventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
|
.startWith(new InitEvent());
|
||||||
).observeOn(AndroidSchedulers.mainThread()).startWith(new InitEvent());
|
|
||||||
|
|
||||||
if (DEBUG) broadcastReceiver.subscribe(getSelfReporter());
|
if (DEBUG) broadcastReceiver.subscribe(getSelfReporter());
|
||||||
}
|
}
|
||||||
|
@ -88,8 +86,7 @@ public abstract class PlayQueue implements Serializable {
|
||||||
if (backup != null) backup.clear();
|
if (backup != null) backup.clear();
|
||||||
if (streams != null) streams.clear();
|
if (streams != null) streams.clear();
|
||||||
|
|
||||||
if (streamsEventBroadcast != null) streamsEventBroadcast.onComplete();
|
if (eventBroadcast != null) eventBroadcast.onComplete();
|
||||||
if (indexEventBroadcast != null) indexEventBroadcast.onComplete();
|
|
||||||
if (reportingReactor != null) reportingReactor.cancel();
|
if (reportingReactor != null) reportingReactor.cancel();
|
||||||
|
|
||||||
broadcastReceiver = null;
|
broadcastReceiver = null;
|
||||||
|
@ -123,15 +120,15 @@ public abstract class PlayQueue implements Serializable {
|
||||||
/**
|
/**
|
||||||
* Returns the current item that should be played.
|
* Returns the current item that should be played.
|
||||||
* */
|
* */
|
||||||
public PlayQueueItem getCurrent() {
|
public PlayQueueItem getItem() {
|
||||||
return get(getIndex());
|
return getItem(getIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the item at the given index.
|
* Returns the item at the given index.
|
||||||
* May throw {@link IndexOutOfBoundsException}.
|
* May throw {@link IndexOutOfBoundsException}.
|
||||||
* */
|
* */
|
||||||
public PlayQueueItem get(int index) {
|
public PlayQueueItem getItem(int index) {
|
||||||
if (index >= streams.size() || streams.get(index) == null) return null;
|
if (index >= streams.size() || streams.get(index) == null) return null;
|
||||||
return streams.get(index);
|
return streams.get(index);
|
||||||
}
|
}
|
||||||
|
@ -160,6 +157,13 @@ public abstract class PlayQueue implements Serializable {
|
||||||
return streams.isEmpty();
|
return streams.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the current play queue is shuffled.
|
||||||
|
* */
|
||||||
|
public boolean isShuffled() {
|
||||||
|
return backup != null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an immutable view of the play queue.
|
* Returns an immutable view of the play queue.
|
||||||
* */
|
* */
|
||||||
|
@ -191,12 +195,14 @@ public abstract class PlayQueue implements Serializable {
|
||||||
public synchronized void setIndex(final int index) {
|
public synchronized void setIndex(final int index) {
|
||||||
if (index == getIndex()) return;
|
if (index == getIndex()) return;
|
||||||
|
|
||||||
|
final int oldIndex = getIndex();
|
||||||
|
|
||||||
int newIndex = index;
|
int newIndex = index;
|
||||||
if (index < 0) newIndex = 0;
|
if (index < 0) newIndex = 0;
|
||||||
if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
|
if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
|
||||||
|
|
||||||
queueIndex.set(newIndex);
|
queueIndex.set(newIndex);
|
||||||
indexEventBroadcast.onNext(new SelectEvent(newIndex));
|
broadcast(new SelectEvent(oldIndex, newIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -213,7 +219,7 @@ public abstract class PlayQueue implements Serializable {
|
||||||
*
|
*
|
||||||
* Will emit a {@link AppendEvent} on any given context.
|
* Will emit a {@link AppendEvent} on any given context.
|
||||||
* */
|
* */
|
||||||
protected synchronized void append(final PlayQueueItem... items) {
|
public synchronized void append(final PlayQueueItem... items) {
|
||||||
streams.addAll(Arrays.asList(items));
|
streams.addAll(Arrays.asList(items));
|
||||||
broadcast(new AppendEvent(items.length));
|
broadcast(new AppendEvent(items.length));
|
||||||
}
|
}
|
||||||
|
@ -223,7 +229,7 @@ public abstract class PlayQueue implements Serializable {
|
||||||
*
|
*
|
||||||
* Will emit a {@link AppendEvent} on any given context.
|
* Will emit a {@link AppendEvent} on any given context.
|
||||||
* */
|
* */
|
||||||
protected synchronized void append(final Collection<PlayQueueItem> items) {
|
public synchronized void append(final Collection<PlayQueueItem> items) {
|
||||||
streams.addAll(items);
|
streams.addAll(items);
|
||||||
broadcast(new AppendEvent(items.size()));
|
broadcast(new AppendEvent(items.size()));
|
||||||
}
|
}
|
||||||
|
@ -235,22 +241,35 @@ public abstract class PlayQueue implements Serializable {
|
||||||
* On cases where the current playing index exceeds the playlist range, it is set to 0.
|
* On cases where the current playing index exceeds the playlist range, it is set to 0.
|
||||||
*
|
*
|
||||||
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
|
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
|
||||||
*
|
|
||||||
* */
|
* */
|
||||||
public synchronized void remove(final int index) {
|
public synchronized void remove(final int index) {
|
||||||
if (index >= streams.size() || index < 0) return;
|
if (index >= streams.size() || index < 0) return;
|
||||||
|
removeInternal(index);
|
||||||
|
broadcast(new RemoveEvent(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report an exception for the item at the current index in order to remove it.
|
||||||
|
*
|
||||||
|
* This is done as a separate event as the underlying manager may have
|
||||||
|
* different implementation regarding exceptions.
|
||||||
|
* */
|
||||||
|
public synchronized void error() {
|
||||||
|
final int index = getIndex();
|
||||||
|
removeInternal(index);
|
||||||
|
broadcast(new ErrorEvent(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void removeInternal(final int index) {
|
||||||
final int currentIndex = queueIndex.get();
|
final int currentIndex = queueIndex.get();
|
||||||
final boolean isCurrent = index == getIndex();
|
|
||||||
|
|
||||||
if (currentIndex > index) {
|
if (currentIndex > index) {
|
||||||
queueIndex.decrementAndGet();
|
queueIndex.decrementAndGet();
|
||||||
} else if (currentIndex >= size()) {
|
} else if (currentIndex >= size()) {
|
||||||
queueIndex.set(0);
|
queueIndex.set(0);
|
||||||
}
|
}
|
||||||
streams.remove(index);
|
|
||||||
|
|
||||||
broadcast(new RemoveEvent(index, isCurrent));
|
streams.remove(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -264,11 +283,11 @@ public abstract class PlayQueue implements Serializable {
|
||||||
* */
|
* */
|
||||||
public synchronized void shuffle() {
|
public synchronized void shuffle() {
|
||||||
backup = new ArrayList<>(streams);
|
backup = new ArrayList<>(streams);
|
||||||
final PlayQueueItem current = getCurrent();
|
final PlayQueueItem current = getItem();
|
||||||
Collections.shuffle(streams);
|
Collections.shuffle(streams);
|
||||||
queueIndex.set(streams.indexOf(current));
|
queueIndex.set(streams.indexOf(current));
|
||||||
|
|
||||||
broadcast(new ReorderEvent(true));
|
broadcast(new ReorderEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -280,12 +299,13 @@ public abstract class PlayQueue implements Serializable {
|
||||||
* */
|
* */
|
||||||
public synchronized void unshuffle() {
|
public synchronized void unshuffle() {
|
||||||
if (backup == null) return;
|
if (backup == null) return;
|
||||||
final PlayQueueItem current = getCurrent();
|
final PlayQueueItem current = getItem();
|
||||||
streams.clear();
|
streams.clear();
|
||||||
streams = backup;
|
streams = backup;
|
||||||
|
backup = null;
|
||||||
queueIndex.set(streams.indexOf(current));
|
queueIndex.set(streams.indexOf(current));
|
||||||
|
|
||||||
broadcast(new ReorderEvent(false));
|
broadcast(new ReorderEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -293,7 +313,9 @@ public abstract class PlayQueue implements Serializable {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void broadcast(final PlayQueueMessage event) {
|
private void broadcast(final PlayQueueMessage event) {
|
||||||
streamsEventBroadcast.onNext(event);
|
if (eventBroadcast != null) {
|
||||||
|
eventBroadcast.onNext(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Subscriber<PlayQueueMessage> getSelfReporter() {
|
private Subscriber<PlayQueueMessage> getSelfReporter() {
|
||||||
|
|
|
@ -7,7 +7,11 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||||
|
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
||||||
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
|
||||||
|
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||||
|
import org.schabi.newpipe.playlist.events.SelectEvent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -38,10 +42,12 @@ import io.reactivex.disposables.Disposable;
|
||||||
public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
private static final String TAG = PlayQueueAdapter.class.toString();
|
private static final String TAG = PlayQueueAdapter.class.toString();
|
||||||
|
|
||||||
|
private static final int ITEM_VIEW_TYPE_ID = 0;
|
||||||
|
private static final int FOOTER_VIEW_TYPE_ID = 1;
|
||||||
|
|
||||||
private final PlayQueueItemBuilder playQueueItemBuilder;
|
private final PlayQueueItemBuilder playQueueItemBuilder;
|
||||||
private final PlayQueue playQueue;
|
private final PlayQueue playQueue;
|
||||||
private boolean showFooter = false;
|
private boolean showFooter = false;
|
||||||
private View header = null;
|
|
||||||
private View footer = null;
|
private View footer = null;
|
||||||
|
|
||||||
private Disposable playQueueReactor;
|
private Disposable playQueueReactor;
|
||||||
|
@ -54,11 +60,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
public View view;
|
public View view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showFooter(final boolean show) {
|
|
||||||
showFooter = show;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayQueueAdapter(final PlayQueue playQueue) {
|
public PlayQueueAdapter(final PlayQueue playQueue) {
|
||||||
this.playQueueItemBuilder = new PlayQueueItemBuilder();
|
this.playQueueItemBuilder = new PlayQueueItemBuilder();
|
||||||
this.playQueue = playQueue;
|
this.playQueue = playQueue;
|
||||||
|
@ -92,7 +93,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(@NonNull PlayQueueMessage playQueueMessage) {
|
public void onNext(@NonNull PlayQueueMessage playQueueMessage) {
|
||||||
notifyDataSetChanged();
|
onPlayQueueChanged(playQueueMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -109,19 +110,46 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
.subscribe(observer);
|
.subscribe(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onPlayQueueChanged(final PlayQueueMessage message) {
|
||||||
|
switch (message.type()) {
|
||||||
|
case SELECT:
|
||||||
|
final SelectEvent selectEvent = (SelectEvent) message;
|
||||||
|
notifyItemChanged(selectEvent.getOldIndex());
|
||||||
|
notifyItemChanged(selectEvent.getNewIndex());
|
||||||
|
break;
|
||||||
|
case APPEND:
|
||||||
|
final AppendEvent appendEvent = (AppendEvent) message;
|
||||||
|
notifyItemRangeInserted(playQueue.size(), appendEvent.getAmount());
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
|
final ErrorEvent errorEvent = (ErrorEvent) message;
|
||||||
|
notifyItemRangeRemoved(errorEvent.index(), 1);
|
||||||
|
notifyItemChanged(errorEvent.index());
|
||||||
|
break;
|
||||||
|
case REMOVE:
|
||||||
|
final RemoveEvent removeEvent = (RemoveEvent) message;
|
||||||
|
notifyItemRangeRemoved(removeEvent.index(), 1);
|
||||||
|
notifyItemChanged(removeEvent.index());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
notifyDataSetChanged();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (playQueueReactor != null) playQueueReactor.dispose();
|
if (playQueueReactor != null) playQueueReactor.dispose();
|
||||||
playQueueReactor = null;
|
playQueueReactor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHeader(View header) {
|
|
||||||
this.header = header;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFooter(View footer) {
|
public void setFooter(View footer) {
|
||||||
this.footer = footer;
|
this.footer = footer;
|
||||||
notifyDataSetChanged();
|
notifyItemChanged(playQueue.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showFooter(final boolean show) {
|
||||||
|
showFooter = show;
|
||||||
|
notifyItemChanged(playQueue.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PlayQueueItem> getItems() {
|
public List<PlayQueueItem> getItems() {
|
||||||
|
@ -131,36 +159,28 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
int count = playQueue.getStreams().size();
|
int count = playQueue.getStreams().size();
|
||||||
if(header != null) count++;
|
|
||||||
if(footer != null && showFooter) count++;
|
if(footer != null && showFooter) count++;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if(header != null && position == 0) {
|
|
||||||
return 0;
|
|
||||||
} else if(header != null) {
|
|
||||||
position--;
|
|
||||||
}
|
|
||||||
if(footer != null && position == playQueue.getStreams().size() && showFooter) {
|
if(footer != null && position == playQueue.getStreams().size() && showFooter) {
|
||||||
return 1;
|
return FOOTER_VIEW_TYPE_ID;
|
||||||
}
|
}
|
||||||
return 2;
|
|
||||||
|
return ITEM_VIEW_TYPE_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 0:
|
case FOOTER_VIEW_TYPE_ID:
|
||||||
return new HFHolder(header);
|
|
||||||
case 1:
|
|
||||||
return new HFHolder(footer);
|
return new HFHolder(footer);
|
||||||
case 2:
|
case ITEM_VIEW_TYPE_ID:
|
||||||
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext())
|
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.play_queue_item, parent, false));
|
||||||
.inflate(R.layout.play_queue_item, parent, false));
|
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Trollolo");
|
Log.e(TAG, "Attempting to create view holder with undefined type: " + type);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,14 +188,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||||
if(holder instanceof PlayQueueItemHolder) {
|
if(holder instanceof PlayQueueItemHolder) {
|
||||||
// Ensure header does not interfere with list building
|
|
||||||
if (header != null) position--;
|
|
||||||
// Build the list item
|
// Build the list item
|
||||||
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position));
|
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position));
|
||||||
// Check if the current item should be selected/highlighted
|
// Check if the current item should be selected/highlighted
|
||||||
holder.itemView.setSelected(playQueue.getIndex() == position);
|
holder.itemView.setSelected(playQueue.getIndex() == position);
|
||||||
} else if(holder instanceof HFHolder && position == 0 && header != null) {
|
|
||||||
((HFHolder) holder).view = header;
|
|
||||||
} else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
|
} else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
|
||||||
((HFHolder) holder).view = footer;
|
((HFHolder) holder).view = footer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.schabi.newpipe.playlist.events;
|
||||||
|
|
||||||
|
|
||||||
|
public class ErrorEvent implements PlayQueueMessage {
|
||||||
|
final private int index;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayQueueEvent type() {
|
||||||
|
return PlayQueueEvent.ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorEvent(final int index) {
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int index() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,10 @@ public enum PlayQueueEvent {
|
||||||
// sent when two streams swap place in the play queue
|
// sent when two streams swap place in the play queue
|
||||||
MOVE,
|
MOVE,
|
||||||
|
|
||||||
// send when queue is shuffled
|
// sent when queue is shuffled
|
||||||
REORDER
|
REORDER,
|
||||||
|
|
||||||
|
// sent when the item at index has caused an exception
|
||||||
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,23 +3,17 @@ package org.schabi.newpipe.playlist.events;
|
||||||
|
|
||||||
public class RemoveEvent implements PlayQueueMessage {
|
public class RemoveEvent implements PlayQueueMessage {
|
||||||
final private int index;
|
final private int index;
|
||||||
final private boolean isCurrent;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlayQueueEvent type() {
|
public PlayQueueEvent type() {
|
||||||
return PlayQueueEvent.REMOVE;
|
return PlayQueueEvent.REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RemoveEvent(final int index, final boolean isCurrent) {
|
public RemoveEvent(final int index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.isCurrent = isCurrent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int index() {
|
public int index() {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCurrent() {
|
|
||||||
return isCurrent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
package org.schabi.newpipe.playlist.events;
|
package org.schabi.newpipe.playlist.events;
|
||||||
|
|
||||||
public class ReorderEvent implements PlayQueueMessage {
|
public class ReorderEvent implements PlayQueueMessage {
|
||||||
final private boolean randomize;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlayQueueEvent type() {
|
public PlayQueueEvent type() {
|
||||||
return PlayQueueEvent.REORDER;
|
return PlayQueueEvent.REORDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReorderEvent(final boolean randomize) {
|
public ReorderEvent() {
|
||||||
this.randomize = randomize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRandomize() {
|
|
||||||
return randomize;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.playlist.events;
|
||||||
|
|
||||||
|
|
||||||
public class SelectEvent implements PlayQueueMessage {
|
public class SelectEvent implements PlayQueueMessage {
|
||||||
|
final private int oldIndex;
|
||||||
final private int newIndex;
|
final private int newIndex;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -9,11 +10,16 @@ public class SelectEvent implements PlayQueueMessage {
|
||||||
return PlayQueueEvent.SELECT;
|
return PlayQueueEvent.SELECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SelectEvent(final int newIndex) {
|
public SelectEvent(final int oldIndex, final int newIndex) {
|
||||||
|
this.oldIndex = oldIndex;
|
||||||
this.newIndex = newIndex;
|
this.newIndex = newIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int index() {
|
public int getOldIndex() {
|
||||||
|
return oldIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNewIndex() {
|
||||||
return newIndex;
|
return newIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue