address network quality review findings

- fall back to the legacy max_bitrate/stream_format keys so existing settings survive the upgrade
- keep the last network mode when there is no active network instead of reporting mobile, so a connectivity gap does not flip modes and cause spurious reloads
- skip the quality reload when the player has ended or advanced mid-read
- keep the reload collector alive across transient failures
- edit stream quality from a local dialog draft so a fast second tap is not lost
- cover the mode mapping, migration, paused reload and equal-quality no-op with tests
This commit is contained in:
austinried
2026-10-04 00:29:59 +00:00
parent 117e8a69fb
commit 9e25a971e6
9 changed files with 209 additions and 62 deletions
@@ -0,0 +1,18 @@
package com.subtracks.data.net
import org.junit.Assert.assertEquals
import org.junit.Test
class NetworkModeTest {
@Test
fun meteredIsMobileAndUnmeteredIsWifi() {
assertEquals(NetworkMode.Mobile, networkModeFor(true, NetworkMode.Wifi))
assertEquals(NetworkMode.Wifi, networkModeFor(false, NetworkMode.Mobile))
}
@Test
fun noActiveNetworkKeepsTheLastMode() {
assertEquals(NetworkMode.Wifi, networkModeFor(null, NetworkMode.Wifi))
assertEquals(NetworkMode.Mobile, networkModeFor(null, NetworkMode.Mobile))
}
}
@@ -1,6 +1,9 @@
package com.subtracks.data.prefs
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.net.NetworkMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -55,6 +58,28 @@ class UserPreferencesTest {
assertEquals(StreamQuality(320, null), prefs.streamQuality(NetworkMode.Wifi).first())
assertEquals(StreamQuality(96, "opus"), prefs.streamQuality(NetworkMode.Mobile).first())
file.delete()
}
@Test
fun streamQualityFallsBackToTheLegacySingleValue() =
runTest {
val file = File.createTempFile("user-prefs", ".preferences_pb").apply { delete() }
val store = PreferenceDataStoreFactory.create(scope = backgroundScope) { file }
store.edit { prefs ->
prefs[intPreferencesKey("max_bitrate")] = 192
prefs[stringPreferencesKey("stream_format")] = "mp3"
}
val prefs = UserPreferences(store)
assertEquals(StreamQuality(192, "mp3"), prefs.streamQuality(NetworkMode.Wifi).first())
assertEquals(StreamQuality(192, "mp3"), prefs.streamQuality(NetworkMode.Mobile).first())
prefs.setStreamQuality(NetworkMode.Mobile, StreamQuality(64, null))
assertEquals(StreamQuality(192, "mp3"), prefs.streamQuality(NetworkMode.Wifi).first())
assertEquals(StreamQuality(64, null), prefs.streamQuality(NetworkMode.Mobile).first())
file.delete()
}
}
@@ -20,6 +20,7 @@ import com.subtracks.data.repo.SourceRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
@@ -39,6 +40,7 @@ class PlaybackControllerTest {
private lateinit var db: SubtracksDatabase
private lateinit var prefs: UserPreferences
private lateinit var networkMode: MutableStateFlow<NetworkMode>
private lateinit var sources: SourceRepository
private lateinit var queues: QueueRepository
private lateinit var handle: FakePlayerHandle
@@ -53,7 +55,8 @@ class PlaybackControllerTest {
.setDriver(BundledSQLiteDriver())
.build()
prefs = fakeUserPreferences()
sources = SourceRepository(db, OkHttpClient(), prefs)
networkMode = MutableStateFlow(NetworkMode.Wifi)
sources = SourceRepository(db, OkHttpClient(), prefs, networkMode = networkMode)
queues = QueueRepository(db)
handle = FakePlayerHandle()
controller = PlaybackController(sources, queues, FakePlayerConnection(handle), dispatcher = dispatcher)
@@ -1039,6 +1042,69 @@ class PlaybackControllerTest {
assertEquals(listOf("prepare", "play"), handle.operations.takeLast(2))
}
@Test
fun changingStreamQualityWhilePausedReloadsWithoutResuming() {
seedAlbum(60, sourceId = 1)
runBlocking { prefs.setStreamQuality(NetworkMode.Wifi, StreamQuality(64, null)) }
await { sources.quality.value == StreamQuality(64, null) }
controller.playAlbum(1, "al1", 10)
await { handle.playWhenReady }
controller.togglePlayPause()
await { !handle.playWhenReady }
handle.positionMs = 33_000L
val before = handle.operations.size
val windows = handle.operations.count { it.startsWith("setWindow") }
runBlocking { prefs.setStreamQuality(NetworkMode.Wifi, StreamQuality(128, "opus")) }
await { handle.operations.count { it.startsWith("setWindow") } > windows }
assertEquals(33_000L, handle.positionMs)
assertFalse(handle.playWhenReady)
assertTrue(handle.operations.drop(before).none { it.startsWith("prepare") || it.startsWith("play") })
}
@Test
fun switchingToTheOtherModeReloadsWithItsQuality() {
seedAlbum(60, sourceId = 1)
runBlocking {
prefs.setStreamQuality(NetworkMode.Wifi, StreamQuality(320, null))
prefs.setStreamQuality(NetworkMode.Mobile, StreamQuality(96, "opus"))
}
await { sources.quality.value == StreamQuality(320, null) }
controller.playAlbum(1, "al1", 10)
await { handle.operations.contains("play") }
handle.positionMs = 12_000L
val windows = handle.operations.count { it.startsWith("setWindow") }
networkMode.value = NetworkMode.Mobile
await { sources.quality.value == StreamQuality(96, "opus") }
await { handle.operations.count { it.startsWith("setWindow") } > windows }
assertEquals("s11", handle.currentItem?.id)
assertEquals(12_000L, handle.positionMs)
}
@Test
fun switchingToTheOtherModeWithEqualQualityDoesNotReload() {
seedAlbum(60, sourceId = 1)
runBlocking {
prefs.setStreamQuality(NetworkMode.Wifi, StreamQuality(96, "opus"))
prefs.setStreamQuality(NetworkMode.Mobile, StreamQuality(96, "opus"))
}
await { sources.quality.value == StreamQuality(96, "opus") }
controller.playAlbum(1, "al1", 10)
await { handle.operations.contains("play") }
val windows = handle.operations.count { it.startsWith("setWindow") }
networkMode.value = NetworkMode.Mobile
runBlocking { delay(300) }
assertEquals(windows, handle.operations.count { it.startsWith("setWindow") })
}
private fun seedAlbum(
count: Int,
sourceId: Long,