fix: downloads use WorkManager foreground service + inline progress
- DownloadWorker: foreground service with notification + setProgressAsync - ActiveDownload state tracked via WorkManager LiveData observation - Downloads tab shows progress bar + % for in-flight downloads - Survives screen lock / background - Fixed FGS type for Android 14+ (FOREGROUND_SERVICE_TYPE_DATA_SYNC)
This commit is contained in:
@@ -5,12 +5,12 @@ import android.os.Environment
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import com.yarisobd.app.data.remote.PiApi
|
||||
import com.yarisobd.app.data.remote.dto.VideoFile
|
||||
import com.yarisobd.app.data.remote.dto.VideoListResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -36,6 +36,12 @@ data class DownloadedFile(
|
||||
val file: File,
|
||||
)
|
||||
|
||||
data class ActiveDownload(
|
||||
val filename: String,
|
||||
val progress: Int, // 0-100
|
||||
val done: Boolean,
|
||||
)
|
||||
|
||||
class DashcamViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val baseUrl = "http://10.42.0.1" // no trailing slash
|
||||
@@ -73,6 +79,9 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
private val _downloadedFiles = MutableStateFlow<List<DownloadedFile>>(emptyList())
|
||||
val downloadedFiles: StateFlow<List<DownloadedFile>> = _downloadedFiles.asStateFlow()
|
||||
|
||||
private val _activeDownloads = MutableStateFlow<Map<String, ActiveDownload>>(emptyMap())
|
||||
val activeDownloads: StateFlow<Map<String, ActiveDownload>> = _activeDownloads.asStateFlow()
|
||||
|
||||
private fun getDownloadDir(): File {
|
||||
// Public Movies/yaris-obd/ — visible in file managers
|
||||
val dir = File(
|
||||
@@ -109,7 +118,6 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
val url = "$baseUrl/api/local-video/file?path=${video.path}"
|
||||
val filename = video.path.substringAfterLast("/")
|
||||
|
||||
// Check if already downloaded
|
||||
val dir = getDownloadDir()
|
||||
val localFile = File(dir, filename)
|
||||
if (localFile.exists() && localFile.length() == video.size && video.size > 0) {
|
||||
@@ -131,10 +139,42 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
.addTag("download_${video.path}")
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(getApplication()).enqueue(request)
|
||||
val wm = WorkManager.getInstance(getApplication())
|
||||
wm.enqueue(request)
|
||||
|
||||
// Observe progress
|
||||
val workerId = request.id
|
||||
_activeDownloads.value = _activeDownloads.value + (filename to ActiveDownload(filename, 0, false))
|
||||
|
||||
wm.getWorkInfoByIdLiveData(workerId).observeForever { info ->
|
||||
if (info == null) return@observeForever
|
||||
|
||||
val progress = info.progress.getInt("pct", 0)
|
||||
val done = info.state == WorkInfo.State.SUCCEEDED || info.state == WorkInfo.State.FAILED
|
||||
|
||||
_activeDownloads.value = _activeDownloads.value + (filename to ActiveDownload(filename, progress, done))
|
||||
|
||||
when (info.state) {
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
viewModelScope.launch {
|
||||
_downloadEvents.emit("✓ Downloaded: $filename")
|
||||
scanDownloads()
|
||||
}
|
||||
_activeDownloads.value = _activeDownloads.value - filename
|
||||
}
|
||||
WorkInfo.State.FAILED -> {
|
||||
val err = info.outputData.getString("error") ?: "Unknown error"
|
||||
viewModelScope.launch {
|
||||
_downloadEvents.emit("✗ Download failed: $err")
|
||||
}
|
||||
_activeDownloads.value = _activeDownloads.value - filename
|
||||
}
|
||||
else -> {} // running / enqueued
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_downloadEvents.emit("⏳ Downloading: $filename (continues in background)")
|
||||
_downloadEvents.emit("⏳ Downloading: $filename")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -34,6 +35,7 @@ import androidx.core.content.FileProvider
|
||||
@Composable
|
||||
fun DownloadsScreen(
|
||||
files: List<DownloadedFile>,
|
||||
activeDownloads: Map<String, ActiveDownload>,
|
||||
onDelete: (DownloadedFile) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
@@ -63,7 +65,45 @@ fun DownloadsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (files.isEmpty()) {
|
||||
if (activeDownloads.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(
|
||||
start = 16.dp, end = 16.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(activeDownloads.entries.toList(), key = { it.key }) { (filename, dl) ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = if (dl.done) "✓ $filename" else "⏳ $filename",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (!dl.done) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { dl.progress / 100f },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = "${dl.progress}%",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files.isEmpty() && activeDownloads.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
|
||||
@@ -54,6 +54,7 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
val error by dashcamVm.error.collectAsState()
|
||||
val downloadProgress by dashcamVm.downloadProgress.collectAsState()
|
||||
val downloadedFiles by dashcamVm.downloadedFiles.collectAsState()
|
||||
val activeDownloads by dashcamVm.activeDownloads.collectAsState()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
@@ -104,6 +105,7 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
onDelete = { dashcamVm.deleteVideo(it) },
|
||||
)
|
||||
1 -> DownloadsScreen(
|
||||
activeDownloads = activeDownloads,
|
||||
files = downloadedFiles,
|
||||
onDelete = { dashcamVm.deleteDownloaded(it) },
|
||||
onRefresh = { dashcamVm.scanDownloads() },
|
||||
|
||||
Reference in New Issue
Block a user