Add pause, resume, and cancel controls for uploads
- Add pause/resume/cancel actions to notification bar - Add pause/resume/cancel buttons to in-app upload progress card - Track upload state (paused, uploading, cancelled) and upload type (chunked vs standard) - Chunked uploads support pause/resume, standard uploads only support cancel - Fix notification to persist cancelled state without flickering
This commit is contained in:
@@ -6,10 +6,18 @@ package com.mattintech.simplelogupload.constant;
|
|||||||
public class AppConstants {
|
public class AppConstants {
|
||||||
public static final String APP_TAG = "SLU::";
|
public static final String APP_TAG = "SLU::";
|
||||||
|
|
||||||
// Broadcast actions
|
// Broadcast actions (service -> UI)
|
||||||
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";
|
public static final String ACTION_UPLOAD_PROGRESS = "com.mattintech.simplelogupload.UPLOAD_PROGRESS";
|
||||||
|
public static final String ACTION_UPLOAD_PAUSED = "com.mattintech.simplelogupload.UPLOAD_PAUSED";
|
||||||
|
public static final String ACTION_UPLOAD_RESUMED = "com.mattintech.simplelogupload.UPLOAD_RESUMED";
|
||||||
|
public static final String ACTION_UPLOAD_CANCELLED = "com.mattintech.simplelogupload.UPLOAD_CANCELLED";
|
||||||
|
|
||||||
|
// Intent actions (UI/notification -> service)
|
||||||
|
public static final String ACTION_PAUSE_UPLOAD = "com.mattintech.simplelogupload.PAUSE_UPLOAD";
|
||||||
|
public static final String ACTION_RESUME_UPLOAD = "com.mattintech.simplelogupload.RESUME_UPLOAD";
|
||||||
|
public static final String ACTION_CANCEL_UPLOAD = "com.mattintech.simplelogupload.CANCEL_UPLOAD";
|
||||||
|
|
||||||
// Intent extras
|
// Intent extras
|
||||||
public static final String EXTRA_UPLOAD_CODE = "upload_code";
|
public static final String EXTRA_UPLOAD_CODE = "upload_code";
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ class FileUploadForegroundService : Service() {
|
|||||||
private var notificationBuilder: NotificationCompat.Builder? = null
|
private var notificationBuilder: NotificationCompat.Builder? = null
|
||||||
private var lastProgressUpdate: Long = 0
|
private var lastProgressUpdate: Long = 0
|
||||||
|
|
||||||
|
// Upload state tracking
|
||||||
|
private var currentUploadManager: ChunkedUploadManager? = null
|
||||||
|
private var currentStandardUploadCall: Call? = null
|
||||||
|
private var uploadState: UploadState = UploadState.IDLE
|
||||||
|
|
||||||
|
enum class UploadState {
|
||||||
|
IDLE, UPLOADING, PAUSED, CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
uploadController = UploadController(this)
|
uploadController = UploadController(this)
|
||||||
@@ -83,6 +92,22 @@ class FileUploadForegroundService : Service() {
|
|||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle control actions
|
||||||
|
when (intent.action) {
|
||||||
|
AppConstants.ACTION_PAUSE_UPLOAD -> {
|
||||||
|
handlePauseUpload()
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
AppConstants.ACTION_RESUME_UPLOAD -> {
|
||||||
|
handleResumeUpload()
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
AppConstants.ACTION_CANCEL_UPLOAD -> {
|
||||||
|
handleCancelUpload()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Intent received, extracting parameters...")
|
Log.d(TAG, "Intent received, extracting parameters...")
|
||||||
|
|
||||||
// Get file path from intent
|
// Get file path from intent
|
||||||
@@ -260,6 +285,10 @@ class FileUploadForegroundService : Service() {
|
|||||||
// Create chunked upload manager directly
|
// Create chunked upload manager directly
|
||||||
val uploadManager = ChunkedUploadManager(this, file, maxDownloads, expiryHours)
|
val uploadManager = ChunkedUploadManager(this, file, maxDownloads, expiryHours)
|
||||||
|
|
||||||
|
// Store reference for pause/resume/cancel control
|
||||||
|
currentUploadManager = uploadManager
|
||||||
|
uploadState = UploadState.UPLOADING
|
||||||
|
|
||||||
// Set progress listener with direct notification updates
|
// Set progress listener with direct notification updates
|
||||||
uploadManager.setProgressListener(object : ChunkedUploadManager.UploadProgressListener {
|
uploadManager.setProgressListener(object : ChunkedUploadManager.UploadProgressListener {
|
||||||
override fun onProgress(bytesUploaded: Long, totalBytes: Long, percentComplete: Int) {
|
override fun onProgress(bytesUploaded: Long, totalBytes: Long, percentComplete: Int) {
|
||||||
@@ -288,6 +317,10 @@ class FileUploadForegroundService : Service() {
|
|||||||
override fun onUploadComplete(result: UploadResult) {
|
override fun onUploadComplete(result: UploadResult) {
|
||||||
Log.d(TAG, "Chunked upload complete: $result")
|
Log.d(TAG, "Chunked upload complete: $result")
|
||||||
|
|
||||||
|
// Clear upload reference
|
||||||
|
currentUploadManager = null
|
||||||
|
uploadState = UploadState.IDLE
|
||||||
|
|
||||||
// Save to history directly if successful
|
// Save to history directly if successful
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
saveToHistory(result)
|
saveToHistory(result)
|
||||||
@@ -298,17 +331,22 @@ class FileUploadForegroundService : Service() {
|
|||||||
|
|
||||||
override fun onError(error: String) {
|
override fun onError(error: String) {
|
||||||
Log.e(TAG, "Chunked upload error: $error")
|
Log.e(TAG, "Chunked upload error: $error")
|
||||||
|
|
||||||
|
// Clear upload reference
|
||||||
|
currentUploadManager = null
|
||||||
|
uploadState = UploadState.IDLE
|
||||||
|
|
||||||
future.complete(UploadResult(error))
|
future.complete(UploadResult(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPaused() {
|
override fun onPaused() {
|
||||||
Log.d(TAG, "Chunked upload paused")
|
Log.d(TAG, "Chunked upload paused")
|
||||||
updateNotification("Upload paused", -1, 100)
|
// Note: updateNotification and sendPausedBroadcast are called in handlePauseUpload()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResumed() {
|
override fun onResumed() {
|
||||||
Log.d(TAG, "Chunked upload resumed")
|
Log.d(TAG, "Chunked upload resumed")
|
||||||
updateNotification("Resuming upload...", -1, 100)
|
// Note: updateNotification and sendResumedBroadcast are called in handleResumeUpload()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -372,13 +410,27 @@ class FileUploadForegroundService : Service() {
|
|||||||
// Show initial progress
|
// Show initial progress
|
||||||
updateNotification("Uploading ${file.name}...", 0, 100)
|
updateNotification("Uploading ${file.name}...", 0, 100)
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
// Create call and store reference for cancellation
|
||||||
|
val call = client.newCall(request)
|
||||||
|
currentStandardUploadCall = call
|
||||||
|
uploadState = UploadState.UPLOADING
|
||||||
|
|
||||||
|
call.enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
Log.e(TAG, "Upload failed", e)
|
Log.e(TAG, "Upload failed", e)
|
||||||
|
|
||||||
|
// Clear upload reference
|
||||||
|
currentStandardUploadCall = null
|
||||||
|
uploadState = UploadState.IDLE
|
||||||
|
|
||||||
future.complete(UploadResult("Upload failed: ${e.message}"))
|
future.complete(UploadResult("Upload failed: ${e.message}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
// Clear upload reference
|
||||||
|
currentStandardUploadCall = null
|
||||||
|
uploadState = UploadState.IDLE
|
||||||
|
|
||||||
response.use {
|
response.use {
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
try {
|
try {
|
||||||
@@ -482,6 +534,7 @@ class FileUploadForegroundService : Service() {
|
|||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setOnlyAlertOnce(true) // Don't alert on every progress update
|
.setOnlyAlertOnce(true) // Don't alert on every progress update
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH) // High priority for better visibility
|
.setPriority(NotificationCompat.PRIORITY_HIGH) // High priority for better visibility
|
||||||
|
.addAction(createCancelAction()) // Add cancel action from the start
|
||||||
.apply {
|
.apply {
|
||||||
// Force immediate notification display on Android 14+ (prevents system delay)
|
// Force immediate notification display on Android 14+ (prevents system delay)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
@@ -506,18 +559,49 @@ class FileUploadForegroundService : Service() {
|
|||||||
|
|
||||||
Log.d(TAG, "Updating notification: $status (progress: $progress/$max)")
|
Log.d(TAG, "Updating notification: $status (progress: $progress/$max)")
|
||||||
|
|
||||||
|
// Ignore updates if upload is cancelled (unless this IS the cancellation message)
|
||||||
|
if (uploadState == UploadState.CANCELLED && !isComplete) {
|
||||||
|
Log.d(TAG, "Ignoring notification update because upload is cancelled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
builder.setContentText(status)
|
builder.setContentText(status)
|
||||||
|
|
||||||
if (progress >= 0) {
|
if (progress >= 0 && !isComplete) {
|
||||||
// Determinate progress
|
// Determinate progress
|
||||||
builder.setProgress(max, progress, false)
|
builder.setProgress(max, progress, false)
|
||||||
} else {
|
} else if (!isComplete) {
|
||||||
// Indeterminate progress
|
// Indeterminate progress
|
||||||
builder.setProgress(max, 0, true)
|
builder.setProgress(max, 0, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When complete, make notification dismissible and remove progress bar
|
// Add action buttons based on upload state
|
||||||
if (isComplete) {
|
if (!isComplete && uploadState != UploadState.CANCELLED) {
|
||||||
|
builder.clearActions() // Remove old actions
|
||||||
|
|
||||||
|
when (uploadState) {
|
||||||
|
UploadState.PAUSED -> {
|
||||||
|
// Show Resume and Cancel buttons
|
||||||
|
builder.addAction(createResumeAction())
|
||||||
|
builder.addAction(createCancelAction())
|
||||||
|
}
|
||||||
|
UploadState.UPLOADING -> {
|
||||||
|
// Show Pause (if chunked) and Cancel buttons
|
||||||
|
if (currentUploadManager != null) {
|
||||||
|
builder.addAction(createPauseAction())
|
||||||
|
}
|
||||||
|
builder.addAction(createCancelAction())
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// For IDLE or other states, just show Cancel
|
||||||
|
builder.addAction(createCancelAction())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When complete or cancelled, make notification dismissible and remove progress bar
|
||||||
|
if (isComplete || uploadState == UploadState.CANCELLED) {
|
||||||
|
builder.clearActions() // Remove all actions
|
||||||
builder.setOngoing(false)
|
builder.setOngoing(false)
|
||||||
.setProgress(0, 0, false) // Remove progress bar
|
.setProgress(0, 0, false) // Remove progress bar
|
||||||
.setAutoCancel(true) // Allow swipe to dismiss
|
.setAutoCancel(true) // Allow swipe to dismiss
|
||||||
@@ -611,4 +695,182 @@ class FileUploadForegroundService : Service() {
|
|||||||
// Send local broadcast to the ViewModel
|
// Send local broadcast to the ViewModel
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a broadcast when upload is paused
|
||||||
|
*/
|
||||||
|
private fun sendPausedBroadcast() {
|
||||||
|
Log.d(TAG, "Sending paused broadcast")
|
||||||
|
val intent = Intent(AppConstants.ACTION_UPLOAD_PAUSED)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a broadcast when upload is resumed
|
||||||
|
*/
|
||||||
|
private fun sendResumedBroadcast() {
|
||||||
|
Log.d(TAG, "Sending resumed broadcast")
|
||||||
|
val intent = Intent(AppConstants.ACTION_UPLOAD_RESUMED)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a broadcast when upload is cancelled
|
||||||
|
*/
|
||||||
|
private fun sendCancelledBroadcast() {
|
||||||
|
Log.d(TAG, "Sending cancelled broadcast")
|
||||||
|
val intent = Intent(AppConstants.ACTION_UPLOAD_CANCELLED)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a pause action for the notification
|
||||||
|
*/
|
||||||
|
private fun createPauseAction(): NotificationCompat.Action {
|
||||||
|
val pauseIntent = Intent(this, FileUploadForegroundService::class.java).apply {
|
||||||
|
action = AppConstants.ACTION_PAUSE_UPLOAD
|
||||||
|
}
|
||||||
|
val pausePendingIntent = PendingIntent.getService(
|
||||||
|
this,
|
||||||
|
1,
|
||||||
|
pauseIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
return NotificationCompat.Action.Builder(
|
||||||
|
android.R.drawable.ic_media_pause,
|
||||||
|
"Pause",
|
||||||
|
pausePendingIntent
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a resume action for the notification
|
||||||
|
*/
|
||||||
|
private fun createResumeAction(): NotificationCompat.Action {
|
||||||
|
val resumeIntent = Intent(this, FileUploadForegroundService::class.java).apply {
|
||||||
|
action = AppConstants.ACTION_RESUME_UPLOAD
|
||||||
|
}
|
||||||
|
val resumePendingIntent = PendingIntent.getService(
|
||||||
|
this,
|
||||||
|
2,
|
||||||
|
resumeIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
return NotificationCompat.Action.Builder(
|
||||||
|
android.R.drawable.ic_media_play,
|
||||||
|
"Resume",
|
||||||
|
resumePendingIntent
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cancel action for the notification
|
||||||
|
*/
|
||||||
|
private fun createCancelAction(): NotificationCompat.Action {
|
||||||
|
val cancelIntent = Intent(this, FileUploadForegroundService::class.java).apply {
|
||||||
|
action = AppConstants.ACTION_CANCEL_UPLOAD
|
||||||
|
}
|
||||||
|
val cancelPendingIntent = PendingIntent.getService(
|
||||||
|
this,
|
||||||
|
3,
|
||||||
|
cancelIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
return NotificationCompat.Action.Builder(
|
||||||
|
android.R.drawable.ic_menu_close_clear_cancel,
|
||||||
|
"Cancel",
|
||||||
|
cancelPendingIntent
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle pause upload request
|
||||||
|
*/
|
||||||
|
private fun handlePauseUpload() {
|
||||||
|
Log.d(TAG, "Handling pause upload request")
|
||||||
|
|
||||||
|
if (uploadState != UploadState.UPLOADING) {
|
||||||
|
Log.w(TAG, "Cannot pause: upload not in progress (state: $uploadState)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadState = UploadState.PAUSED
|
||||||
|
|
||||||
|
currentUploadManager?.let { manager ->
|
||||||
|
manager.pauseUpload()
|
||||||
|
updateNotification("Upload paused", -1, 100)
|
||||||
|
sendPausedBroadcast()
|
||||||
|
Log.d(TAG, "Chunked upload paused successfully")
|
||||||
|
} ?: run {
|
||||||
|
Log.w(TAG, "Cannot pause standard upload (not supported)")
|
||||||
|
updateNotification("Cannot pause standard upload", -1, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle resume upload request
|
||||||
|
*/
|
||||||
|
private fun handleResumeUpload() {
|
||||||
|
Log.d(TAG, "Handling resume upload request")
|
||||||
|
|
||||||
|
if (uploadState != UploadState.PAUSED) {
|
||||||
|
Log.w(TAG, "Cannot resume: upload not paused (state: $uploadState)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadState = UploadState.UPLOADING
|
||||||
|
|
||||||
|
currentUploadManager?.let { manager ->
|
||||||
|
manager.resumeUpload()
|
||||||
|
updateNotification("Resuming upload...", -1, 100)
|
||||||
|
sendResumedBroadcast()
|
||||||
|
Log.d(TAG, "Chunked upload resumed successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle cancel upload request
|
||||||
|
*/
|
||||||
|
private fun handleCancelUpload() {
|
||||||
|
Log.d(TAG, "Handling cancel upload request")
|
||||||
|
|
||||||
|
uploadState = UploadState.CANCELLED
|
||||||
|
|
||||||
|
// Cancel chunked upload
|
||||||
|
currentUploadManager?.let { manager ->
|
||||||
|
manager.cancelUpload()
|
||||||
|
currentUploadManager = null
|
||||||
|
Log.d(TAG, "Cancelled chunked upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel standard upload
|
||||||
|
currentStandardUploadCall?.let { call ->
|
||||||
|
call.cancel()
|
||||||
|
currentStandardUploadCall = null
|
||||||
|
Log.d(TAG, "Cancelled standard upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear upload in progress flag
|
||||||
|
PreferencesUtil.setUploadInProgress(this, false, null)
|
||||||
|
|
||||||
|
// Send cancelled broadcast
|
||||||
|
sendCancelledBroadcast()
|
||||||
|
|
||||||
|
// Update notification
|
||||||
|
updateNotification("Upload cancelled", -1, 100, isComplete = true)
|
||||||
|
|
||||||
|
// Stop service after a short delay
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
stopForeground(STOP_FOREGROUND_DETACH)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
stopForeground(false)
|
||||||
|
}
|
||||||
|
stopSelf()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ package com.mattintech.simplelogupload.ui.screens
|
|||||||
import androidx.compose.foundation.layout.*
|
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.Close
|
||||||
import androidx.compose.material.icons.filled.History
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.Pause
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Settings
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -113,7 +117,7 @@ fun MainScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Uploading",
|
text = if (uiState.isPaused) "Upload Paused" else "Uploading",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
@@ -149,6 +153,70 @@ fun MainScreen(
|
|||||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status message
|
||||||
|
uiState.uploadStatusMessage?.let { message ->
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control buttons
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (uiState.isPaused) {
|
||||||
|
// Show Resume button
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.resumeUpload() }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "Resume",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Resume")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show Pause button (only for chunked uploads)
|
||||||
|
if (uiState.isChunkedUpload) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.pauseUpload() }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Pause,
|
||||||
|
contentDescription = "Pause",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Pause")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// Cancel button (always shown)
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.cancelUpload() }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = "Cancel",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import com.mattintech.simplelogupload.constant.AppConstants
|
|||||||
import com.mattintech.simplelogupload.controller.FileController
|
import com.mattintech.simplelogupload.controller.FileController
|
||||||
import com.mattintech.simplelogupload.controller.UploadController
|
import com.mattintech.simplelogupload.controller.UploadController
|
||||||
import com.mattintech.simplelogupload.model.UploadResult
|
import com.mattintech.simplelogupload.model.UploadResult
|
||||||
|
import com.mattintech.simplelogupload.service.FileUploadForegroundService
|
||||||
|
import com.mattintech.simplelogupload.upload.UploadStrategySelector
|
||||||
import com.mattintech.simplelogupload.util.PreferencesUtil
|
import com.mattintech.simplelogupload.util.PreferencesUtil
|
||||||
import com.mattintech.simplelogupload.util.ZipUtil
|
import com.mattintech.simplelogupload.util.ZipUtil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -30,6 +32,9 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
data class MainUiState(
|
data class MainUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val isPaused: Boolean = false,
|
||||||
|
val isChunkedUpload: Boolean = false,
|
||||||
|
val uploadStatusMessage: String? = null,
|
||||||
val selectedFile: File? = null,
|
val selectedFile: File? = null,
|
||||||
val selectedFileName: String? = null,
|
val selectedFileName: String? = null,
|
||||||
val dumpstateFiles: List<File> = emptyList(),
|
val dumpstateFiles: List<File> = emptyList(),
|
||||||
@@ -150,6 +155,41 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
totalBytes = 0
|
totalBytes = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AppConstants.ACTION_UPLOAD_PAUSED -> {
|
||||||
|
Log.d(TAG, "Upload paused")
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isPaused = true,
|
||||||
|
uploadStatusMessage = "Upload paused"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AppConstants.ACTION_UPLOAD_RESUMED -> {
|
||||||
|
Log.d(TAG, "Upload resumed")
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isPaused = false,
|
||||||
|
uploadStatusMessage = "Resuming upload..."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AppConstants.ACTION_UPLOAD_CANCELLED -> {
|
||||||
|
Log.d(TAG, "Upload cancelled")
|
||||||
|
|
||||||
|
// Clean up temporary zip file if it exists
|
||||||
|
currentUploadTempZipFile?.let { tempFile ->
|
||||||
|
if (tempFile.exists()) {
|
||||||
|
val deleted = tempFile.delete()
|
||||||
|
Log.d(TAG, "Cleaned up temp zip file after cancellation: ${tempFile.name}, deleted: $deleted")
|
||||||
|
}
|
||||||
|
currentUploadTempZipFile = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isPaused = false,
|
||||||
|
uploadProgress = 0,
|
||||||
|
bytesUploaded = 0,
|
||||||
|
totalBytes = 0,
|
||||||
|
uploadStatusMessage = "Upload cancelled"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,6 +198,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
addAction(AppConstants.ACTION_UPLOAD_PROGRESS)
|
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)
|
||||||
|
addAction(AppConstants.ACTION_UPLOAD_PAUSED)
|
||||||
|
addAction(AppConstants.ACTION_UPLOAD_RESUMED)
|
||||||
|
addAction(AppConstants.ACTION_UPLOAD_CANCELLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(getApplication())
|
LocalBroadcastManager.getInstance(getApplication())
|
||||||
@@ -288,8 +331,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
currentUploadMaxDownloads = maxDownloads
|
currentUploadMaxDownloads = maxDownloads
|
||||||
currentUploadExpiryHours = expiryHours
|
currentUploadExpiryHours = expiryHours
|
||||||
|
|
||||||
|
// Determine if this will be a chunked upload
|
||||||
|
val isChunked = UploadStrategySelector.shouldUseChunkedUpload(getApplication(), file)
|
||||||
|
Log.d(TAG, "Upload will use ${if (isChunked) "chunked" else "standard"} strategy")
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
isLoading = true,
|
isLoading = true,
|
||||||
|
isChunkedUpload = isChunked,
|
||||||
|
isPaused = false,
|
||||||
|
uploadStatusMessage = null,
|
||||||
showUploadParamsDialog = false
|
showUploadParamsDialog = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -358,6 +408,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
currentUploadMaxDownloads = maxDownloads
|
currentUploadMaxDownloads = maxDownloads
|
||||||
currentUploadExpiryHours = expiryHours
|
currentUploadExpiryHours = expiryHours
|
||||||
|
|
||||||
|
// Determine if this will be a chunked upload
|
||||||
|
val isChunked = UploadStrategySelector.shouldUseChunkedUpload(getApplication(), fileToUpload)
|
||||||
|
Log.d(TAG, "Upload will use ${if (isChunked) "chunked" else "standard"} strategy")
|
||||||
|
|
||||||
|
// Update state with upload type
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isChunkedUpload = isChunked,
|
||||||
|
isPaused = false,
|
||||||
|
uploadStatusMessage = null
|
||||||
|
)
|
||||||
|
|
||||||
uploadController.startUploadService(fileToUpload, maxDownloads, expiryHours)
|
uploadController.startUploadService(fileToUpload, maxDownloads, expiryHours)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error uploading dumpstate files", e)
|
Log.e(TAG, "Error uploading dumpstate files", e)
|
||||||
@@ -378,6 +439,39 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the current upload
|
||||||
|
*/
|
||||||
|
fun pauseUpload() {
|
||||||
|
Log.d(TAG, "Pause upload requested")
|
||||||
|
val intent = Intent(getApplication(), FileUploadForegroundService::class.java).apply {
|
||||||
|
action = AppConstants.ACTION_PAUSE_UPLOAD
|
||||||
|
}
|
||||||
|
getApplication<Application>().startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume the paused upload
|
||||||
|
*/
|
||||||
|
fun resumeUpload() {
|
||||||
|
Log.d(TAG, "Resume upload requested")
|
||||||
|
val intent = Intent(getApplication(), FileUploadForegroundService::class.java).apply {
|
||||||
|
action = AppConstants.ACTION_RESUME_UPLOAD
|
||||||
|
}
|
||||||
|
getApplication<Application>().startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the current upload
|
||||||
|
*/
|
||||||
|
fun cancelUpload() {
|
||||||
|
Log.d(TAG, "Cancel upload requested")
|
||||||
|
val intent = Intent(getApplication(), FileUploadForegroundService::class.java).apply {
|
||||||
|
action = AppConstants.ACTION_CANCEL_UPLOAD
|
||||||
|
}
|
||||||
|
getApplication<Application>().startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
// Unregister broadcast receiver
|
// Unregister broadcast receiver
|
||||||
|
|||||||
Reference in New Issue
Block a user