react-native-queue-player 1.0.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +12 -6
  2. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +15 -3
  3. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +57 -10
  4. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +45 -16
  5. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceArtworkLoaderTest.kt +28 -0
  6. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +1 -1
  7. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +103 -0
  8. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +1 -1
  9. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  10. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +3 -3
  11. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +7 -7
  12. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +6 -6
  13. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +2 -2
  14. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +1 -1
  15. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +1 -1
  16. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +1 -1
  17. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +1 -1
  18. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +41 -2
  19. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +34 -1
  20. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +1 -1
  21. package/ios/QueueSkipArithmetic.swift +55 -9
  22. package/ios/Tests/AVQueueBuilderTests.swift +9 -9
  23. package/ios/Tests/SkipIndexTests.swift +97 -0
  24. package/ios/TrackPlayer.swift +37 -13
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/types.js +9 -0
  27. package/lib/module/types.js.map +1 -1
  28. package/lib/typescript/TrackPlayer.nitro.d.ts +7 -7
  29. package/lib/typescript/index.d.ts +1 -1
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/types.d.ts +24 -0
  32. package/lib/typescript/types.d.ts.map +1 -1
  33. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +4 -0
  34. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +7 -1
  35. package/nitrogen/generated/android/c++/JSkipToPreviousBehavior.hpp +58 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/SkipToPreviousBehavior.kt +23 -0
  38. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +18 -0
  39. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  40. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +3 -0
  41. package/nitrogen/generated/ios/swift/PlayerConfig.swift +12 -1
  42. package/nitrogen/generated/ios/swift/SkipToPreviousBehavior.swift +40 -0
  43. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +8 -1
  44. package/nitrogen/generated/shared/c++/SkipToPreviousBehavior.hpp +76 -0
  45. package/package.json +1 -1
  46. package/src/TrackPlayer.nitro.ts +7 -7
  47. package/src/index.ts +1 -0
  48. package/src/types.ts +25 -0
package/README.md CHANGED
@@ -140,9 +140,17 @@ Lock-screen / Now Playing controls and remote commands work automatically once t
140
140
 
141
141
  ## Common UX patterns
142
142
 
143
- ### Apple-Music-style "tap Previous restarts when position > 3s"
143
+ ### Restart-or-previous skip-back (the "tap Previous restarts, then goes back" model)
144
144
 
145
- The library ships the **primitive** (`canSkipPrevious` = "is there a previous track under the current repeat mode") and leaves the **policy** (rewind-vs-skip threshold) to your app:
145
+ The standard music-player behaviour — a skip-back restarts the current track when it's past a few seconds, otherwise goes to the previous track is built in. Enable it with `skipToPreviousBehavior`:
146
+
147
+ ```ts
148
+ await player.configure({ skipToPreviousBehavior: 'restart-or-previous' });
149
+ ```
150
+
151
+ Now every `skipToPrevious()` — from your UI, the lock screen, Bluetooth, Android Auto, or CarPlay — restarts the current track (seeks to 0) when the position is past ~3 s, and goes to the previous track under it. Because it's native, it stays consistent across every surface (a JS handler can't cleanly intercept the OS Previous button), and the restart is a seek so it does **not** fire `onTrackChange`. With the option enabled, `canSkipPrevious` stays `true` while a track is loaded, so the Previous control never greys out — even on the first track. The default `'previous'` keeps skip-back always going to the previous track. (Local playback only; during cast, Previous routes to the receiver.)
152
+
153
+ For a different threshold or bespoke logic on **your own** in-app button (the OS controls follow `skipToPreviousBehavior`), leave the option at its default and branch on the live position:
146
154
 
147
155
  ```tsx
148
156
  import { getTrackPlayer, useProgress } from 'react-native-queue-player';
@@ -151,18 +159,16 @@ function PreviousButton() {
151
159
  const { position } = useProgress();
152
160
  const onPress = async () => {
153
161
  const player = getTrackPlayer();
154
- if (position > 3) await player.seekTo(0);
162
+ if (position > 5) await player.seekTo(0);
155
163
  else await player.skipToPrevious();
156
164
  };
157
165
  return /* your <Button> here */;
158
166
  }
159
167
  ```
160
168
 
161
- The 3-second threshold is the Apple Music default; tune to taste.
162
-
163
169
  ### Skip-button enable/disable from `useCanSkip`
164
170
 
165
- `canSkipNext` / `canSkipPrevious` already account for repeat mode: under `off` they go false at queue boundaries; under `queue` they wrap and stay true; under `track` they stay true.
171
+ `canSkipNext` / `canSkipPrevious` account for repeat mode: under `off` they go false at queue boundaries; under `queue` they wrap and stay true; under `track` they stay true. With `skipToPreviousBehavior: 'restart-or-previous'`, `canSkipPrevious` stays true whenever a track is loaded (the Previous control never greys out).
166
172
 
167
173
  ```tsx
168
174
  const { canSkipNext, canSkipPrevious } = useCanSkip();
@@ -762,8 +762,15 @@ class PlaybackService : HeadlessJsMediaService() {
762
762
  * (the same one ExoPlayer drives for HTTP playback), so consumer
763
763
  * `userAgent` + custom HTTP headers configured on the player's
764
764
  * `MediaSource.Factory` propagate into artwork fetches without a
765
- * second HTTP client. `CacheBitmapLoader` deduplicates repeat
766
- * requests for the same URI inside a single foreground session.
765
+ * second HTTP client. It mirrors Media3's own default loader config:
766
+ * `setMaximumOutputDimension(MediaSession.getBitmapDimensionLimit(...))`
767
+ * downsamples an oversized consumer image during decode (device-adaptive
768
+ * cap; no full-resolution bitmap for a small control), and
769
+ * `setMakeShared(true)` puts the bitmap in shared memory so the System UI
770
+ * process reuses it instead of copying. Supplying our own loader (for the
771
+ * placeholder guarantee below) had otherwise dropped both defaults, which
772
+ * is what left artwork decoding unbounded. `CacheBitmapLoader` deduplicates
773
+ * repeat requests for the same URI inside a single foreground session.
767
774
  * The outer [PlaceholderFallbackBitmapLoader] guarantees a bitmap
768
775
  * always reaches the notification — the OEM-skin failure mode
769
776
  * where a null bitmap drops the transport controls is closed.
@@ -776,7 +783,12 @@ class PlaybackService : HeadlessJsMediaService() {
776
783
  @VisibleForTesting
777
784
  internal fun buildBitmapLoader(): PlaceholderFallbackBitmapLoader =
778
785
  PlaceholderFallbackBitmapLoader(
779
- delegate = CacheBitmapLoader(DataSourceBitmapLoader.Builder(this).build()),
786
+ delegate = CacheBitmapLoader(
787
+ DataSourceBitmapLoader.Builder(this)
788
+ .setMaximumOutputDimension(MediaSession.getBitmapDimensionLimit(this))
789
+ .setMakeShared(true)
790
+ .build()
791
+ ),
780
792
  // Read on each fallback so a placeholder set at `configure` (after
781
793
  // this loader was built in onCreate) is reflected — `PlaceholderArtwork`
782
794
  // returns the consumer image when one is configured, else the built-in.
@@ -59,9 +59,9 @@ internal object QueueSkipArithmetic {
59
59
  * Previous-track index. Same early-bail rules as [computeNext].
60
60
  *
61
61
  * OFF and TRACK both return null at index 0 (no previous track, no
62
- * wrap). Apps that want the Apple-Music-style "tap Previous at
63
- * track 0 restarts current" UX implement that in their button
64
- * handler.
62
+ * wrap). The "restart current when past the threshold" model is
63
+ * available via `PlayerConfig.skipToPreviousBehavior` (resolved by
64
+ * [resolveSkipToPrevious]).
65
65
  */
66
66
  fun computePrevious(
67
67
  trackCount: Int,
@@ -99,15 +99,62 @@ internal object QueueSkipArithmetic {
99
99
  * OFF and TRACK are false at index 0 (no wrap); QUEUE is always
100
100
  * true with an active track (wraps).
101
101
  *
102
- * **Apps that want the Apple-Music-style "tap Previous restarts
103
- * when position > 3s" UX** should implement that in their button
104
- * handler: read playback position, branch to `seekTo(0)` vs
105
- * `skipToPrevious()` themselves. Lib intentionally ships the
106
- * primitive (skip if there's a track to skip to), not the policy.
102
+ * When [restartEnabled] is true (`PlayerConfig.skipToPreviousBehavior
103
+ * == RESTART_OR_PREVIOUS`), Previous is enabled whenever a track is
104
+ * loaded even at index 0, where past the threshold it restarts the
105
+ * current track so the control never greys out.
107
106
  */
108
107
  fun canSkipPrevious(
109
108
  trackCount: Int,
110
109
  currentIndex: Int,
111
- repeatMode: QueueSkipRepeatMode
112
- ): Boolean = computePrevious(trackCount, currentIndex, repeatMode) != null
110
+ repeatMode: QueueSkipRepeatMode,
111
+ restartEnabled: Boolean = false
112
+ ): Boolean =
113
+ if (restartEnabled) trackCount > 0
114
+ else computePrevious(trackCount, currentIndex, repeatMode) != null
115
+
116
+ /**
117
+ * Position (ms) at or below which a restart-or-previous skip-back goes
118
+ * to the previous track; past it, the current track restarts. Matches
119
+ * Media3's default `maxSeekToPreviousPosition` (industry-standard 3 s).
120
+ */
121
+ const val RESTART_THRESHOLD_MS: Long = 3000L
122
+
123
+ /** What a skip-to-previous should do, resolved from position + behavior. */
124
+ sealed interface SkipToPreviousAction {
125
+ /** Navigate to this queue index (a real track change). */
126
+ data class Previous(val index: Int) : SkipToPreviousAction
127
+ /** Restart the current track (seek to 0) — not a track change. */
128
+ object RestartCurrent : SkipToPreviousAction
129
+ /** Nothing to do (empty queue). */
130
+ object NoOp : SkipToPreviousAction
131
+ }
132
+
133
+ /**
134
+ * Resolves a skip-to-previous. When [restartEnabled] is false this is
135
+ * exactly [computePrevious] (previous track, or nothing). When true it
136
+ * applies the threshold model, matching Media3 `BasePlayer.seekToPrevious`:
137
+ * a previous track exists AND `positionMs <= thresholdMs` -> previous;
138
+ * otherwise (past threshold, or first track with no previous) -> restart
139
+ * the current track.
140
+ */
141
+ fun resolveSkipToPrevious(
142
+ trackCount: Int,
143
+ currentIndex: Int,
144
+ repeatMode: QueueSkipRepeatMode,
145
+ restartEnabled: Boolean,
146
+ positionMs: Long,
147
+ thresholdMs: Long = RESTART_THRESHOLD_MS
148
+ ): SkipToPreviousAction {
149
+ if (trackCount <= 0 || currentIndex < 0) return SkipToPreviousAction.NoOp
150
+ val prev = computePrevious(trackCount, currentIndex, repeatMode)
151
+ if (!restartEnabled) {
152
+ return if (prev != null) SkipToPreviousAction.Previous(prev) else SkipToPreviousAction.NoOp
153
+ }
154
+ return if (prev != null && positionMs <= thresholdMs) {
155
+ SkipToPreviousAction.Previous(prev)
156
+ } else {
157
+ SkipToPreviousAction.RestartCurrent
158
+ }
159
+ }
113
160
  }
@@ -201,7 +201,7 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
201
201
  visualizationEnabled = null,
202
202
  clampSeekToBuffered = null,
203
203
  lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null,
204
- progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
204
+ progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
205
205
  )
206
206
 
207
207
  @Volatile
@@ -2171,25 +2171,46 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
2171
2171
  }
2172
2172
 
2173
2173
  /**
2174
- * Standard skip-previous: go to the previous track if there is one
2175
- * (per the arithmetic helper; OFF/TRACK don't wrap, QUEUE wraps).
2176
- * Under TRACK this navigates like OFF and repeat-one persists. Apps
2177
- * that want the Apple-Music-style "tap Previous restarts when
2178
- * position > 3s" UX implement that in their button handler — the
2179
- * lib provides the primitive (skip if there's a track to skip to),
2180
- * not the policy.
2174
+ * Skip-previous, resolved by [QueueSkipArithmetic.resolveSkipToPrevious]
2175
+ * from `PlayerConfig.skipToPreviousBehavior`. With the default PREVIOUS this
2176
+ * goes to the previous track if there is one (OFF/TRACK don't wrap, QUEUE
2177
+ * wraps; TRACK navigates like OFF and repeat-one persists). With
2178
+ * RESTART_OR_PREVIOUS it restarts the current track (seek to 0) when past
2179
+ * the threshold, otherwise goes to the previous track.
2181
2180
  */
2182
2181
  @VisibleForTesting
2183
2182
  internal fun skipToPreviousInternal() {
2184
2183
  val p = player ?: return
2185
- val prev = computePreviousIndex() ?: return
2186
- currentTrackIndex = prev
2187
- pendingTrackChangeReason = TrackChangeReason.USER_SKIP_PREVIOUS
2188
- if (prev < p.mediaItemCount) {
2189
- serviceBinder?.engine?.seekTo(prev, 0L)
2184
+ val action = QueueSkipArithmetic.resolveSkipToPrevious(
2185
+ trackCount = tracks.size,
2186
+ currentIndex = currentTrackIndex,
2187
+ repeatMode = repeatModeAsSkipMode(),
2188
+ restartEnabled = skipRestartEnabled(),
2189
+ positionMs = serviceBinder?.engine?.currentPositionMs ?: 0L
2190
+ )
2191
+ when (action) {
2192
+ is QueueSkipArithmetic.SkipToPreviousAction.Previous -> {
2193
+ val prev = action.index
2194
+ currentTrackIndex = prev
2195
+ pendingTrackChangeReason = TrackChangeReason.USER_SKIP_PREVIOUS
2196
+ if (prev < p.mediaItemCount) {
2197
+ serviceBinder?.engine?.seekTo(prev, 0L)
2198
+ }
2199
+ rescheduleLookahead()
2200
+ recomputeCapabilities()
2201
+ }
2202
+ QueueSkipArithmetic.SkipToPreviousAction.RestartCurrent -> {
2203
+ // Restart the current track from 0 — a seek, not a track change.
2204
+ // A same-index seek emits no onMediaItemTransition, so no
2205
+ // onTrackChange; leave currentTrackIndex + pendingTrackChangeReason
2206
+ // untouched (stashing a reason no transition consumes would leak
2207
+ // into the next real skip).
2208
+ if (currentTrackIndex in 0 until p.mediaItemCount) {
2209
+ serviceBinder?.engine?.seekTo(currentTrackIndex, 0L)
2210
+ }
2211
+ }
2212
+ QueueSkipArithmetic.SkipToPreviousAction.NoOp -> return
2190
2213
  }
2191
- rescheduleLookahead()
2192
- recomputeCapabilities()
2193
2214
  }
2194
2215
 
2195
2216
  /**
@@ -2221,6 +2242,14 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
2221
2242
  RepeatMode.QUEUE -> QueueSkipRepeatMode.QUEUE
2222
2243
  }
2223
2244
 
2245
+ /**
2246
+ * Whether skip-to-previous restarts the current track past the threshold
2247
+ * (`PlayerConfig.skipToPreviousBehavior == RESTART_OR_PREVIOUS`). Read live
2248
+ * from the current config at each skip + capability recompute.
2249
+ */
2250
+ private fun skipRestartEnabled(): Boolean =
2251
+ config.skipToPreviousBehavior == SkipToPreviousBehavior.RESTART_OR_PREVIOUS
2252
+
2224
2253
  /**
2225
2254
  * Recompute [cachedSkipCapability] from
2226
2255
  * authoritative state and fire [onSkipCapabilityChange] if either
@@ -2245,7 +2274,7 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
2245
2274
  val mode = repeatModeAsSkipMode()
2246
2275
  val count = tracks.size
2247
2276
  val cur = currentTrackIndex
2248
- val newPrev = QueueSkipArithmetic.canSkipPrevious(count, cur, mode)
2277
+ val newPrev = QueueSkipArithmetic.canSkipPrevious(count, cur, mode, skipRestartEnabled())
2249
2278
  val newNext = QueueSkipArithmetic.canSkipNext(count, cur, mode)
2250
2279
  val current = cachedSkipCapability
2251
2280
  val capChanged =
@@ -3,7 +3,9 @@ package com.margelo.nitro.queueplayer
3
3
  import androidx.media3.common.util.UnstableApi
4
4
  import androidx.media3.datasource.DataSourceBitmapLoader
5
5
  import androidx.media3.session.CacheBitmapLoader
6
+ import androidx.media3.session.MediaSession
6
7
  import org.junit.After
8
+ import org.junit.Assert.assertEquals
7
9
  import org.junit.Assert.assertNotNull
8
10
  import org.junit.Assert.assertSame
9
11
  import org.junit.Assert.assertTrue
@@ -78,6 +80,32 @@ class PlaybackServiceArtworkLoaderTest {
78
80
  )
79
81
  }
80
82
 
83
+ @Test
84
+ fun `DataSourceBitmapLoader mirrors Media3's default cap + shared-memory bitmap`() {
85
+ val loader = service.buildBitmapLoader()
86
+ val cached = loader.javaClass.getDeclaredField("delegate")
87
+ .apply { isAccessible = true }.get(loader) as CacheBitmapLoader
88
+ val backing = cached.javaClass.getDeclaredField("bitmapLoader")
89
+ .apply { isAccessible = true }.get(cached) as DataSourceBitmapLoader
90
+ val maxDim = DataSourceBitmapLoader::class.java
91
+ .getDeclaredField("maximumOutputDimension")
92
+ .apply { isAccessible = true }
93
+ .getInt(backing)
94
+ assertEquals(
95
+ "artwork decode is capped to Media3's device-adaptive limit (no full-res allocation)",
96
+ MediaSession.getBitmapDimensionLimit(service),
97
+ maxDim
98
+ )
99
+ val makeShared = DataSourceBitmapLoader::class.java
100
+ .getDeclaredField("makeShared")
101
+ .apply { isAccessible = true }
102
+ .getBoolean(backing)
103
+ assertTrue(
104
+ "decoded bitmap is shared-memory so the System UI process reuses it instead of copying",
105
+ makeShared
106
+ )
107
+ }
108
+
81
109
  @Test
82
110
  fun `repeat builds reuse the same placeholder bitmap`() {
83
111
  // The loader holds a placeholder *provider* closure; invoking it
@@ -785,7 +785,7 @@ class PlaybackServiceCallbackTest {
785
785
  audioCategoryOptions = null,
786
786
  visualizationEnabled = null,
787
787
  clampSeekToBuffered = null,
788
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null,
788
+ lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null,
789
789
  ),
790
790
  context,
791
791
  )
@@ -340,4 +340,107 @@ class QueueSkipArithmeticTest {
340
340
  assertFalse(QueueSkipArithmetic.canSkipNext(1, 0, QueueSkipRepeatMode.TRACK))
341
341
  assertFalse(QueueSkipArithmetic.canSkipPrevious(1, 0, QueueSkipRepeatMode.TRACK))
342
342
  }
343
+
344
+ // --- resolveSkipToPrevious — mode OFF (position ignored, current behavior)
345
+
346
+ private fun resolve(
347
+ count: Int = 3, index: Int, mode: QueueSkipRepeatMode,
348
+ restart: Boolean, positionMs: Long
349
+ ) = QueueSkipArithmetic.resolveSkipToPrevious(count, index, mode, restart, positionMs)
350
+
351
+ private val previous1 = QueueSkipArithmetic.SkipToPreviousAction.Previous(1)
352
+ private val previous2 = QueueSkipArithmetic.SkipToPreviousAction.Previous(2)
353
+ private val restart = QueueSkipArithmetic.SkipToPreviousAction.RestartCurrent
354
+ private val noOp = QueueSkipArithmetic.SkipToPreviousAction.NoOp
355
+
356
+ @Test
357
+ fun `resolve mode OFF ignores position and goes previous`() {
358
+ assertEquals(previous1, resolve(index = 2, mode = QueueSkipRepeatMode.OFF, restart = false, positionMs = 99_000L))
359
+ }
360
+
361
+ @Test
362
+ fun `resolve mode OFF first track is noop`() {
363
+ assertEquals(noOp, resolve(index = 0, mode = QueueSkipRepeatMode.OFF, restart = false, positionMs = 99_000L))
364
+ }
365
+
366
+ @Test
367
+ fun `resolve mode OFF queue wraps`() {
368
+ assertEquals(previous2, resolve(index = 0, mode = QueueSkipRepeatMode.QUEUE, restart = false, positionMs = 0L))
369
+ }
370
+
371
+ @Test
372
+ fun `resolve mode OFF empty queue is noop`() {
373
+ assertEquals(noOp, resolve(count = 0, index = -1, mode = QueueSkipRepeatMode.OFF, restart = false, positionMs = 0L))
374
+ }
375
+
376
+ // --- resolveSkipToPrevious — mode ON (3s threshold model)
377
+
378
+ @Test
379
+ fun `resolve past threshold restarts even with a previous`() {
380
+ for (pos in listOf(3001L, 5000L, 600_000L)) {
381
+ assertEquals("position $pos > 3000 restarts", restart,
382
+ resolve(index = 2, mode = QueueSkipRepeatMode.OFF, restart = true, positionMs = pos))
383
+ }
384
+ }
385
+
386
+ @Test
387
+ fun `resolve threshold boundary goes previous, one past restarts`() {
388
+ assertEquals(previous1, resolve(index = 2, mode = QueueSkipRepeatMode.OFF, restart = true, positionMs = 3000L))
389
+ assertEquals(restart, resolve(index = 2, mode = QueueSkipRepeatMode.OFF, restart = true, positionMs = 3001L))
390
+ }
391
+
392
+ @Test
393
+ fun `resolve under threshold goes previous`() {
394
+ for (pos in listOf(0L, 1L, 2999L)) {
395
+ assertEquals("position $pos <= 3000 goes previous", previous1,
396
+ resolve(index = 2, mode = QueueSkipRepeatMode.OFF, restart = true, positionMs = pos))
397
+ }
398
+ }
399
+
400
+ @Test
401
+ fun `resolve first track under threshold restarts when no previous`() {
402
+ assertEquals(restart, resolve(index = 0, mode = QueueSkipRepeatMode.OFF, restart = true, positionMs = 1000L))
403
+ assertEquals(restart, resolve(index = 0, mode = QueueSkipRepeatMode.TRACK, restart = true, positionMs = 1000L))
404
+ }
405
+
406
+ @Test
407
+ fun `resolve first track under threshold queue wraps`() {
408
+ assertEquals(previous2, resolve(index = 0, mode = QueueSkipRepeatMode.QUEUE, restart = true, positionMs = 1000L))
409
+ }
410
+
411
+ @Test
412
+ fun `resolve first track past threshold restarts under every mode`() {
413
+ for (mode in QueueSkipRepeatMode.values()) {
414
+ assertEquals("first track past threshold restarts under $mode", restart,
415
+ resolve(index = 0, mode = mode, restart = true, positionMs = 9000L))
416
+ }
417
+ }
418
+
419
+ @Test
420
+ fun `resolve mode ON empty queue is noop`() {
421
+ assertEquals(noOp, resolve(count = 0, index = -1, mode = QueueSkipRepeatMode.OFF, restart = true, positionMs = 9000L))
422
+ }
423
+
424
+ // --- canSkipPrevious with restartEnabled (forced true)
425
+
426
+ @Test
427
+ fun `canSkipPrevious forced true at every index and mode`() {
428
+ for (mode in QueueSkipRepeatMode.values()) {
429
+ for (index in 0 until 3) {
430
+ assertTrue("restart mode forces canSkipPrevious true at index $index under $mode",
431
+ QueueSkipArithmetic.canSkipPrevious(3, index, mode, restartEnabled = true))
432
+ }
433
+ }
434
+ }
435
+
436
+ @Test
437
+ fun `canSkipPrevious restartEnabled false for empty queue`() {
438
+ assertFalse(QueueSkipArithmetic.canSkipPrevious(0, -1, QueueSkipRepeatMode.OFF, restartEnabled = true))
439
+ }
440
+
441
+ @Test
442
+ fun `canSkipPrevious restart disabled matches existing`() {
443
+ assertFalse(QueueSkipArithmetic.canSkipPrevious(3, 0, QueueSkipRepeatMode.OFF, restartEnabled = false))
444
+ assertTrue(QueueSkipArithmetic.canSkipPrevious(3, 1, QueueSkipRepeatMode.OFF, restartEnabled = false))
445
+ }
343
446
  }
@@ -66,7 +66,7 @@ class TrackPlayerAirPlayMetadataWiringTest {
66
66
  retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null,
67
67
  audioCategoryOptions = null, visualizationEnabled = null,
68
68
  clampSeekToBuffered = null,
69
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null,
69
+ lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null,
70
70
  ),
71
71
  context(),
72
72
  )
@@ -44,7 +44,7 @@ class TrackPlayerAudioFocusTest {
44
44
  player = TrackPlayer()
45
45
  serviceHandle = bindPlaybackServiceForTest(player, context())
46
46
  player.configureInternal(
47
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
47
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
48
48
  )
49
49
  }
50
50
 
@@ -51,7 +51,7 @@ class TrackPlayerEventsTest {
51
51
  player = TrackPlayer()
52
52
  serviceHandle = bindPlaybackServiceForTest(player, context())
53
53
  player.configureInternal(
54
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
54
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
55
55
  )
56
56
  }
57
57
 
@@ -230,7 +230,7 @@ class TrackPlayerEventsTest {
230
230
  // the public effects — a new configure + getStateInternal
231
231
  // should return NONE.
232
232
  player.configureInternal(
233
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
233
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
234
234
  )
235
235
  assertEquals(PlayerState.NONE, player.getStateInternal())
236
236
  }
@@ -479,7 +479,7 @@ class TrackPlayerEventsTest {
479
479
  serviceHandle.destroy()
480
480
  serviceHandle = bindPlaybackServiceForTest(player, context())
481
481
  player.configureInternal(
482
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
482
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
483
483
  )
484
484
  // No listener is bound to the new ExoPlayer's actual STATE_ENDED
485
485
  // path in a Robolectric test, but the callback slot itself is
@@ -68,7 +68,7 @@ class TrackPlayerLifecycleTest {
68
68
  autoRetries = null,
69
69
  retryBackoffMs = null,
70
70
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
71
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
71
+ lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
72
72
  )
73
73
  player.configureInternal(config, context())
74
74
 
@@ -92,7 +92,7 @@ class TrackPlayerLifecycleTest {
92
92
  // public path owns the destroy step. This test drives the
93
93
  // destroy + reconstruct shape directly to mirror what
94
94
  // `configure()` does end-to-end.
95
- val empty = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
95
+ val empty = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
96
96
 
97
97
  player.configureInternal(empty, context())
98
98
  val firstInstance = player.player
@@ -125,12 +125,12 @@ class TrackPlayerLifecycleTest {
125
125
  val first = PlayerConfig(
126
126
  httpHeaders = mapOf("A" to "1"), userAgent = "ua-1",
127
127
  autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
128
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
128
+ lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
129
129
  )
130
130
  val second = PlayerConfig(
131
131
  httpHeaders = mapOf("B" to "2"), userAgent = "ua-2",
132
132
  autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
133
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
133
+ lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
134
134
  )
135
135
 
136
136
  player.configureInternal(first, context())
@@ -144,7 +144,7 @@ class TrackPlayerLifecycleTest {
144
144
  val configured = PlayerConfig(
145
145
  httpHeaders = mapOf("h" to "v"), userAgent = "ua",
146
146
  autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
147
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
147
+ lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
148
148
  )
149
149
  player.configureInternal(configured, context())
150
150
  // Non-default playback state the consumer set post-configure.
@@ -180,7 +180,7 @@ class TrackPlayerLifecycleTest {
180
180
  @Test
181
181
  fun `destroyInternal is safe to call twice`() {
182
182
  player.configureInternal(
183
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
183
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
184
184
  )
185
185
  player.destroyInternal()
186
186
  player.destroyInternal()
@@ -189,7 +189,7 @@ class TrackPlayerLifecycleTest {
189
189
 
190
190
  @Test
191
191
  fun `configureInternal after destroyInternal rebuilds the ExoPlayer`() {
192
- val cfg = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
192
+ val cfg = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
193
193
 
194
194
  player.configureInternal(cfg, context())
195
195
  val first = player.player
@@ -80,7 +80,7 @@ class TrackPlayerLookaheadConfigTest {
80
80
  httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
81
81
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
82
82
  visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = 250.0, lookaheadCacheEvictionPolicy = null,
83
- progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
83
+ progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
84
84
  ),
85
85
  context()
86
86
  )
@@ -120,7 +120,7 @@ class TrackPlayerLookaheadConfigTest {
120
120
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
121
121
  visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null,
122
122
  lookaheadCacheEvictionPolicy = policy,
123
- progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
123
+ progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
124
124
  )
125
125
 
126
126
  // --- setLookaheadCacheInternal
@@ -298,7 +298,7 @@ class TrackPlayerLookaheadConfigTest {
298
298
  @Test
299
299
  fun `re-configure with changed httpHeaders rebuilds the player (writer + cache)`() {
300
300
  player.configureInternal(
301
- PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-1"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null),
301
+ PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-1"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null),
302
302
  context()
303
303
  )
304
304
  val firstWriter = player.lookaheadCacheWriter
@@ -307,7 +307,7 @@ class TrackPlayerLookaheadConfigTest {
307
307
  assertNotNull(firstCache)
308
308
 
309
309
  player.configureInternal(
310
- PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-2"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null),
310
+ PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-2"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null),
311
311
  context()
312
312
  )
313
313
 
@@ -339,7 +339,7 @@ class TrackPlayerLookaheadConfigTest {
339
339
  // simulated here by `destroyInternal` between
340
340
  // `configureInternal` calls) yields a fresh writer instance,
341
341
  // even when headers are unchanged.
342
- val cfg = PlayerConfig(httpHeaders = mapOf("X-Auth" to "token"), userAgent = "rnqp/test", autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
342
+ val cfg = PlayerConfig(httpHeaders = mapOf("X-Auth" to "token"), userAgent = "rnqp/test", autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
343
343
  player.configureInternal(cfg, context())
344
344
  val firstWriter = player.lookaheadCacheWriter
345
345
 
@@ -413,5 +413,5 @@ class TrackPlayerLookaheadConfigTest {
413
413
 
414
414
  // --- Helpers
415
415
 
416
- private fun emptyConfig() = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
416
+ private fun emptyConfig() = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
417
417
  }
@@ -45,7 +45,7 @@ class TrackPlayerMutationTest {
45
45
  player = TrackPlayer()
46
46
  serviceHandle = bindPlaybackServiceForTest(player, context())
47
47
  player.configureInternal(
48
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
48
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
49
49
  )
50
50
  player.setQueueInternal((1..5).map { track("t$it") })
51
51
  // cur defaults to 0 after setQueue; tests needing mid-queue
@@ -127,7 +127,7 @@ class TrackPlayerMutationTest {
127
127
  val freshHandle = bindPlaybackServiceForTest(fresh, context())
128
128
  try {
129
129
  fresh.configureInternal(
130
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
130
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
131
131
  )
132
132
  assertEquals(-1, fresh.currentTrackIndex)
133
133
  assertEquals(Player.STATE_IDLE, fresh.player!!.playbackState)
@@ -35,7 +35,7 @@ class TrackPlayerProgressThrottleTest {
35
35
  httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
36
36
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
37
37
  visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null,
38
- progressUpdateIntervalMs = foregroundMs, backgroundProgressUpdateIntervalMs = backgroundMs, placeholderArtworkUri = null
38
+ progressUpdateIntervalMs = foregroundMs, backgroundProgressUpdateIntervalMs = backgroundMs, placeholderArtworkUri = null, skipToPreviousBehavior = null
39
39
  )
40
40
 
41
41
  @Before
@@ -47,7 +47,7 @@ class TrackPlayerQueueChangeTest {
47
47
  player = TrackPlayer()
48
48
  serviceHandle = bindPlaybackServiceForTest(player, context())
49
49
  player.configureInternal(
50
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
50
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
51
51
  )
52
52
  player.setQueueInternal((1..5).map { track("t$it") })
53
53
  }
@@ -55,7 +55,7 @@ class TrackPlayerQueueTest {
55
55
  player = TrackPlayer()
56
56
  serviceHandle = bindPlaybackServiceForTest(player, context())
57
57
  player.configureInternal(
58
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
58
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
59
59
  )
60
60
  }
61
61