rename playlist to player.playqueue

This commit is contained in:
Christian Schabesberger 2018-04-15 20:35:00 +02:00
parent 8ed9d71e14
commit bcfd8a2450
47 changed files with 88 additions and 88 deletions

View file

@ -48,7 +48,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;

View file

@ -69,9 +69,9 @@ import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueAdapter;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueue;
import org.schabi.newpipe.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.SerializedCache;
import java.io.IOException;

View file

@ -62,10 +62,10 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItemBuilder;
import org.schabi.newpipe.playqueue.PlayQueueItemHolder;
import org.schabi.newpipe.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;

View file

@ -7,7 +7,7 @@ import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playqueue.PlayQueue;
import java.io.Serializable;

View file

@ -64,7 +64,7 @@ import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.old.PlayVideoActivity;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;

View file

@ -32,11 +32,11 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueAdapter;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItemBuilder;
import org.schabi.newpipe.playqueue.PlayQueueItemHolder;
import org.schabi.newpipe.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;

View file

@ -68,7 +68,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;

View file

@ -24,9 +24,9 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.playqueue.PlayQueue;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.playqueue.SinglePlayQueue;
import java.text.DecimalFormat;
import java.text.NumberFormat;

View file

@ -7,7 +7,7 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import java.io.IOException;

View file

@ -7,7 +7,7 @@ import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import java.io.IOException;

View file

@ -4,7 +4,7 @@ import android.support.annotation.NonNull;
import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
public interface ManagedMediaSource extends MediaSource {
/**

View file

@ -6,7 +6,7 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import java.io.IOException;

View file

@ -5,7 +5,7 @@ import android.support.v4.media.MediaDescriptionCompat;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
public class BasePlayerMediaSession implements MediaSessionCallback {
private BasePlayer player;

View file

@ -16,12 +16,12 @@ import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.playqueue.PlayQueue;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import org.schabi.newpipe.playqueue.events.MoveEvent;
import org.schabi.newpipe.playqueue.events.PlayQueueEvent;
import org.schabi.newpipe.playqueue.events.RemoveEvent;
import org.schabi.newpipe.playqueue.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
@ -45,7 +45,7 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
import static org.schabi.newpipe.playqueue.PlayQueue.DEBUG;
public class MediaSourceManager {
@NonNull private final String TAG = "MediaSourceManager@" + hashCode();

View file

@ -6,7 +6,7 @@ import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playqueue.PlayQueueItem;
import java.util.List;

View file

@ -0,0 +1,133 @@
package org.schabi.newpipe.playqueue;
import android.util.Log;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.SingleObserver;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> extends PlayQueue {
boolean isInitial;
boolean isComplete;
int serviceId;
String baseUrl;
String nextUrl;
transient Disposable fetchReactor;
AbstractInfoPlayQueue(final U item) {
this(item.getServiceId(), item.getUrl(), null, Collections.<StreamInfoItem>emptyList(), 0);
}
AbstractInfoPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<StreamInfoItem> streams,
final int index) {
super(index, extractListItems(streams));
this.baseUrl = url;
this.nextUrl = nextPageUrl;
this.serviceId = serviceId;
this.isInitial = streams.isEmpty();
this.isComplete = !isInitial && (nextPageUrl == null || nextPageUrl.isEmpty());
}
abstract protected String getTag();
@Override
public boolean isComplete() {
return isComplete;
}
SingleObserver<T> getHeadListObserver() {
return new SingleObserver<T>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || !isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) {
d.dispose();
} else {
fetchReactor = d;
}
}
@Override
public void onSuccess(@NonNull T result) {
isInitial = false;
if (!result.hasNextPage()) isComplete = true;
nextUrl = result.getNextPageUrl();
append(extractListItems(result.getRelatedItems()));
fetchReactor.dispose();
fetchReactor = null;
}
@Override
public void onError(@NonNull Throwable e) {
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true;
append(); // Notify change
}
};
}
SingleObserver<ListExtractor.InfoItemsPage> getNextPageObserver() {
return new SingleObserver<ListExtractor.InfoItemsPage>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) {
d.dispose();
} else {
fetchReactor = d;
}
}
@Override
public void onSuccess(@NonNull ListExtractor.InfoItemsPage result) {
if (!result.hasNextPage()) isComplete = true;
nextUrl = result.getNextPageUrl();
append(extractListItems(result.getItems()));
fetchReactor.dispose();
fetchReactor = null;
}
@Override
public void onError(@NonNull Throwable e) {
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true;
append(); // Notify change
}
};
}
@Override
public void dispose() {
super.dispose();
if (fetchReactor != null) fetchReactor.dispose();
fetchReactor = null;
}
private static List<PlayQueueItem> extractListItems(final List<StreamInfoItem> infos) {
List<PlayQueueItem> result = new ArrayList<>();
for (final InfoItem stream : infos) {
if (stream instanceof StreamInfoItem) {
result.add(new PlayQueueItem((StreamInfoItem) stream));
}
}
return result;
}
}

View file

@ -0,0 +1,50 @@
package org.schabi.newpipe.playqueue;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, ChannelInfoItem> {
public ChannelPlayQueue(final ChannelInfoItem item) {
super(item);
}
public ChannelPlayQueue(final ChannelInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
}
public ChannelPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}
@Override
protected String getTag() {
return "ChannelPlayQueue@" + Integer.toHexString(hashCode());
}
@Override
public void fetch() {
if (this.isInitial) {
ExtractorHelper.getChannelInfo(this.serviceId, this.baseUrl, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());
}
}
}

View file

@ -0,0 +1,431 @@
package org.schabi.newpipe.playqueue;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.playqueue.events.AppendEvent;
import org.schabi.newpipe.playqueue.events.ErrorEvent;
import org.schabi.newpipe.playqueue.events.InitEvent;
import org.schabi.newpipe.playqueue.events.MoveEvent;
import org.schabi.newpipe.playqueue.events.PlayQueueEvent;
import org.schabi.newpipe.playqueue.events.RecoveryEvent;
import org.schabi.newpipe.playqueue.events.RemoveEvent;
import org.schabi.newpipe.playqueue.events.ReorderEvent;
import org.schabi.newpipe.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.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.subjects.BehaviorSubject;
/**
* PlayQueue is responsible for keeping track of a list of streams and the index of
* the stream that should be currently playing.
*
* 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.
*
* This class can be serialized for passing intents, but in order to start the
* message bus, it must be initialized.
* */
public abstract class PlayQueue implements Serializable {
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode());
public static final boolean DEBUG = true;
private ArrayList<PlayQueueItem> backup;
private ArrayList<PlayQueueItem> streams;
@NonNull private final AtomicInteger queueIndex;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient Subscription reportingReactor;
PlayQueue(final int index, final List<PlayQueueItem> startWith) {
streams = new ArrayList<>();
streams.addAll(startWith);
queueIndex = new AtomicInteger(index);
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist actions
//////////////////////////////////////////////////////////////////////////*/
/**
* Initializes the play queue message buses.
*
* Also starts a self reporter for logging if debug mode is enabled.
* */
public void init() {
eventBroadcast = BehaviorSubject.create();
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(AndroidSchedulers.mainThread())
.startWith(new InitEvent());
if (DEBUG) broadcastReceiver.subscribe(getSelfReporter());
}
/**
* Dispose the play queue by stopping all message buses.
* */
public void dispose() {
if (eventBroadcast != null) eventBroadcast.onComplete();
if (reportingReactor != null) reportingReactor.cancel();
eventBroadcast = null;
broadcastReceiver = null;
reportingReactor = null;
}
/**
* Checks if the queue is complete.
*
* A queue is complete if it has loaded all items in an external playlist
* single stream or local queues are always complete.
* */
public abstract boolean isComplete();
/**
* Load partial queue in the background, does nothing if the queue is complete.
* */
public abstract void fetch();
/*//////////////////////////////////////////////////////////////////////////
// Readonly ops
//////////////////////////////////////////////////////////////////////////*/
/**
* Returns the current index that should be played.
* */
public int getIndex() {
return queueIndex.get();
}
/**
* Returns the current item that should be played.
* */
public PlayQueueItem getItem() {
return getItem(getIndex());
}
/**
* Returns the item at the given index.
* May throw {@link IndexOutOfBoundsException}.
* */
public PlayQueueItem getItem(int index) {
if (index < 0 || index >= streams.size() || streams.get(index) == null) 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.
* */
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);
}
/**
* Returns the current size of play queue.
* */
public int size() {
return streams.size();
}
/**
* Checks if the play queue is empty.
* */
public boolean isEmpty() {
return streams.isEmpty();
}
/**
* Determines if the current play queue is shuffled.
* */
public boolean isShuffled() {
return backup != null;
}
/**
* Returns an immutable view of the play queue.
* */
@NonNull
public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams);
}
/**
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
* */
@Nullable
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver;
}
/*//////////////////////////////////////////////////////////////////////////
// Write ops
//////////////////////////////////////////////////////////////////////////*/
/**
* Changes the current playing index to a new index.
*
* This method is guarded using in a circular manner for index exceeding the play queue size.
*
* Will emit a {@link SelectEvent} if the index is not the current playing index.
* */
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;
queueIndex.set(newIndex);
broadcast(new SelectEvent(oldIndex, newIndex));
}
/**
* Changes the current playing index by an offset amount.
*
* Will emit a {@link SelectEvent} if offset is non-zero.
* */
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)
* */
public synchronized void append(@NonNull final PlayQueueItem... items) {
append(Arrays.asList(items));
}
/**
* Appends the given {@link PlayQueueItem}s to the current play queue.
*
* 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.
*
* Will emit a {@link AppendEvent} on any given context.
* */
public synchronized void append(@NonNull final List<PlayQueueItem> items) {
List<PlayQueueItem> itemList = new ArrayList<>(items);
if (isShuffled()) {
backup.addAll(itemList);
Collections.shuffle(itemList);
}
streams.addAll(itemList);
broadcast(new AppendEvent(itemList.size()));
}
/**
* Removes the item at the given index from the play queue.
*
* 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.
*
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
* */
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 the course of action:
* if the error can be skipped or the current item should be removed.
*
* This is done as a separate event as the underlying manager may have
* different implementation regarding exceptions.
* */
public synchronized void error(final boolean skippable) {
final int index = getIndex();
if (skippable) {
queueIndex.incrementAndGet();
} else {
removeInternal(index);
}
broadcast(new ErrorEvent(index, getIndex(), skippable));
}
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) {
final int backupIndex = backup.indexOf(getItem(removeIndex));
backup.remove(backupIndex);
}
streams.remove(removeIndex);
}
/**
* Moves a queue item at the source index to the target index.
*
* 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>.
* */
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));
}
/**
* Sets the recovery record of the item at the index.
*
* Broadcasts a recovery event.
* */
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.
*
* Broadcasts a recovery event.
* */
public synchronized void unsetRecovery(final int index) {
setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
}
/**
* Shuffles the current play queue.
*
* 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.
*
* Will emit a {@link ReorderEvent} in any context.
* */
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);
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/**
* Unshuffles the current play queue if a backup play queue exists.
*
* This method undoes shuffling and index will be set to the previously playing item if found,
* otherwise, the index will reset to 0.
*
* Will emit a {@link ReorderEvent} if a backup exists.
* */
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);
}
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/*//////////////////////////////////////////////////////////////////////////
// Rx Broadcast
//////////////////////////////////////////////////////////////////////////*/
private void broadcast(@NonNull final PlayQueueEvent event) {
if (eventBroadcast != null) {
eventBroadcast.onNext(event);
}
}
private Subscriber<PlayQueueEvent> getSelfReporter() {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(Subscription s) {
if (reportingReactor != null) reportingReactor.cancel();
reportingReactor = s;
reportingReactor.request(1);
}
@Override
public void onNext(PlayQueueEvent event) {
Log.d(TAG, "Received broadcast: " + event.type().name() + ". Current index: " + getIndex() + ", play queue length: " + size() + ".");
reportingReactor.request(1);
}
@Override
public void onError(Throwable t) {
Log.e(TAG, "Received broadcast error", t);
}
@Override
public void onComplete() {
Log.d(TAG, "Broadcast is shutting down.");
}
};
}
}

View file

@ -0,0 +1,211 @@
package org.schabi.newpipe.playqueue;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.playqueue.events.AppendEvent;
import org.schabi.newpipe.playqueue.events.ErrorEvent;
import org.schabi.newpipe.playqueue.events.MoveEvent;
import org.schabi.newpipe.playqueue.events.PlayQueueEvent;
import org.schabi.newpipe.playqueue.events.RemoveEvent;
import org.schabi.newpipe.playqueue.events.SelectEvent;
import java.util.List;
import io.reactivex.Observer;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
/**
* Created by Christian Schabesberger on 01.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoListAdapter.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = PlayQueueAdapter.class.toString();
private static final int ITEM_VIEW_TYPE_ID = 0;
private static final int FOOTER_VIEW_TYPE_ID = 1;
private final PlayQueueItemBuilder playQueueItemBuilder;
private final PlayQueue playQueue;
private boolean showFooter = false;
private View footer = null;
private Disposable playQueueReactor;
public class HFHolder extends RecyclerView.ViewHolder {
public HFHolder(View v) {
super(v);
view = v;
}
public View view;
}
public PlayQueueAdapter(final Context context, final PlayQueue playQueue) {
if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalStateException("Play Queue has not been initialized.");
}
this.playQueueItemBuilder = new PlayQueueItemBuilder(context);
this.playQueue = playQueue;
playQueue.getBroadcastReceiver().toObservable().subscribe(getReactor());
}
private Observer<PlayQueueEvent> getReactor() {
return new Observer<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (playQueueReactor != null) playQueueReactor.dispose();
playQueueReactor = d;
}
@Override
public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage);
}
@Override
public void onError(@NonNull Throwable e) {}
@Override
public void onComplete() {
dispose();
}
};
}
private void onPlayQueueChanged(final PlayQueueEvent message) {
switch (message.type()) {
case RECOVERY:
// Do nothing.
break;
case SELECT:
final SelectEvent selectEvent = (SelectEvent) message;
notifyItemChanged(selectEvent.getOldIndex());
notifyItemChanged(selectEvent.getNewIndex());
break;
case APPEND:
final AppendEvent appendEvent = (AppendEvent) message;
notifyItemRangeInserted(playQueue.size(), appendEvent.getAmount());
break;
case ERROR:
final ErrorEvent errorEvent = (ErrorEvent) message;
if (!errorEvent.isSkippable()) {
notifyItemRemoved(errorEvent.getErrorIndex());
}
notifyItemChanged(errorEvent.getErrorIndex());
notifyItemChanged(errorEvent.getQueueIndex());
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) message;
notifyItemRemoved(removeEvent.getRemoveIndex());
notifyItemChanged(removeEvent.getQueueIndex());
break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) message;
notifyItemMoved(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case INIT:
case REORDER:
default:
notifyDataSetChanged();
break;
}
}
public void dispose() {
if (playQueueReactor != null) playQueueReactor.dispose();
playQueueReactor = null;
}
public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) {
playQueueItemBuilder.setOnSelectedListener(listener);
}
public void unsetSelectedListener() {
playQueueItemBuilder.setOnSelectedListener(null);
}
public void setFooter(View footer) {
this.footer = footer;
notifyItemChanged(playQueue.size());
}
public void showFooter(final boolean show) {
showFooter = show;
notifyItemChanged(playQueue.size());
}
public List<PlayQueueItem> getItems() {
return playQueue.getStreams();
}
@Override
public int getItemCount() {
int count = playQueue.getStreams().size();
if(footer != null && showFooter) count++;
return count;
}
@Override
public int getItemViewType(int position) {
if(footer != null && position == playQueue.getStreams().size() && showFooter) {
return FOOTER_VIEW_TYPE_ID;
}
return ITEM_VIEW_TYPE_ID;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
switch(type) {
case FOOTER_VIEW_TYPE_ID:
return new HFHolder(footer);
case ITEM_VIEW_TYPE_ID:
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.play_queue_item, parent, false));
default:
Log.e(TAG, "Attempting to create view holder with undefined type: " + type);
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof PlayQueueItemHolder) {
final PlayQueueItemHolder itemHolder = (PlayQueueItemHolder) holder;
// Build the list item
playQueueItemBuilder.buildStreamInfoItem(itemHolder, playQueue.getStreams().get(position));
// Check if the current item should be selected/highlighted
final boolean isSelected = playQueue.getIndex() == position;
itemHolder.itemSelected.setVisibility(isSelected ? View.VISIBLE : View.INVISIBLE);
itemHolder.itemView.setSelected(isSelected);
} else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}
}

View file

@ -0,0 +1,115 @@
package org.schabi.newpipe.playqueue;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
public class PlayQueueItem implements Serializable {
public final static long RECOVERY_UNSET = Long.MIN_VALUE;
private final static String EMPTY_STRING = "";
@NonNull final private String title;
@NonNull final private String url;
final private int serviceId;
final private long duration;
@NonNull final private String thumbnailUrl;
@NonNull final private String uploader;
@NonNull final private StreamType streamType;
private long recoveryPosition;
private Throwable error;
PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
if (info.getStartPosition() > 0)
setRecoveryPosition(info.getStartPosition() * 1000);
}
PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
}
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
final int serviceId, final long duration,
@Nullable final String thumbnailUrl, @Nullable final String uploader,
@NonNull final StreamType streamType) {
this.title = name != null ? name : EMPTY_STRING;
this.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId;
this.duration = duration;
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET;
}
@NonNull
public String getTitle() {
return title;
}
@NonNull
public String getUrl() {
return url;
}
public int getServiceId() {
return serviceId;
}
public long getDuration() {
return duration;
}
@NonNull
public String getThumbnailUrl() {
return thumbnailUrl;
}
@NonNull
public String getUploader() {
return uploader;
}
@NonNull
public StreamType getStreamType() {
return streamType;
}
public long getRecoveryPosition() {
return recoveryPosition;
}
@Nullable
public Throwable getError() {
return error;
}
@NonNull
public Single<StreamInfo> getStream() {
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
.subscribeOn(Schedulers.io())
.doOnError(throwable -> error = throwable);
}
////////////////////////////////////////////////////////////////////////////
// Item States, keep external access out
////////////////////////////////////////////////////////////////////////////
/*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
this.recoveryPosition = recoveryPosition;
}
}

View file

@ -0,0 +1,76 @@
package org.schabi.newpipe.playqueue;
import android.content.Context;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
public interface OnSelectedListener {
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
void onStartDrag(PlayQueueItemHolder viewHolder);
}
private OnSelectedListener onItemClickListener;
public PlayQueueItemBuilder(final Context context) {}
public void setOnSelectedListener(OnSelectedListener listener) {
this.onItemClickListener = listener;
}
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(),
NewPipe.getNameOfService(item.getServiceId())));
if (item.getDuration() > 0) {
holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
} else {
holder.itemDurationView.setVisibility(View.GONE);
}
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) {
onItemClickListener.selected(item, view);
}
});
holder.itemRoot.setOnLongClickListener(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 (view, motionEvent) -> {
view.performClick();
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN
&& onItemClickListener != null) {
onItemClickListener.onStartDrag(holder);
}
return false;
};
}
}

View file

@ -0,0 +1,49 @@
package org.schabi.newpipe.playqueue;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
/**
* Created by Christian Schabesberger on 01.08.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
public final ImageView itemSelected, itemThumbnailView, itemHandle;
public final View itemRoot;
public PlayQueueItemHolder(View v) {
super(v);
itemRoot = v.findViewById(R.id.itemRoot);
itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView);
itemDurationView = v.findViewById(R.id.itemDurationView);
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
itemSelected = v.findViewById(R.id.itemSelected);
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
itemHandle = v.findViewById(R.id.itemHandle);
}
}

View file

@ -0,0 +1,52 @@
package org.schabi.newpipe.playqueue;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback {
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
public PlayQueueItemTouchCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
public abstract void onMove(final int sourceIndex, final int targetIndex);
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
int viewSizeOutOfBounds, int totalSize,
long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
onMove(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) {}
}

View file

@ -0,0 +1,50 @@
package org.schabi.newpipe.playqueue;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo, PlaylistInfoItem> {
public PlaylistPlayQueue(final PlaylistInfoItem item) {
super(item);
}
public PlaylistPlayQueue(final PlaylistInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
}
public PlaylistPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}
@Override
protected String getTag() {
return "PlaylistPlayQueue@" + Integer.toHexString(hashCode());
}
@Override
public void fetch() {
if (this.isInitial) {
ExtractorHelper.getPlaylistInfo(this.serviceId, this.baseUrl, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());
}
}
}

View file

@ -0,0 +1,38 @@
package org.schabi.newpipe.playqueue;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfoItem item) {
super(0, Collections.singletonList(new PlayQueueItem(item)));
}
public SinglePlayQueue(final StreamInfo info) {
super(0, Collections.singletonList(new PlayQueueItem(info)));
}
public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
super(index, playQueueItemsOf(items));
}
private static List<PlayQueueItem> playQueueItemsOf(List<StreamInfoItem> items) {
List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
playQueueItems.add(new PlayQueueItem(item));
}
return playQueueItems;
}
@Override
public boolean isComplete() {
return true;
}
@Override
public void fetch() {}
}

View file

@ -0,0 +1,299 @@
package org.schabi.newpipe.playqueue;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
public abstract class StatisticsPlaylistFragment
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
private View headerPlayAllButton;
private View headerPopupButton;
private View headerBackgroundButton;
@State
protected Parcelable itemsListState;
/* Used for independent events */
private Subscription databaseSubscription;
private HistoryRecordManager recordManager;
///////////////////////////////////////////////////////////////////////////
// Abstracts
///////////////////////////////////////////////////////////////////////////
protected abstract String getName();
protected abstract List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results);
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Creation
///////////////////////////////////////////////////////////////////////////
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
recordManager = new HistoryRecordManager(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
setTitle(getName());
}
@Override
protected View getListHeader() {
final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control,
itemsList, false);
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
return headerRootLayout;
}
@Override
protected void initListeners() {
super.initListeners();
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
@Override
public void selected(LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem;
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
item.serviceId, item.url, item.title);
}
}
@Override
public void held(LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
showStreamDialog((StreamStatisticsEntry) selectedItem);
}
}
});
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Loading
///////////////////////////////////////////////////////////////////////////
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
recordManager.getStreamStatistics()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHistoryObserver());
}
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Destruction
///////////////////////////////////////////////////////////////////////////
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (itemListAdapter != null) itemListAdapter.unsetSelectedListener();
if (headerBackgroundButton != null) headerBackgroundButton.setOnClickListener(null);
if (headerPlayAllButton != null) headerPlayAllButton.setOnClickListener(null);
if (headerPopupButton != null) headerPopupButton.setOnClickListener(null);
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = null;
}
@Override
public void onDestroy() {
super.onDestroy();
recordManager = null;
itemsListState = null;
}
///////////////////////////////////////////////////////////////////////////
// Statistics Loader
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<StreamStatisticsEntry>> getHistoryObserver() {
return new Subscriber<List<StreamStatisticsEntry>>() {
@Override
public void onSubscribe(Subscription s) {
showLoading();
if (databaseSubscription != null) databaseSubscription.cancel();
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(List<StreamStatisticsEntry> streams) {
handleResult(streams);
if (databaseSubscription != null) databaseSubscription.request(1);
}
@Override
public void onError(Throwable exception) {
StatisticsPlaylistFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<StreamStatisticsEntry> result) {
super.handleResult(result);
if (itemListAdapter == null) return;
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
itemListAdapter.addItems(processResult(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
headerPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
hideLoading();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected void resetFragment() {
super.resetFragment();
if (databaseSubscription != null) databaseSubscription.cancel();
}
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
"none", "History Statistics", R.string.general_error);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void showStreamDialog(final StreamStatisticsEntry item) {
final Context context = getContext();
final Activity activity = getActivity();
if (context == null || context.getResources() == null || getActivity() == null) return;
final StreamInfoItem infoItem = item.toStreamInfoItem();
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup),
};
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
switch (i) {
case 0:
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem));
break;
case 1:
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem));
break;
case 2:
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
break;
case 3:
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
break;
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
default:
break;
}
};
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
}
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
private PlayQueue getPlayQueue(final int index) {
if (itemListAdapter == null) {
return new SinglePlayQueue(Collections.emptyList(), 0);
}
final List<LocalItem> infoItems = itemListAdapter.getItemsList();
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
for (final LocalItem item : infoItems) {
if (item instanceof StreamStatisticsEntry) {
streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem());
}
}
return new SinglePlayQueue(streamInfoItems, index);
}
}

View file

@ -0,0 +1,19 @@
package org.schabi.newpipe.playqueue.events;
public class AppendEvent implements PlayQueueEvent {
final private int amount;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.APPEND;
}
public AppendEvent(final int amount) {
this.amount = amount;
}
public int getAmount() {
return amount;
}
}

View file

@ -0,0 +1,31 @@
package org.schabi.newpipe.playqueue.events;
public class ErrorEvent implements PlayQueueEvent {
final private int errorIndex;
final private int queueIndex;
final private boolean skippable;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.ERROR;
}
public ErrorEvent(final int errorIndex, final int queueIndex, final boolean skippable) {
this.errorIndex = errorIndex;
this.queueIndex = queueIndex;
this.skippable = skippable;
}
public int getErrorIndex() {
return errorIndex;
}
public int getQueueIndex() {
return queueIndex;
}
public boolean isSkippable() {
return skippable;
}
}

View file

@ -0,0 +1,8 @@
package org.schabi.newpipe.playqueue.events;
public class InitEvent implements PlayQueueEvent {
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.INIT;
}
}

View file

@ -0,0 +1,24 @@
package org.schabi.newpipe.playqueue.events;
public class MoveEvent implements PlayQueueEvent {
final private int fromIndex;
final private int toIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.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;
}
}

View file

@ -0,0 +1,7 @@
package org.schabi.newpipe.playqueue.events;
import java.io.Serializable;
public interface PlayQueueEvent extends Serializable {
PlayQueueEventType type();
}

View file

@ -0,0 +1,27 @@
package org.schabi.newpipe.playqueue.events;
public enum PlayQueueEventType {
INIT,
// sent when the index is changed
SELECT,
// sent when more streams are added to the play queue
APPEND,
// sent when a pending stream is removed from the play queue
REMOVE,
// sent when two streams swap place in the play queue
MOVE,
// sent when queue is shuffled
REORDER,
// sent when recovery record is set on a stream
RECOVERY,
// sent when the item at index has caused an exception
ERROR
}

View file

@ -0,0 +1,25 @@
package org.schabi.newpipe.playqueue.events;
public class RecoveryEvent implements PlayQueueEvent {
final private int index;
final private long position;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.RECOVERY;
}
public RecoveryEvent(final int index, final long position) {
this.index = index;
this.position = position;
}
public int getIndex() {
return index;
}
public long getPosition() {
return position;
}
}

View file

@ -0,0 +1,25 @@
package org.schabi.newpipe.playqueue.events;
public class RemoveEvent implements PlayQueueEvent {
final private int removeIndex;
final private int queueIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REMOVE;
}
public RemoveEvent(final int removeIndex, final int queueIndex) {
this.removeIndex = removeIndex;
this.queueIndex = queueIndex;
}
public int getQueueIndex() {
return queueIndex;
}
public int getRemoveIndex() {
return removeIndex;
}
}

View file

@ -0,0 +1,24 @@
package org.schabi.newpipe.playqueue.events;
public class ReorderEvent implements PlayQueueEvent {
private final int fromSelectedIndex;
private final int toSelectedIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REORDER;
}
public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) {
this.fromSelectedIndex = fromSelectedIndex;
this.toSelectedIndex = toSelectedIndex;
}
public int getFromSelectedIndex() {
return fromSelectedIndex;
}
public int getToSelectedIndex() {
return toSelectedIndex;
}
}

View file

@ -0,0 +1,25 @@
package org.schabi.newpipe.playqueue.events;
public class SelectEvent implements PlayQueueEvent {
final private int oldIndex;
final private int newIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.SELECT;
}
public SelectEvent(final int oldIndex, final int newIndex) {
this.oldIndex = oldIndex;
this.newIndex = newIndex;
}
public int getOldIndex() {
return oldIndex;
}
public int getNewIndex() {
return newIndex;
}
}