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