1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-06-18 11:20:00 +00:00
NewPipe/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
TiA4f8R 79e98db3bd
Apply the requested changes and little improvements
Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality.

Apply the requested changes and do little improvements
Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality.
2021-01-16 13:23:42 +01:00

535 lines
16 KiB
Java

package org.schabi.newpipe.player.playqueue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
import org.schabi.newpipe.player.playqueue.events.InitEvent;
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent;
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
import org.schabi.newpipe.player.playqueue.events.SelectEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
/**
* PlayQueue is responsible for keeping track of a list of streams and the index of
* the stream that should be currently playing.
* <p>
* This class contains basic manipulation of a playlist while also functions as a
* message bus, providing all listeners with new updates to the play queue.
* </p>
* <p>
* This class can be serialized for passing intents, but in order to start the
* message bus, it must be initialized.
* </p>
*/
public abstract class PlayQueue implements Serializable {
public static final boolean DEBUG = MainActivity.DEBUG;
private ArrayList<PlayQueueItem> backup;
private ArrayList<PlayQueueItem> streams;
@NonNull
private final AtomicInteger queueIndex;
private final ArrayList<PlayQueueItem> history;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient boolean disposed;
PlayQueue(final int index, final List<PlayQueueItem> startWith) {
streams = new ArrayList<>();
streams.addAll(startWith);
history = new ArrayList<>();
if (streams.size() > index) {
history.add(streams.get(index));
}
queueIndex = new AtomicInteger(index);
disposed = false;
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist actions
//////////////////////////////////////////////////////////////////////////*/
/**
* Initializes the play queue message buses.
* <p>
* Also starts a self reporter for logging if debug mode is enabled.
* </p>
*/
public void init() {
eventBroadcast = BehaviorSubject.create();
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(AndroidSchedulers.mainThread())
.startWithItem(new InitEvent());
}
/**
* Dispose the play queue by stopping all message buses.
*/
public void dispose() {
if (eventBroadcast != null) {
eventBroadcast.onComplete();
}
eventBroadcast = null;
broadcastReceiver = null;
disposed = true;
}
/**
* Checks if the queue is complete.
* <p>
* A queue is complete if it has loaded all items in an external playlist
* single stream or local queues are always complete.
* </p>
*
* @return whether the queue is complete
*/
public abstract boolean isComplete();
/**
* Load partial queue in the background, does nothing if the queue is complete.
*/
public abstract void fetch();
/*//////////////////////////////////////////////////////////////////////////
// Readonly ops
//////////////////////////////////////////////////////////////////////////*/
/**
* @return the current index that should be played
*/
public int getIndex() {
return queueIndex.get();
}
/**
* Changes the current playing index to a new index.
* <p>
* This method is guarded using in a circular manner for index exceeding the play queue size.
* </p>
* <p>
* Will emit a {@link SelectEvent} if the index is not the current playing index.
* </p>
*
* @param index the index to be set
*/
public synchronized void setIndex(final int index) {
final int oldIndex = getIndex();
int newIndex = index;
if (index < 0) {
newIndex = 0;
}
if (index >= streams.size()) {
newIndex = isComplete() ? index % streams.size() : streams.size() - 1;
}
if (oldIndex != newIndex) {
history.add(streams.get(newIndex));
}
queueIndex.set(newIndex);
broadcast(new SelectEvent(oldIndex, newIndex));
}
/**
* @return the current item that should be played, or null if the queue is empty
*/
@Nullable
public PlayQueueItem getItem() {
return getItem(getIndex());
}
/**
* @param index the index of the item to return
* @return the item at the given index, or null if the index is out of bounds
*/
@Nullable
public PlayQueueItem getItem(final int index) {
if (index < 0 || index >= streams.size()) {
return null;
}
return streams.get(index);
}
/**
* Returns the index of the given item using referential equality.
* May be null despite play queue contains identical item.
*
* @param item the item to find the index of
* @return the index of the given item
*/
public int indexOf(@NonNull final PlayQueueItem item) {
// referential equality, can't think of a better way to do this
// todo: better than this
return streams.indexOf(item);
}
/**
* @return the current size of play queue.
*/
public int size() {
return streams.size();
}
/**
* Checks if the play queue is empty.
*
* @return whether the play queue is empty
*/
public boolean isEmpty() {
return streams.isEmpty();
}
/**
* Determines if the current play queue is shuffled.
*
* @return whether the play queue is shuffled
*/
public boolean isShuffled() {
return backup != null;
}
/**
* @return an immutable view of the play queue
*/
@NonNull
public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams);
}
/*//////////////////////////////////////////////////////////////////////////
// Write ops
//////////////////////////////////////////////////////////////////////////*/
/**
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
*
* @return the play queue's update broadcast
*/
@Nullable
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver;
}
/**
* Changes the current playing index by an offset amount.
* <p>
* Will emit a {@link SelectEvent} if offset is non-zero.
* </p>
*
* @param offset the offset relative to the current index
*/
public synchronized void offsetIndex(final int offset) {
setIndex(getIndex() + offset);
}
/**
* Appends the given {@link PlayQueueItem}s to the current play queue.
*
* @see #append(List items)
* @param items {@link PlayQueueItem}s to append
*/
public synchronized void append(@NonNull final PlayQueueItem... items) {
append(Arrays.asList(items));
}
/**
* Appends the given {@link PlayQueueItem}s to the current play queue.
* <p>
* If the play queue is shuffled, then append the items to the backup queue as is and
* append the shuffle items to the play queue.
* </p>
* <p>
* Will emit a {@link AppendEvent} on any given context.
* </p>
*
* @param items {@link PlayQueueItem}s to append
*/
public synchronized void append(@NonNull final List<PlayQueueItem> items) {
final List<PlayQueueItem> itemList = new ArrayList<>(items);
if (isShuffled()) {
backup.addAll(itemList);
Collections.shuffle(itemList);
}
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued()
&& !itemList.get(0).isAutoQueued()) {
streams.remove(streams.size() - 1);
}
streams.addAll(itemList);
broadcast(new AppendEvent(itemList.size()));
}
/**
* Removes the item at the given index from the play queue.
* <p>
* The current playing index will decrement if it is greater than the index being removed.
* On cases where the current playing index exceeds the playlist range, it is set to 0.
* </p>
* <p>
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
* </p>
*
* @param index the index of the item to remove
*/
public synchronized void remove(final int index) {
if (index >= streams.size() || index < 0) {
return;
}
removeInternal(index);
broadcast(new RemoveEvent(index, getIndex()));
}
/**
* Report an exception for the item at the current index in order and skip to the next one
* <p>
* This is done as a separate event as the underlying manager may have
* different implementation regarding exceptions.
* </p>
*/
public synchronized void error() {
final int oldIndex = getIndex();
queueIndex.incrementAndGet();
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
broadcast(new ErrorEvent(oldIndex, getIndex()));
}
private synchronized void removeInternal(final int removeIndex) {
final int currentIndex = queueIndex.get();
final int size = size();
if (currentIndex > removeIndex) {
queueIndex.decrementAndGet();
} else if (currentIndex >= size) {
queueIndex.set(currentIndex % (size - 1));
} else if (currentIndex == removeIndex && currentIndex == size - 1) {
queueIndex.set(0);
}
if (backup != null) {
backup.remove(getItem(removeIndex));
}
history.remove(streams.remove(removeIndex));
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
}
/**
* Moves a queue item at the source index to the target index.
* <p>
* If the item being moved is the currently playing, then the current playing index is set
* to that of the target.
* If the moved item is not the currently playing and moves to an index <b>AFTER</b> the
* current playing index, then the current playing index is decremented.
* Vice versa if the an item after the currently playing is moved <b>BEFORE</b>.
* </p>
*
* @param source the original index of the item
* @param target the new index of the item
*/
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();
}
final PlayQueueItem playQueueItem = streams.remove(source);
playQueueItem.setAutoQueued(false);
streams.add(target, playQueueItem);
broadcast(new MoveEvent(source, target));
}
/**
* Sets the recovery record of the item at the index.
* <p>
* Broadcasts a recovery event.
* </p>
*
* @param index index of the item
* @param position the recovery position
*/
public synchronized void setRecovery(final int index, final long position) {
if (index < 0 || index >= streams.size()) {
return;
}
streams.get(index).setRecoveryPosition(position);
broadcast(new RecoveryEvent(index, position));
}
/**
* Revoke the recovery record of the item at the index.
* <p>
* Broadcasts a recovery event.
* </p>
*
* @param index index of the item
*/
public synchronized void unsetRecovery(final int index) {
setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
}
/**
* Shuffles the current play queue.
* <p>
* This method first backs up the existing play queue and item being played.
* Then a newly shuffled play queue will be generated along with currently
* playing item placed at the beginning of the queue.
* </p>
* <p>
* Will emit a {@link ReorderEvent} in any context.
* </p>
*/
public synchronized void shuffle() {
if (backup == null) {
backup = new ArrayList<>(streams);
}
final int originIndex = getIndex();
final PlayQueueItem current = getItem();
Collections.shuffle(streams);
final int newIndex = streams.indexOf(current);
if (newIndex != -1) {
streams.add(0, streams.remove(newIndex));
}
queueIndex.set(0);
if (streams.size() > 0) {
history.add(streams.get(0));
}
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/**
* Unshuffles the current play queue if a backup play queue exists.
* <p>
* This method undoes shuffling and index will be set to the previously playing item if found,
* otherwise, the index will reset to 0.
* </p>
* <p>
* Will emit a {@link ReorderEvent} if a backup exists.
* </p>
*/
public synchronized void unshuffle() {
if (backup == null) {
return;
}
final int originIndex = getIndex();
final PlayQueueItem current = getItem();
streams.clear();
streams = backup;
backup = null;
final int newIndex = streams.indexOf(current);
if (newIndex != -1) {
queueIndex.set(newIndex);
} else {
queueIndex.set(0);
}
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
}
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/**
* Selects previous played item.
*
* This method removes currently playing item from history and
* starts playing the last item from history if it exists
*
* @return true if history is not empty and the item can be played
* */
public synchronized boolean previous() {
if (history.size() <= 1) {
return false;
}
history.remove(history.size() - 1);
final PlayQueueItem last = history.remove(history.size() - 1);
setIndex(indexOf(last));
return true;
}
/*
* Compares two PlayQueues. Useful when a user switches players but queue is the same so
* we don't have to do anything with new queue.
* This method also gives a chance to track history of items in a queue in
* VideoDetailFragment without duplicating items from two identical queues
* */
@Override
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof PlayQueue)
|| getStreams().size() != ((PlayQueue) obj).getStreams().size()) {
return false;
}
final PlayQueue other = (PlayQueue) obj;
for (int i = 0; i < getStreams().size(); i++) {
if (!getItem(i).getUrl().equals(other.getItem(i).getUrl())) {
return false;
}
}
return true;
}
public boolean isDisposed() {
return disposed;
}
/*//////////////////////////////////////////////////////////////////////////
// Rx Broadcast
//////////////////////////////////////////////////////////////////////////*/
private void broadcast(@NonNull final PlayQueueEvent event) {
if (eventBroadcast != null) {
eventBroadcast.onNext(event);
}
}
}