-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.
This commit is contained in:
John Zhen Mo 2018-02-17 11:55:45 -08:00
parent 88ac821070
commit d936ca6b89
12 changed files with 144 additions and 125 deletions

View File

@ -73,7 +73,7 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0' implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1' implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:3.0.1' 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:1.5.0'
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0' debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'

View File

@ -81,6 +81,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; 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; 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 (playbackManager != null) playbackManager.dispose();
if (audioReactor != null) audioReactor.abandonAudioFocus(); if (audioReactor != null) audioReactor.abandonAudioFocus();
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose(); if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
if (playQueueAdapter != null) {
playQueueAdapter.unsetSelectedListener();
playQueueAdapter.dispose();
}
} }
public void destroy() { public void destroy() {
@ -460,11 +469,11 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
final PlayQueueItem currentSourceItem = playQueue.getItem(); final PlayQueueItem currentSourceItem = playQueue.getItem();
// Check if already playing correct window // Check if already playing correct window
final boolean isCurrentWindowCorrect = final boolean isCurrentPeriodCorrect =
simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex; simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex;
// Check if recovering // Check if recovering
if (isCurrentWindowCorrect && currentSourceItem != null) { if (isCurrentPeriodCorrect && currentSourceItem != null) {
/* Recovering with sub-second position may cause a long buffer delay in ExoPlayer, /* Recovering with sub-second position may cause a long buffer delay in ExoPlayer,
* rounding this position to the nearest second will help alleviate this.*/ * rounding this position to the nearest second will help alleviate this.*/
final long position = currentSourceItem.getRecoveryPosition(); final long position = currentSourceItem.getRecoveryPosition();
@ -605,17 +614,25 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
} }
@Override @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 // Refresh the playback if there is a transition to the next video
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); final int newWindowIndex = simpleExoPlayer.getCurrentPeriodIndex();
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]");
// If the user selects a new track, then the discontinuity occurs after the index is changed. /* Discontinuity reasons!! Thank you ExoPlayer lords */
// Therefore, the only source that causes a discrepancy would be gapless transition, switch (reason) {
// which can only offset the current track by +1. case DISCONTINUITY_REASON_PERIOD_TRANSITION:
if (newWindowIndex == playQueue.getIndex() + 1 || if (newWindowIndex == playQueue.getIndex()) {
(newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) { registerView();
playQueue.offsetIndex(+1); } else {
playQueue.offsetIndex(+1);
}
break;
case DISCONTINUITY_REASON_SEEK:
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
case DISCONTINUITY_REASON_INTERNAL:
default:
break;
} }
playbackManager.load(); playbackManager.load();
} }
@ -625,6 +642,16 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]"); 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 // Playback Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -668,19 +695,14 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
if (currentSourceIndex != playQueue.getIndex()) { if (currentSourceIndex != playQueue.getIndex()) {
Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex + Log.e(TAG, "Play Queue may be desynchronized: item index=[" + currentSourceIndex +
"], queue index=[" + playQueue.getIndex() + "]"); "], 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; final long startPos = info != null ? info.start_position : 0;
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
" at: " + getTimeString((int)startPos)); " at: " + getTimeString((int)startPos));
simpleExoPlayer.seekTo(currentSourceIndex, startPos); simpleExoPlayer.seekTo(currentSourceIndex, startPos);
} }
// TODO: update exoplayer to 2.6.x in order to register view count on repeated streams registerView();
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
.subscribe(
ignored -> {/* successful */},
error -> Log.e(TAG, "Player onViewed() failure: ", error)
));
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url); initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
} }
@ -814,6 +836,15 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
// Utils // 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() { protected void reload() {
if (playbackManager != null) { if (playbackManager != null) {
playbackManager.reset(); playbackManager.reset();

View File

@ -61,6 +61,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; 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 View rootView;
private RecyclerView itemsList; private RecyclerView itemsList;
@ -211,6 +214,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
unbindService(serviceConnection); unbindService(serviceConnection);
serviceBound = false; serviceBound = false;
stopPlayerListener(); 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; player = null;
} }
} }
@ -385,7 +397,19 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ItemTouchHelper.SimpleCallback getItemTouchCallback() { private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
@Override @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()) { if (source.getItemViewType() != target.getItemViewType()) {
return false; return false;
} }

View File

@ -181,7 +181,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
public void onAudioInputFormatChanged(Format format) {} public void onAudioInputFormatChanged(Format format) {}
@Override @Override
public void onAudioTrackUnderrun(int i, long l, long l1) {} public void onAudioSinkUnderrun(int bufferSize,
long bufferSizeMs,
long elapsedSinceLastFeedMs) {}
@Override @Override
public void onAudioDisabled(DecoderCounters decoderCounters) {} public void onAudioDisabled(DecoderCounters decoderCounters) {}

View File

@ -2,6 +2,7 @@ package org.schabi.newpipe.player.helper;
import android.content.Context; import android.content.Context;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Renderer; 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.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator; 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 class LoadController implements LoadControl {
public static final String TAG = "LoadController"; public static final String TAG = "LoadController";
@ -23,16 +26,17 @@ public class LoadController implements LoadControl {
public LoadController(final Context context) { public LoadController(final Context context) {
this(PlayerHelper.getMinBufferMs(context), this(PlayerHelper.getMinBufferMs(context),
PlayerHelper.getMaxBufferMs(context), PlayerHelper.getMaxBufferMs(context),
PlayerHelper.getBufferForPlaybackMs(context), PlayerHelper.getBufferForPlaybackMs(context));
PlayerHelper.getBufferForPlaybackAfterRebufferMs(context));
} }
public LoadController(final int minBufferMs, public LoadController(final int minBufferMs,
final int maxBufferMs, final int maxBufferMs,
final long bufferForPlaybackMs, final int bufferForPlaybackMs) {
final long bufferForPlaybackAfterRebufferMs) { final DefaultAllocator allocator = new DefaultAllocator(true,
final DefaultAllocator allocator = new DefaultAllocator(true, 65536); C.DEFAULT_BUFFER_SEGMENT_SIZE);
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View File

@ -113,12 +113,8 @@ public class PlayerHelper {
return 30000; return 30000;
} }
public static long getBufferForPlaybackMs(@NonNull final Context context) { public static int getBufferForPlaybackMs(@NonNull final Context context) {
return 2500L; return 2500;
}
public static long getBufferForPlaybackAfterRebufferMs(@NonNull final Context context) {
return 5000L;
} }
public static boolean isUsingDSP(@NonNull final Context context) { public static boolean isUsingDSP(@NonNull final Context context) {

View File

@ -114,32 +114,10 @@ public final class DeferredMediaSource implements MediaSource {
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); 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() loader = stream.getStream()
.observeOn(Schedulers.io()) .map(streamInfo -> onStreamInfoReceived(stream, streamInfo))
.map(onReceive)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError); .subscribe(this::onMediaSourceReceived, this::onStreamInfoError);
} }
private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item, private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,

View File

@ -4,7 +4,6 @@ import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import org.reactivestreams.Subscriber; import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
@ -21,8 +20,8 @@ import java.util.concurrent.TimeUnit;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull; import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
@ -46,7 +45,9 @@ public class MediaSourceManager {
private DynamicConcatenatingMediaSource sources; private DynamicConcatenatingMediaSource sources;
private Subscription playQueueReactor; private Subscription playQueueReactor;
private SerialDisposable syncReactor; private CompositeDisposable syncReactor;
private PlayQueueItem syncedItem;
private boolean isBlocked; private boolean isBlocked;
@ -68,7 +69,7 @@ public class MediaSourceManager {
this.windowSize = windowSize; this.windowSize = windowSize;
this.loadDebounceMillis = loadDebounceMillis; this.loadDebounceMillis = loadDebounceMillis;
this.syncReactor = new SerialDisposable(); this.syncReactor = new CompositeDisposable();
this.debouncedLoadSignal = PublishSubject.create(); this.debouncedLoadSignal = PublishSubject.create();
this.debouncedLoader = getDebouncedLoader(); this.debouncedLoader = getDebouncedLoader();
@ -86,12 +87,7 @@ public class MediaSourceManager {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private DeferredMediaSource.Callback getSourceBuilder() { private DeferredMediaSource.Callback getSourceBuilder() {
return new DeferredMediaSource.Callback() { return playbackListener::sourceOf;
@Override
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
return playbackListener.sourceOf(item, info);
}
};
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -241,22 +237,28 @@ public class MediaSourceManager {
final PlayQueueItem currentItem = playQueue.getItem(); final PlayQueueItem currentItem = playQueue.getItem();
if (currentItem == null) return; if (currentItem == null) return;
final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() { final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
@Override final Consumer<Throwable> onError = throwable -> {
public void accept(StreamInfo streamInfo) throws Exception { Log.e(TAG, "Sync error:", throwable);
playbackListener.sync(currentItem, streamInfo); syncInternal(currentItem, null);
}
}; };
final Consumer<Throwable> onError = new Consumer<Throwable>() { final Disposable sync = currentItem.getStream()
@Override .observeOn(AndroidSchedulers.mainThread())
public void accept(Throwable throwable) throws Exception { .subscribe(onSuccess, onError);
Log.e(TAG, "Sync error:", throwable); syncReactor.add(sync);
playbackListener.sync(currentItem,null); }
}
};
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() { private void loadDebounced() {
@ -313,12 +315,7 @@ public class MediaSourceManager {
return debouncedLoadSignal return debouncedLoadSignal
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS) .debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() { .subscribe(timestamp -> loadImmediate());
@Override
public void accept(Long timestamp) throws Exception {
loadImmediate();
}
});
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Media Source List Manipulation // Media Source List Manipulation

View File

@ -33,6 +33,8 @@ public interface PlaybackListener {
* Signals to the listener to synchronize the player's window to the manager's * Signals to the listener to synchronize the player's window to the manager's
* window. * window.
* *
* Occurs once only per play queue item change.
*
* May be called only after unblock is called. * May be called only after unblock is called.
* */ * */
void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info); void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);

View File

@ -73,6 +73,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
playQueueItemBuilder.setOnSelectedListener(listener); playQueueItemBuilder.setOnSelectedListener(listener);
} }
public void unsetSelectedListener() {
playQueueItemBuilder.setOnSelectedListener(null);
}
private void startReactor() { private void startReactor() {
final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() { final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
@Override @Override

View File

@ -104,17 +104,9 @@ public class PlayQueueItem implements Serializable {
@NonNull @NonNull
private Single<StreamInfo> getInfo() { 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) return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> error = throwable);
.doOnError(onError);
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View File

@ -53,24 +53,18 @@ public class PlayQueueItemBuilder {
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions); ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
holder.itemRoot.setOnClickListener(new View.OnClickListener() { holder.itemRoot.setOnClickListener(view -> {
@Override if (onItemClickListener != null) {
public void onClick(View view) { onItemClickListener.selected(item, view);
if (onItemClickListener != null) {
onItemClickListener.selected(item, view);
}
} }
}); });
holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() { holder.itemRoot.setOnLongClickListener(view -> {
@Override if (onItemClickListener != null) {
public boolean onLongClick(View view) { onItemClickListener.held(item, view);
if (onItemClickListener != null) { return true;
onItemClickListener.held(item, view);
return true;
}
return false;
} }
return false;
}); });
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder)); holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
@ -78,26 +72,21 @@ public class PlayQueueItemBuilder {
} }
private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) { private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
return new View.OnTouchListener() { return (view, motionEvent) -> {
@Override view.performClick();
public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN
view.performClick(); && onItemClickListener != null) {
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { onItemClickListener.onStartDrag(holder);
onItemClickListener.onStartDrag(holder);
}
return false;
} }
return false;
}; };
} }
private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) { private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
final BitmapProcessor bitmapProcessor = new BitmapProcessor() { final BitmapProcessor bitmapProcessor = bitmap -> {
@Override final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
public Bitmap process(Bitmap bitmap) { bitmap.recycle();
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false); return resizedBitmap;
bitmap.recycle();
return resizedBitmap;
}
}; };
return new DisplayImageOptions.Builder() return new DisplayImageOptions.Builder()