adding first iteration of chunked file support

This commit is contained in:
2025-04-16 21:41:07 -04:00
parent 76b93e3363
commit d32fd4690d
9 changed files with 1736 additions and 6 deletions

View File

@@ -3,6 +3,7 @@
package="com.mattintech.simplelogupload">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

View File

@@ -8,11 +8,25 @@ public class NetworkConstants {
// via PreferencesUtil to allow for customization
public static final String SERVER_URL_BASE = "/upload";
// Chunked upload endpoints (relative to server base URL)
public static final String CHUNKED_UPLOAD_START = "/upload/start";
public static final String CHUNKED_UPLOAD_CHUNK = "/upload/chunk";
public static final String CHUNKED_UPLOAD_COMPLETE = "/upload/complete";
// Upload parameters
public static final String PARAM_FILE = "file";
public static final String PARAM_MAX_DOWNLOADS = "max_downloads";
public static final String PARAM_EXPIRY_HOURS = "expiry_hours";
// Chunked upload parameters
public static final String PARAM_UPLOAD_ID = "upload_id";
public static final String PARAM_CHUNK_INDEX = "chunk_index";
public static final String PARAM_TOTAL_CHUNKS = "total_chunks";
public static final String PARAM_CHUNK = "chunk";
public static final String PARAM_FILENAME = "filename";
public static final String PARAM_FILESIZE = "filesize";
public static final String PARAM_CHUNK_SIZE = "chunk_size";
// Authentication
public static final String CLIENT_KEY_HEADER = "X-Client-Key";

View File

@@ -11,6 +11,8 @@ import com.mattintech.simplelogupload.db.UploadHistoryRepository;
import com.mattintech.simplelogupload.model.UploadHistory;
import com.mattintech.simplelogupload.model.UploadResult;
import com.mattintech.simplelogupload.service.FileUploadForegroundService;
import com.mattintech.simplelogupload.upload.ChunkedUploadManager;
import com.mattintech.simplelogupload.upload.UploadStrategySelector;
import com.mattintech.simplelogupload.util.PreferencesUtil;
import java.io.File;
@@ -66,7 +68,14 @@ public class UploadController {
serviceIntent.putExtra(FileUploadForegroundService.EXTRA_MAX_DOWNLOADS, NetworkConstants.DEFAULT_MAX_DOWNLOADS);
serviceIntent.putExtra(FileUploadForegroundService.EXTRA_EXPIRY_HOURS, NetworkConstants.DEFAULT_EXPIRY_HOURS);
context.startService(serviceIntent);
// On Android 12+ (API 31+), we need to use startForegroundService
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
Log.d(TAG, "Starting foreground service (Android 12+)");
context.startForegroundService(serviceIntent);
} else {
Log.d(TAG, "Starting service (pre-Android 12)");
context.startService(serviceIntent);
}
}
/**
@@ -86,6 +95,106 @@ public class UploadController {
return future;
}
// Determine whether to use chunked upload or standard upload
boolean useChunkedUpload = UploadStrategySelector.shouldUseChunkedUpload(context, file);
String networkType = UploadStrategySelector.getNetworkTypeName(context);
Log.d(TAG, "Uploading file: " + file.getName() + ", Size: " + file.length() +
", Network: " + networkType + ", Chunked: " + useChunkedUpload);
if (useChunkedUpload) {
// Use chunked upload for large files
return performChunkedUpload(file, maxDownloads, expiryHours);
} else {
// Use standard upload for smaller files
return performStandardUpload(file, maxDownloads, expiryHours);
}
}
/**
* Perform a chunked upload for large files
*
* @param file The file to upload
* @param maxDownloads Maximum number of downloads
* @param expiryHours Expiry time in hours
* @return CompletableFuture containing the upload result
*/
private CompletableFuture<UploadResult> performChunkedUpload(
File file, int maxDownloads, int expiryHours) {
CompletableFuture<UploadResult> future = new CompletableFuture<>();
// Create chunked upload manager
ChunkedUploadManager uploadManager = new ChunkedUploadManager(
context, file, maxDownloads, expiryHours);
// Set progress listener if needed
uploadManager.setProgressListener(new ChunkedUploadManager.UploadProgressListener() {
@Override
public void onProgress(long bytesUploaded, long totalBytes, int percentComplete) {
// This could be used to update UI or notify a service
Log.d(TAG, "Chunked upload progress: " + percentComplete + "%");
}
@Override
public void onChunkComplete(int chunkIndex, int totalChunks) {
Log.d(TAG, "Chunk " + chunkIndex + "/" + totalChunks + " complete");
}
@Override
public void onUploadComplete(UploadResult result) {
Log.d(TAG, "Chunked upload complete: " + result);
// Save to history
if (result.isSuccess()) {
saveToHistory(result);
}
future.complete(result);
}
@Override
public void onError(String error) {
Log.e(TAG, "Chunked upload error: " + error);
future.complete(new UploadResult(error));
}
@Override
public void onPaused() {
Log.d(TAG, "Chunked upload paused");
}
@Override
public void onResumed() {
Log.d(TAG, "Chunked upload resumed");
}
});
// Start the upload
uploadManager.startUpload()
.thenAccept(result -> {
// This is handled by the listener, but we need to catch any exceptions
})
.exceptionally(ex -> {
Log.e(TAG, "Exception during chunked upload", ex);
future.complete(new UploadResult("Exception during upload: " + ex.getMessage()));
return null;
});
return future;
}
/**
* Perform a standard (non-chunked) upload for smaller files
*
* @param file The file to upload
* @param maxDownloads Maximum number of downloads
* @param expiryHours Expiry time in hours
* @return CompletableFuture containing the upload result
*/
private CompletableFuture<UploadResult> performStandardUpload(
File file, int maxDownloads, int expiryHours) {
CompletableFuture<UploadResult> future = new CompletableFuture<>();
// Create OkHttpClient with timeout configuration
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(NetworkConstants.CONNECTION_TIMEOUT, TimeUnit.SECONDS)
@@ -94,7 +203,7 @@ public class UploadController {
.retryOnConnectionFailure(true)
.build();
Log.d(TAG, "Uploading file: " + file.getName() + ", Size: " + file.length());
Log.d(TAG, "Using standard upload for file: " + file.getName());
RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream"));
MultipartBody requestBody = new MultipartBody.Builder()

View File

@@ -23,6 +23,7 @@ import com.mattintech.simplelogupload.constant.NetworkConstants;
import com.mattintech.simplelogupload.controller.FileController;
import com.mattintech.simplelogupload.controller.UploadController;
import com.mattintech.simplelogupload.model.UploadResult;
import com.mattintech.simplelogupload.upload.UploadStrategySelector;
import com.mattintech.simplelogupload.view.MainActivity;
import java.io.File;
@@ -92,9 +93,17 @@ public class FileUploadForegroundService extends Service {
}
// Create a notification and run the service in the foreground
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// For Android 14+ (API 34+)
startForeground(NOTIFICATION_ID, createInitialNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
try {
Log.d(TAG, "Starting foreground service with DATA_SYNC type");
int dataSync = 1; // ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC value is 1
startForeground(NOTIFICATION_ID, createInitialNotification(), dataSync);
} catch (Exception e) {
Log.e(TAG, "Error starting foreground service with type", e);
stopSelf();
return START_NOT_STICKY;
}
} else {
// For Android 13 and below
startForeground(NOTIFICATION_ID, createInitialNotification());
@@ -153,7 +162,17 @@ public class FileUploadForegroundService extends Service {
// Update notification to show uploading
updateNotification("Uploading " + file.getName() + "...");
// Upload the file
// Log network information
String networkType = UploadStrategySelector.getNetworkTypeName(this);
boolean isWifi = UploadStrategySelector.isWifiConnection(this);
boolean useChunked = UploadStrategySelector.shouldUseChunkedUpload(this, file);
Log.d(TAG, "Starting upload - Network: " + networkType + ", Chunked: " + useChunked);
// Update notification with network info
updateNotification("Uploading on " + networkType +
(useChunked ? " (chunked)" : "") + "...");
uploadController.uploadFile(file, maxDownloads, expiryHours)
.thenAccept(result -> {
if (result.isSuccess()) {
@@ -251,4 +270,4 @@ public class FileUploadForegroundService extends Service {
sendBroadcast(intent);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
package com.mattintech.simplelogupload.upload;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import com.mattintech.simplelogupload.constant.AppConstants;
import java.io.File;
/**
* Helper class to determine the best upload strategy based on file size and network conditions
*/
public class UploadStrategySelector {
private static final String TAG = AppConstants.APP_TAG + "UploadStrategy";
// Threshold for using chunked uploads
private static final long CHUNKED_UPLOAD_THRESHOLD_WIFI = 50 * 1024 * 1024; // 50MB for WiFi
private static final long CHUNKED_UPLOAD_THRESHOLD_MOBILE = 10 * 1024 * 1024; // 10MB for mobile
/**
* Check if chunked uploads should be used for the given file
*
* @param context Application context
* @param file The file to upload
* @return True if chunked upload should be used, false otherwise
*/
public static boolean shouldUseChunkedUpload(Context context, File file) {
if (file == null) {
return false;
}
long fileSize = file.length();
boolean isWifiConnection = isWifiConnection(context);
long threshold = isWifiConnection ? CHUNKED_UPLOAD_THRESHOLD_WIFI : CHUNKED_UPLOAD_THRESHOLD_MOBILE;
boolean useChunked = fileSize > threshold;
Log.d(TAG, "File size: " + fileSize + " bytes, Connection: " + (isWifiConnection ? "WiFi" : "Mobile") +
", Using chunked upload: " + useChunked);
return useChunked;
}
/**
* Check if the device is currently connected to WiFi
*
* @param context Application context
* @return True if connected to WiFi, false otherwise
*/
public static boolean isWifiConnection(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;
}
/**
* Get the network type name for logging
*
* @param context Application context
* @return String representing the network type ("WiFi", "Mobile", or "Unknown")
*/
public static String getNetworkTypeName(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isConnected()) {
return "Unknown";
}
if (info.getType() == ConnectivityManager.TYPE_WIFI) {
return "WiFi";
}
if (info.getType() == ConnectivityManager.TYPE_MOBILE) {
String subType = "";
switch (info.getSubtype()) {
case 13: // LTE
subType = "LTE";
break;
case 19: // 5G
subType = "5G";
break;
case 20: // 5G+
subType = "5G+";
break;
default:
subType = info.getSubtypeName();
}
return "Mobile (" + subType + ")";
}
return "Unknown";
}
}