From 690af88db9be1899827f2f3a12dc9e782eb5d08c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 11 Jun 2025 06:19:52 +0530 Subject: [PATCH 1/5] Add PlayQueueItem equals and hashCode --- .../fragments/detail/VideoDetailFragment.java | 13 ++++---- .../newpipe/player/playqueue/PlayQueue.java | 31 ++++++------------- .../player/playqueue/PlayQueueItem.java | 13 ++++++++ .../player/playqueue/PlayQueueItemTest.java | 10 +++--- .../player/playqueue/PlayQueueTest.java | 12 ++++--- 5 files changed, 40 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 17ae16325..fb6e2d79e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -852,8 +852,7 @@ public final class VideoDetailFragment if (playQueue == null) { playQueue = new SinglePlayQueue(result); } - if (stack.isEmpty() || !stack.peek().getPlayQueue() - .equalStreams(playQueue)) { + if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(playQueue)) { stack.push(new StackItem(serviceId, url, title, playQueue)); } } @@ -1739,7 +1738,7 @@ public final class VideoDetailFragment // deleted/added items inside Channel/Playlist queue and makes possible to have // a history of played items @Nullable final StackItem stackPeek = stack.peek(); - if (stackPeek != null && !stackPeek.getPlayQueue().equalStreams(queue)) { + if (stackPeek != null && !stackPeek.getPlayQueue().equals(queue)) { @Nullable final PlayQueueItem playQueueItem = queue.getItem(); if (playQueueItem != null) { stack.push(new StackItem(playQueueItem.getServiceId(), playQueueItem.getUrl(), @@ -1803,7 +1802,7 @@ public final class VideoDetailFragment // They are not equal when user watches something in popup while browsing in fragment and // then changes screen orientation. In that case the fragment will set itself as // a service listener and will receive initial call to onMetadataUpdate() - if (!queue.equalStreams(playQueue)) { + if (!queue.equals(playQueue)) { return; } @@ -2073,9 +2072,10 @@ public final class VideoDetailFragment private StackItem findQueueInStack(final PlayQueue queue) { StackItem item = null; final Iterator iterator = stack.descendingIterator(); + while (iterator.hasNext()) { final StackItem next = iterator.next(); - if (next.getPlayQueue().equalStreams(queue)) { + if (next.getPlayQueue().equals(queue)) { item = next; break; } @@ -2089,8 +2089,7 @@ public final class VideoDetailFragment // Player will have STATE_IDLE when a user pressed back button if (isClearingQueueConfirmationRequired(activity) && playerIsNotStopped() - && activeQueue != null - && !activeQueue.equalStreams(playQueue)) { + && !Objects.equals(activeQueue, playQueue)) { showClearingQueueConfirmation(onAllow); } else { onAllow.run(); diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index cfa2ab316..2d28d240f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -518,31 +518,18 @@ public abstract class PlayQueue implements Serializable { * This method also gives a chance to track history of items in a queue in * VideoDetailFragment without duplicating items from two identical queues */ - public boolean equalStreams(@Nullable final PlayQueue other) { - if (other == null) { - return false; - } - if (size() != other.size()) { - return false; - } - for (int i = 0; i < size(); i++) { - final PlayQueueItem stream = streams.get(i); - final PlayQueueItem otherStream = other.streams.get(i); - // Check is based on serviceId and URL - if (stream.getServiceId() != otherStream.getServiceId() - || !stream.getUrl().equals(otherStream.getUrl())) { - return false; - } - } - return true; + @Override + public boolean equals(final Object o) { + return o instanceof PlayQueue playQueue && streams.equals(playQueue.streams); + } + + @Override + public int hashCode() { + return streams.hashCode(); } public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) { - if (equalStreams(other)) { - //noinspection ConstantConditions - return other.getIndex() == getIndex(); //NOSONAR: other is not null - } - return false; + return equals(other) && other.getIndex() == getIndex(); } public boolean isDisposed() { diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java index 759c51267..8f41ceb60 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java @@ -11,6 +11,7 @@ import org.schabi.newpipe.util.ExtractorHelper; import java.io.Serializable; import java.util.List; +import java.util.Objects; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -139,4 +140,16 @@ public class PlayQueueItem implements Serializable { public void setAutoQueued(final boolean autoQueued) { isAutoQueued = autoQueued; } + + @Override + public boolean equals(final Object o) { + return o instanceof PlayQueueItem item + && serviceId == item.serviceId + && url.equals(item.url); + } + + @Override + public int hashCode() { + return Objects.hash(url, serviceId); + } } diff --git a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java index d10d33f7e..ef1b36d32 100644 --- a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java +++ b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java @@ -1,19 +1,17 @@ package org.schabi.newpipe.player.playqueue; -import org.junit.Test; - import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; public class PlayQueueItemTest { public static final String URL = "MY_URL"; @Test - public void equalsMustNotBeOverloaded() { + public void equalsMustWork() { final PlayQueueItem a = PlayQueueTest.makeItemWithUrl(URL); final PlayQueueItem b = PlayQueueTest.makeItemWithUrl(URL); - assertEquals(a, a); - assertNotEquals(a, b); // they should compare different even if they have the same data + assertEquals(a, b); } } diff --git a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java index 022089f37..24212b786 100644 --- a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java +++ b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueTest.java @@ -3,6 +3,8 @@ package org.schabi.newpipe.player.playqueue; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; @@ -13,12 +15,14 @@ import java.util.Objects; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; +@RunWith(Enclosed.class) @SuppressWarnings("checkstyle:HideUtilityClassConstructor") public class PlayQueueTest { static PlayQueue makePlayQueue(final int index, final List streams) { @@ -168,7 +172,7 @@ public class PlayQueueTest { final List streams = Collections.nCopies(5, item1); final PlayQueue queue1 = makePlayQueue(0, streams); final PlayQueue queue2 = makePlayQueue(0, streams); - assertTrue(queue1.equalStreams(queue2)); + assertEquals(queue1, queue2); assertTrue(queue1.equalStreamsAndIndex(queue2)); } @@ -177,7 +181,7 @@ public class PlayQueueTest { final List streams = Collections.nCopies(5, item1); final PlayQueue queue1 = makePlayQueue(1, streams); final PlayQueue queue2 = makePlayQueue(4, streams); - assertTrue(queue1.equalStreams(queue2)); + assertEquals(queue1, queue2); assertFalse(queue1.equalStreamsAndIndex(queue2)); } @@ -187,7 +191,7 @@ public class PlayQueueTest { final List streams2 = Collections.nCopies(5, item2); final PlayQueue queue1 = makePlayQueue(0, streams1); final PlayQueue queue2 = makePlayQueue(0, streams2); - assertFalse(queue1.equalStreams(queue2)); + assertNotEquals(queue1, queue2); } @Test @@ -196,7 +200,7 @@ public class PlayQueueTest { final List streams2 = Collections.nCopies(6, item2); final PlayQueue queue1 = makePlayQueue(0, streams1); final PlayQueue queue2 = makePlayQueue(0, streams2); - assertFalse(queue1.equalStreams(queue2)); + assertNotEquals(queue1, queue2); } } } From e2a02a1f869995492aa7d7459a1f48eaae36b85c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 11 Jun 2025 07:39:29 +0530 Subject: [PATCH 2/5] Fix some issues --- .../schabi/newpipe/fragments/detail/VideoDetailFragment.java | 1 - .../schabi/newpipe/player/playqueue/PlayQueueItemTest.java | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index fb6e2d79e..5e0373122 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -2072,7 +2072,6 @@ public final class VideoDetailFragment private StackItem findQueueInStack(final PlayQueue queue) { StackItem item = null; final Iterator iterator = stack.descendingIterator(); - while (iterator.hasNext()) { final StackItem next = iterator.next(); if (next.getPlayQueue().equals(queue)) { diff --git a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java index ef1b36d32..9addcfc1e 100644 --- a/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java +++ b/app/src/test/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTest.java @@ -1,9 +1,9 @@ package org.schabi.newpipe.player.playqueue; -import static org.junit.Assert.assertEquals; - import org.junit.Test; +import static org.junit.Assert.assertEquals; + public class PlayQueueItemTest { public static final String URL = "MY_URL"; From bb7873d157524bf62541e755ee3c8879f14045c5 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 11 Jun 2025 08:13:13 +0530 Subject: [PATCH 3/5] Fix Sonar warning --- .../java/org/schabi/newpipe/player/playqueue/PlayQueue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index 2d28d240f..a474b624b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -529,7 +529,7 @@ public abstract class PlayQueue implements Serializable { } public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) { - return equals(other) && other.getIndex() == getIndex(); + return equals(other) && other.getIndex() == getIndex(); //NOSONAR: other is not null } public boolean isDisposed() { From c2b6c71947e766b388fdf8bf47aa750124eb0152 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 4 Jul 2025 06:34:48 +0530 Subject: [PATCH 4/5] Rename .java to .kt --- .../newpipe/player/playqueue/{PlayQueue.java => PlayQueue.kt} | 0 .../player/playqueue/{PlayQueueItem.java => PlayQueueItem.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/schabi/newpipe/player/playqueue/{PlayQueue.java => PlayQueue.kt} (100%) rename app/src/main/java/org/schabi/newpipe/player/playqueue/{PlayQueueItem.java => PlayQueueItem.kt} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt similarity index 100% rename from app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java rename to app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt similarity index 100% rename from app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java rename to app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt From 31f8dd05a7628b1e60a034c20dca4eb7e516005f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 4 Jul 2025 06:34:49 +0530 Subject: [PATCH 5/5] Convert play queue classes to Kotlin --- .../org/schabi/newpipe/player/Player.java | 6 +- .../player/playback/MediaSourceManager.java | 2 +- .../newpipe/player/playqueue/PlayQueue.kt | 563 ++++++++---------- .../newpipe/player/playqueue/PlayQueueItem.kt | 202 ++----- 4 files changed, 319 insertions(+), 454 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index d3e3ff1df..094032a06 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -397,7 +397,7 @@ public final class Player implements PlaybackListener, Listener { && newQueue.size() == 1 && newQueue.getItem() != null && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null && newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl()) - && newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + && newQueue.getItem().getRecoveryPosition() != Long.MIN_VALUE) { // Player can have state = IDLE when playback is stopped or failed // and we should retry in this case if (simpleExoPlayer.getPlaybackState() @@ -425,7 +425,7 @@ public final class Player implements PlaybackListener, Listener { && !samePlayQueue && !newQueue.isEmpty() && newQueue.getItem() != null - && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { + && newQueue.getItem().getRecoveryPosition() == Long.MIN_VALUE) { databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem()) .observeOn(AndroidSchedulers.mainThread()) // Do not place initPlayback() in doFinally() because @@ -1588,7 +1588,7 @@ public final class Player implements PlaybackListener, Listener { } // sync the player index with the queue index, and seek to the correct position - if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + if (item.getRecoveryPosition() != Long.MIN_VALUE) { simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition()); playQueue.unsetRecovery(playQueueIndex); } else { diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 88d7145bc..9092906fa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -38,9 +38,9 @@ import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.subjects.PublishSubject; +import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException; -import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG; import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis; public class MediaSourceManager { diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt index a474b624b..1ae7e5cdb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt @@ -1,189 +1,173 @@ -package org.schabi.newpipe.player.playqueue; +package org.schabi.newpipe.player.playqueue -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.player.playqueue.events.AppendEvent; -import org.schabi.newpipe.player.playqueue.events.ErrorEvent; -import org.schabi.newpipe.player.playqueue.events.InitEvent; -import org.schabi.newpipe.player.playqueue.events.MoveEvent; -import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent; -import org.schabi.newpipe.player.playqueue.events.RecoveryEvent; -import org.schabi.newpipe.player.playqueue.events.RemoveEvent; -import org.schabi.newpipe.player.playqueue.events.ReorderEvent; -import org.schabi.newpipe.player.playqueue.events.SelectEvent; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.BackpressureStrategy; -import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.subjects.BehaviorSubject; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.BackpressureStrategy +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.subjects.BehaviorSubject +import org.schabi.newpipe.player.playqueue.events.AppendEvent +import org.schabi.newpipe.player.playqueue.events.ErrorEvent +import org.schabi.newpipe.player.playqueue.events.InitEvent +import org.schabi.newpipe.player.playqueue.events.MoveEvent +import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent +import org.schabi.newpipe.player.playqueue.events.RecoveryEvent +import org.schabi.newpipe.player.playqueue.events.RemoveEvent +import org.schabi.newpipe.player.playqueue.events.ReorderEvent +import org.schabi.newpipe.player.playqueue.events.SelectEvent +import java.io.Serializable +import java.util.Collections +import java.util.concurrent.atomic.AtomicInteger /** * 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 { - public static final boolean DEBUG = MainActivity.DEBUG; - @NonNull - private final AtomicInteger queueIndex; - private final List history = new ArrayList<>(); +abstract class PlayQueue internal constructor( + index: Int, + startWith: List, +) : Serializable { + private val queueIndex = AtomicInteger(index) + private val history = mutableListOf() + private var backup = mutableListOf() + private var streams = startWith.toMutableList() - private List backup; - private List streams; + @Transient + private var eventBroadcast: BehaviorSubject? = null - private transient BehaviorSubject eventBroadcast; - private transient Flowable broadcastReceiver; - private transient boolean disposed = false; + /** + * Returns the play queue's update broadcast. + * May be null if the play queue message bus is not initialized. + * + * @return the play queue's update broadcast + */ + @Transient + var broadcastReceiver: Flowable? = null + private set - PlayQueue(final int index, final List startWith) { - streams = new ArrayList<>(startWith); + @Transient + var isDisposed: Boolean = false + private set - if (streams.size() > index) { - history.add(streams.get(index)); + init { + if (streams.size > index) { + history.add(streams[index]) } - - 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(); + fun init() { + eventBroadcast = BehaviorSubject.create() - broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) + broadcastReceiver = + eventBroadcast!! + .toFlowable(BackpressureStrategy.BUFFER) .observeOn(AndroidSchedulers.mainThread()) - .startWithItem(new InitEvent()); + .startWithItem(InitEvent()) } /** * Dispose the play queue by stopping all message buses. */ - public void dispose() { - if (eventBroadcast != null) { - eventBroadcast.onComplete(); - } - - eventBroadcast = null; - broadcastReceiver = null; - disposed = true; + open fun dispose() { + eventBroadcast?.onComplete() + eventBroadcast = null + broadcastReceiver = null + this.isDisposed = true } /** * 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. - *

* * @return whether the queue is complete */ - public abstract boolean isComplete(); + abstract val isComplete: Boolean /** * Load partial queue in the background, does nothing if the queue is complete. */ - public abstract void fetch(); + abstract fun fetch() /*////////////////////////////////////////////////////////////////////////// // Readonly ops - //////////////////////////////////////////////////////////////////////////*/ - - /** - * @return the current index that should be played - */ - public int getIndex() { - return queueIndex.get(); - } - - /** - * Changes the current playing index to a new index. - *

- * 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. - *

- * - * @param index the index to be set - */ - public synchronized void setIndex(final int index) { - final int oldIndex = getIndex(); - - final int newIndex; - - if (index < 0) { - newIndex = 0; - } else if (index < streams.size()) { - // Regular assignment for index in bounds - newIndex = index; - } else if (streams.isEmpty()) { - // Out of bounds from here on - // Need to check if stream is empty to prevent arithmetic error and negative index - newIndex = 0; - } else if (isComplete()) { - // Circular indexing - newIndex = index % streams.size(); - } else { - // Index of last element - newIndex = streams.size() - 1; - } - - queueIndex.set(newIndex); - - if (oldIndex != newIndex) { - history.add(streams.get(newIndex)); - } - - /* - TODO: Documentation states that a SelectEvent will only be emitted if the new index is... - different from the old one but this is emitted regardless? Not sure what this what it does - exactly so I won't touch it + ////////////////////////////////////////////////////////////////////////// */ + @set:Synchronized + var index: Int = 0 + /** + * @return the current index that should be played */ - broadcast(new SelectEvent(oldIndex, newIndex)); - } + get() = queueIndex.get() + + /** + * 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 [SelectEvent] if the index is not the current playing index. + * + * @param index the index to be set + */ + set(index) { + val oldIndex = field + + val newIndex: Int + + if (index < 0) { + newIndex = 0 + } else if (index < streams.size) { + // Regular assignment for index in bounds + newIndex = index + } else if (streams.isEmpty()) { + // Out of bounds from here on + // Need to check if stream is empty to prevent arithmetic error and negative index + newIndex = 0 + } else if (this.isComplete) { + // Circular indexing + newIndex = index % streams.size + } else { + // Index of last element + newIndex = streams.size - 1 + } + + queueIndex.set(newIndex) + + if (oldIndex != newIndex) { + history.add(streams[newIndex]) + } + + /* + TODO: Documentation states that a SelectEvent will only be emitted if the new index is... + different from the old one but this is emitted regardless? Not sure what this what it does + exactly so I won't touch it + */ + broadcast(SelectEvent(oldIndex, newIndex)) + } /** * @return the current item that should be played, or null if the queue is empty */ - @Nullable - public PlayQueueItem getItem() { - return getItem(getIndex()); - } + val item get() = getItem(this.index) /** * @param index the index of the item to return * @return the item at the given index, or null if the index is out of bounds */ - @Nullable - public PlayQueueItem getItem(final int index) { - if (index < 0 || index >= streams.size()) { - return null; - } - return streams.get(index); - } + fun getItem(index: Int) = streams.getOrNull(index) /** * Returns the index of the given item using referential equality. @@ -192,303 +176,280 @@ public abstract class PlayQueue implements Serializable { * @param item the item to find the index of * @return the index of the given item */ - public int indexOf(@NonNull final PlayQueueItem item) { - return streams.indexOf(item); - } + fun indexOf(item: PlayQueueItem): Int = streams.indexOf(item) /** * @return the current size of play queue. */ - public int size() { - return streams.size(); - } + fun size(): Int = streams.size /** * Checks if the play queue is empty. * * @return whether the play queue is empty */ - public boolean isEmpty() { - return streams.isEmpty(); - } + val isEmpty: Boolean + get() = streams.isEmpty() /** * Determines if the current play queue is shuffled. * * @return whether the play queue is shuffled */ - public boolean isShuffled() { - return backup != null; - } + val isShuffled: Boolean + get() = backup.isNotEmpty() /** * @return an immutable view of the play queue */ - @NonNull - public List getStreams() { - return Collections.unmodifiableList(streams); - } + fun getStreams(): List = Collections.unmodifiableList(streams) /*////////////////////////////////////////////////////////////////////////// // Write ops - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Returns the play queue's update broadcast. - * May be null if the play queue message bus is not initialized. - * - * @return the play queue's update broadcast - */ - @Nullable - public Flowable getBroadcastReceiver() { - return broadcastReceiver; - } + ////////////////////////////////////////////////////////////////////////// */ /** * Changes the current playing index by an offset amount. - *

- * Will emit a {@link SelectEvent} if offset is non-zero. - *

+ * + * Will emit a [SelectEvent] if offset is non-zero. * * @param offset the offset relative to the current index */ - public synchronized void offsetIndex(final int offset) { - setIndex(getIndex() + offset); + @Synchronized + fun offsetIndex(offset: Int) { + this.index += offset } /** * Notifies that a change has occurred. */ - public synchronized void notifyChange() { - broadcast(new AppendEvent(0)); + @Synchronized + fun notifyChange() { + broadcast(AppendEvent(0)) } /** - * Appends the given {@link PlayQueueItem}s to the current play queue. - *

+ * Appends the given [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. - *

* - * @param items {@link PlayQueueItem}s to append + * Will emit a [AppendEvent] on any given context. + * + * @param items [PlayQueueItem]s to append */ - public synchronized void append(@NonNull final List items) { - final List itemList = new ArrayList<>(items); + @Synchronized + fun append(items: List) { + val itemList = items.toMutableList() - if (isShuffled()) { - backup.addAll(itemList); - Collections.shuffle(itemList); + if (this.isShuffled) { + backup.addAll(itemList) + itemList.shuffle() } - if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() - && !itemList.get(0).isAutoQueued()) { - streams.remove(streams.size() - 1); + if (!streams.isEmpty() && streams.last().isAutoQueued && !itemList[0].isAutoQueued) { + streams.removeAt(streams.lastIndex) } - streams.addAll(itemList); + streams.addAll(itemList) - broadcast(new AppendEvent(itemList.size())); + broadcast(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. - *

+ * + * Will emit a [RemoveEvent] if the index is within the play queue index range. * * @param index the index of the item to remove */ - public synchronized void remove(final int index) { - if (index >= streams.size() || index < 0) { - return; + @Synchronized + fun remove(index: Int) { + if (index >= streams.size || index < 0) { + return } - removeInternal(index); - broadcast(new RemoveEvent(index, getIndex())); + removeInternal(index) + broadcast(RemoveEvent(index, this.index)) } /** * Report an exception for the item at the current index in order and skip to the next one - *

+ * * This is done as a separate event as the underlying manager may have * different implementation regarding exceptions. - *

*/ - public synchronized void error() { - final int oldIndex = getIndex(); - queueIndex.incrementAndGet(); - if (streams.size() > queueIndex.get()) { - history.add(streams.get(queueIndex.get())); + @Synchronized + fun error() { + val oldIndex = this.index + queueIndex.incrementAndGet() + if (streams.size > queueIndex.get()) { + history.add(streams[queueIndex.get()]) } - broadcast(new ErrorEvent(oldIndex, getIndex())); + broadcast(ErrorEvent(oldIndex, this.index)) } - private synchronized void removeInternal(final int removeIndex) { - final int currentIndex = queueIndex.get(); - final int size = size(); + @Synchronized + private fun removeInternal(removeIndex: Int) { + val currentIndex = queueIndex.get() + val size = size() if (currentIndex > removeIndex) { - queueIndex.decrementAndGet(); - + queueIndex.decrementAndGet() } else if (currentIndex >= size) { - queueIndex.set(currentIndex % (size - 1)); - + queueIndex.set(currentIndex % (size - 1)) } else if (currentIndex == removeIndex && currentIndex == size - 1) { - queueIndex.set(0); + queueIndex.set(0) } - if (backup != null) { - backup.remove(getItem(removeIndex)); - } + backup.remove(getItem(removeIndex)!!) - history.remove(streams.remove(removeIndex)); - if (streams.size() > queueIndex.get()) { - history.add(streams.get(queueIndex.get())); + history.remove(streams.removeAt(removeIndex)) + if (streams.size > queueIndex.get()) { + history.add(streams[queueIndex.get()]) } } /** * 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 AFTER the + * If the moved item is not the currently playing and moves to an index **AFTER** the * current playing index, then the current playing index is decremented. - * Vice versa if the an item after the currently playing is moved BEFORE. - *

+ * Vice versa if the an item after the currently playing is moved **BEFORE**. * * @param source the original index of the item * @param target the new index of the item */ - public synchronized void move(final int source, final int target) { + @Synchronized + fun move( + source: Int, + target: Int, + ) { if (source < 0 || target < 0) { - return; + return } - if (source >= streams.size() || target >= streams.size()) { - return; + if (source >= streams.size || target >= streams.size) { + return } - final int current = getIndex(); + val current = this.index if (source == current) { - queueIndex.set(target); + queueIndex.set(target) } else if (source < current && target >= current) { - queueIndex.decrementAndGet(); + queueIndex.decrementAndGet() } else if (source > current && target <= current) { - queueIndex.incrementAndGet(); + queueIndex.incrementAndGet() } - final PlayQueueItem playQueueItem = streams.remove(source); - playQueueItem.setAutoQueued(false); - streams.add(target, playQueueItem); - broadcast(new MoveEvent(source, target)); + val playQueueItem = streams.removeAt(source) + playQueueItem.isAutoQueued = false + streams.add(target, playQueueItem) + broadcast(MoveEvent(source, target)) } /** * Sets the recovery record of the item at the index. - *

+ * * Broadcasts a recovery event. - *

* * @param index index of the item * @param position the recovery position */ - public synchronized void setRecovery(final int index, final long position) { - if (index < 0 || index >= streams.size()) { - return; + @Synchronized + fun setRecovery( + index: Int, + position: Long, + ) { + streams.getOrNull(index)?.let { + it.recoveryPosition = position + broadcast(RecoveryEvent(index, position)) } - - streams.get(index).setRecoveryPosition(position); - broadcast(new RecoveryEvent(index, position)); } /** * Revoke the recovery record of the item at the index. - *

+ * * Broadcasts a recovery event. - *

* * @param index index of the item */ - public synchronized void unsetRecovery(final int index) { - setRecovery(index, PlayQueueItem.RECOVERY_UNSET); + @Synchronized + fun unsetRecovery(index: Int) { + setRecovery(index, Long.Companion.MIN_VALUE) } /** * 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. This item will also be added to the history. - *

- *

- * Will emit a {@link ReorderEvent} if shuffled. - *

+ * + * Will emit a [ReorderEvent] if shuffled. * * @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on * top, so shuffling a size-2 list does nothing) */ - public synchronized void shuffle() { + @Synchronized + fun shuffle() { // Create a backup if it doesn't already exist // Note: The backup-list has to be created at all cost (even when size <= 2). // Otherwise it's not possible to enter shuffle-mode! - if (backup == null) { - backup = new ArrayList<>(streams); + if (backup.isEmpty()) { + backup = streams.toMutableList() } // Can't shuffle a list that's empty or only has one element if (size() <= 2) { - return; + return } - final int originalIndex = getIndex(); - final PlayQueueItem currentItem = getItem(); + val originalIndex = this.index + val currentItem = this.item - Collections.shuffle(streams); + streams.shuffle() // Move currentItem to the head of the queue - streams.remove(currentItem); - streams.add(0, currentItem); - queueIndex.set(0); + streams.remove(currentItem!!) + streams.add(0, currentItem) + queueIndex.set(0) - history.add(currentItem); + history.add(currentItem) - broadcast(new ReorderEvent(originalIndex, 0)); + broadcast(ReorderEvent(originalIndex, 0)) } /** * 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. - *

+ * + * Will emit a [ReorderEvent] if a backup exists. */ - public synchronized void unshuffle() { - if (backup == null) { - return; + @Synchronized + fun unshuffle() { + if (backup.isEmpty()) { + return } - final int originIndex = getIndex(); - final PlayQueueItem current = getItem(); + val originIndex = this.index + val current = this.item - streams = backup; - backup = null; + streams = backup + backup = mutableListOf() - final int newIndex = streams.indexOf(current); + val newIndex = streams.indexOf(current!!) if (newIndex != -1) { - queueIndex.set(newIndex); + queueIndex.set(newIndex) } else { - queueIndex.set(0); + queueIndex.set(0) } - if (streams.size() > queueIndex.get()) { - history.add(streams.get(queueIndex.get())); + if (streams.size > queueIndex.get()) { + history.add(streams[queueIndex.get()]) } - broadcast(new ReorderEvent(originIndex, queueIndex.get())); + broadcast(ReorderEvent(originIndex, queueIndex.get())) } /** @@ -498,18 +459,19 @@ public abstract class PlayQueue implements Serializable { * starts playing the last item from history if it exists * * @return true if history is not empty and the item can be played - * */ - public synchronized boolean previous() { - if (history.size() <= 1) { - return false; + */ + @Synchronized + fun previous(): Boolean { + if (history.size <= 1) { + return false } - history.remove(history.size() - 1); + history.removeAt(history.size - 1) - final PlayQueueItem last = history.remove(history.size() - 1); - setIndex(indexOf(last)); + val last = history.removeAt(history.size - 1) + this.index = indexOf(last) - return true; + return true } /* @@ -518,31 +480,18 @@ public abstract class PlayQueue implements Serializable { * This method also gives a chance to track history of items in a queue in * VideoDetailFragment without duplicating items from two identical queues */ - @Override - public boolean equals(final Object o) { - return o instanceof PlayQueue playQueue && streams.equals(playQueue.streams); + override fun equals(o: Any?): Boolean = o is PlayQueue && streams == o.streams + + override fun hashCode(): Int = streams.hashCode() + + fun equalStreamsAndIndex(other: PlayQueue?): Boolean { + return equals(other) && other!!.index == this.index // NOSONAR: other is not null } - @Override - public int hashCode() { - return streams.hashCode(); - } - - public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) { - return equals(other) && other.getIndex() == getIndex(); //NOSONAR: other is not null - } - - public boolean isDisposed() { - return disposed; - } /*////////////////////////////////////////////////////////////////////////// - // Rx Broadcast - //////////////////////////////////////////////////////////////////////////*/ - - private void broadcast(@NonNull final PlayQueueEvent event) { - if (eventBroadcast != null) { - eventBroadcast.onNext(event); - } + // Rx Broadcast + ////////////////////////////////////////////////////////////////////////// */ + private fun broadcast(event: PlayQueueEvent) { + eventBroadcast?.onNext(event) } } - diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt index 8f41ceb60..d6b4b0402 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.kt @@ -1,155 +1,71 @@ -package org.schabi.newpipe.player.playqueue; +package org.schabi.newpipe.player.playqueue -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.extractor.Image +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 java.util.Objects -import org.schabi.newpipe.extractor.Image; -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; +class PlayQueueItem private constructor( + val title: String, + val url: String, + val serviceId: Int, + val duration: Long, + val thumbnails: List, + val uploader: String, + val uploaderUrl: String?, + val streamType: StreamType, +) : Serializable { + // + // ////////////////////////////////////////////////////////////////////// */ + // Item States, keep external access out + // + // ////////////////////////////////////////////////////////////////////// */ + var isAutoQueued: Boolean = false -import java.io.Serializable; -import java.util.List; -import java.util.Objects; + // package-private + var recoveryPosition = Long.Companion.MIN_VALUE + var error: Throwable? = null + private set -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public class PlayQueueItem implements Serializable { - public static final long RECOVERY_UNSET = Long.MIN_VALUE; - private static final String EMPTY_STRING = ""; - - @NonNull - private final String title; - @NonNull - private final String url; - private final int serviceId; - private final long duration; - @NonNull - private final List thumbnails; - @NonNull - private final String uploader; - private final String uploaderUrl; - @NonNull - private final StreamType streamType; - - private boolean isAutoQueued; - - private long recoveryPosition; - private Throwable error; - - PlayQueueItem(@NonNull final StreamInfo info) { - this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), - info.getThumbnails(), info.getUploaderName(), - info.getUploaderUrl(), info.getStreamType()); - - if (info.getStartPosition() > 0) { - setRecoveryPosition(info.getStartPosition() * 1000); + constructor(info: StreamInfo) : this( + info.name.orEmpty(), + info.url.orEmpty(), + info.serviceId, + info.duration, + info.thumbnails, + info.uploaderName.orEmpty(), + info.uploaderUrl, + info.streamType, + ) { + if (info.startPosition > 0) { + this.recoveryPosition = info.startPosition * 1000 } } - PlayQueueItem(@NonNull final StreamInfoItem item) { - this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(), - item.getThumbnails(), item.getUploaderName(), - item.getUploaderUrl(), item.getStreamType()); - } + constructor(item: StreamInfoItem) : this( + item.name.orEmpty(), + item.url.orEmpty(), + item.serviceId, + item.duration, + item.thumbnails, + item.uploaderName.orEmpty(), + item.uploaderUrl, + item.streamType, + ) - @SuppressWarnings("ParameterNumber") - private PlayQueueItem(@Nullable final String name, @Nullable final String url, - final int serviceId, final long duration, - final List thumbnails, @Nullable final String uploader, - final String uploaderUrl, @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.thumbnails = thumbnails; - this.uploader = uploader != null ? uploader : EMPTY_STRING; - this.uploaderUrl = uploaderUrl; - 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 List getThumbnails() { - return thumbnails; - } - - @NonNull - public String getUploader() { - return uploader; - } - - public String getUploaderUrl() { - return uploaderUrl; - } - - @NonNull - public StreamType getStreamType() { - return streamType; - } - - public long getRecoveryPosition() { - return recoveryPosition; - } - - /*package-private*/ void setRecoveryPosition(final long recoveryPosition) { - this.recoveryPosition = recoveryPosition; - } - - @Nullable - public Throwable getError() { - return error; - } - - @NonNull - public Single getStream() { - return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false) + val stream: Single + get() = + ExtractorHelper + .getStreamInfo(serviceId, url, false) .subscribeOn(Schedulers.io()) - .doOnError(throwable -> error = throwable); - } + .doOnError { throwable -> error = throwable } - public boolean isAutoQueued() { - return isAutoQueued; - } + override fun equals(o: Any?) = o is PlayQueueItem && serviceId == o.serviceId && url == o.url - //////////////////////////////////////////////////////////////////////////// - // Item States, keep external access out - //////////////////////////////////////////////////////////////////////////// - - public void setAutoQueued(final boolean autoQueued) { - isAutoQueued = autoQueued; - } - - @Override - public boolean equals(final Object o) { - return o instanceof PlayQueueItem item - && serviceId == item.serviceId - && url.equals(item.url); - } - - @Override - public int hashCode() { - return Objects.hash(url, serviceId); - } + override fun hashCode() = Objects.hash(url, serviceId) }