mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
let the user allow downloads on mobile data
Downloads asked the platform for an unmetered network and nothing more, so there was no way to run them on mobile. A setting under Downloads now decides, defaulting to Wi-Fi only, and it also governs roaming: the platform permits roaming by default, which would have contradicted the Wi-Fi-only choice. The flag rides the enqueue request like the download quality does, cached in the source repository so the poller does not read the preference per row.
This commit is contained in:
@@ -4,6 +4,7 @@ data class EngineRequest(
|
||||
val uri: String,
|
||||
val path: String,
|
||||
val title: String,
|
||||
val allowMetered: Boolean = false,
|
||||
)
|
||||
|
||||
enum class EngineStatus { Pending, Running, Completed, Failed }
|
||||
|
||||
@@ -15,8 +15,10 @@ class SystemDownloadEngine(
|
||||
.setTitle(request.title)
|
||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_MUSIC, request.path)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||
// Downloads wait for an unmetered network until the preference for them exists.
|
||||
.setAllowedOverMetered(false)
|
||||
// Wi-Fi only unless the preference allows a metered network, and roaming counts as
|
||||
// one of those; the platform otherwise permits it by default.
|
||||
.setAllowedOverMetered(request.allowMetered)
|
||||
.setAllowedOverRoaming(request.allowMetered)
|
||||
return manager().enqueue(download)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.subtracks.data.prefs
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
@@ -88,6 +89,14 @@ class UserPreferences(
|
||||
|
||||
private val downloadQualityKey = stringPreferencesKey("download_quality")
|
||||
|
||||
fun downloadOverMetered(): Flow<Boolean> = store.data.map { prefs -> prefs[downloadOverMeteredKey] ?: false }
|
||||
|
||||
suspend fun setDownloadOverMetered(allowed: Boolean) {
|
||||
store.edit { prefs -> prefs[downloadOverMeteredKey] = allowed }
|
||||
}
|
||||
|
||||
private val downloadOverMeteredKey = booleanPreferencesKey("download_over_metered")
|
||||
|
||||
fun syncConcurrency(): Flow<Int> = store.data.map { prefs -> (prefs[syncConcurrencyKey] ?: DEFAULT_FETCH_CONCURRENCY).coerceAtLeast(1) }
|
||||
|
||||
suspend fun setSyncConcurrency(value: Int) {
|
||||
|
||||
@@ -163,7 +163,12 @@ class DownloadRepository(
|
||||
val engineId =
|
||||
try {
|
||||
engine.enqueue(
|
||||
EngineRequest(uri = url, path = enginePath(row.sourceId, row.songId), title = title),
|
||||
EngineRequest(
|
||||
uri = url,
|
||||
path = enginePath(row.sourceId, row.songId),
|
||||
title = title,
|
||||
allowMetered = sourceRepository.downloadsAllowedOverMetered(),
|
||||
),
|
||||
)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
|
||||
@@ -52,10 +52,18 @@ class SourceRepository(
|
||||
@Volatile
|
||||
private var downloadQuality = StreamQuality()
|
||||
|
||||
@Volatile
|
||||
private var allowMeteredDownloads = false
|
||||
|
||||
fun downloadsAllowedOverMetered(): Boolean = allowMeteredDownloads
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
prefs.downloadQuality().collect { downloadQuality = it }
|
||||
}
|
||||
scope.launch {
|
||||
prefs.downloadOverMetered().collect { allowMeteredDownloads = it }
|
||||
}
|
||||
scope.launch {
|
||||
combine(
|
||||
db.sourcesDao().activeSubsonicConfig(),
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -70,6 +71,7 @@ fun SettingsRoute(
|
||||
val mobileQuality by viewModel.mobileQuality.collectAsStateWithLifecycle()
|
||||
val syncConcurrency by viewModel.syncConcurrency.collectAsStateWithLifecycle()
|
||||
val downloadQuality by viewModel.downloadQuality.collectAsStateWithLifecycle()
|
||||
val downloadOverMetered by viewModel.downloadOverMetered.collectAsStateWithLifecycle()
|
||||
SettingsScreen(
|
||||
sources = sources,
|
||||
activeSourceId = activeSourceId,
|
||||
@@ -77,12 +79,14 @@ fun SettingsRoute(
|
||||
mobileQuality = mobileQuality,
|
||||
syncConcurrency = syncConcurrency,
|
||||
downloadQuality = downloadQuality,
|
||||
downloadOverMetered = downloadOverMetered,
|
||||
onSelectSource = viewModel::selectSource,
|
||||
onEditServer = onEditServer,
|
||||
onWifiQualityChange = viewModel::setWifiQuality,
|
||||
onMobileQualityChange = viewModel::setMobileQuality,
|
||||
onSyncConcurrencyChange = viewModel::setSyncConcurrency,
|
||||
onDownloadQualityChange = viewModel::setDownloadQuality,
|
||||
onDownloadOverMeteredChange = viewModel::setDownloadOverMetered,
|
||||
onAddServer = onAddServer,
|
||||
onOpenDownloads = onOpenDownloads,
|
||||
onBack = onBack,
|
||||
@@ -98,12 +102,14 @@ fun SettingsScreen(
|
||||
mobileQuality: StreamQuality,
|
||||
syncConcurrency: Int,
|
||||
downloadQuality: StreamQuality,
|
||||
downloadOverMetered: Boolean,
|
||||
onSelectSource: (Long) -> Unit,
|
||||
onEditServer: (Long) -> Unit,
|
||||
onWifiQualityChange: (StreamQuality) -> Unit,
|
||||
onMobileQualityChange: (StreamQuality) -> Unit,
|
||||
onSyncConcurrencyChange: (Int) -> Unit,
|
||||
onDownloadQualityChange: (StreamQuality) -> Unit,
|
||||
onDownloadOverMeteredChange: (Boolean) -> Unit,
|
||||
onAddServer: () -> Unit,
|
||||
onOpenDownloads: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
@@ -199,6 +205,17 @@ fun SettingsScreen(
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text("Download over mobile data") },
|
||||
supportingContent = { Text(if (downloadOverMetered) "Wi-Fi and mobile" else "Wi-Fi only") },
|
||||
trailingContent = {
|
||||
Switch(checked = downloadOverMetered, onCheckedChange = onDownloadOverMeteredChange)
|
||||
},
|
||||
modifier = Modifier.clickable { onDownloadOverMeteredChange(!downloadOverMetered) },
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text("Manage downloads") },
|
||||
|
||||
@@ -38,6 +38,11 @@ class SettingsViewModel(
|
||||
.downloadQuality()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), StreamQuality())
|
||||
|
||||
val downloadOverMetered: StateFlow<Boolean> =
|
||||
userPreferences
|
||||
.downloadOverMetered()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
||||
|
||||
val syncConcurrency: StateFlow<Int> =
|
||||
userPreferences
|
||||
.syncConcurrency()
|
||||
@@ -59,6 +64,10 @@ class SettingsViewModel(
|
||||
viewModelScope.launch { userPreferences.setDownloadQuality(quality) }
|
||||
}
|
||||
|
||||
fun setDownloadOverMetered(allowed: Boolean) {
|
||||
viewModelScope.launch { userPreferences.setDownloadOverMetered(allowed) }
|
||||
}
|
||||
|
||||
fun setSyncConcurrency(value: Int) {
|
||||
viewModelScope.launch { userPreferences.setSyncConcurrency(value) }
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class DownloadRepositoryTest {
|
||||
private lateinit var repository: DownloadRepository
|
||||
private lateinit var artwork: ArtworkStore
|
||||
private lateinit var dir: File
|
||||
private val prefs = fakeUserPreferences()
|
||||
private val messages = CopyOnWriteArrayList<String>()
|
||||
private val requestedArt = CopyOnWriteArrayList<String>()
|
||||
private val artFetchedAfterTheEngineRequest = CopyOnWriteArrayList<Boolean>()
|
||||
@@ -73,7 +74,7 @@ class DownloadRepositoryTest {
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
artwork = ArtworkStore(File(context.cacheDir, "art-${System.nanoTime()}"))
|
||||
sources = SourceRepository(db, OkHttpClient(), fakeUserPreferences(), artwork)
|
||||
sources = SourceRepository(db, OkHttpClient(), prefs, artwork)
|
||||
engine = FakeDownloadEngine()
|
||||
dir = File(context.cacheDir, "downloads-${System.nanoTime()}")
|
||||
repository =
|
||||
@@ -956,6 +957,29 @@ class DownloadRepositoryTest {
|
||||
assertNull(repository.localUri("s1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadsAreWifiOnlyUntilThePreferenceAllowsMetered() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
runBlocking { repository.download(1, "s1") }
|
||||
assertFalse(
|
||||
engine.requests
|
||||
.single()
|
||||
.second.allowMetered,
|
||||
)
|
||||
|
||||
prefs.setDownloadOverMetered(true)
|
||||
await { sources.downloadsAllowedOverMetered() }
|
||||
|
||||
runBlocking { repository.download(1, "s2") }
|
||||
|
||||
assertTrue(
|
||||
engine.requests
|
||||
.last()
|
||||
.second.allowMetered,
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestedSongIds(): List<String> = engine.requests.map { it.second.path.substringAfterLast('/') }
|
||||
|
||||
private fun seedSongs(tracks: IntRange) {
|
||||
|
||||
@@ -344,12 +344,14 @@ class AppScreenshotTest {
|
||||
mobileQuality = StreamQuality(192, "mp3"),
|
||||
syncConcurrency = 4,
|
||||
downloadQuality = StreamQuality(128, "opus"),
|
||||
downloadOverMetered = false,
|
||||
onSelectSource = {},
|
||||
onEditServer = {},
|
||||
onWifiQualityChange = {},
|
||||
onMobileQualityChange = {},
|
||||
onSyncConcurrencyChange = {},
|
||||
onDownloadQualityChange = {},
|
||||
onDownloadOverMeteredChange = {},
|
||||
onAddServer = {},
|
||||
onOpenDownloads = {},
|
||||
onBack = {},
|
||||
|
||||
Reference in New Issue
Block a user