From d936ca6b89440124584be0940db4b8ae82148c0c Mon Sep 17 00:00:00 2001
From: John Zhen Mo <zhenmogukl@gmail.com>
Date: Sat, 17 Feb 2018 11:55:45 -0800
Subject: [PATCH] -Added view registration on repeats. -Added drag reorder
 speed clamping to play queue list. -Fixed service player activity memory
 leak. -Fixed media source manager sync disposable fallthrough causing NPE.
 -Fixed thread bouncing during play queue item async stream resolution.
 -Updated ExoPlayer to 2.6.0.

---
 app/build.gradle                              |  2 +-
 .../org/schabi/newpipe/player/BasePlayer.java | 69 ++++++++++++++-----
 .../newpipe/player/ServicePlayerActivity.java | 26 ++++++-
 .../newpipe/player/helper/AudioReactor.java   |  4 +-
 .../newpipe/player/helper/LoadController.java | 16 +++--
 .../newpipe/player/helper/PlayerHelper.java   |  8 +--
 .../player/playback/DeferredMediaSource.java  | 26 +------
 .../player/playback/MediaSourceManager.java   | 55 +++++++--------
 .../player/playback/PlaybackListener.java     |  2 +
 .../newpipe/playlist/PlayQueueAdapter.java    |  4 ++
 .../newpipe/playlist/PlayQueueItem.java       | 10 +--
 .../playlist/PlayQueueItemBuilder.java        | 47 +++++--------
 12 files changed, 144 insertions(+), 125 deletions(-)

diff --git a/app/build.gradle b/app/build.gradle
index 0244ae4b9..ea4d5384d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -73,7 +73,7 @@ dependencies {
     implementation 'de.hdodenhof:circleimageview:2.2.0'
     implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
     implementation 'com.nononsenseapps:filepicker:3.0.1'
-    implementation 'com.google.android.exoplayer:exoplayer:r2.5.4'
+    implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
 
     debugImplementation 'com.facebook.stetho:stetho:1.5.0'
     debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
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 7558f1375..07d567d57 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -81,6 +81,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.CompositeDisposable;
 import io.reactivex.disposables.Disposable;
 
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
 import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
 
 /**
@@ -279,6 +283,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         if (playbackManager != null) playbackManager.dispose();
         if (audioReactor != null) audioReactor.abandonAudioFocus();
         if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
+
+        if (playQueueAdapter != null) {
+            playQueueAdapter.unsetSelectedListener();
+            playQueueAdapter.dispose();
+        }
     }
 
     public void destroy() {
@@ -460,11 +469,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         final PlayQueueItem currentSourceItem = playQueue.getItem();
 
         // Check if already playing correct window
-        final boolean isCurrentWindowCorrect =
-                simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex;
+        final boolean isCurrentPeriodCorrect =
+                simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex;
 
         // Check if recovering
-        if (isCurrentWindowCorrect && currentSourceItem != null) {
+        if (isCurrentPeriodCorrect && currentSourceItem != null) {
             /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer,
              * rounding this position to the nearest second will help alleviate this.*/
             final long position = currentSourceItem.getRecoveryPosition();
@@ -605,17 +614,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
     }
 
     @Override
-    public void onPositionDiscontinuity() {
+    public void onPositionDiscontinuity(int reason) {
+        if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with reason = [" + reason + "]");
         // Refresh the playback if there is a transition to the next video
-        final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
-        if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]");
+        final int newWindowIndex = simpleExoPlayer.getCurrentPeriodIndex();
 
-        // If the user selects a new track, then the discontinuity occurs after the index is changed.
-        // Therefore, the only source that causes a discrepancy would be gapless transition,
-        // which can only offset the current track by +1.
-        if (newWindowIndex == playQueue.getIndex() + 1 ||
-                (newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) {
-            playQueue.offsetIndex(+1);
+        /* Discontinuity reasons!! Thank you ExoPlayer lords */
+        switch (reason) {
+            case DISCONTINUITY_REASON_PERIOD_TRANSITION:
+                if (newWindowIndex == playQueue.getIndex()) {
+                    registerView();
+                } else {
+                    playQueue.offsetIndex(+1);
+                }
+                break;
+            case DISCONTINUITY_REASON_SEEK:
+            case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
+            case DISCONTINUITY_REASON_INTERNAL:
+            default:
+                break;
         }
         playbackManager.load();
     }
@@ -625,6 +642,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]");
     }
 
+    @Override
+    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+        if (DEBUG) Log.d(TAG, "onShuffleModeEnabledChanged() called with: " +
+                "mode = [" + shuffleModeEnabled + "]");
+    }
+
+    @Override
+    public void onSeekProcessed() {
+        if (DEBUG) Log.d(TAG, "onSeekProcessed() called");
+    }
     /*//////////////////////////////////////////////////////////////////////////
     // Playback Listener
     //////////////////////////////////////////////////////////////////////////*/
@@ -668,19 +695,14 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
         if (currentSourceIndex != playQueue.getIndex()) {
             Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex +
                     "], queue index=[" + playQueue.getIndex() + "]");
-        } else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) {
+        } else if (simpleExoPlayer.getCurrentPeriodIndex() != currentSourceIndex || !isPlaying()) {
             final long startPos = info != null ? info.start_position : 0;
             if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
                     " at: " + getTimeString((int)startPos));
             simpleExoPlayer.seekTo(currentSourceIndex, startPos);
         }
 
-        // TODO: update exoplayer to 2.6.x in order to register view count on repeated streams
-        databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
-                .subscribe(
-                        ignored -> {/* successful */},
-                        error -> Log.e(TAG, "Player onViewed() failure: ", error)
-                ));
+        registerView();
         initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
     }
 
@@ -814,6 +836,15 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
     // Utils
     //////////////////////////////////////////////////////////////////////////*/
 
+    private void registerView() {
+        if (databaseUpdateReactor == null || recordManager == null || currentInfo == null) return;
+        databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
+                .subscribe(
+                        ignored -> {/* successful */},
+                        error -> Log.e(TAG, "Player onViewed() failure: ", error)
+                ));
+    }
+
     protected void reload() {
         if (playbackManager != null) {
             playbackManager.reset();
diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
index 5518357a8..1378d9a80 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -61,6 +61,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
 
     private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
 
+    private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
+    private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
+
     private View rootView;
 
     private RecyclerView itemsList;
@@ -211,6 +214,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
             unbindService(serviceConnection);
             serviceBound = false;
             stopPlayerListener();
+
+            if (player != null && player.getPlayQueueAdapter() != null) {
+                player.getPlayQueueAdapter().unsetSelectedListener();
+            }
+            if (itemsList != null) itemsList.setAdapter(null);
+            if (itemTouchHelper != null) itemTouchHelper.attachToRecyclerView(null);
+
+            itemsList = null;
+            itemTouchHelper = null;
             player = null;
         }
     }
@@ -385,7 +397,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
     private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
         return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
             @Override
-            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
+            public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
+                                                    int viewSizeOutOfBounds, int totalSize,
+                                                    long msSinceStartScroll) {
+                final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
+                        viewSizeOutOfBounds, totalSize, msSinceStartScroll);
+                final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
+                        Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
+                return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
+            }
+
+            @Override
+            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
+                                  RecyclerView.ViewHolder target) {
                 if (source.getItemViewType() != target.getItemViewType()) {
                     return false;
                 }
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
index 4e031a0dd..2c85cfc34 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
@@ -181,7 +181,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
     public void onAudioInputFormatChanged(Format format) {}
 
     @Override
-    public void onAudioTrackUnderrun(int i, long l, long l1) {}
+    public void onAudioSinkUnderrun(int bufferSize,
+                                    long bufferSizeMs,
+                                    long elapsedSinceLastFeedMs) {}
 
     @Override
     public void onAudioDisabled(DecoderCounters decoderCounters) {}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
index acc20f5b0..be7b8efde 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
@@ -2,6 +2,7 @@ package org.schabi.newpipe.player.helper;
 
 import android.content.Context;
 
+import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.DefaultLoadControl;
 import com.google.android.exoplayer2.LoadControl;
 import com.google.android.exoplayer2.Renderer;
@@ -10,6 +11,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
 import com.google.android.exoplayer2.upstream.Allocator;
 import com.google.android.exoplayer2.upstream.DefaultAllocator;
 
+import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
+
 public class LoadController implements LoadControl {
 
     public static final String TAG = "LoadController";
@@ -23,16 +26,17 @@ public class LoadController implements LoadControl {
     public LoadController(final Context context) {
         this(PlayerHelper.getMinBufferMs(context),
                 PlayerHelper.getMaxBufferMs(context),
-                PlayerHelper.getBufferForPlaybackMs(context),
-                PlayerHelper.getBufferForPlaybackAfterRebufferMs(context));
+                PlayerHelper.getBufferForPlaybackMs(context));
     }
 
     public LoadController(final int minBufferMs,
                           final int maxBufferMs,
-                          final long bufferForPlaybackMs,
-                          final long bufferForPlaybackAfterRebufferMs) {
-        final DefaultAllocator allocator = new DefaultAllocator(true, 65536);
-        internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);
+                          final int bufferForPlaybackMs) {
+        final DefaultAllocator allocator = new DefaultAllocator(true,
+                C.DEFAULT_BUFFER_SEGMENT_SIZE);
+
+        internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
+                bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
     }
 
     /*//////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index 476838f13..4929be9a3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -113,12 +113,8 @@ public class PlayerHelper {
         return 30000;
     }
 
-    public static long getBufferForPlaybackMs(@NonNull final Context context) {
-        return 2500L;
-    }
-
-    public static long getBufferForPlaybackAfterRebufferMs(@NonNull final Context context) {
-        return 5000L;
+    public static int getBufferForPlaybackMs(@NonNull final Context context) {
+        return 2500;
     }
 
     public static boolean isUsingDSP(@NonNull final Context context) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java
index b0990f56a..3ae744d18 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/DeferredMediaSource.java
@@ -114,32 +114,10 @@ public final class DeferredMediaSource implements MediaSource {
 
         Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
 
-        final Function<StreamInfo, MediaSource> onReceive = new Function<StreamInfo, MediaSource>() {
-            @Override
-            public MediaSource apply(StreamInfo streamInfo) throws Exception {
-                return onStreamInfoReceived(stream, streamInfo);
-            }
-        };
-
-        final Consumer<MediaSource> onSuccess = new Consumer<MediaSource>() {
-            @Override
-            public void accept(MediaSource mediaSource) throws Exception {
-                onMediaSourceReceived(mediaSource);
-            }
-        };
-
-        final Consumer<Throwable> onError = new Consumer<Throwable>() {
-            @Override
-            public void accept(Throwable throwable) throws Exception {
-                onStreamInfoError(throwable);
-            }
-        };
-
         loader = stream.getStream()
-                .observeOn(Schedulers.io())
-                .map(onReceive)
+                .map(streamInfo -> onStreamInfoReceived(stream, streamInfo))
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(onSuccess, onError);
+                .subscribe(this::onMediaSourceReceived, this::onStreamInfoError);
     }
 
     private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,
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 04f1606fa..baf2b9c29 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
@@ -4,7 +4,6 @@ import android.support.annotation.Nullable;
 import android.util.Log;
 
 import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
-import com.google.android.exoplayer2.source.MediaSource;
 
 import org.reactivestreams.Subscriber;
 import org.reactivestreams.Subscription;
@@ -21,8 +20,8 @@ import java.util.concurrent.TimeUnit;
 
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.CompositeDisposable;
 import io.reactivex.disposables.Disposable;
-import io.reactivex.disposables.SerialDisposable;
 import io.reactivex.functions.Consumer;
 import io.reactivex.subjects.PublishSubject;
 
@@ -46,7 +45,9 @@ public class MediaSourceManager {
     private DynamicConcatenatingMediaSource sources;
 
     private Subscription playQueueReactor;
-    private SerialDisposable syncReactor;
+    private CompositeDisposable syncReactor;
+
+    private PlayQueueItem syncedItem;
 
     private boolean isBlocked;
 
@@ -68,7 +69,7 @@ public class MediaSourceManager {
         this.windowSize = windowSize;
         this.loadDebounceMillis = loadDebounceMillis;
 
-        this.syncReactor = new SerialDisposable();
+        this.syncReactor = new CompositeDisposable();
         this.debouncedLoadSignal = PublishSubject.create();
         this.debouncedLoader = getDebouncedLoader();
 
@@ -86,12 +87,7 @@ public class MediaSourceManager {
     //////////////////////////////////////////////////////////////////////////*/
 
     private DeferredMediaSource.Callback getSourceBuilder() {
-        return new DeferredMediaSource.Callback() {
-            @Override
-            public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
-                return playbackListener.sourceOf(item, info);
-            }
-        };
+        return playbackListener::sourceOf;
     }
 
     /*//////////////////////////////////////////////////////////////////////////
@@ -241,22 +237,28 @@ public class MediaSourceManager {
         final PlayQueueItem currentItem = playQueue.getItem();
         if (currentItem == null) return;
 
-        final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() {
-            @Override
-            public void accept(StreamInfo streamInfo) throws Exception {
-                playbackListener.sync(currentItem, streamInfo);
-            }
+        final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
+        final Consumer<Throwable> onError = throwable -> {
+            Log.e(TAG, "Sync error:", throwable);
+            syncInternal(currentItem, null);
         };
 
-        final Consumer<Throwable> onError = new Consumer<Throwable>() {
-            @Override
-            public void accept(Throwable throwable) throws Exception {
-                Log.e(TAG, "Sync error:", throwable);
-                playbackListener.sync(currentItem,null);
-            }
-        };
+        final Disposable sync = currentItem.getStream()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(onSuccess, onError);
+        syncReactor.add(sync);
+    }
 
-        syncReactor.set(currentItem.getStream().subscribe(syncPlayback, onError));
+    private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item,
+                              @Nullable final StreamInfo info) {
+        if (playQueue == null || playbackListener == null) return;
+
+        // Sync each new item once only and ensure the current item is up to date
+        // with the play queue
+        if (playQueue.getItem() != syncedItem && playQueue.getItem() == item) {
+            syncedItem = item;
+            playbackListener.sync(syncedItem,info);
+        }
     }
 
     private void loadDebounced() {
@@ -313,12 +315,7 @@ public class MediaSourceManager {
         return debouncedLoadSignal
                 .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(new Consumer<Long>() {
-            @Override
-            public void accept(Long timestamp) throws Exception {
-                loadImmediate();
-            }
-        });
+                .subscribe(timestamp -> loadImmediate());
     }
     /*//////////////////////////////////////////////////////////////////////////
     // Media Source List Manipulation
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
index dfed04c01..c6fdde656 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlaybackListener.java
@@ -33,6 +33,8 @@ public interface PlaybackListener {
      * Signals to the listener to synchronize the player's window to the manager's
      * window.
      *
+     * Occurs once only per play queue item change.
+     *
      * May be called only after unblock is called.
      * */
     void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
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 e16693ec6..cd833c1ab 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java
@@ -73,6 +73,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
         playQueueItemBuilder.setOnSelectedListener(listener);
     }
 
+    public void unsetSelectedListener() {
+        playQueueItemBuilder.setOnSelectedListener(null);
+    }
+
     private void startReactor() {
         final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
             @Override
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java
index f8e7b8655..752dc223d 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItem.java
@@ -104,17 +104,9 @@ public class PlayQueueItem implements Serializable {
 
     @NonNull
     private Single<StreamInfo> getInfo() {
-        final Consumer<Throwable> onError = new Consumer<Throwable>() {
-            @Override
-            public void accept(Throwable throwable) throws Exception {
-                error = throwable;
-            }
-        };
-
         return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
                 .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .doOnError(onError);
+                .doOnError(throwable -> error = throwable);
     }
 
     ////////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java
index 82277a4e7..73cdf1113 100644
--- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java
+++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueItemBuilder.java
@@ -53,24 +53,18 @@ public class PlayQueueItemBuilder {
 
         ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
 
-        holder.itemRoot.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                if (onItemClickListener != null) {
-                    onItemClickListener.selected(item, view);
-                }
+        holder.itemRoot.setOnClickListener(view -> {
+            if (onItemClickListener != null) {
+                onItemClickListener.selected(item, view);
             }
         });
 
-        holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() {
-            @Override
-            public boolean onLongClick(View view) {
-                if (onItemClickListener != null) {
-                    onItemClickListener.held(item, view);
-                    return true;
-                }
-                return false;
+        holder.itemRoot.setOnLongClickListener(view -> {
+            if (onItemClickListener != null) {
+                onItemClickListener.held(item, view);
+                return true;
             }
+            return false;
         });
 
         holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
@@ -78,26 +72,21 @@ public class PlayQueueItemBuilder {
     }
 
     private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
-        return new View.OnTouchListener() {
-            @Override
-            public boolean onTouch(View view, MotionEvent motionEvent) {
-                view.performClick();
-                if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
-                    onItemClickListener.onStartDrag(holder);
-                }
-                return false;
+        return (view, motionEvent) -> {
+            view.performClick();
+            if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN
+                    && onItemClickListener != null) {
+                onItemClickListener.onStartDrag(holder);
             }
+            return false;
         };
     }
 
     private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
-        final BitmapProcessor bitmapProcessor = new BitmapProcessor() {
-            @Override
-            public Bitmap process(Bitmap bitmap) {
-                final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
-                bitmap.recycle();
-                return resizedBitmap;
-            }
+        final BitmapProcessor bitmapProcessor = bitmap -> {
+            final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
+            bitmap.recycle();
+            return resizedBitmap;
         };
 
         return new DisplayImageOptions.Builder()