mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
address second review: shuffle cache invalidation, locking, ended latch
This commit is contained in:
@@ -109,11 +109,8 @@ class QueueRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val (entries, order, shuffled) = loaded
|
val (entries, order, shuffled) = loaded
|
||||||
if (shuffled && order == null) {
|
val valid = if (shuffled && order == null) null else order
|
||||||
setShuffle(false, null)
|
return QueueSnapshot(entries, valid, queueVersion)
|
||||||
return QueueSnapshot(entries, null, queueVersion)
|
|
||||||
}
|
|
||||||
return QueueSnapshot(entries, order, queueVersion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun modes(): QueueModes {
|
suspend fun modes(): QueueModes {
|
||||||
@@ -126,15 +123,24 @@ class QueueRepository(
|
|||||||
order: LongArray?,
|
order: LongArray?,
|
||||||
) {
|
) {
|
||||||
val seed = if (enabled && order != null) System.nanoTime() else 0L
|
val seed = if (enabled && order != null) System.nanoTime() else 0L
|
||||||
dao.clearShuffleOrder()
|
db.useWriterConnection { transactor ->
|
||||||
if (enabled && order != null) {
|
transactor.immediateTransaction {
|
||||||
dao.insertShuffleOrder(order.mapIndexed { index, flat -> ShuffleOrder(index.toLong(), flat) })
|
dao.clearShuffleOrder()
|
||||||
|
if (enabled && order != null) {
|
||||||
|
dao.insertShuffleOrder(order.mapIndexed { index, flat -> ShuffleOrder(index.toLong(), flat) })
|
||||||
|
}
|
||||||
|
dao.setCursor(cursorRow().copy(shuffleEnabled = enabled, shuffleSeed = seed))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dao.setCursor(cursorRow().copy(shuffleEnabled = enabled, shuffleSeed = seed))
|
|
||||||
cachedShuffleSeed = 0
|
cachedShuffleSeed = 0
|
||||||
cachedShuffleOrder = null
|
cachedShuffleOrder = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invalidateLibraryCache() {
|
||||||
|
cachedFlatIds = null
|
||||||
|
queueVersion++
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setRepeat(mode: Int) = dao.setCursor(cursorRow().copy(repeatMode = mode))
|
suspend fun setRepeat(mode: Int) = dao.setCursor(cursorRow().copy(repeatMode = mode))
|
||||||
|
|
||||||
private suspend fun loadShuffleOrder(
|
private suspend fun loadShuffleOrder(
|
||||||
@@ -271,7 +277,7 @@ class QueueRepository(
|
|||||||
|
|
||||||
private suspend fun flatIds(snapshot: QueueSnapshot): List<String> {
|
private suspend fun flatIds(snapshot: QueueSnapshot): List<String> {
|
||||||
val cached = cachedFlatIds
|
val cached = cachedFlatIds
|
||||||
if (cached != null && cachedFlatVersion == snapshot.version) return cached
|
if (cached != null && cachedFlatVersion == snapshot.version && cached.size.toLong() == snapshot.size) return cached
|
||||||
val ids = ArrayList<String>(snapshot.size.toInt())
|
val ids = ArrayList<String>(snapshot.size.toInt())
|
||||||
for (resolved in snapshot.entries) {
|
for (resolved in snapshot.entries) {
|
||||||
val entry = resolved.entry
|
val entry = resolved.entry
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.subtracks.data.sync
|
package com.subtracks.data.sync
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.subtracks.data.repo.QueueRepository
|
||||||
import com.subtracks.data.repo.SourceRepository
|
import com.subtracks.data.repo.SourceRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -23,6 +24,7 @@ sealed interface SyncStatus {
|
|||||||
|
|
||||||
class SyncManager(
|
class SyncManager(
|
||||||
private val sourceRepository: SourceRepository,
|
private val sourceRepository: SourceRepository,
|
||||||
|
private val queueRepository: QueueRepository,
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
private val _status = MutableStateFlow<SyncStatus>(SyncStatus.Idle)
|
private val _status = MutableStateFlow<SyncStatus>(SyncStatus.Idle)
|
||||||
@@ -36,7 +38,10 @@ class SyncManager(
|
|||||||
_status.value = SyncStatus.Running
|
_status.value = SyncStatus.Running
|
||||||
_status.value =
|
_status.value =
|
||||||
sourceRepository.sync().fold(
|
sourceRepository.sync().fold(
|
||||||
onSuccess = { SyncStatus.Success },
|
onSuccess = {
|
||||||
|
queueRepository.invalidateLibraryCache()
|
||||||
|
SyncStatus.Success
|
||||||
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
Log.w(TAG, "Sync failed", error)
|
Log.w(TAG, "Sync failed", error)
|
||||||
SyncStatus.Failed(error.message ?: "Sync failed")
|
SyncStatus.Failed(error.message ?: "Sync failed")
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun appModule(
|
|||||||
single { SourceRepository(get(), get(), get()) }
|
single { SourceRepository(get(), get(), get()) }
|
||||||
single { LibraryRepository(get(), get()) }
|
single { LibraryRepository(get(), get()) }
|
||||||
single<ArtworkSeedStore> { ArtworkSeedRepository(get()) }
|
single<ArtworkSeedStore> { ArtworkSeedRepository(get()) }
|
||||||
single { SyncManager(get()) }
|
single { SyncManager(get(), get()) }
|
||||||
single { QueueRepository(get()) }
|
single { QueueRepository(get()) }
|
||||||
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get()) }
|
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get()) }
|
||||||
single { PlaybackController(get(), get(), get()) }
|
single { PlaybackController(get(), get(), get()) }
|
||||||
|
|||||||
@@ -537,7 +537,15 @@ class PlaybackController(
|
|||||||
shuffleEnabled = modes.shuffle
|
shuffleEnabled = modes.shuffle
|
||||||
repeatMode = modes.repeat.toRepeatMode()
|
repeatMode = modes.repeat.toRepeatMode()
|
||||||
player?.setRepeatOne(repeatMode == RepeatMode.One)
|
player?.setRepeatOne(repeatMode == RepeatMode.One)
|
||||||
val snapshot = readSnapshot()
|
var snapshot = queueRepository.snapshot()
|
||||||
|
if (shuffleEnabled && !snapshot.shuffled && snapshot.size > 0L) {
|
||||||
|
val start = queueRepository.cursor().coerceIn(0, snapshot.size - 1)
|
||||||
|
queueRepository.setShuffle(true, shuffledOrder(snapshot.size, start))
|
||||||
|
queueRepository.setCursor(0)
|
||||||
|
snapshot = queueRepository.snapshot()
|
||||||
|
} else if (!snapshot.shuffled) {
|
||||||
|
shuffleEnabled = false
|
||||||
|
}
|
||||||
this.snapshot = snapshot
|
this.snapshot = snapshot
|
||||||
val queueSourceId =
|
val queueSourceId =
|
||||||
snapshot.entries
|
snapshot.entries
|
||||||
@@ -600,22 +608,24 @@ class PlaybackController(
|
|||||||
|
|
||||||
private fun jumpTo(target: Long) {
|
private fun jumpTo(target: Long) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val snapshot = readSnapshot()
|
startLock.withLock {
|
||||||
this@PlaybackController.snapshot = snapshot
|
val snapshot = readSnapshot()
|
||||||
if (snapshot.size == 0L) return@launch
|
this@PlaybackController.snapshot = snapshot
|
||||||
val current = currentPosition() ?: return@launch
|
if (snapshot.size == 0L) return@withLock
|
||||||
val dest = target.coerceIn(0, snapshot.size - 1)
|
val current = currentPosition() ?: return@withLock
|
||||||
val wasEnded = player?.isEnded == true
|
val dest = target.coerceIn(0, snapshot.size - 1)
|
||||||
if (dest == current && !wasEnded) return@launch
|
val wasEnded = player?.isEnded == true
|
||||||
windowJob?.cancel()
|
if (dest == current && !wasEnded) return@withLock
|
||||||
queueRepository.setCursor(dest)
|
windowJob?.cancel()
|
||||||
shiftWindow(dest)
|
queueRepository.setCursor(dest)
|
||||||
val player = player ?: return@launch
|
shiftWindowLocked(dest)
|
||||||
player.ensurePrepared()
|
val player = player ?: return@withLock
|
||||||
player.seekToIndex((dest - windowStart).toInt())
|
player.ensurePrepared()
|
||||||
if (wasEnded) player.play()
|
player.seekToIndex((dest - windowStart).toInt())
|
||||||
endedHandled = false
|
if (wasEnded) player.play()
|
||||||
refresh(dest)
|
endedHandled = false
|
||||||
|
refresh(dest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,7 +712,7 @@ class PlaybackController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvents() {
|
override fun onEvents() {
|
||||||
if (player?.isEnded == true) handleEnded()
|
if (player?.isEnded == true) handleEnded() else endedHandled = false
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,7 +740,10 @@ class PlaybackController(
|
|||||||
|
|
||||||
private suspend fun readSnapshot(): QueueSnapshot {
|
private suspend fun readSnapshot(): QueueSnapshot {
|
||||||
val snapshot = queueRepository.snapshot()
|
val snapshot = queueRepository.snapshot()
|
||||||
if (!snapshot.shuffled) shuffleEnabled = false
|
if (!snapshot.shuffled) {
|
||||||
|
if (shuffleEnabled && snapshot.size > 0L) queueRepository.setShuffle(false, null)
|
||||||
|
shuffleEnabled = false
|
||||||
|
}
|
||||||
return snapshot
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ fun QueueRoute(
|
|||||||
sourceRepository: SourceRepository = koinInject(),
|
sourceRepository: SourceRepository = koinInject(),
|
||||||
) {
|
) {
|
||||||
val playback by controller.state.collectAsStateWithLifecycle()
|
val playback by controller.state.collectAsStateWithLifecycle()
|
||||||
LaunchedEffect(Unit) { viewModel.open() }
|
LaunchedEffect(playback.shuffle) { viewModel.open() }
|
||||||
QueueScreen(
|
QueueScreen(
|
||||||
rows = viewModel.rows,
|
rows = viewModel.rows,
|
||||||
ready = viewModel.ready,
|
ready = viewModel.ready,
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ class QueueRepositoryTest {
|
|||||||
(0 until snapshot.size).mapNotNull { repository.itemAt(snapshot, it)?.song?.id }
|
(0 until snapshot.size).mapNotNull { repository.itemAt(snapshot, it)?.song?.id }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun aStaleShuffleOrderIsDroppedWhenTheQueueLengthChanges() =
|
fun aStaleShuffleOrderIsIgnoredWhenTheQueueLengthChanges() =
|
||||||
runTest {
|
runTest {
|
||||||
seedLibrary()
|
seedLibrary()
|
||||||
repository.replace(listOf(repository.albumEntry(1, "al1")))
|
repository.replace(listOf(repository.albumEntry(1, "al1")))
|
||||||
@@ -331,7 +331,6 @@ class QueueRepositoryTest {
|
|||||||
db.libraryDao().upsertSongs(listOf(song("s6", "al1", track = 4, album = "First Album")))
|
db.libraryDao().upsertSongs(listOf(song("s6", "al1", track = 4, album = "First Album")))
|
||||||
|
|
||||||
assertFalse(repository.snapshot().shuffled)
|
assertFalse(repository.snapshot().shuffled)
|
||||||
assertFalse(repository.modes().shuffle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun seedLibrary() {
|
private suspend fun seedLibrary() {
|
||||||
|
|||||||
@@ -819,6 +819,27 @@ class PlaybackControllerTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun repeatAllLoopsAfterEndingWithRepeatOff() {
|
||||||
|
seedAlbum(2, sourceId = 1)
|
||||||
|
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await { controller.state.value.isPlaying }
|
||||||
|
|
||||||
|
handle.finish()
|
||||||
|
handle.emitEvents()
|
||||||
|
await { !controller.state.value.isPlaying }
|
||||||
|
|
||||||
|
controller.togglePlayPause()
|
||||||
|
await { controller.state.value.isPlaying }
|
||||||
|
controller.cycleRepeat()
|
||||||
|
assertEquals(RepeatMode.All, controller.state.value.repeat)
|
||||||
|
|
||||||
|
handle.finish()
|
||||||
|
handle.emitEvents()
|
||||||
|
await { controller.state.value.isPlaying }
|
||||||
|
}
|
||||||
|
|
||||||
private fun seedAlbum(
|
private fun seedAlbum(
|
||||||
count: Int,
|
count: Int,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
|
|||||||
Reference in New Issue
Block a user