diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1ce4bcf..522bee7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,16 @@
+
+
+
+
diff --git a/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt b/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt
index 399a8b7..e3785ef 100644
--- a/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt
+++ b/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt
@@ -3,6 +3,7 @@ package com.yarisobd.app
import android.app.Application
import android.content.Context
import android.os.Environment
+import android.webkit.MimeTypeMap
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.yarisobd.app.data.remote.PiApi
@@ -32,6 +33,12 @@ data class DownloadProgress(
val totalBytes: Long,
)
+data class DownloadedFile(
+ val name: String,
+ val size: Long,
+ val file: File,
+)
+
class DashcamViewModel(application: Application) : AndroidViewModel(application) {
private val baseUrl = "http://10.42.0.1" // no trailing slash
@@ -66,8 +73,22 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
private val _downloadEvents = MutableSharedFlow()
val downloadEvents = _downloadEvents.asSharedFlow()
+ private val _downloadedFiles = MutableStateFlow>(emptyList())
+ val downloadedFiles: StateFlow> = _downloadedFiles.asStateFlow()
+
+ private fun getDownloadDir(): File {
+ // Public Movies/yaris-obd/ — visible in file managers
+ val dir = File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
+ "yaris-obd"
+ )
+ dir.mkdirs()
+ return dir
+ }
+
init {
loadVideos()
+ scanDownloads()
}
fun loadVideos() {
@@ -134,6 +155,7 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
"✓ Downloaded: ${localFile.name} (${downloaded} bytes)"
}
_downloadEvents.emit(result)
+ scanDownloads()
} catch (e: Exception) {
_downloadProgress.value = _downloadProgress.value - video.path
_downloadEvents.emit("✗ Download failed: ${e.message}")
@@ -141,6 +163,25 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
}
}
+ fun scanDownloads() {
+ val dir = getDownloadDir()
+ val files = dir.listFiles()?.filter { it.isFile }?.map { f ->
+ DownloadedFile(name = f.name, size = f.length(), file = f)
+ }?.sortedByDescending { it.file.lastModified() } ?: emptyList()
+ _downloadedFiles.value = files
+ }
+
+ fun deleteDownloaded(file: DownloadedFile) {
+ viewModelScope.launch {
+ if (file.file.delete()) {
+ _downloadEvents.emit("✓ Deleted: ${file.name}")
+ scanDownloads()
+ } else {
+ _downloadEvents.emit("✗ Could not delete: ${file.name}")
+ }
+ }
+ }
+
fun deleteVideo(video: VideoFile) {
viewModelScope.launch {
try {
diff --git a/android/app/src/main/java/com/yarisobd/app/DownloadsScreen.kt b/android/app/src/main/java/com/yarisobd/app/DownloadsScreen.kt
new file mode 100644
index 0000000..664f7ad
--- /dev/null
+++ b/android/app/src/main/java/com/yarisobd/app/DownloadsScreen.kt
@@ -0,0 +1,150 @@
+package com.yarisobd.app
+
+import android.content.Intent
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.core.content.FileProvider
+
+@Composable
+fun DownloadsScreen(
+ files: List,
+ onDelete: (DownloadedFile) -> Unit,
+ onRefresh: () -> Unit,
+) {
+ val context = LocalContext.current
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ )
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = "Downloads",
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = "${files.size} files in Movies/yaris-obd/",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ if (files.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "No downloaded files yet.\nDownload a video from the Dashcam tab.",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ } else {
+ LazyColumn(
+ contentPadding = androidx.compose.foundation.layout.PaddingValues(
+ start = 16.dp, end = 16.dp, bottom = 16.dp
+ ),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(files, key = { it.name }) { file ->
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ // Open with system video player
+ val uri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.fileprovider",
+ file.file
+ )
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, "video/*")
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ context.startActivity(intent)
+ },
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = file.name,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = formatBytes(file.size),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Button(
+ onClick = { onDelete(file) },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ ),
+ modifier = Modifier.height(36.dp)
+ ) {
+ Text("✕", style = MaterialTheme.typography.bodySmall)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun formatBytes(bytes: Long): String {
+ return when {
+ bytes >= 1_000_000_000 -> "%.1f GB".format(bytes / 1_000_000_000.0)
+ bytes >= 1_000_000 -> "%.1f MB".format(bytes / 1_000_000.0)
+ bytes >= 1_000 -> "%.1f KB".format(bytes / 1_000.0)
+ else -> "$bytes B"
+ }
+}
diff --git a/android/app/src/main/java/com/yarisobd/app/MainActivity.kt b/android/app/src/main/java/com/yarisobd/app/MainActivity.kt
index 492ccfe..a3d1926 100644
--- a/android/app/src/main/java/com/yarisobd/app/MainActivity.kt
+++ b/android/app/src/main/java/com/yarisobd/app/MainActivity.kt
@@ -53,6 +53,7 @@ fun YarisMainScreen(client: OkHttpClient) {
val isLoading by dashcamVm.isLoading.collectAsState()
val error by dashcamVm.error.collectAsState()
val downloadProgress by dashcamVm.downloadProgress.collectAsState()
+ val downloadedFiles by dashcamVm.downloadedFiles.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
var selectedTab by remember { mutableIntStateOf(0) }
@@ -77,7 +78,13 @@ fun YarisMainScreen(client: OkHttpClient) {
NavigationBarItem(
selected = selectedTab == 1,
onClick = { selectedTab = 1 },
- icon = { Text("🔌") },
+ icon = { Text("�") },
+ label = { Text("Downloads") }
+ )
+ NavigationBarItem(
+ selected = selectedTab == 2,
+ onClick = { selectedTab = 2 },
+ icon = { Text("�🔌") },
label = { Text("Test") }
)
}
@@ -96,7 +103,12 @@ fun YarisMainScreen(client: OkHttpClient) {
onDownload = { dashcamVm.downloadVideo(it) },
onDelete = { dashcamVm.deleteVideo(it) },
)
- 1 -> DualNetworkTestScreen(
+ 1 -> DownloadsScreen(
+ files = downloadedFiles,
+ onDelete = { dashcamVm.deleteDownloaded(it) },
+ onRefresh = { dashcamVm.scanDownloads() },
+ )
+ 2 -> DualNetworkTestScreen(
piClient = client,
serverClient = client,
)
diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..ebbbd14
--- /dev/null
+++ b/android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+