diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 887759640..9585edfce 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -36,6 +36,7 @@ import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.RemoteViews; +import android.widget.Toast; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -402,6 +403,7 @@ public final class BackgroundPlayer extends Service { @Override public void onError(Exception exception) { exception.printStackTrace(); + Toast.makeText(context, "Failed to play this audio", Toast.LENGTH_SHORT).show(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 127594956..07938e134 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -33,12 +33,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity private static final String TAG = "BGPlayerActivity"; - private boolean isServiceBound; + private boolean serviceBound; private ServiceConnection serviceConnection; private BackgroundPlayer.BasePlayerImpl player; - private boolean isSeeking; + private boolean seeking; //////////////////////////////////////////////////////////////////////////// // Views @@ -104,9 +104,9 @@ public class BackgroundPlayerActivity extends AppCompatActivity @Override protected void onStop() { super.onStop(); - if(isServiceBound) { + if(serviceBound) { unbindService(serviceConnection); - isServiceBound = false; + serviceBound = false; } } @@ -119,7 +119,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity @Override public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "Background player service is disconnected"); - isServiceBound = false; + serviceBound = false; player = null; finish(); } @@ -132,7 +132,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity if (player == null) { finish(); } else { - isServiceBound = true; + serviceBound = true; buildComponents(); player.setActivityListener(BackgroundPlayerActivity.this); @@ -220,13 +220,13 @@ public class BackgroundPlayerActivity extends AppCompatActivity @Override public void onStartTrackingTouch(SeekBar seekBar) { - isSeeking = true; + seeking = true; } @Override public void onStopTrackingTouch(SeekBar seekBar) { player.simpleExoPlayer.seekTo(seekBar.getProgress()); - isSeeking = false; + seeking = false; } //////////////////////////////////////////////////////////////////////////// @@ -284,7 +284,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity progressEndTime.setText(Localization.getDurationString(duration / 1000)); // Set current time if not seeking - if (!isSeeking) { + if (!seeking) { progressSeekBar.setProgress(currentProgress); progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 7a014b3be..6b6dbbba1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -664,8 +664,22 @@ public abstract class BasePlayer implements Player.EventListener, @Override public void onPlayerError(ExoPlaybackException 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 diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java index 0d25f5d59..67279091f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/DeferredMediaSource.java @@ -30,27 +30,37 @@ import io.reactivex.schedulers.Schedulers; public final class DeferredMediaSource implements MediaSource { 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; + /** + * This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load. + * */ 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_DISPOSED = 3; 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); } private PlayQueueItem stream; private Callback callback; + private int state; private MediaSource mediaSource; + /* Custom internal objects */ private Disposable loader; - private ExoPlayer exoPlayer; private Listener listener; private Throwable error; @@ -62,6 +72,17 @@ public final class DeferredMediaSource implements MediaSource { 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. * */ @@ -72,54 +93,37 @@ public final class DeferredMediaSource implements MediaSource { this.state = STATE_PREPARED; } - public int state() { - return state; - } - /** * 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 * called once only. * - * If loading fails here, an error will be propagated out and result in a - * {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated + * If loading fails here, an error will be propagated out and result in an + * {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, which is delegated * to the player. * */ 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()); final Consumer onSuccess = new Consumer() { @Override public void accept(StreamInfo streamInfo) throws Exception { - 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); + onStreamInfoReceived(streamInfo); } }; final Consumer onError = new Consumer() { @Override public void accept(Throwable throwable) throws Exception { - Log.e(TAG, "Loading error:", throwable); - error = throwable; - state = STATE_LOADED; + onStreamInfoError(throwable); } }; @@ -129,6 +133,38 @@ public final class DeferredMediaSource implements MediaSource { .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 public void maybeThrowSourceInfoRefreshError() throws IOException { if (error != null) { @@ -145,19 +181,27 @@ public final class DeferredMediaSource implements MediaSource { return mediaSource.createPeriod(mediaPeriodId, allocator); } + /** + * Releases the media period (buffers). + * + * This may be called after {@link #releaseSource releaseSource}. + * */ @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (mediaSource == null) { - Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred."); - } else { - mediaSource.releasePeriod(mediaPeriod); - } + 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 public void releaseSource() { - state = STATE_DISPOSED; - if (mediaSource != null) { 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 */ - stream = null; - callback = null; + loader = null; exoPlayer = null; listener = null; + error = null; + + state = STATE_INIT; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 2c87a7b0f..313dbb377 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -107,11 +107,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { /** * 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() { // The current item has higher priority final int currentIndex = playQueue.getIndex(); - final PlayQueueItem currentItem = playQueue.get(currentIndex); + final PlayQueueItem currentItem = playQueue.getItem(currentIndex); if (currentItem == null) return; load(currentItem); @@ -121,12 +123,24 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { final int rightBound = Math.min(playQueue.size(), rightLimit); final List items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); + // Do a round robin final int excess = rightLimit - playQueue.size(); if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); 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 //////////////////////////////////////////////////////////////////////////*/ @@ -141,44 +155,8 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { } @Override - public void onNext(@NonNull PlayQueueMessage event) { - // why no pattern matching in Java =( - 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); + public void onNext(@NonNull PlayQueueMessage playQueueMessage) { + onPlayQueueChanged(playQueueMessage); } @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 //////////////////////////////////////////////////////////////////////////*/ @@ -220,7 +237,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { } private void sync() { - final PlayQueueItem currentItem = playQueue.getCurrent(); + final PlayQueueItem currentItem = playQueue.getItem(); final Consumer syncPlayback = new Consumer() { @Override diff --git a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java index 65eecc62a..019b684d4 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/ExternalPlayQueue.java @@ -20,8 +20,6 @@ import io.reactivex.schedulers.Schedulers; public final class ExternalPlayQueue extends PlayQueue { private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode()); - private static final int RETRY_COUNT = 2; - private boolean isComplete; private int serviceId; @@ -54,7 +52,6 @@ public final class ExternalPlayQueue extends PlayQueue { ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .retry(RETRY_COUNT) .subscribe(getPlaylistObserver()); } @@ -75,6 +72,9 @@ public final class ExternalPlayQueue extends PlayQueue { nextUrl = result.nextItemsUrl; append(extractPlaylistItems(result.nextItemsList)); + + fetchReactor.dispose(); + fetchReactor = null; } @Override diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java index b159a354d..72a73e238 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -6,6 +6,7 @@ import android.util.Log; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; 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.PlayQueueMessage; import org.schabi.newpipe.playlist.events.RemoveEvent; @@ -44,8 +45,7 @@ public abstract class PlayQueue implements Serializable { private ArrayList streams; private final AtomicInteger queueIndex; - private transient BehaviorSubject streamsEventBroadcast; - private transient BehaviorSubject indexEventBroadcast; + private transient BehaviorSubject eventBroadcast; private transient Flowable broadcastReceiver; 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. * */ public void init() { - streamsEventBroadcast = BehaviorSubject.create(); - indexEventBroadcast = BehaviorSubject.create(); + eventBroadcast = BehaviorSubject.create(); - broadcastReceiver = Flowable.merge( - streamsEventBroadcast.toFlowable(BackpressureStrategy.BUFFER), - indexEventBroadcast.toFlowable(BackpressureStrategy.BUFFER) - ).observeOn(AndroidSchedulers.mainThread()).startWith(new InitEvent()); + broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) + .observeOn(AndroidSchedulers.mainThread()) + .startWith(new InitEvent()); if (DEBUG) broadcastReceiver.subscribe(getSelfReporter()); } @@ -88,8 +86,7 @@ public abstract class PlayQueue implements Serializable { if (backup != null) backup.clear(); if (streams != null) streams.clear(); - if (streamsEventBroadcast != null) streamsEventBroadcast.onComplete(); - if (indexEventBroadcast != null) indexEventBroadcast.onComplete(); + if (eventBroadcast != null) eventBroadcast.onComplete(); if (reportingReactor != null) reportingReactor.cancel(); broadcastReceiver = null; @@ -123,15 +120,15 @@ public abstract class PlayQueue implements Serializable { /** * Returns the current item that should be played. * */ - public PlayQueueItem getCurrent() { - return get(getIndex()); + public PlayQueueItem getItem() { + return getItem(getIndex()); } /** * Returns the item at the given index. * May throw {@link IndexOutOfBoundsException}. * */ - public PlayQueueItem get(int index) { + public PlayQueueItem getItem(int index) { if (index >= streams.size() || streams.get(index) == null) return null; return streams.get(index); } @@ -160,6 +157,13 @@ public abstract class PlayQueue implements Serializable { 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. * */ @@ -191,12 +195,14 @@ public abstract class PlayQueue implements Serializable { public synchronized void setIndex(final int index) { if (index == getIndex()) return; + final int oldIndex = getIndex(); + int newIndex = index; if (index < 0) newIndex = 0; if (index >= streams.size()) newIndex = isComplete() ? index % streams.size() : streams.size() - 1; 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. * */ - protected synchronized void append(final PlayQueueItem... items) { + public synchronized void append(final PlayQueueItem... items) { streams.addAll(Arrays.asList(items)); broadcast(new AppendEvent(items.length)); } @@ -223,7 +229,7 @@ public abstract class PlayQueue implements Serializable { * * Will emit a {@link AppendEvent} on any given context. * */ - protected synchronized void append(final Collection items) { + public synchronized void append(final Collection items) { streams.addAll(items); 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. * * Will emit a {@link RemoveEvent} if the index is within the play queue index range. - * * */ public synchronized void remove(final int index) { 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 boolean isCurrent = index == getIndex(); if (currentIndex > index) { queueIndex.decrementAndGet(); } else if (currentIndex >= size()) { 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() { backup = new ArrayList<>(streams); - final PlayQueueItem current = getCurrent(); + final PlayQueueItem current = getItem(); Collections.shuffle(streams); 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() { if (backup == null) return; - final PlayQueueItem current = getCurrent(); + final PlayQueueItem current = getItem(); streams.clear(); streams = backup; + backup = null; 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) { - streamsEventBroadcast.onNext(event); + if (eventBroadcast != null) { + eventBroadcast.onNext(event); + } } private Subscriber getSelfReporter() { diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java index edb56474c..27a7fee8f 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -7,7 +7,11 @@ import android.view.View; import android.view.ViewGroup; 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.RemoveEvent; +import org.schabi.newpipe.playlist.events.SelectEvent; import java.util.List; @@ -38,10 +42,12 @@ import io.reactivex.disposables.Disposable; public class PlayQueueAdapter extends RecyclerView.Adapter { 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 PlayQueue playQueue; private boolean showFooter = false; - private View header = null; private View footer = null; private Disposable playQueueReactor; @@ -54,11 +60,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter getItems() { @@ -131,36 +159,28 @@ public class PlayQueueAdapter extends RecyclerView.Adapter