From e7335977c0da8eec21d89b3c419ef2efc437388f Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Sat, 27 Dec 2025 20:46:04 -0500 Subject: [PATCH] Add persistent upload progress indicator across all tabs - Add progress broadcast from service to ViewModel with bytes and percentage - Display global progress bar above tab content in MainScreen - Show upload progress (MB uploaded/total, percentage) persistently - Remove redundant in-tab progress indicators - Progress stays visible when switching between Upload and Dumpstate tabs - Matches cloud storage app UX (Google Drive, Dropbox) --- .../constant/AppConstants.java | 6 +- .../service/FileUploadForegroundService.kt | 15 +++ .../simplelogupload/ui/screens/MainScreen.kt | 112 ++++++++++++++---- .../ui/screens/tabs/DumpstateTab.kt | 10 -- .../ui/screens/tabs/GeneralUploadTab.kt | 11 -- .../viewmodel/MainViewModel.kt | 27 ++++- 6 files changed, 135 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/mattintech/simplelogupload/constant/AppConstants.java b/app/src/main/java/com/mattintech/simplelogupload/constant/AppConstants.java index 7f51c77..9013b6a 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/constant/AppConstants.java +++ b/app/src/main/java/com/mattintech/simplelogupload/constant/AppConstants.java @@ -9,10 +9,14 @@ public class AppConstants { // Broadcast actions public static final String ACTION_UPLOAD_COMPLETE = "com.mattintech.simplelogupload.UPLOAD_COMPLETE"; public static final String ACTION_UPLOAD_ERROR = "com.mattintech.simplelogupload.UPLOAD_ERROR"; - + public static final String ACTION_UPLOAD_PROGRESS = "com.mattintech.simplelogupload.UPLOAD_PROGRESS"; + // Intent extras public static final String EXTRA_UPLOAD_CODE = "upload_code"; public static final String EXTRA_UPLOAD_ERROR = "upload_error"; + public static final String EXTRA_BYTES_UPLOADED = "bytes_uploaded"; + public static final String EXTRA_TOTAL_BYTES = "total_bytes"; + public static final String EXTRA_PERCENT_COMPLETE = "percent_complete"; // File patterns public static final String DUMPSTATE_FILE_PATTERN = "dumpState_.*\\.zip"; diff --git a/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.kt b/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.kt index 43308f8..190923f 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.kt @@ -253,6 +253,7 @@ class FileUploadForegroundService : Service() { ) updateNotification(statusText, percentComplete, 100) + sendProgressBroadcast(bytesUploaded, totalBytes, percentComplete) Log.d(TAG, "Upload progress: $percentComplete% ($uploadedMB MB / $totalMB MB)") } } @@ -552,4 +553,18 @@ class FileUploadForegroundService : Service() { sendBroadcast(intent) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } + + /** + * Send a broadcast with upload progress update + */ + private fun sendProgressBroadcast(bytesUploaded: Long, totalBytes: Long, percentComplete: Int) { + val intent = Intent(AppConstants.ACTION_UPLOAD_PROGRESS).apply { + putExtra(AppConstants.EXTRA_BYTES_UPLOADED, bytesUploaded) + putExtra(AppConstants.EXTRA_TOTAL_BYTES, totalBytes) + putExtra(AppConstants.EXTRA_PERCENT_COMPLETE, percentComplete) + } + + // Send local broadcast to the ViewModel + LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + } } diff --git a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/MainScreen.kt b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/MainScreen.kt index 99e9ce8..e8d76ea 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/MainScreen.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/MainScreen.kt @@ -1,6 +1,6 @@ package com.mattintech.simplelogupload.ui.screens -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.History @@ -8,7 +8,10 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Upload import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.mattintech.simplelogupload.ui.screens.tabs.DumpstateTab import com.mattintech.simplelogupload.ui.screens.tabs.GeneralUploadTab @@ -31,6 +34,7 @@ fun MainScreen( ) { val isSamsungDevice = remember { DeviceUtil.isSamsungDevice() } var selectedTabIndex by remember { mutableStateOf(0) } + val uiState by viewModel.uiState.collectAsState() Scaffold( topBar = { @@ -85,27 +89,93 @@ fun MainScreen( } } ) { paddingValues -> - when { - // Non-Samsung device: show only general upload - !isSamsungDevice -> { - GeneralUploadTab( - modifier = Modifier.padding(paddingValues), - viewModel = viewModel - ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + // Global upload progress indicator - visible across all tabs + if (uiState.isLoading) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Column( + modifier = Modifier.padding(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Uploading", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + if (uiState.totalBytes > 0) { + Text( + text = "${uiState.uploadProgress}%", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Progress bar + LinearProgressIndicator( + progress = { uiState.uploadProgress / 100f }, + modifier = Modifier + .fillMaxWidth() + .height(6.dp), + ) + + // Progress details + if (uiState.totalBytes > 0) { + Spacer(modifier = Modifier.height(4.dp)) + val uploadedMB = uiState.bytesUploaded / (1024f * 1024f) + val totalMB = uiState.totalBytes / (1024f * 1024f) + Text( + text = String.format("%.1f / %.1f MB", uploadedMB, totalMB), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } } - // Samsung device with tab 0 selected: general upload - selectedTabIndex == 0 -> { - GeneralUploadTab( - modifier = Modifier.padding(paddingValues), - viewModel = viewModel - ) - } - // Samsung device with tab 1 selected: dumpstate - else -> { - DumpstateTab( - modifier = Modifier.padding(paddingValues), - viewModel = viewModel - ) + + // Tab content + when { + // Non-Samsung device: show only general upload + !isSamsungDevice -> { + GeneralUploadTab( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel + ) + } + // Samsung device with tab 0 selected: general upload + selectedTabIndex == 0 -> { + GeneralUploadTab( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel + ) + } + // Samsung device with tab 1 selected: dumpstate + else -> { + DumpstateTab( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel + ) + } } } } diff --git a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt index 34707a8..7bc2b50 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt @@ -199,16 +199,6 @@ fun DumpstateTab( ) } - // Loading indicator - if (uiState.isLoading) { - Spacer(modifier = Modifier.height(16.dp)) - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } } } } diff --git a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt index 9d28f1e..84d1635 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt @@ -226,17 +226,6 @@ fun GeneralUploadTab( } } - // Loading indicator - if (uiState.isLoading) { - Spacer(modifier = Modifier.height(24.dp)) - CircularProgressIndicator() - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Uploading...", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } } // Upload params dialog diff --git a/app/src/main/java/com/mattintech/simplelogupload/viewmodel/MainViewModel.kt b/app/src/main/java/com/mattintech/simplelogupload/viewmodel/MainViewModel.kt index 30f26bf..73f8f40 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/viewmodel/MainViewModel.kt @@ -33,7 +33,10 @@ data class MainUiState( val uploadResult: UploadResult? = null, val errorMessage: String? = null, val showUploadParamsDialog: Boolean = false, - val showUploadResultDialog: Boolean = false + val showUploadResultDialog: Boolean = false, + val uploadProgress: Int = 0, + val bytesUploaded: Long = 0, + val totalBytes: Long = 0 ) /** @@ -66,6 +69,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { uploadReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { + AppConstants.ACTION_UPLOAD_PROGRESS -> { + val bytesUploaded = intent.getLongExtra(AppConstants.EXTRA_BYTES_UPLOADED, 0) + val totalBytes = intent.getLongExtra(AppConstants.EXTRA_TOTAL_BYTES, 0) + val percentComplete = intent.getIntExtra(AppConstants.EXTRA_PERCENT_COMPLETE, 0) + + _uiState.value = _uiState.value.copy( + uploadProgress = percentComplete, + bytesUploaded = bytesUploaded, + totalBytes = totalBytes + ) + } AppConstants.ACTION_UPLOAD_COMPLETE -> { val code = intent.getStringExtra(AppConstants.EXTRA_UPLOAD_CODE) Log.d(TAG, "Upload completed: $code") @@ -86,7 +100,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { _uiState.value = _uiState.value.copy( isLoading = false, uploadResult = result, - showUploadResultDialog = true + showUploadResultDialog = true, + uploadProgress = 0, + bytesUploaded = 0, + totalBytes = 0 ) } AppConstants.ACTION_UPLOAD_ERROR -> { @@ -95,7 +112,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { _uiState.value = _uiState.value.copy( isLoading = false, - errorMessage = error ?: "Unknown error" + errorMessage = error ?: "Unknown error", + uploadProgress = 0, + bytesUploaded = 0, + totalBytes = 0 ) } } @@ -103,6 +123,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } val filter = IntentFilter().apply { + addAction(AppConstants.ACTION_UPLOAD_PROGRESS) addAction(AppConstants.ACTION_UPLOAD_COMPLETE) addAction(AppConstants.ACTION_UPLOAD_ERROR) }