diff --git a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/SettingsScreen.kt b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/SettingsScreen.kt index 6c5658e..181b4ca 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/SettingsScreen.kt @@ -58,6 +58,9 @@ fun SettingsScreen( var askBeforeUpload by remember { mutableStateOf(PreferencesUtil.getAskBeforeUpload(context)) } + var enableParallelUploads by remember { + mutableStateOf(PreferencesUtil.isParallelUploadsEnabled(context)) + } var clientKeyVisible by remember { mutableStateOf(false) } var serverAddressError by remember { mutableStateOf(null) } @@ -176,6 +179,7 @@ fun SettingsScreen( PreferencesUtil.setDefaultMaxDownloads(context, maxDl!!) PreferencesUtil.setDefaultExpiryHours(context, expiry!!) PreferencesUtil.setAskBeforeUpload(context, askBeforeUpload) + PreferencesUtil.setParallelUploadsEnabled(context, enableParallelUploads) Toast.makeText(context, "Settings saved successfully!", Toast.LENGTH_SHORT).show() onNavigateBack() @@ -189,6 +193,7 @@ fun SettingsScreen( defaultMaxDownloads = PreferencesUtil.getDefaultMaxDownloads(context).toString() defaultExpiryHours = PreferencesUtil.getDefaultExpiryHours(context).toString() askBeforeUpload = PreferencesUtil.getAskBeforeUpload(context) + enableParallelUploads = PreferencesUtil.isParallelUploadsEnabled(context) // Clear errors 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)) // Action buttons diff --git a/app/src/main/java/com/mattintech/simplelogupload/upload/ChunkedUploadManager.java b/app/src/main/java/com/mattintech/simplelogupload/upload/ChunkedUploadManager.java index 3e84155..6be9fd9 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/upload/ChunkedUploadManager.java +++ b/app/src/main/java/com/mattintech/simplelogupload/upload/ChunkedUploadManager.java @@ -48,12 +48,12 @@ public class ChunkedUploadManager { // Chunk size configuration 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; // Max parallel uploads 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 private static final int MAX_RETRY_ATTEMPTS = 3; @@ -118,15 +118,21 @@ public class ChunkedUploadManager { // Set up connectivity manager for network monitoring this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - + // Determine initial connection type detectConnectionType(); - + // Configure chunk size based on connection type this.chunkSize = isWifiConnection ? WIFI_CHUNK_SIZE : MOBILE_CHUNK_SIZE; - - // Configure max parallel uploads based on connection type - this.maxParallelUploads = isWifiConnection ? WIFI_MAX_PARALLEL : MOBILE_MAX_PARALLEL; + + // Configure max parallel uploads based on connection type and user preference + 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 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, "Total size: " + totalBytes + " bytes, Chunks: " + totalChunks); - Log.d(TAG, "Connection type: " + (isWifiConnection ? "WiFi" : "Mobile") + - ", Chunk size: " + chunkSize + " bytes"); + Log.d(TAG, "Connection type: " + (isWifiConnection ? "WiFi" : "Mobile") + + ", Chunk size: " + chunkSize + " bytes" + + ", Parallel uploads: " + maxParallelUploads + (parallelEnabled ? " (enabled)" : " (disabled)")); } /** @@ -661,33 +668,40 @@ public class ChunkedUploadManager { // Verify all chunks are uploaded before finalizing boolean allChunksUploaded = true; Set missingChunks = new HashSet<>(); - + for (int i = 0; i < totalChunks; i++) { if (!completedChunks.contains(i)) { missingChunks.add(i); allChunksUploaded = false; } } - + if (!allChunksUploaded) { Log.e(TAG, "Cannot finalize: missing chunks " + missingChunks); future.complete(new UploadResult("Cannot finalize: missing chunks " + missingChunks)); return future; } - + // Get server URL and client key from preferences String serverBaseUrl = PreferencesUtil.getServerBaseUrl(context); String clientKey = PreferencesUtil.getClientKey(context); - + // Before finalizing, verify with the server which chunks it has received verifyChunksWithServer() .thenAccept(serverMissingChunks -> { if (!serverMissingChunks.isEmpty()) { Log.e(TAG, "Server missing chunks: " + serverMissingChunks + ", reuploading missing chunks"); - + // 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) { 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 @@ -802,12 +816,19 @@ public class ChunkedUploadManager { // Automatically retry these specific chunks if (!missingChunks.isEmpty()) { - Log.d(TAG, "Server reported missing chunks: " + missingChunks + + Log.d(TAG, "Server reported missing chunks: " + missingChunks + ". Will retry these chunks specifically."); - + // Remove these chunks from completedChunks + // Also subtract their bytes from uploadedBytes to prevent progress > 100% for (Integer chunkIndex : missingChunks) { 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) diff --git a/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java b/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java index a1cb3af..cc2f220 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java +++ b/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java @@ -20,6 +20,7 @@ public class PreferencesUtil { 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_FILE_NAME = "upload_file_name"; + public static final String KEY_ENABLE_PARALLEL_UPLOADS = "enable_parallel_uploads"; // Default values 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_EXPIRY_HOURS = 24; private static final boolean DEFAULT_ASK_BEFORE_UPLOAD = true; + private static final boolean DEFAULT_ENABLE_PARALLEL_UPLOADS = false; /** * 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_EXPIRY_HOURS, DEFAULT_EXPIRY_HOURS); editor.putBoolean(KEY_ASK_BEFORE_UPLOAD, DEFAULT_ASK_BEFORE_UPLOAD); + editor.putBoolean(KEY_ENABLE_PARALLEL_UPLOADS, DEFAULT_ENABLE_PARALLEL_UPLOADS); editor.apply(); } @@ -295,4 +298,24 @@ public class PreferencesUtil { public static String getUploadFileName(Context context) { 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(); + } }