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:
@@ -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";
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user