Add configurable parallel uploads and improve chunk upload settings

- Add parallel uploads toggle in settings (experimental, disabled by default)
- Increase mobile chunk size from 2MB to 4MB
- Configure parallel uploads: 3 chunks on WiFi, 2 on mobile (if enabled), 1 sequential (if disabled)
- Fix progress tracking when retrying missing chunks (subtract bytes to prevent >100%)
This commit is contained in:
2025-12-27 22:20:01 -05:00
parent 41363028aa
commit 04b3bea454
3 changed files with 103 additions and 16 deletions

View File

@@ -58,6 +58,9 @@ fun SettingsScreen(
var askBeforeUpload by remember { var askBeforeUpload by remember {
mutableStateOf(PreferencesUtil.getAskBeforeUpload(context)) mutableStateOf(PreferencesUtil.getAskBeforeUpload(context))
} }
var enableParallelUploads by remember {
mutableStateOf(PreferencesUtil.isParallelUploadsEnabled(context))
}
var clientKeyVisible by remember { mutableStateOf(false) } var clientKeyVisible by remember { mutableStateOf(false) }
var serverAddressError by remember { mutableStateOf<String?>(null) } var serverAddressError by remember { mutableStateOf<String?>(null) }
@@ -176,6 +179,7 @@ fun SettingsScreen(
PreferencesUtil.setDefaultMaxDownloads(context, maxDl!!) PreferencesUtil.setDefaultMaxDownloads(context, maxDl!!)
PreferencesUtil.setDefaultExpiryHours(context, expiry!!) PreferencesUtil.setDefaultExpiryHours(context, expiry!!)
PreferencesUtil.setAskBeforeUpload(context, askBeforeUpload) PreferencesUtil.setAskBeforeUpload(context, askBeforeUpload)
PreferencesUtil.setParallelUploadsEnabled(context, enableParallelUploads)
Toast.makeText(context, "Settings saved successfully!", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Settings saved successfully!", Toast.LENGTH_SHORT).show()
onNavigateBack() onNavigateBack()
@@ -189,6 +193,7 @@ fun SettingsScreen(
defaultMaxDownloads = PreferencesUtil.getDefaultMaxDownloads(context).toString() defaultMaxDownloads = PreferencesUtil.getDefaultMaxDownloads(context).toString()
defaultExpiryHours = PreferencesUtil.getDefaultExpiryHours(context).toString() defaultExpiryHours = PreferencesUtil.getDefaultExpiryHours(context).toString()
askBeforeUpload = PreferencesUtil.getAskBeforeUpload(context) askBeforeUpload = PreferencesUtil.getAskBeforeUpload(context)
enableParallelUploads = PreferencesUtil.isParallelUploadsEnabled(context)
// Clear errors // Clear errors
serverAddressError = null serverAddressError = null
@@ -381,6 +386,44 @@ fun SettingsScreen(
} }
} }
Spacer(modifier = Modifier.height(8.dp))
// Parallel Uploads Switch
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Upload Mode",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (enableParallelUploads)
"Parallel: 2 chunks simultaneously (Experimental)"
else
"Sequential: One chunk at a time (Recommended)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = enableParallelUploads,
onCheckedChange = { enableParallelUploads = it }
)
}
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// Action buttons // Action buttons

View File

@@ -48,12 +48,12 @@ public class ChunkedUploadManager {
// Chunk size configuration // Chunk size configuration
private static final int WIFI_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB for WiFi private static final int WIFI_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB for WiFi
private static final int MOBILE_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB for mobile networks private static final int MOBILE_CHUNK_SIZE = 4 * 1024 * 1024; // 4MB for mobile networks (increased from 2MB)
private static final int DEFAULT_CHUNK_SIZE = WIFI_CHUNK_SIZE; private static final int DEFAULT_CHUNK_SIZE = WIFI_CHUNK_SIZE;
// Max parallel uploads // Max parallel uploads
private static final int WIFI_MAX_PARALLEL = 3; private static final int WIFI_MAX_PARALLEL = 3;
private static final int MOBILE_MAX_PARALLEL = 1; private static final int MOBILE_MAX_PARALLEL = 2; // 2 parallel chunks on mobile (testing with nginx config updates)
// Retry configuration // Retry configuration
private static final int MAX_RETRY_ATTEMPTS = 3; private static final int MAX_RETRY_ATTEMPTS = 3;
@@ -118,15 +118,21 @@ public class ChunkedUploadManager {
// Set up connectivity manager for network monitoring // Set up connectivity manager for network monitoring
this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
// Determine initial connection type // Determine initial connection type
detectConnectionType(); detectConnectionType();
// Configure chunk size based on connection type // Configure chunk size based on connection type
this.chunkSize = isWifiConnection ? WIFI_CHUNK_SIZE : MOBILE_CHUNK_SIZE; this.chunkSize = isWifiConnection ? WIFI_CHUNK_SIZE : MOBILE_CHUNK_SIZE;
// Configure max parallel uploads based on connection type // Configure max parallel uploads based on connection type and user preference
this.maxParallelUploads = isWifiConnection ? WIFI_MAX_PARALLEL : MOBILE_MAX_PARALLEL; boolean parallelEnabled = PreferencesUtil.isParallelUploadsEnabled(context);
if (isWifiConnection) {
this.maxParallelUploads = WIFI_MAX_PARALLEL;
} else {
// On mobile, use parallel uploads only if enabled in settings
this.maxParallelUploads = parallelEnabled ? MOBILE_MAX_PARALLEL : 1;
}
// Calculate total chunks // Calculate total chunks
this.totalChunks = (int) Math.ceil((double) file.length() / chunkSize); this.totalChunks = (int) Math.ceil((double) file.length() / chunkSize);
@@ -139,8 +145,9 @@ public class ChunkedUploadManager {
Log.d(TAG, "Created chunked upload manager for file: " + file.getName()); Log.d(TAG, "Created chunked upload manager for file: " + file.getName());
Log.d(TAG, "Total size: " + totalBytes + " bytes, Chunks: " + totalChunks); Log.d(TAG, "Total size: " + totalBytes + " bytes, Chunks: " + totalChunks);
Log.d(TAG, "Connection type: " + (isWifiConnection ? "WiFi" : "Mobile") + Log.d(TAG, "Connection type: " + (isWifiConnection ? "WiFi" : "Mobile") +
", Chunk size: " + chunkSize + " bytes"); ", Chunk size: " + chunkSize + " bytes" +
", Parallel uploads: " + maxParallelUploads + (parallelEnabled ? " (enabled)" : " (disabled)"));
} }
/** /**
@@ -661,33 +668,40 @@ public class ChunkedUploadManager {
// Verify all chunks are uploaded before finalizing // Verify all chunks are uploaded before finalizing
boolean allChunksUploaded = true; boolean allChunksUploaded = true;
Set<Integer> missingChunks = new HashSet<>(); Set<Integer> missingChunks = new HashSet<>();
for (int i = 0; i < totalChunks; i++) { for (int i = 0; i < totalChunks; i++) {
if (!completedChunks.contains(i)) { if (!completedChunks.contains(i)) {
missingChunks.add(i); missingChunks.add(i);
allChunksUploaded = false; allChunksUploaded = false;
} }
} }
if (!allChunksUploaded) { if (!allChunksUploaded) {
Log.e(TAG, "Cannot finalize: missing chunks " + missingChunks); Log.e(TAG, "Cannot finalize: missing chunks " + missingChunks);
future.complete(new UploadResult("Cannot finalize: missing chunks " + missingChunks)); future.complete(new UploadResult("Cannot finalize: missing chunks " + missingChunks));
return future; return future;
} }
// Get server URL and client key from preferences // Get server URL and client key from preferences
String serverBaseUrl = PreferencesUtil.getServerBaseUrl(context); String serverBaseUrl = PreferencesUtil.getServerBaseUrl(context);
String clientKey = PreferencesUtil.getClientKey(context); String clientKey = PreferencesUtil.getClientKey(context);
// Before finalizing, verify with the server which chunks it has received // Before finalizing, verify with the server which chunks it has received
verifyChunksWithServer() verifyChunksWithServer()
.thenAccept(serverMissingChunks -> { .thenAccept(serverMissingChunks -> {
if (!serverMissingChunks.isEmpty()) { if (!serverMissingChunks.isEmpty()) {
Log.e(TAG, "Server missing chunks: " + serverMissingChunks + ", reuploading missing chunks"); Log.e(TAG, "Server missing chunks: " + serverMissingChunks + ", reuploading missing chunks");
// Reset these chunks as not uploaded so they'll be retried // Reset these chunks as not uploaded so they'll be retried
// Also subtract their bytes from uploadedBytes to prevent progress > 100%
for (Integer chunkIndex : serverMissingChunks) { for (Integer chunkIndex : serverMissingChunks) {
completedChunks.remove(chunkIndex); completedChunks.remove(chunkIndex);
// Calculate chunk size and subtract from uploaded bytes
long start = (long) chunkIndex * chunkSize;
long end = Math.min(start + chunkSize, file.length());
long chunkBytes = end - start;
uploadedBytes.addAndGet(-chunkBytes);
} }
// Retry uploading the missing chunks // Retry uploading the missing chunks
@@ -802,12 +816,19 @@ public class ChunkedUploadManager {
// Automatically retry these specific chunks // Automatically retry these specific chunks
if (!missingChunks.isEmpty()) { if (!missingChunks.isEmpty()) {
Log.d(TAG, "Server reported missing chunks: " + missingChunks + Log.d(TAG, "Server reported missing chunks: " + missingChunks +
". Will retry these chunks specifically."); ". Will retry these chunks specifically.");
// Remove these chunks from completedChunks // Remove these chunks from completedChunks
// Also subtract their bytes from uploadedBytes to prevent progress > 100%
for (Integer chunkIndex : missingChunks) { for (Integer chunkIndex : missingChunks) {
completedChunks.remove(chunkIndex); completedChunks.remove(chunkIndex);
// Calculate chunk size and subtract from uploaded bytes
long start = (long) chunkIndex * chunkSize;
long end = Math.min(start + chunkSize, file.length());
long chunkBytes = end - start;
uploadedBytes.addAndGet(-chunkBytes);
} }
retrySpecificChunks(missingChunks) retrySpecificChunks(missingChunks)

View File

@@ -20,6 +20,7 @@ public class PreferencesUtil {
public static final String KEY_ASK_BEFORE_UPLOAD = "ask_before_upload"; public static final String KEY_ASK_BEFORE_UPLOAD = "ask_before_upload";
public static final String KEY_UPLOAD_IN_PROGRESS = "upload_in_progress"; public static final String KEY_UPLOAD_IN_PROGRESS = "upload_in_progress";
public static final String KEY_UPLOAD_FILE_NAME = "upload_file_name"; public static final String KEY_UPLOAD_FILE_NAME = "upload_file_name";
public static final String KEY_ENABLE_PARALLEL_UPLOADS = "enable_parallel_uploads";
// Default values // Default values
private static final String DEFAULT_SERVER_ADDRESS = ""; private static final String DEFAULT_SERVER_ADDRESS = "";
@@ -30,6 +31,7 @@ public class PreferencesUtil {
private static final int DEFAULT_MAX_DOWNLOADS = 5; private static final int DEFAULT_MAX_DOWNLOADS = 5;
private static final int DEFAULT_EXPIRY_HOURS = 24; private static final int DEFAULT_EXPIRY_HOURS = 24;
private static final boolean DEFAULT_ASK_BEFORE_UPLOAD = true; private static final boolean DEFAULT_ASK_BEFORE_UPLOAD = true;
private static final boolean DEFAULT_ENABLE_PARALLEL_UPLOADS = false;
/** /**
* Get the shared preferences instance * Get the shared preferences instance
@@ -140,6 +142,7 @@ public class PreferencesUtil {
editor.putInt(KEY_DEFAULT_MAX_DOWNLOADS, DEFAULT_MAX_DOWNLOADS); editor.putInt(KEY_DEFAULT_MAX_DOWNLOADS, DEFAULT_MAX_DOWNLOADS);
editor.putInt(KEY_DEFAULT_EXPIRY_HOURS, DEFAULT_EXPIRY_HOURS); editor.putInt(KEY_DEFAULT_EXPIRY_HOURS, DEFAULT_EXPIRY_HOURS);
editor.putBoolean(KEY_ASK_BEFORE_UPLOAD, DEFAULT_ASK_BEFORE_UPLOAD); editor.putBoolean(KEY_ASK_BEFORE_UPLOAD, DEFAULT_ASK_BEFORE_UPLOAD);
editor.putBoolean(KEY_ENABLE_PARALLEL_UPLOADS, DEFAULT_ENABLE_PARALLEL_UPLOADS);
editor.apply(); editor.apply();
} }
@@ -295,4 +298,24 @@ public class PreferencesUtil {
public static String getUploadFileName(Context context) { public static String getUploadFileName(Context context) {
return getPreferences(context).getString(KEY_UPLOAD_FILE_NAME, null); return getPreferences(context).getString(KEY_UPLOAD_FILE_NAME, null);
} }
/**
* Get the parallel uploads enabled preference
*
* @param context The application context
* @return True if parallel uploads are enabled, false otherwise
*/
public static boolean isParallelUploadsEnabled(Context context) {
return getPreferences(context).getBoolean(KEY_ENABLE_PARALLEL_UPLOADS, DEFAULT_ENABLE_PARALLEL_UPLOADS);
}
/**
* Set the parallel uploads enabled preference
*
* @param context The application context
* @param enabled True to enable parallel uploads, false otherwise
*/
public static void setParallelUploadsEnabled(Context context, boolean enabled) {
getPreferences(context).edit().putBoolean(KEY_ENABLE_PARALLEL_UPLOADS, enabled).apply();
}
} }