mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	-Fixed deferred media source from releasing reused resources.
-Fixed external play queue to load more than once. -Fixed wrong item removal due to player error. -Added new event to indicate error to play queue. -Changed player error to skip item instead of removing. -Modified play queue adapter to update changed items only. -Removed headers from play queue adapter. -Merged event broadcast on play queue. -Changed toast on player error. -Modified remove event to no longer indicate current index status. -Modified move event to no longer indicate randomization status. -Added shuffle check to play queue.
This commit is contained in:
		 John Zhen M
					John Zhen M
				
			
				
					committed by
					
						 John Zhen Mo
						John Zhen Mo
					
				
			
			
				
	
			
			
			 John Zhen Mo
						John Zhen Mo
					
				
			
						parent
						
							a9aee21e58
						
					
				
				
					commit
					eebd83d6ac
				
			| @@ -36,6 +36,7 @@ import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
| import android.util.Log; | ||||
| import android.widget.RemoteViews; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import com.google.android.exoplayer2.PlaybackParameters; | ||||
| import com.google.android.exoplayer2.Player; | ||||
| @@ -402,6 +403,7 @@ public final class BackgroundPlayer extends Service { | ||||
|         @Override | ||||
|         public void onError(Exception exception) { | ||||
|             exception.printStackTrace(); | ||||
|             Toast.makeText(context, "Failed to play this audio", Toast.LENGTH_SHORT).show(); | ||||
|         } | ||||
|  | ||||
|         /*////////////////////////////////////////////////////////////////////////// | ||||
|   | ||||
| @@ -33,12 +33,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity | ||||
|  | ||||
|     private static final String TAG = "BGPlayerActivity"; | ||||
|  | ||||
|     private boolean isServiceBound; | ||||
|     private boolean serviceBound; | ||||
|     private ServiceConnection serviceConnection; | ||||
|  | ||||
|     private BackgroundPlayer.BasePlayerImpl player; | ||||
|  | ||||
|     private boolean isSeeking; | ||||
|     private boolean seeking; | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
|     // Views | ||||
| @@ -104,9 +104,9 @@ public class BackgroundPlayerActivity extends AppCompatActivity | ||||
|     @Override | ||||
|     protected void onStop() { | ||||
|         super.onStop(); | ||||
|         if(isServiceBound) { | ||||
|         if(serviceBound) { | ||||
|             unbindService(serviceConnection); | ||||
|             isServiceBound = false; | ||||
|             serviceBound = false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -119,7 +119,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity | ||||
|             @Override | ||||
|             public void onServiceDisconnected(ComponentName name) { | ||||
|                 Log.d(TAG, "Background player service is disconnected"); | ||||
|                 isServiceBound = false; | ||||
|                 serviceBound = false; | ||||
|                 player = null; | ||||
|                 finish(); | ||||
|             } | ||||
| @@ -132,7 +132,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity | ||||
|                 if (player == null) { | ||||
|                     finish(); | ||||
|                 } else { | ||||
|                     isServiceBound = true; | ||||
|                     serviceBound = true; | ||||
|                     buildComponents(); | ||||
|  | ||||
|                     player.setActivityListener(BackgroundPlayerActivity.this); | ||||
| @@ -220,13 +220,13 @@ public class BackgroundPlayerActivity extends AppCompatActivity | ||||
|  | ||||
|     @Override | ||||
|     public void onStartTrackingTouch(SeekBar seekBar) { | ||||
|         isSeeking = true; | ||||
|         seeking = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onStopTrackingTouch(SeekBar seekBar) { | ||||
|         player.simpleExoPlayer.seekTo(seekBar.getProgress()); | ||||
|         isSeeking = false; | ||||
|         seeking = false; | ||||
|     } | ||||
|  | ||||
|     //////////////////////////////////////////////////////////////////////////// | ||||
| @@ -284,7 +284,7 @@ public class BackgroundPlayerActivity extends AppCompatActivity | ||||
|         progressEndTime.setText(Localization.getDurationString(duration / 1000)); | ||||
|  | ||||
|         // Set current time if not seeking | ||||
|         if (!isSeeking) { | ||||
|         if (!seeking) { | ||||
|             progressSeekBar.setProgress(currentProgress); | ||||
|             progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000)); | ||||
|         } | ||||
|   | ||||
| @@ -664,8 +664,22 @@ public abstract class BasePlayer implements Player.EventListener, | ||||
|     @Override | ||||
|     public void onPlayerError(ExoPlaybackException error) { | ||||
|         if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); | ||||
|         playQueue.remove(playQueue.getIndex()); | ||||
|  | ||||
|         // If the current window is seekable, then the error is produced by transitioning into | ||||
|         // bad window, therefore we simply increment the current index. | ||||
|         // This is done because ExoPlayer reports the exception before window is | ||||
|         // transitioned due to seamless playback. | ||||
|         if (!simpleExoPlayer.isCurrentWindowSeekable()) { | ||||
|             playQueue.error(); | ||||
|             onError(error); | ||||
|         } else { | ||||
|             playQueue.offsetIndex(+1); | ||||
|         } | ||||
|  | ||||
|         // Player error causes ExoPlayer to go back to IDLE state, which requires resetting | ||||
|         // preparing a new media source. | ||||
|         playbackManager.reset(); | ||||
|         playbackManager.load(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -30,27 +30,37 @@ import io.reactivex.schedulers.Schedulers; | ||||
| public final class DeferredMediaSource implements MediaSource { | ||||
|     private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode()); | ||||
|  | ||||
|     private int state = -1; | ||||
|  | ||||
|     /** | ||||
|      * This state indicates the {@link DeferredMediaSource} has just been initialized or reset. | ||||
|      * The source must be prepared and loaded again before playback. | ||||
|      * */ | ||||
|     public final static int STATE_INIT = 0; | ||||
|     /** | ||||
|      * This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load. | ||||
|      * */ | ||||
|     public final static int STATE_PREPARED = 1; | ||||
|     /** | ||||
|      * This state indicates the {@link DeferredMediaSource} has been loaded without errors and | ||||
|      * is ready for playback. | ||||
|      * */ | ||||
|     public final static int STATE_LOADED = 2; | ||||
|     public final static int STATE_DISPOSED = 3; | ||||
|  | ||||
|     public interface Callback { | ||||
|         /** | ||||
|          * Player-specific MediaSource resolution from given StreamInfo. | ||||
|          * Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution | ||||
|          * from a given StreamInfo. | ||||
|          * */ | ||||
|         MediaSource sourceOf(final StreamInfo info); | ||||
|     } | ||||
|  | ||||
|     private PlayQueueItem stream; | ||||
|     private Callback callback; | ||||
|     private int state; | ||||
|  | ||||
|     private MediaSource mediaSource; | ||||
|  | ||||
|     /* Custom internal objects */ | ||||
|     private Disposable loader; | ||||
|  | ||||
|     private ExoPlayer exoPlayer; | ||||
|     private Listener listener; | ||||
|     private Throwable error; | ||||
| @@ -62,6 +72,17 @@ public final class DeferredMediaSource implements MediaSource { | ||||
|         this.state = STATE_INIT; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the current state of the {@link DeferredMediaSource}. | ||||
|      * | ||||
|      * @see DeferredMediaSource#STATE_INIT | ||||
|      * @see DeferredMediaSource#STATE_PREPARED | ||||
|      * @see DeferredMediaSource#STATE_LOADED | ||||
|      * */ | ||||
|     public int state() { | ||||
|         return state; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parameters are kept in the class for delayed preparation. | ||||
|      * */ | ||||
| @@ -72,28 +93,47 @@ public final class DeferredMediaSource implements MediaSource { | ||||
|         this.state = STATE_PREPARED; | ||||
|     } | ||||
|  | ||||
|     public int state() { | ||||
|         return state; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Externally controlled loading. This method fully prepares the source to be used | ||||
|      * like any other native MediaSource. | ||||
|      * like any other native {@link com.google.android.exoplayer2.source.MediaSource}. | ||||
|      * | ||||
|      * Ideally, this should be called after this source has entered PREPARED state and | ||||
|      * called once only. | ||||
|      * | ||||
|      * If loading fails here, an error will be propagated out and result in a | ||||
|      * {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated | ||||
|      * If loading fails here, an error will be propagated out and result in an | ||||
|      * {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, which is delegated | ||||
|      * to the player. | ||||
|      * */ | ||||
|     public synchronized void load() { | ||||
|         if (state != STATE_PREPARED || stream == null || loader != null) return; | ||||
|         if (stream == null) { | ||||
|             Log.e(TAG, "Stream Info missing, media source loading terminated."); | ||||
|             return; | ||||
|         } | ||||
|         if (state != STATE_PREPARED || loader != null) return; | ||||
|  | ||||
|         Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl()); | ||||
|  | ||||
|         final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() { | ||||
|             @Override | ||||
|             public void accept(StreamInfo streamInfo) throws Exception { | ||||
|                 onStreamInfoReceived(streamInfo); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(Throwable throwable) throws Exception { | ||||
|             onStreamInfoError(throwable); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         loader = stream.getStream() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(onSuccess, onError); | ||||
|     } | ||||
|  | ||||
|     private void onStreamInfoReceived(final StreamInfo streamInfo) { | ||||
|         Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl()); | ||||
|         state = STATE_LOADED; | ||||
|  | ||||
| @@ -112,23 +152,19 @@ public final class DeferredMediaSource implements MediaSource { | ||||
|  | ||||
|         mediaSource.prepareSource(exoPlayer, false, listener); | ||||
|     } | ||||
|         }; | ||||
|  | ||||
|         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(Throwable throwable) throws Exception { | ||||
|     private void onStreamInfoError(final Throwable throwable) { | ||||
|         Log.e(TAG, "Loading error:", throwable); | ||||
|         error = throwable; | ||||
|         state = STATE_LOADED; | ||||
|     } | ||||
|         }; | ||||
|  | ||||
|         loader = stream.getStream() | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(onSuccess, onError); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Delegate all errors to the player after {@link #load() load} is complete. | ||||
|      * | ||||
|      * Specifically, this method is called after an exception has occurred during loading or | ||||
|      * {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}. | ||||
|      * */ | ||||
|     @Override | ||||
|     public void maybeThrowSourceInfoRefreshError() throws IOException { | ||||
|         if (error != null) { | ||||
| @@ -145,19 +181,27 @@ public final class DeferredMediaSource implements MediaSource { | ||||
|         return mediaSource.createPeriod(mediaPeriodId, allocator); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Releases the media period (buffers). | ||||
|      * | ||||
|      * This may be called after {@link #releaseSource releaseSource}. | ||||
|      * */ | ||||
|     @Override | ||||
|     public void releasePeriod(MediaPeriod mediaPeriod) { | ||||
|         if (mediaSource == null) { | ||||
|             Log.e(TAG, "releasePeriod() called when media source is null, memory leak may have occurred."); | ||||
|         } else { | ||||
|         mediaSource.releasePeriod(mediaPeriod); | ||||
|     } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Cleans up all internal custom objects creating during loading. | ||||
|      * | ||||
|      * This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource} | ||||
|      * is released or when the player is stopped. | ||||
|      * | ||||
|      * This method should not release or set null the resources passed in through the constructor. | ||||
|      * This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}. | ||||
|      * */ | ||||
|     @Override | ||||
|     public void releaseSource() { | ||||
|         state = STATE_DISPOSED; | ||||
|  | ||||
|         if (mediaSource != null) { | ||||
|             mediaSource.releaseSource(); | ||||
|         } | ||||
| @@ -166,9 +210,11 @@ public final class DeferredMediaSource implements MediaSource { | ||||
|         } | ||||
|  | ||||
|         /* Do not set mediaSource as null here as it may be called through releasePeriod */ | ||||
|         stream = null; | ||||
|         callback = null; | ||||
|         loader = null; | ||||
|         exoPlayer = null; | ||||
|         listener = null; | ||||
|         error = null; | ||||
|  | ||||
|         state = STATE_INIT; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -107,11 +107,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { | ||||
|  | ||||
|     /** | ||||
|      * Loads the current playing stream and the streams within its WINDOW_SIZE bound. | ||||
|      * | ||||
|      * Unblocks the player once the item at the current index is loaded. | ||||
|      * */ | ||||
|     public void load() { | ||||
|         // The current item has higher priority | ||||
|         final int currentIndex = playQueue.getIndex(); | ||||
|         final PlayQueueItem currentItem = playQueue.get(currentIndex); | ||||
|         final PlayQueueItem currentItem = playQueue.getItem(currentIndex); | ||||
|         if (currentItem == null) return; | ||||
|         load(currentItem); | ||||
|  | ||||
| @@ -121,12 +123,24 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { | ||||
|         final int rightBound = Math.min(playQueue.size(), rightLimit); | ||||
|         final List<PlayQueueItem> items = new ArrayList<>(playQueue.getStreams().subList(leftBound, rightBound)); | ||||
|  | ||||
|         // Do a round robin | ||||
|         final int excess = rightLimit - playQueue.size(); | ||||
|         if (excess >= 0) items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess))); | ||||
|  | ||||
|         for (final PlayQueueItem item: items) load(item); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Blocks the player and repopulate the sources. | ||||
|      * | ||||
|      * Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}. | ||||
|      * */ | ||||
|     public void reset() { | ||||
|         tryBlock(); | ||||
|         resetSources(); | ||||
|         populateSources(); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Event Reactor | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -141,7 +155,19 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(@NonNull PlayQueueMessage event) { | ||||
|             public void onNext(@NonNull PlayQueueMessage playQueueMessage) { | ||||
|                 onPlayQueueChanged(playQueueMessage); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(@NonNull Throwable e) {} | ||||
|  | ||||
|             @Override | ||||
|             public void onComplete() {} | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private void onPlayQueueChanged(final PlayQueueMessage event) { | ||||
|         // why no pattern matching in Java =( | ||||
|         switch (event.type()) { | ||||
|             case APPEND: | ||||
| @@ -150,21 +176,20 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { | ||||
|             case SELECT: | ||||
|                 if (isCurrentIndexLoaded()) { | ||||
|                     sync(); | ||||
|                 } else { | ||||
|                     reset(); | ||||
|                 } | ||||
|                 break; | ||||
|             case REMOVE: | ||||
|                 final RemoveEvent removeEvent = (RemoveEvent) event; | ||||
|                         if (!removeEvent.isCurrent()) { | ||||
|                 remove(removeEvent.index()); | ||||
|                 break; | ||||
|                         } | ||||
|                         // Reset the sources if the index to remove is the current playing index | ||||
|             case INIT: | ||||
|             case REORDER: | ||||
|                         tryBlock(); | ||||
|                         resetSources(); | ||||
|                         populateSources(); | ||||
|                 reset(); | ||||
|                 break; | ||||
|             case ERROR: | ||||
|             case MOVE: | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
| @@ -181,14 +206,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { | ||||
|         if (playQueueReactor != null) playQueueReactor.request(1); | ||||
|     } | ||||
|  | ||||
|             @Override | ||||
|             public void onError(@NonNull Throwable e) {} | ||||
|  | ||||
|             @Override | ||||
|             public void onComplete() {} | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Internal Helpers | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -220,7 +237,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback { | ||||
|     } | ||||
|  | ||||
|     private void sync() { | ||||
|         final PlayQueueItem currentItem = playQueue.getCurrent(); | ||||
|         final PlayQueueItem currentItem = playQueue.getItem(); | ||||
|  | ||||
|         final Consumer<StreamInfo> syncPlayback = new Consumer<StreamInfo>() { | ||||
|             @Override | ||||
|   | ||||
| @@ -20,8 +20,6 @@ import io.reactivex.schedulers.Schedulers; | ||||
| public final class ExternalPlayQueue extends PlayQueue { | ||||
|     private final String TAG = "ExternalPlayQueue@" + Integer.toHexString(hashCode()); | ||||
|  | ||||
|     private static final int RETRY_COUNT = 2; | ||||
|  | ||||
|     private boolean isComplete; | ||||
|  | ||||
|     private int serviceId; | ||||
| @@ -54,7 +52,6 @@ public final class ExternalPlayQueue extends PlayQueue { | ||||
|        ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .retry(RETRY_COUNT) | ||||
|                 .subscribe(getPlaylistObserver()); | ||||
|     } | ||||
|  | ||||
| @@ -75,6 +72,9 @@ public final class ExternalPlayQueue extends PlayQueue { | ||||
|                 nextUrl = result.nextItemsUrl; | ||||
|  | ||||
|                 append(extractPlaylistItems(result.nextItemsList)); | ||||
|  | ||||
|                 fetchReactor.dispose(); | ||||
|                 fetchReactor = null; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import android.util.Log; | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| import org.schabi.newpipe.playlist.events.AppendEvent; | ||||
| import org.schabi.newpipe.playlist.events.ErrorEvent; | ||||
| import org.schabi.newpipe.playlist.events.InitEvent; | ||||
| import org.schabi.newpipe.playlist.events.PlayQueueMessage; | ||||
| import org.schabi.newpipe.playlist.events.RemoveEvent; | ||||
| @@ -44,8 +45,7 @@ public abstract class PlayQueue implements Serializable { | ||||
|     private ArrayList<PlayQueueItem> streams; | ||||
|     private final AtomicInteger queueIndex; | ||||
|  | ||||
|     private transient BehaviorSubject<PlayQueueMessage> streamsEventBroadcast; | ||||
|     private transient BehaviorSubject<PlayQueueMessage> indexEventBroadcast; | ||||
|     private transient BehaviorSubject<PlayQueueMessage> eventBroadcast; | ||||
|     private transient Flowable<PlayQueueMessage> broadcastReceiver; | ||||
|     private transient Subscription reportingReactor; | ||||
|  | ||||
| @@ -70,13 +70,11 @@ public abstract class PlayQueue implements Serializable { | ||||
|      * Also starts a self reporter for logging if debug mode is enabled. | ||||
|      * */ | ||||
|     public void init() { | ||||
|         streamsEventBroadcast = BehaviorSubject.create(); | ||||
|         indexEventBroadcast = BehaviorSubject.create(); | ||||
|         eventBroadcast = BehaviorSubject.create(); | ||||
|  | ||||
|         broadcastReceiver = Flowable.merge( | ||||
|                 streamsEventBroadcast.toFlowable(BackpressureStrategy.BUFFER), | ||||
|                 indexEventBroadcast.toFlowable(BackpressureStrategy.BUFFER) | ||||
|         ).observeOn(AndroidSchedulers.mainThread()).startWith(new InitEvent()); | ||||
|         broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .startWith(new InitEvent()); | ||||
|  | ||||
|         if (DEBUG) broadcastReceiver.subscribe(getSelfReporter()); | ||||
|     } | ||||
| @@ -88,8 +86,7 @@ public abstract class PlayQueue implements Serializable { | ||||
|         if (backup != null) backup.clear(); | ||||
|         if (streams != null) streams.clear(); | ||||
|  | ||||
|         if (streamsEventBroadcast != null) streamsEventBroadcast.onComplete(); | ||||
|         if (indexEventBroadcast != null) indexEventBroadcast.onComplete(); | ||||
|         if (eventBroadcast != null) eventBroadcast.onComplete(); | ||||
|         if (reportingReactor != null) reportingReactor.cancel(); | ||||
|  | ||||
|         broadcastReceiver = null; | ||||
| @@ -123,15 +120,15 @@ public abstract class PlayQueue implements Serializable { | ||||
|     /** | ||||
|      * Returns the current item that should be played. | ||||
|      * */ | ||||
|     public PlayQueueItem getCurrent() { | ||||
|         return get(getIndex()); | ||||
|     public PlayQueueItem getItem() { | ||||
|         return getItem(getIndex()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the item at the given index. | ||||
|      * May throw {@link IndexOutOfBoundsException}. | ||||
|      * */ | ||||
|     public PlayQueueItem get(int index) { | ||||
|     public PlayQueueItem getItem(int index) { | ||||
|         if (index >= streams.size() || streams.get(index) == null) return null; | ||||
|         return streams.get(index); | ||||
|     } | ||||
| @@ -160,6 +157,13 @@ public abstract class PlayQueue implements Serializable { | ||||
|         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. | ||||
|      * */ | ||||
| @@ -191,12 +195,14 @@ public abstract class PlayQueue implements Serializable { | ||||
|     public synchronized void setIndex(final int index) { | ||||
|         if (index == getIndex()) return; | ||||
|  | ||||
|         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); | ||||
|         indexEventBroadcast.onNext(new SelectEvent(newIndex)); | ||||
|         broadcast(new SelectEvent(oldIndex, newIndex)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -213,7 +219,7 @@ public abstract class PlayQueue implements Serializable { | ||||
|      * | ||||
|      * Will emit a {@link AppendEvent} on any given context. | ||||
|      * */ | ||||
|     protected synchronized void append(final PlayQueueItem... items) { | ||||
|     public synchronized void append(final PlayQueueItem... items) { | ||||
|         streams.addAll(Arrays.asList(items)); | ||||
|         broadcast(new AppendEvent(items.length)); | ||||
|     } | ||||
| @@ -223,7 +229,7 @@ public abstract class PlayQueue implements Serializable { | ||||
|      * | ||||
|      * Will emit a {@link AppendEvent} on any given context. | ||||
|      * */ | ||||
|     protected synchronized void append(final Collection<PlayQueueItem> items) { | ||||
|     public synchronized void append(final Collection<PlayQueueItem> items) { | ||||
|         streams.addAll(items); | ||||
|         broadcast(new AppendEvent(items.size())); | ||||
|     } | ||||
| @@ -235,22 +241,35 @@ public abstract class PlayQueue implements Serializable { | ||||
|      * 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)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Report an exception for the item at the current index in order to remove it. | ||||
|      * | ||||
|      * This is done as a separate event as the underlying manager may have | ||||
|      * different implementation regarding exceptions. | ||||
|      * */ | ||||
|     public synchronized void error() { | ||||
|         final int index = getIndex(); | ||||
|         removeInternal(index); | ||||
|         broadcast(new ErrorEvent(index)); | ||||
|     } | ||||
|  | ||||
|     private synchronized void removeInternal(final int index) { | ||||
|         final int currentIndex = queueIndex.get(); | ||||
|         final boolean isCurrent = index == getIndex(); | ||||
|  | ||||
|         if (currentIndex > index) { | ||||
|             queueIndex.decrementAndGet(); | ||||
|         } else if (currentIndex >= size()) { | ||||
|             queueIndex.set(0); | ||||
|         } | ||||
|         streams.remove(index); | ||||
|  | ||||
|         broadcast(new RemoveEvent(index, isCurrent)); | ||||
|         streams.remove(index); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -264,11 +283,11 @@ public abstract class PlayQueue implements Serializable { | ||||
|      * */ | ||||
|     public synchronized void shuffle() { | ||||
|         backup = new ArrayList<>(streams); | ||||
|         final PlayQueueItem current = getCurrent(); | ||||
|         final PlayQueueItem current = getItem(); | ||||
|         Collections.shuffle(streams); | ||||
|         queueIndex.set(streams.indexOf(current)); | ||||
|  | ||||
|         broadcast(new ReorderEvent(true)); | ||||
|         broadcast(new ReorderEvent()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -280,12 +299,13 @@ public abstract class PlayQueue implements Serializable { | ||||
|      * */ | ||||
|     public synchronized void unshuffle() { | ||||
|         if (backup == null) return; | ||||
|         final PlayQueueItem current = getCurrent(); | ||||
|         final PlayQueueItem current = getItem(); | ||||
|         streams.clear(); | ||||
|         streams = backup; | ||||
|         backup = null; | ||||
|         queueIndex.set(streams.indexOf(current)); | ||||
|  | ||||
|         broadcast(new ReorderEvent(false)); | ||||
|         broadcast(new ReorderEvent()); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -293,7 +313,9 @@ public abstract class PlayQueue implements Serializable { | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     private void broadcast(final PlayQueueMessage event) { | ||||
|         streamsEventBroadcast.onNext(event); | ||||
|         if (eventBroadcast != null) { | ||||
|             eventBroadcast.onNext(event); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private Subscriber<PlayQueueMessage> getSelfReporter() { | ||||
|   | ||||
| @@ -7,7 +7,11 @@ import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.playlist.events.AppendEvent; | ||||
| import org.schabi.newpipe.playlist.events.ErrorEvent; | ||||
| import org.schabi.newpipe.playlist.events.PlayQueueMessage; | ||||
| import org.schabi.newpipe.playlist.events.RemoveEvent; | ||||
| import org.schabi.newpipe.playlist.events.SelectEvent; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| @@ -38,10 +42,12 @@ import io.reactivex.disposables.Disposable; | ||||
| 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 header = null; | ||||
|     private View footer = null; | ||||
|  | ||||
|     private Disposable playQueueReactor; | ||||
| @@ -54,11 +60,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | ||||
|         public View view; | ||||
|     } | ||||
|  | ||||
|     public void showFooter(final boolean show) { | ||||
|         showFooter = show; | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public PlayQueueAdapter(final PlayQueue playQueue) { | ||||
|         this.playQueueItemBuilder = new PlayQueueItemBuilder(); | ||||
|         this.playQueue = playQueue; | ||||
| @@ -92,7 +93,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | ||||
|  | ||||
|             @Override | ||||
|             public void onNext(@NonNull PlayQueueMessage playQueueMessage) { | ||||
|                 notifyDataSetChanged(); | ||||
|                 onPlayQueueChanged(playQueueMessage); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
| @@ -109,19 +110,46 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | ||||
|                 .subscribe(observer); | ||||
|     } | ||||
|  | ||||
|     private void onPlayQueueChanged(final PlayQueueMessage message) { | ||||
|         switch (message.type()) { | ||||
|             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; | ||||
|                 notifyItemRangeRemoved(errorEvent.index(), 1); | ||||
|                 notifyItemChanged(errorEvent.index()); | ||||
|                 break; | ||||
|             case REMOVE: | ||||
|                 final RemoveEvent removeEvent = (RemoveEvent) message; | ||||
|                 notifyItemRangeRemoved(removeEvent.index(), 1); | ||||
|                 notifyItemChanged(removeEvent.index()); | ||||
|                 break; | ||||
|             default: | ||||
|                 notifyDataSetChanged(); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void dispose() { | ||||
|         if (playQueueReactor != null) playQueueReactor.dispose(); | ||||
|         playQueueReactor = null; | ||||
|     } | ||||
|  | ||||
|     public void setHeader(View header) { | ||||
|         this.header = header; | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public void setFooter(View footer) { | ||||
|         this.footer = footer; | ||||
|         notifyDataSetChanged(); | ||||
|         notifyItemChanged(playQueue.size()); | ||||
|     } | ||||
|  | ||||
|     public void showFooter(final boolean show) { | ||||
|         showFooter = show; | ||||
|         notifyItemChanged(playQueue.size()); | ||||
|     } | ||||
|  | ||||
|     public List<PlayQueueItem> getItems() { | ||||
| @@ -131,36 +159,28 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | ||||
|     @Override | ||||
|     public int getItemCount() { | ||||
|         int count = playQueue.getStreams().size(); | ||||
|         if(header != null) count++; | ||||
|         if(footer != null && showFooter) count++; | ||||
|         return count; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getItemViewType(int position) { | ||||
|         if(header != null && position == 0) { | ||||
|             return 0; | ||||
|         } else if(header != null) { | ||||
|             position--; | ||||
|         } | ||||
|         if(footer != null && position == playQueue.getStreams().size() && showFooter) { | ||||
|             return 1; | ||||
|             return FOOTER_VIEW_TYPE_ID; | ||||
|         } | ||||
|         return 2; | ||||
|  | ||||
|         return ITEM_VIEW_TYPE_ID; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) { | ||||
|         switch(type) { | ||||
|             case 0: | ||||
|                 return new HFHolder(header); | ||||
|             case 1: | ||||
|             case FOOTER_VIEW_TYPE_ID: | ||||
|                 return new HFHolder(footer); | ||||
|             case 2: | ||||
|                 return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()) | ||||
|                         .inflate(R.layout.play_queue_item, parent, false)); | ||||
|             case ITEM_VIEW_TYPE_ID: | ||||
|                 return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.play_queue_item, parent, false)); | ||||
|             default: | ||||
|                 Log.e(TAG, "Trollolo"); | ||||
|                 Log.e(TAG, "Attempting to create view holder with undefined type: " + type); | ||||
|                 return null; | ||||
|         } | ||||
|     } | ||||
| @@ -168,14 +188,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold | ||||
|     @Override | ||||
|     public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { | ||||
|         if(holder instanceof PlayQueueItemHolder) { | ||||
|             // Ensure header does not interfere with list building | ||||
|             if (header != null) position--; | ||||
|             // Build the list item | ||||
|             playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position)); | ||||
|             // Check if the current item should be selected/highlighted | ||||
|             holder.itemView.setSelected(playQueue.getIndex() == position); | ||||
|         } else if(holder instanceof HFHolder && position == 0 && header != null) { | ||||
|             ((HFHolder) holder).view = header; | ||||
|         } else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) { | ||||
|             ((HFHolder) holder).view = footer; | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,19 @@ | ||||
| package org.schabi.newpipe.playlist.events; | ||||
|  | ||||
|  | ||||
| public class ErrorEvent implements PlayQueueMessage { | ||||
|     final private int index; | ||||
|  | ||||
|     @Override | ||||
|     public PlayQueueEvent type() { | ||||
|         return PlayQueueEvent.ERROR; | ||||
|     } | ||||
|  | ||||
|     public ErrorEvent(final int index) { | ||||
|         this.index = index; | ||||
|     } | ||||
|  | ||||
|     public int index() { | ||||
|         return index; | ||||
|     } | ||||
| } | ||||
| @@ -15,7 +15,10 @@ public enum PlayQueueEvent { | ||||
|     // sent when two streams swap place in the play queue | ||||
|     MOVE, | ||||
|  | ||||
|     // send when queue is shuffled | ||||
|     REORDER | ||||
|     // sent when queue is shuffled | ||||
|     REORDER, | ||||
|  | ||||
|     // sent when the item at index has caused an exception | ||||
|     ERROR | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,23 +3,17 @@ package org.schabi.newpipe.playlist.events; | ||||
|  | ||||
| public class RemoveEvent implements PlayQueueMessage { | ||||
|     final private int index; | ||||
|     final private boolean isCurrent; | ||||
|  | ||||
|     @Override | ||||
|     public PlayQueueEvent type() { | ||||
|         return PlayQueueEvent.REMOVE; | ||||
|     } | ||||
|  | ||||
|     public RemoveEvent(final int index, final boolean isCurrent) { | ||||
|     public RemoveEvent(final int index) { | ||||
|         this.index = index; | ||||
|         this.isCurrent = isCurrent; | ||||
|     } | ||||
|  | ||||
|     public int index() { | ||||
|         return index; | ||||
|     } | ||||
|  | ||||
|     public boolean isCurrent() { | ||||
|         return isCurrent; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,18 +1,12 @@ | ||||
| package org.schabi.newpipe.playlist.events; | ||||
|  | ||||
| public class ReorderEvent implements PlayQueueMessage { | ||||
|     final private boolean randomize; | ||||
|  | ||||
|     @Override | ||||
|     public PlayQueueEvent type() { | ||||
|         return PlayQueueEvent.REORDER; | ||||
|     } | ||||
|  | ||||
|     public ReorderEvent(final boolean randomize) { | ||||
|         this.randomize = randomize; | ||||
|     } | ||||
|     public ReorderEvent() { | ||||
|  | ||||
|     public boolean isRandomize() { | ||||
|         return randomize; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package org.schabi.newpipe.playlist.events; | ||||
|  | ||||
|  | ||||
| public class SelectEvent implements PlayQueueMessage { | ||||
|     final private int oldIndex; | ||||
|     final private int newIndex; | ||||
|  | ||||
|     @Override | ||||
| @@ -9,11 +10,16 @@ public class SelectEvent implements PlayQueueMessage { | ||||
|         return PlayQueueEvent.SELECT; | ||||
|     } | ||||
|  | ||||
|     public SelectEvent(final int newIndex) { | ||||
|     public SelectEvent(final int oldIndex, final int newIndex) { | ||||
|         this.oldIndex = oldIndex; | ||||
|         this.newIndex = newIndex; | ||||
|     } | ||||
|  | ||||
|     public int index() { | ||||
|     public int getOldIndex() { | ||||
|         return oldIndex; | ||||
|     } | ||||
|  | ||||
|     public int getNewIndex() { | ||||
|         return newIndex; | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user