adding first iteration of chunked file support
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user