From 2e414cfd6393b4c577291503f9e72fbaa74a783a Mon Sep 17 00:00:00 2001 From: John Zhen M Date: Sun, 8 Oct 2017 22:43:07 -0700 Subject: [PATCH] - Added move mechanic to background player through handles (on both thumbnail and icon). - Added remove and open detail as long click popup dropdown on background player. - Vastly simplified list manipulation in MediaSourceManager by delegating most control to DynamicConcatenatingMediaSource. --- .../newpipe/player/BackgroundPlayer.java | 12 -- .../player/BackgroundPlayerActivity.java | 120 +++++++++++++++++- .../org/schabi/newpipe/player/BasePlayer.java | 57 ++++----- .../player/playback/MediaSourceManager.java | 66 +++------- .../schabi/newpipe/playlist/PlayQueue.java | 18 +++ .../newpipe/playlist/PlayQueueAdapter.java | 7 + .../playlist/PlayQueueItemBuilder.java | 38 +++++- .../newpipe/playlist/PlayQueueItemHolder.java | 3 +- .../newpipe/playlist/events/MoveEvent.java | 24 ++++ app/src/main/res/layout/play_queue_item.xml | 17 ++- 10 files changed, 254 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/playlist/events/MoveEvent.java 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 03bc734db..479a73347 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -155,18 +155,6 @@ public final class BackgroundPlayer extends Service { context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); } - public void onOpenDetail(Context context, String videoUrl, String videoTitle) { - if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]"); - Intent i = new Intent(context, MainActivity.class); - i.putExtra(Constants.KEY_SERVICE_ID, 0); - i.putExtra(Constants.KEY_URL, videoUrl); - i.putExtra(Constants.KEY_TITLE, videoTitle); - i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); - context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - } - private void onClose() { if (basePlayerImpl != null) { basePlayerImpl.stopActivityBinding(); 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 07938e134..c90fc095d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Build; @@ -10,21 +11,28 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; +import android.support.v7.widget.helper.ItemTouchHelper; import android.util.Log; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ImageButton; +import android.widget.PopupMenu; import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItemBuilder; +import org.schabi.newpipe.playlist.PlayQueueItemHolder; import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.ThemeHelper; @@ -44,9 +52,14 @@ public class BackgroundPlayerActivity extends AppCompatActivity // Views //////////////////////////////////////////////////////////////////////////// + private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; + private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61; + private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97; + private View rootView; private RecyclerView itemsList; + private ItemTouchHelper itemTouchHelper; private TextView metadataTitle; private TextView metadataArtist; @@ -157,14 +170,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity itemsList.setLayoutManager(new LinearLayoutManager(this)); itemsList.setAdapter(player.playQueueAdapter); itemsList.setClickable(true); + itemsList.setLongClickable(true); - player.playQueueAdapter.setSelectedListener(new PlayQueueItemBuilder.OnSelectedListener() { - @Override - public void selected(PlayQueueItem item) { - final int index = player.playQueue.indexOf(item); - if (index != -1) player.playQueue.setIndex(index); - } - }); + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(itemsList); + + player.playQueueAdapter.setSelectedListener(getOnSelectedListener()); } private void buildMetadata() { @@ -192,6 +203,101 @@ public class BackgroundPlayerActivity extends AppCompatActivity forwardButton.setOnClickListener(this); } + private void buildItemPopupMenu(final PlayQueueItem item, final View view) { + final PopupMenu menu = new PopupMenu(this, view); + final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, "Remove"); + remove.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + final int index = player.playQueue.indexOf(item); + if (index != -1) player.playQueue.remove(index); + return true; + } + }); + + final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, "Detail"); + detail.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + onOpenDetail(BackgroundPlayerActivity.this, item.getUrl(), item.getTitle()); + return true; + } + }); + + menu.show(); + } + + //////////////////////////////////////////////////////////////////////////// + // Component Helpers + //////////////////////////////////////////////////////////////////////////// + + 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) { + if (source.getItemViewType() != target.getItemViewType()) { + return false; + } + + final int sourceIndex = source.getLayoutPosition(); + final int targetIndex = target.getLayoutPosition(); + player.playQueue.move(sourceIndex, targetIndex); + return true; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {} + }; + } + + private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { + return new PlayQueueItemBuilder.OnSelectedListener() { + @Override + public void selected(PlayQueueItem item, View view) { + final int index = player.playQueue.indexOf(item); + if (index == -1) return; + + if (player.playQueue.getIndex() == index) { + player.onRestart(); + } else { + player.playQueue.setIndex(index); + } + } + + @Override + public void held(PlayQueueItem item, View view) { + final int index = player.playQueue.indexOf(item); + if (index != -1) buildItemPopupMenu(item, view); + } + + @Override + public void onStartDrag(PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder); + } + }; + } + + private void onOpenDetail(Context context, String videoUrl, String videoTitle) { + Intent i = new Intent(context, MainActivity.class); + i.putExtra(Constants.KEY_SERVICE_ID, 0); + i.putExtra(Constants.KEY_URL, videoUrl); + i.putExtra(Constants.KEY_TITLE, videoTitle); + i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + //////////////////////////////////////////////////////////////////////////// // Component On-Click Listener //////////////////////////////////////////////////////////////////////////// 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 3607a2770..6bdff821b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -559,27 +559,18 @@ public abstract class BasePlayer implements Player.EventListener, } /*////////////////////////////////////////////////////////////////////////// - // Timeline + // ExoPlayer Listener //////////////////////////////////////////////////////////////////////////*/ - private void refreshTimeline() { - playbackManager.load(); + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); - final int currentSourceIndex = playbackManager.getCurrentSourceIndex(); - - // Sanity checks - if (currentSourceIndex < 0) return; + final int currentSourceIndex = playQueue.getIndex(); // Check if already playing correct window final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex; - // Check if on wrong window - if (!isCurrentWindowCorrect) { - final long startPos = currentInfo != null ? currentInfo.start_position : 0; - if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); - simpleExoPlayer.seekTo(currentSourceIndex, startPos); - } - // Check if recovering if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) { // todo: figure out exactly why this is the case @@ -591,17 +582,10 @@ public abstract class BasePlayer implements Player.EventListener, simpleExoPlayer.seekTo(roundedPos); isRecovery = false; } - } - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount()); - - refreshTimeline(); + if (playbackManager != null) { + playbackManager.load(); + } } @Override @@ -709,14 +693,12 @@ public abstract class BasePlayer implements Player.EventListener, public void onPositionDiscontinuity() { // Refresh the playback if there is a transition to the next video final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - final int newQueueIndex = playbackManager.getQueueIndexOf(newWindowIndex); - if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " + - "window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]"); + 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. // Therefore, the only source that causes a discrepancy would be autoplay, // which can only offset the current track by +1. - if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1); + if (newWindowIndex != playQueue.getIndex()) playQueue.offsetIndex(+1); } @Override @@ -751,12 +733,16 @@ public abstract class BasePlayer implements Player.EventListener, @Override public void sync(@Nullable final StreamInfo info) { - if (simpleExoPlayer == null) return; + if (info == null || simpleExoPlayer == null) return; if (DEBUG) Log.d(TAG, "Syncing..."); - refreshTimeline(); - - if (info == null) return; + // Check if on wrong window + final int currentSourceIndex = playQueue.getIndex(); + if (!(simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex)) { + final long startPos = currentInfo != null ? currentInfo.start_position : 0; + if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos)); + simpleExoPlayer.seekTo(currentSourceIndex, startPos); + } currentInfo = info; initThumbnail(info.thumbnail_url); @@ -830,6 +816,13 @@ public abstract class BasePlayer implements Player.EventListener, playQueue.offsetIndex(+1); } + public void onRestart() { + if (playQueue == null) return; + if (DEBUG) Log.d(TAG, "onRestart() called"); + + simpleExoPlayer.seekToDefaultPosition(); + } + public void seekBy(int milliSeconds) { if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) 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 313dbb377..f40ce978a 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 @@ -12,6 +12,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.mediasource.DeferredMediaSource; import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueueItem; +import org.schabi.newpipe.playlist.events.ErrorEvent; +import org.schabi.newpipe.playlist.events.MoveEvent; import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.RemoveEvent; @@ -29,16 +31,12 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { // One-side rolling window size for default loading // Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback // todo: inject this parameter, allow user settings perhaps - private static final int WINDOW_SIZE = 2; + private static final int WINDOW_SIZE = 1; private PlaybackListener playbackListener; private PlayQueue playQueue; private DynamicConcatenatingMediaSource sources; - // sourceToQueueIndex maps media source index to play queue index - // Invariant 1: this list is sorted in ascending order - // Invariant 2: this list contains no duplicates - private List sourceToQueueIndex; private Subscription playQueueReactor; private SerialDisposable syncReactor; @@ -53,7 +51,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { this.syncReactor = new SerialDisposable(); this.sources = new DynamicConcatenatingMediaSource(); - this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList()); playQueue.getBroadcastReceiver() .observeOn(AndroidSchedulers.mainThread()) @@ -72,22 +69,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { /*////////////////////////////////////////////////////////////////////////// // Exposed Methods //////////////////////////////////////////////////////////////////////////*/ - - /** - * Returns the media source index of the currently playing stream. - * */ - public int getCurrentSourceIndex() { - return sourceToQueueIndex.indexOf(playQueue.getIndex()); - } - - /** - * Returns the play queue index of a given media source playlist index. - * */ - public int getQueueIndexOf(final int sourceIndex) { - if (sourceIndex < 0 || sourceIndex >= sourceToQueueIndex.size()) return -1; - return sourceToQueueIndex.get(sourceIndex); - } - /** * Dispose the manager and releases all message buses and loaders. * */ @@ -95,12 +76,10 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { if (playQueueReactor != null) playQueueReactor.cancel(); if (syncReactor != null) syncReactor.dispose(); if (sources != null) sources.releaseSource(); - if (sourceToQueueIndex != null) sourceToQueueIndex.clear(); playQueueReactor = null; syncReactor = null; sources = null; - sourceToQueueIndex = null; playbackListener = null; playQueue = null; } @@ -174,11 +153,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { populateSources(); break; case SELECT: - if (isCurrentIndexLoaded()) { - sync(); - } else { - reset(); - } + sync(); break; case REMOVE: final RemoveEvent removeEvent = (RemoveEvent) event; @@ -188,8 +163,11 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { case REORDER: reset(); break; - case ERROR: case MOVE: + final MoveEvent moveEvent = (MoveEvent) event; + move(moveEvent.getFromIndex(), moveEvent.getToIndex()); + break; + case ERROR: default: break; } @@ -214,10 +192,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE; } - private boolean isCurrentIndexLoaded() { - return getCurrentSourceIndex() != -1; - } - private boolean tryBlock() { if (!isBlocked) { playbackListener.block(); @@ -228,7 +202,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { } private boolean tryUnblock() { - if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) { + if (isPlayQueueReady() && isBlocked) { isBlocked = false; playbackListener.unblock(sources); return true; @@ -270,7 +244,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { private void resetSources() { if (this.sources != null) this.sources.releaseSource(); - if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear(); this.sources = new DynamicConcatenatingMediaSource(); } @@ -294,12 +267,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { private void insert(final int queueIndex, final DeferredMediaSource source) { if (queueIndex < 0) return; - int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex); - if (pos < 0) { - final int sourceIndex = -pos-1; - sourceToQueueIndex.add(sourceIndex, queueIndex); - sources.addMediaSource(sourceIndex, source); - } + sources.addMediaSource(queueIndex, source); } /** @@ -310,15 +278,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { private void remove(final int queueIndex) { if (queueIndex < 0) return; - final int sourceIndex = sourceToQueueIndex.indexOf(queueIndex); - if (sourceIndex == -1) return; + sources.removeMediaSource(queueIndex); + } - sourceToQueueIndex.remove(sourceIndex); - sources.removeMediaSource(sourceIndex); + private void move(final int source, final int target) { + if (source < 0 || target < 0) return; + if (source >= sources.getSize() || target >= sources.getSize()) return; - // Will be slow on really large arrays, fast enough for typical use case - for (int i = sourceIndex; i < sourceToQueueIndex.size(); i++) { - sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1); - } + sources.moveMediaSource(source, target); } } 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 72a73e238..becebc534 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueue.java @@ -8,6 +8,7 @@ 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.MoveEvent; import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.ReorderEvent; @@ -272,6 +273,23 @@ public abstract class PlayQueue implements Serializable { streams.remove(index); } + public synchronized void move(final int source, final int target) { + if (source < 0 || target < 0) return; + if (source >= streams.size() || target >= streams.size()) return; + + final int current = getIndex(); + if (source == current) { + queueIndex.set(target); + } else if (source < current && target >= current) { + queueIndex.decrementAndGet(); + } else if (source > current && target <= current) { + queueIndex.incrementAndGet(); + } + + streams.add(target, streams.remove(source)); + broadcast(new MoveEvent(source, target)); + } + /** * Shuffles the current play queue. * 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 27a7fee8f..8e33b7141 100644 --- a/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/playlist/PlayQueueAdapter.java @@ -9,6 +9,7 @@ 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.MoveEvent; import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.SelectEvent; @@ -131,6 +132,12 @@ public class PlayQueueAdapter extends RecyclerView.Adapter + + + tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. "/> + \ No newline at end of file