- 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.
This commit is contained in:
parent
f5b5982e1c
commit
2e414cfd63
|
@ -155,18 +155,6 @@ public final class BackgroundPlayer extends Service {
|
||||||
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
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() {
|
private void onClose() {
|
||||||
if (basePlayerImpl != null) {
|
if (basePlayerImpl != null) {
|
||||||
basePlayerImpl.stopActivityBinding();
|
basePlayerImpl.stopActivityBinding();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.os.Build;
|
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.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.PopupMenu;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||||
import org.schabi.newpipe.settings.SettingsActivity;
|
import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
@ -44,9 +52,14 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
// Views
|
// 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 View rootView;
|
||||||
|
|
||||||
private RecyclerView itemsList;
|
private RecyclerView itemsList;
|
||||||
|
private ItemTouchHelper itemTouchHelper;
|
||||||
|
|
||||||
private TextView metadataTitle;
|
private TextView metadataTitle;
|
||||||
private TextView metadataArtist;
|
private TextView metadataArtist;
|
||||||
|
@ -157,14 +170,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
itemsList.setLayoutManager(new LinearLayoutManager(this));
|
itemsList.setLayoutManager(new LinearLayoutManager(this));
|
||||||
itemsList.setAdapter(player.playQueueAdapter);
|
itemsList.setAdapter(player.playQueueAdapter);
|
||||||
itemsList.setClickable(true);
|
itemsList.setClickable(true);
|
||||||
|
itemsList.setLongClickable(true);
|
||||||
|
|
||||||
player.playQueueAdapter.setSelectedListener(new PlayQueueItemBuilder.OnSelectedListener() {
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
@Override
|
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||||
public void selected(PlayQueueItem item) {
|
|
||||||
final int index = player.playQueue.indexOf(item);
|
player.playQueueAdapter.setSelectedListener(getOnSelectedListener());
|
||||||
if (index != -1) player.playQueue.setIndex(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildMetadata() {
|
private void buildMetadata() {
|
||||||
|
@ -192,6 +203,101 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
||||||
forwardButton.setOnClickListener(this);
|
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
|
// Component On-Click Listener
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -559,27 +559,18 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Timeline
|
// ExoPlayer Listener
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private void refreshTimeline() {
|
@Override
|
||||||
playbackManager.load();
|
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||||
|
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
|
||||||
|
|
||||||
final int currentSourceIndex = playbackManager.getCurrentSourceIndex();
|
final int currentSourceIndex = playQueue.getIndex();
|
||||||
|
|
||||||
// Sanity checks
|
|
||||||
if (currentSourceIndex < 0) return;
|
|
||||||
|
|
||||||
// Check if already playing correct window
|
// Check if already playing correct window
|
||||||
final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex;
|
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
|
// Check if recovering
|
||||||
if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
|
if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
|
||||||
// todo: figure out exactly why this is the case
|
// todo: figure out exactly why this is the case
|
||||||
|
@ -591,17 +582,10 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
simpleExoPlayer.seekTo(roundedPos);
|
simpleExoPlayer.seekTo(roundedPos);
|
||||||
isRecovery = false;
|
isRecovery = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
if (playbackManager != null) {
|
||||||
// ExoPlayer Listener
|
playbackManager.load();
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
|
||||||
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
|
|
||||||
|
|
||||||
refreshTimeline();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -709,14 +693,12 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
public void onPositionDiscontinuity() {
|
public void onPositionDiscontinuity() {
|
||||||
// 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.getCurrentWindowIndex();
|
||||||
final int newQueueIndex = playbackManager.getQueueIndexOf(newWindowIndex);
|
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]");
|
||||||
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " +
|
|
||||||
"window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]");
|
|
||||||
|
|
||||||
// If the user selects a new track, then the discontinuity occurs after the index is changed.
|
// 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,
|
// Therefore, the only source that causes a discrepancy would be autoplay,
|
||||||
// which can only offset the current track by +1.
|
// which can only offset the current track by +1.
|
||||||
if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
|
if (newWindowIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -751,12 +733,16 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sync(@Nullable final StreamInfo info) {
|
public void sync(@Nullable final StreamInfo info) {
|
||||||
if (simpleExoPlayer == null) return;
|
if (info == null || simpleExoPlayer == null) return;
|
||||||
if (DEBUG) Log.d(TAG, "Syncing...");
|
if (DEBUG) Log.d(TAG, "Syncing...");
|
||||||
|
|
||||||
refreshTimeline();
|
// Check if on wrong window
|
||||||
|
final int currentSourceIndex = playQueue.getIndex();
|
||||||
if (info == null) return;
|
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;
|
currentInfo = info;
|
||||||
initThumbnail(info.thumbnail_url);
|
initThumbnail(info.thumbnail_url);
|
||||||
|
@ -830,6 +816,13 @@ public abstract class BasePlayer implements Player.EventListener,
|
||||||
playQueue.offsetIndex(+1);
|
playQueue.offsetIndex(+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onRestart() {
|
||||||
|
if (playQueue == null) return;
|
||||||
|
if (DEBUG) Log.d(TAG, "onRestart() called");
|
||||||
|
|
||||||
|
simpleExoPlayer.seekToDefaultPosition();
|
||||||
|
}
|
||||||
|
|
||||||
public void seekBy(int milliSeconds) {
|
public void seekBy(int milliSeconds) {
|
||||||
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
|
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
|
||||||
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))
|
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))
|
||||||
|
|
|
@ -12,6 +12,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
|
import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
|
||||||
import org.schabi.newpipe.playlist.PlayQueue;
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
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.PlayQueueMessage;
|
||||||
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
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
|
// One-side rolling window size for default loading
|
||||||
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
|
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
|
||||||
// todo: inject this parameter, allow user settings perhaps
|
// 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 PlaybackListener playbackListener;
|
||||||
private PlayQueue playQueue;
|
private PlayQueue playQueue;
|
||||||
|
|
||||||
private DynamicConcatenatingMediaSource sources;
|
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<Integer> sourceToQueueIndex;
|
|
||||||
|
|
||||||
private Subscription playQueueReactor;
|
private Subscription playQueueReactor;
|
||||||
private SerialDisposable syncReactor;
|
private SerialDisposable syncReactor;
|
||||||
|
@ -53,7 +51,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
this.syncReactor = new SerialDisposable();
|
this.syncReactor = new SerialDisposable();
|
||||||
|
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
|
|
||||||
|
|
||||||
playQueue.getBroadcastReceiver()
|
playQueue.getBroadcastReceiver()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -72,22 +69,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Exposed Methods
|
// 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.
|
* 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 (playQueueReactor != null) playQueueReactor.cancel();
|
||||||
if (syncReactor != null) syncReactor.dispose();
|
if (syncReactor != null) syncReactor.dispose();
|
||||||
if (sources != null) sources.releaseSource();
|
if (sources != null) sources.releaseSource();
|
||||||
if (sourceToQueueIndex != null) sourceToQueueIndex.clear();
|
|
||||||
|
|
||||||
playQueueReactor = null;
|
playQueueReactor = null;
|
||||||
syncReactor = null;
|
syncReactor = null;
|
||||||
sources = null;
|
sources = null;
|
||||||
sourceToQueueIndex = null;
|
|
||||||
playbackListener = null;
|
playbackListener = null;
|
||||||
playQueue = null;
|
playQueue = null;
|
||||||
}
|
}
|
||||||
|
@ -174,11 +153,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
populateSources();
|
populateSources();
|
||||||
break;
|
break;
|
||||||
case SELECT:
|
case SELECT:
|
||||||
if (isCurrentIndexLoaded()) {
|
sync();
|
||||||
sync();
|
|
||||||
} else {
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case REMOVE:
|
case REMOVE:
|
||||||
final RemoveEvent removeEvent = (RemoveEvent) event;
|
final RemoveEvent removeEvent = (RemoveEvent) event;
|
||||||
|
@ -188,8 +163,11 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
case REORDER:
|
case REORDER:
|
||||||
reset();
|
reset();
|
||||||
break;
|
break;
|
||||||
case ERROR:
|
|
||||||
case MOVE:
|
case MOVE:
|
||||||
|
final MoveEvent moveEvent = (MoveEvent) event;
|
||||||
|
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -214,10 +192,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
|
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCurrentIndexLoaded() {
|
|
||||||
return getCurrentSourceIndex() != -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean tryBlock() {
|
private boolean tryBlock() {
|
||||||
if (!isBlocked) {
|
if (!isBlocked) {
|
||||||
playbackListener.block();
|
playbackListener.block();
|
||||||
|
@ -228,7 +202,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryUnblock() {
|
private boolean tryUnblock() {
|
||||||
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
|
if (isPlayQueueReady() && isBlocked) {
|
||||||
isBlocked = false;
|
isBlocked = false;
|
||||||
playbackListener.unblock(sources);
|
playbackListener.unblock(sources);
|
||||||
return true;
|
return true;
|
||||||
|
@ -270,7 +244,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
|
|
||||||
private void resetSources() {
|
private void resetSources() {
|
||||||
if (this.sources != null) this.sources.releaseSource();
|
if (this.sources != null) this.sources.releaseSource();
|
||||||
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
|
|
||||||
|
|
||||||
this.sources = new DynamicConcatenatingMediaSource();
|
this.sources = new DynamicConcatenatingMediaSource();
|
||||||
}
|
}
|
||||||
|
@ -294,12 +267,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
private void insert(final int queueIndex, final DeferredMediaSource source) {
|
private void insert(final int queueIndex, final DeferredMediaSource source) {
|
||||||
if (queueIndex < 0) return;
|
if (queueIndex < 0) return;
|
||||||
|
|
||||||
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex);
|
sources.addMediaSource(queueIndex, source);
|
||||||
if (pos < 0) {
|
|
||||||
final int sourceIndex = -pos-1;
|
|
||||||
sourceToQueueIndex.add(sourceIndex, queueIndex);
|
|
||||||
sources.addMediaSource(sourceIndex, source);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -310,15 +278,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
||||||
private void remove(final int queueIndex) {
|
private void remove(final int queueIndex) {
|
||||||
if (queueIndex < 0) return;
|
if (queueIndex < 0) return;
|
||||||
|
|
||||||
final int sourceIndex = sourceToQueueIndex.indexOf(queueIndex);
|
sources.removeMediaSource(queueIndex);
|
||||||
if (sourceIndex == -1) return;
|
}
|
||||||
|
|
||||||
sourceToQueueIndex.remove(sourceIndex);
|
private void move(final int source, final int target) {
|
||||||
sources.removeMediaSource(sourceIndex);
|
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
|
sources.moveMediaSource(source, target);
|
||||||
for (int i = sourceIndex; i < sourceToQueueIndex.size(); i++) {
|
|
||||||
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.playlist.events.AppendEvent;
|
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||||
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
||||||
import org.schabi.newpipe.playlist.events.InitEvent;
|
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.PlayQueueMessage;
|
||||||
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||||
import org.schabi.newpipe.playlist.events.ReorderEvent;
|
import org.schabi.newpipe.playlist.events.ReorderEvent;
|
||||||
|
@ -272,6 +273,23 @@ public abstract class PlayQueue implements Serializable {
|
||||||
streams.remove(index);
|
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.
|
* Shuffles the current play queue.
|
||||||
*
|
*
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.view.ViewGroup;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.playlist.events.AppendEvent;
|
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||||
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
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.PlayQueueMessage;
|
||||||
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||||
import org.schabi.newpipe.playlist.events.SelectEvent;
|
import org.schabi.newpipe.playlist.events.SelectEvent;
|
||||||
|
@ -131,6 +132,12 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
notifyItemRangeRemoved(removeEvent.index(), 1);
|
notifyItemRangeRemoved(removeEvent.index(), 1);
|
||||||
notifyItemChanged(removeEvent.index());
|
notifyItemChanged(removeEvent.index());
|
||||||
break;
|
break;
|
||||||
|
case MOVE:
|
||||||
|
final MoveEvent moveEvent = (MoveEvent) message;
|
||||||
|
notifyItemMoved(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||||
|
break;
|
||||||
|
case INIT:
|
||||||
|
case REORDER:
|
||||||
default:
|
default:
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package org.schabi.newpipe.playlist;
|
package org.schabi.newpipe.playlist;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
@ -17,7 +16,9 @@ public class PlayQueueItemBuilder {
|
||||||
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
||||||
|
|
||||||
public interface OnSelectedListener {
|
public interface OnSelectedListener {
|
||||||
void selected(PlayQueueItem item);
|
void selected(PlayQueueItem item, View view);
|
||||||
|
void held(PlayQueueItem item, View view);
|
||||||
|
void onStartDrag(PlayQueueItemHolder viewHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private OnSelectedListener onItemClickListener;
|
private OnSelectedListener onItemClickListener;
|
||||||
|
@ -28,7 +29,7 @@ public class PlayQueueItemBuilder {
|
||||||
this.onItemClickListener = listener;
|
this.onItemClickListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void buildStreamInfoItem(PlayQueueItemHolder holder, final PlayQueueItem item) {
|
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
|
||||||
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
|
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
|
||||||
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
|
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
|
||||||
|
|
||||||
|
@ -44,10 +45,37 @@ public class PlayQueueItemBuilder {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (onItemClickListener != null) {
|
if (onItemClickListener != null) {
|
||||||
onItemClickListener.selected(item);
|
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.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
|
||||||
|
holder.itemHandle.setOnTouchListener(getOnTouchListener(holder));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final DisplayImageOptions IMAGE_OPTIONS =
|
private static final DisplayImageOptions IMAGE_OPTIONS =
|
||||||
|
|
|
@ -32,7 +32,7 @@ import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
|
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
|
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
|
||||||
public final ImageView itemThumbnailView;
|
public final ImageView itemThumbnailView, itemHandle;
|
||||||
|
|
||||||
public final View itemRoot;
|
public final View itemRoot;
|
||||||
|
|
||||||
|
@ -43,5 +43,6 @@ public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
|
||||||
itemDurationView = v.findViewById(R.id.itemDurationView);
|
itemDurationView = v.findViewById(R.id.itemDurationView);
|
||||||
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
|
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
|
||||||
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
|
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemHandle = v.findViewById(R.id.itemHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.schabi.newpipe.playlist.events;
|
||||||
|
|
||||||
|
public class MoveEvent implements PlayQueueMessage {
|
||||||
|
final private int fromIndex;
|
||||||
|
final private int toIndex;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayQueueEvent type() {
|
||||||
|
return PlayQueueEvent.MOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MoveEvent(final int oldIndex, final int newIndex) {
|
||||||
|
this.fromIndex = oldIndex;
|
||||||
|
this.toIndex = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFromIndex() {
|
||||||
|
return fromIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getToIndex() {
|
||||||
|
return toIndex;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,16 @@
|
||||||
android:src="@drawable/dummy_thumbnail"
|
android:src="@drawable/dummy_thumbnail"
|
||||||
tools:ignore="RtlHardcoded"/>
|
tools:ignore="RtlHardcoded"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/itemHandle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="?attr/filter"
|
||||||
|
tools:ignore="ContentDescription,RtlHardcoded"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/itemDurationView"
|
android:id="@+id/itemDurationView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -50,13 +60,15 @@
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:layout_toRightOf="@id/itemThumbnailView"
|
android:layout_toRightOf="@id/itemThumbnailView"
|
||||||
android:layout_toEndOf="@id/itemThumbnailView"
|
android:layout_toEndOf="@id/itemThumbnailView"
|
||||||
|
android:layout_toLeftOf="@id/itemHandle"
|
||||||
|
android:layout_toStartOf="@id/itemHandle"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:lines="1"
|
android:lines="1"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
android:textSize="@dimen/video_item_search_title_text_size"
|
android:textSize="@dimen/video_item_search_title_text_size"
|
||||||
android:textColor="?attr/selector_color"
|
android:textColor="?attr/selector_color"
|
||||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
|
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. "/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/itemAdditionalDetails"
|
android:id="@+id/itemAdditionalDetails"
|
||||||
|
@ -65,9 +77,12 @@
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_toRightOf="@id/itemThumbnailView"
|
android:layout_toRightOf="@id/itemThumbnailView"
|
||||||
android:layout_toEndOf="@id/itemThumbnailView"
|
android:layout_toEndOf="@id/itemThumbnailView"
|
||||||
|
android:layout_toLeftOf="@id/itemHandle"
|
||||||
|
android:layout_toStartOf="@id/itemHandle"
|
||||||
android:lines="1"
|
android:lines="1"
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
||||||
android:textColor="?attr/selector_color"
|
android:textColor="?attr/selector_color"
|
||||||
tools:text="Uploader"/>
|
tools:text="Uploader"/>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
Loading…
Reference in New Issue