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)
This commit is contained in:
2025-12-27 20:46:04 -05:00
parent 4557d13064
commit e7335977c0
6 changed files with 135 additions and 46 deletions

View File

@@ -9,10 +9,14 @@ public class AppConstants {
// Broadcast actions // Broadcast actions
public static final String ACTION_UPLOAD_COMPLETE = "com.mattintech.simplelogupload.UPLOAD_COMPLETE"; 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_ERROR = "com.mattintech.simplelogupload.UPLOAD_ERROR";
public static final String ACTION_UPLOAD_PROGRESS = "com.mattintech.simplelogupload.UPLOAD_PROGRESS";
// Intent extras // Intent extras
public static final String EXTRA_UPLOAD_CODE = "upload_code"; public static final String EXTRA_UPLOAD_CODE = "upload_code";
public static final String EXTRA_UPLOAD_ERROR = "upload_error"; 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 // File patterns
public static final String DUMPSTATE_FILE_PATTERN = "dumpState_.*\\.zip"; public static final String DUMPSTATE_FILE_PATTERN = "dumpState_.*\\.zip";

View File

@@ -253,6 +253,7 @@ class FileUploadForegroundService : Service() {
) )
updateNotification(statusText, percentComplete, 100) updateNotification(statusText, percentComplete, 100)
sendProgressBroadcast(bytesUploaded, totalBytes, percentComplete)
Log.d(TAG, "Upload progress: $percentComplete% ($uploadedMB MB / $totalMB MB)") Log.d(TAG, "Upload progress: $percentComplete% ($uploadedMB MB / $totalMB MB)")
} }
} }
@@ -552,4 +553,18 @@ class FileUploadForegroundService : Service() {
sendBroadcast(intent) sendBroadcast(intent)
LocalBroadcastManager.getInstance(this).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)
}
} }

View File

@@ -1,6 +1,6 @@
package com.mattintech.simplelogupload.ui.screens 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.Icons
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.History 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.material.icons.filled.Upload
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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 androidx.lifecycle.viewmodel.compose.viewModel
import com.mattintech.simplelogupload.ui.screens.tabs.DumpstateTab import com.mattintech.simplelogupload.ui.screens.tabs.DumpstateTab
import com.mattintech.simplelogupload.ui.screens.tabs.GeneralUploadTab import com.mattintech.simplelogupload.ui.screens.tabs.GeneralUploadTab
@@ -31,6 +34,7 @@ fun MainScreen(
) { ) {
val isSamsungDevice = remember { DeviceUtil.isSamsungDevice() } val isSamsungDevice = remember { DeviceUtil.isSamsungDevice() }
var selectedTabIndex by remember { mutableStateOf(0) } var selectedTabIndex by remember { mutableStateOf(0) }
val uiState by viewModel.uiState.collectAsState()
Scaffold( Scaffold(
topBar = { topBar = {
@@ -85,28 +89,94 @@ fun MainScreen(
} }
} }
) { paddingValues -> ) { paddingValues ->
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
)
}
}
}
}
// Tab content
when { when {
// Non-Samsung device: show only general upload // Non-Samsung device: show only general upload
!isSamsungDevice -> { !isSamsungDevice -> {
GeneralUploadTab( GeneralUploadTab(
modifier = Modifier.padding(paddingValues), modifier = Modifier.fillMaxSize(),
viewModel = viewModel viewModel = viewModel
) )
} }
// Samsung device with tab 0 selected: general upload // Samsung device with tab 0 selected: general upload
selectedTabIndex == 0 -> { selectedTabIndex == 0 -> {
GeneralUploadTab( GeneralUploadTab(
modifier = Modifier.padding(paddingValues), modifier = Modifier.fillMaxSize(),
viewModel = viewModel viewModel = viewModel
) )
} }
// Samsung device with tab 1 selected: dumpstate // Samsung device with tab 1 selected: dumpstate
else -> { else -> {
DumpstateTab( DumpstateTab(
modifier = Modifier.padding(paddingValues), modifier = Modifier.fillMaxSize(),
viewModel = viewModel viewModel = viewModel
) )
} }
} }
} }
}
} }

View File

@@ -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()
}
}
} }
} }
} }

View File

@@ -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 // Upload params dialog

View File

@@ -33,7 +33,10 @@ data class MainUiState(
val uploadResult: UploadResult? = null, val uploadResult: UploadResult? = null,
val errorMessage: String? = null, val errorMessage: String? = null,
val showUploadParamsDialog: Boolean = false, 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() { uploadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
when (intent.action) { 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 -> { AppConstants.ACTION_UPLOAD_COMPLETE -> {
val code = intent.getStringExtra(AppConstants.EXTRA_UPLOAD_CODE) val code = intent.getStringExtra(AppConstants.EXTRA_UPLOAD_CODE)
Log.d(TAG, "Upload completed: $code") Log.d(TAG, "Upload completed: $code")
@@ -86,7 +100,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
isLoading = false, isLoading = false,
uploadResult = result, uploadResult = result,
showUploadResultDialog = true showUploadResultDialog = true,
uploadProgress = 0,
bytesUploaded = 0,
totalBytes = 0
) )
} }
AppConstants.ACTION_UPLOAD_ERROR -> { AppConstants.ACTION_UPLOAD_ERROR -> {
@@ -95,7 +112,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
isLoading = false, 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 { val filter = IntentFilter().apply {
addAction(AppConstants.ACTION_UPLOAD_PROGRESS)
addAction(AppConstants.ACTION_UPLOAD_COMPLETE) addAction(AppConstants.ACTION_UPLOAD_COMPLETE)
addAction(AppConstants.ACTION_UPLOAD_ERROR) addAction(AppConstants.ACTION_UPLOAD_ERROR)
} }