support deleting files from the app and specify upload params

This commit is contained in:
2025-04-15 18:35:47 -04:00
parent 05351ce364
commit 111ac2ee94
12 changed files with 668 additions and 60 deletions

View File

@@ -15,8 +15,12 @@ Note - this requires that you leverage SimpleFileUpload-Server
- Copy codes for easy sharing - Copy codes for easy sharing
- Support for Android 14+ with proper foreground service implementation - Support for Android 14+ with proper foreground service implementation
- MVC architecture for clean code separation - MVC architecture for clean code separation
- Custom upload parameters (expiry time, download limit)
- Server-side file deletion from history
## Recent Updates ## Recent Updates
- Added server-side file deletion when removing items from history
- Added custom upload parameters for maximum downloads and expiry time
- Added multi-select capability for dumpstate files - Added multi-select capability for dumpstate files
- Implemented zipping multiple files into a single package for upload - Implemented zipping multiple files into a single package for upload
- Added in-place expandable file list to avoid popup dialogs - Added in-place expandable file list to avoid popup dialogs
@@ -31,14 +35,77 @@ Note - this requires that you leverage SimpleFileUpload-Server
## How to Use ## How to Use
1. **Upload a File**: Tap "Select File from Storage" to pick any file from your device. 1. **Upload a File**: Tap "Select File from Storage" to pick any file from your device.
2. **Upload a Dumpstate**: The app automatically detects Samsung dumpstate files on your device. 2. **Set Upload Parameters**: When uploading, set custom parameters such as maximum downloads allowed and expiry time.
3. **Multi-select Mode**: When multiple dumpstate files are detected, tap "Show Files" to view and select multiple files for upload. 3. **Upload a Dumpstate**: The app automatically detects Samsung dumpstate files on your device.
4. **Select Files**: Check multiple files to upload them as a single package. 4. **Multi-select Mode**: When multiple dumpstate files are detected, tap "Show Files" to view and select multiple files for upload.
5. **View History**: Access your upload history from the menu to find previously uploaded file codes. 5. **Select Files**: Check multiple files to upload them as a single package.
6. **View History**: Access your upload history from the menu to find previously uploaded file codes.
7. **Delete From Server**: When deleting items from history, files will also be removed from the server.
## Upcoming Features ## Upcoming Features
- Progress tracking for large files - Progress tracking for large files
- Dark mode support - Dark mode support
- Upload history filtering and search - Upload history filtering and search
- QR code generation for quick sharing - QR code generation for quick sharing
- Custom upload parameters (expiry time, download limit)
## Feature Details
### Custom Upload Parameters
The app allows you to customize how your uploads are handled on the server:
#### Maximum Downloads
- **Purpose**: Limit how many times a file can be downloaded
- **Default**: 1 download (file is removed after first download)
- **Options**:
- Set to any positive number to limit downloads
- Set to 0 for unlimited downloads
- **How to use**: Enter your desired number in the "Maximum Downloads" field when uploading a file
#### Expiry Time
- **Purpose**: Set how long the file remains available on the server
- **Default**: 24 hours (1 day)
- **Maximum**: 24 hours (server-enforced limit)
- **How to use**: Enter your desired number of hours in the "Expiry Time" field when uploading
#### Upload Parameter Dialog
When you initiate an upload (either a selected file or dumpstate), the app will present a dialog where you can set these parameters before the actual upload begins. If you don't modify the fields, the default values will be used.
### Server-Side File Deletion
The app integrates with the server's delete endpoint to remove files from the server when you delete them from your local history.
#### Single Item Deletion
When you delete a single item from your upload history:
1. The app attempts to delete the file from the server using its upload code
2. Regardless of server response, the item is removed from your local history
3. You will receive feedback about whether the server deletion was successful
#### Bulk Deletion (Clear History)
When you clear all history:
1. The app attempts to delete each file from the server
2. All items are removed from your local history
3. You will receive feedback about whether any of the server deletions were successful
#### Benefits
- **Privacy**: Ensures your uploaded files don't remain on the server indefinitely
- **Resource Management**: Helps keep the server storage clean
- **Control**: Gives you full control over the lifecycle of your uploaded files
#### API Integration
The app uses the server's delete endpoint:
```
DELETE /delete/<code>
```
This endpoint requires the same client authentication key used for uploads. The server returns:
- 200 OK: File successfully deleted
- 404 Not Found: File doesn't exist (already deleted or expired)
- 401 Unauthorized: Invalid client key

View File

@@ -208,4 +208,64 @@ public class UploadController {
return null; return null;
}); });
} }
/**
* Delete a file from the server using its upload code
*
* @param uploadCode The upload code of the file to delete
* @return CompletableFuture containing a boolean indicating success or failure
*/
public CompletableFuture<Boolean> deleteFileFromServer(String uploadCode) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
if (uploadCode == null || uploadCode.isEmpty()) {
Log.e(TAG, "Cannot delete file with empty upload code");
future.complete(false);
return future;
}
// Create OkHttpClient with timeout configuration
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(NetworkConstants.CONNECTION_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(NetworkConstants.READ_TIMEOUT, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
// Get server base URL and client key from preferences
String serverBaseUrl = PreferencesUtil.getServerBaseUrl(context);
String clientKey = PreferencesUtil.getClientKey(context);
// Create the delete URL
String deleteUrl = serverBaseUrl + "/delete/" + uploadCode;
Log.d(TAG, "Deleting file from server: " + deleteUrl);
// Build request with client-key header
Request request = new Request.Builder()
.url(deleteUrl)
.header(NetworkConstants.CLIENT_KEY_HEADER, clientKey)
.delete()
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "Server delete failed", e);
future.complete(false);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
Log.d(TAG, "Server delete successful for code: " + uploadCode);
future.complete(true);
} else {
String errorBody = response.body().string();
Log.e(TAG, "Server delete failed with code: " + response.code() + ", Body: " + errorBody);
future.complete(false);
}
}
});
return future;
}
} }

View File

@@ -32,6 +32,14 @@ public interface UploadHistoryDao {
@Query("SELECT * FROM upload_history ORDER BY upload_time DESC") @Query("SELECT * FROM upload_history ORDER BY upload_time DESC")
LiveData<List<UploadHistory>> getAllHistory(); LiveData<List<UploadHistory>> getAllHistory();
/**
* Get all history entries ordered by upload time (most recent first) as a direct list
*
* @return List of history entries
*/
@Query("SELECT * FROM upload_history ORDER BY upload_time DESC")
List<UploadHistory> getAllHistoryDirect();
/** /**
* Get a history entry by ID * Get a history entry by ID
* *

View File

@@ -36,6 +36,17 @@ public class UploadHistoryRepository {
return mAllHistory; return mAllHistory;
} }
/**
* Get all history entries as a one-time operation
*
* @return CompletableFuture containing a list of all history entries
*/
public CompletableFuture<List<UploadHistory>> getAllHistoryOneTime() {
return CompletableFuture.supplyAsync(() ->
mUploadHistoryDao.getAllHistoryDirect(),
AppDatabase.databaseWriteExecutor);
}
/** /**
* Insert a new history entry asynchronously * Insert a new history entry asynchronously
* *

View File

@@ -60,6 +60,15 @@ public class FileUploadForegroundService extends Service {
int maxDownloads = intent.getIntExtra(EXTRA_MAX_DOWNLOADS, NetworkConstants.DEFAULT_MAX_DOWNLOADS); int maxDownloads = intent.getIntExtra(EXTRA_MAX_DOWNLOADS, NetworkConstants.DEFAULT_MAX_DOWNLOADS);
int expiryHours = intent.getIntExtra(EXTRA_EXPIRY_HOURS, NetworkConstants.DEFAULT_EXPIRY_HOURS); int expiryHours = intent.getIntExtra(EXTRA_EXPIRY_HOURS, NetworkConstants.DEFAULT_EXPIRY_HOURS);
// Log parameters for debugging
Log.d(TAG, "Upload parameters - Max Downloads: " + maxDownloads + ", Expiry Hours: " + expiryHours);
// Validate expiry hours
if (expiryHours > NetworkConstants.MAX_EXPIRY_HOURS) {
Log.w(TAG, "Expiry hours exceeded maximum allowed value, capping at " + NetworkConstants.MAX_EXPIRY_HOURS);
expiryHours = NetworkConstants.MAX_EXPIRY_HOURS;
}
if (filePath == null) { if (filePath == null) {
Log.e(TAG, "No file path provided"); Log.e(TAG, "No file path provided");
// Try to find the most recent dump state file // Try to find the most recent dump state file
@@ -91,12 +100,16 @@ public class FileUploadForegroundService extends Service {
startForeground(NOTIFICATION_ID, createInitialNotification()); startForeground(NOTIFICATION_ID, createInitialNotification());
} }
// Create final copies for use in the callback
final int finalMaxDownloads = maxDownloads;
final int finalExpiryHours = expiryHours;
// Prepare file for upload // Prepare file for upload
FileController.prepareFileForUpload(file, new FileController.FilePrepareCallback() { FileController.prepareFileForUpload(file, new FileController.FilePrepareCallback() {
@Override @Override
public void onSuccess(File preparedFile) { public void onSuccess(File preparedFile) {
Log.d(TAG, "File prepared for upload: " + preparedFile.getName()); Log.d(TAG, "File prepared for upload: " + preparedFile.getName());
uploadFile(preparedFile, maxDownloads, expiryHours); uploadFile(preparedFile, finalMaxDownloads, finalExpiryHours);
} }
@Override @Override

View File

@@ -91,6 +91,18 @@ public class PreferencesUtil {
getPreferences(context).edit().putString(KEY_CLIENT_KEY, clientKey).apply(); getPreferences(context).edit().putString(KEY_CLIENT_KEY, clientKey).apply();
} }
/**
* Get the server base URL (address + port) without any path
*
* @param context The application context
* @return The server base URL
*/
public static String getServerBaseUrl(Context context) {
String address = getServerAddress(context);
int port = getServerPort(context);
return "http://" + address + ":" + port;
}
/** /**
* Get the complete server URL (address + port + path) * Get the complete server URL (address + port + path)
* *
@@ -98,9 +110,7 @@ public class PreferencesUtil {
* @return The complete server URL * @return The complete server URL
*/ */
public static String getServerUrl(Context context) { public static String getServerUrl(Context context) {
String address = getServerAddress(context); return getServerBaseUrl(context) + "/upload";
int port = getServerPort(context);
return "http://" + address + ":" + port + "/upload";
} }
/** /**

View File

@@ -252,34 +252,45 @@ public class MainActivity extends AppCompatActivity {
List<File> selectedFiles = dumpstateAdapter.getSelectedFiles(); List<File> selectedFiles = dumpstateAdapter.getSelectedFiles();
int fileCount = selectedFiles.size(); int fileCount = selectedFiles.size();
// Update UI // Show upload parameters dialog first
progressBar.setVisibility(View.VISIBLE); new UploadParamsDialog(this, new UploadParamsDialog.UploadParamsListener() {
uploadDumpstateButton.setEnabled(false); @Override
dumpstateStatusText.setText(getString(R.string.preparing_multiple_files, fileCount)); public void onUploadParamsSet(int maxDownloads, int expiryHours) {
// Update UI
// Prepare and upload multiple files progressBar.setVisibility(View.VISIBLE);
FileController.prepareMultipleFilesForUploadAsync(selectedFiles, this) uploadDumpstateButton.setEnabled(false);
.thenAccept(zipFile -> { dumpstateStatusText.setText(getString(R.string.preparing_multiple_files, fileCount));
Log.d(TAG, "Multiple files zipped successfully: " + zipFile.getAbsolutePath());
// Prepare and upload multiple files
runOnUiThread(() -> { FileController.prepareMultipleFilesForUploadAsync(selectedFiles, MainActivity.this)
dumpstateStatusText.setText(getString(R.string.uploading_multiple_files, fileCount)); .thenAccept(zipFile -> {
}); Log.d(TAG, "Multiple files zipped successfully: " + zipFile.getAbsolutePath());
// Upload the zip file runOnUiThread(() -> {
startUploadService(zipFile); dumpstateStatusText.setText(getString(R.string.uploading_multiple_files, fileCount));
}) });
.exceptionally(ex -> {
Log.e(TAG, "Error preparing multiple files", ex); // Upload the zip file with parameters
runOnUiThread(() -> { startUploadService(zipFile, maxDownloads, expiryHours);
progressBar.setVisibility(View.GONE); })
uploadDumpstateButton.setEnabled(true); .exceptionally(ex -> {
Toast.makeText(MainActivity.this, Log.e(TAG, "Error preparing multiple files", ex);
getString(R.string.file_error, ex.getMessage()), runOnUiThread(() -> {
Toast.LENGTH_LONG).show(); progressBar.setVisibility(View.GONE);
}); uploadDumpstateButton.setEnabled(true);
return null; Toast.makeText(MainActivity.this,
}); getString(R.string.file_error, ex.getMessage()),
Toast.LENGTH_LONG).show();
});
return null;
});
}
@Override
public void onUploadParamsCancelled() {
// User cancelled, do nothing
}
}).show();
} }
@Override @Override
@@ -389,13 +400,8 @@ public class MainActivity extends AppCompatActivity {
return; return;
} }
// Update UI // Show upload parameters dialog
progressBar.setVisibility(View.VISIBLE); showUploadParamsDialog(selectedFile);
uploadSelectedFileButton.setEnabled(false);
selectedFileText.setText(getString(R.string.uploading_file, selectedFile.getName()));
// Start the upload service
startUploadService(selectedFile);
} }
/** /**
@@ -413,7 +419,7 @@ public class MainActivity extends AppCompatActivity {
if (dumpstateFiles.length == 1) { if (dumpstateFiles.length == 1) {
// Only one dumpstate file, use it directly // Only one dumpstate file, use it directly
dumpstateFile = dumpstateFiles[0]; dumpstateFile = dumpstateFiles[0];
startDumpstateUpload(dumpstateFile); showUploadParamsDialog(dumpstateFile);
} else { } else {
// Multiple dumpstate files are available, suggest expanding the list // Multiple dumpstate files are available, suggest expanding the list
if (!isDumpstateListExpanded) { if (!isDumpstateListExpanded) {
@@ -421,7 +427,7 @@ public class MainActivity extends AppCompatActivity {
} else { } else {
// If list is already expanded, use the first file // If list is already expanded, use the first file
dumpstateFile = dumpstateFiles[0]; dumpstateFile = dumpstateFiles[0];
startDumpstateUpload(dumpstateFile); showUploadParamsDialog(dumpstateFile);
} }
} }
} }
@@ -430,8 +436,10 @@ public class MainActivity extends AppCompatActivity {
* Start uploading the selected dumpstate file * Start uploading the selected dumpstate file
* *
* @param file The dumpstate file to upload * @param file The dumpstate file to upload
* @param maxDownloads The maximum number of downloads
* @param expiryHours The expiry time in hours
*/ */
private void startDumpstateUpload(File file) { private void startDumpstateUpload(File file, int maxDownloads, int expiryHours) {
if (file == null || !file.exists()) { if (file == null || !file.exists()) {
Toast.makeText(this, R.string.no_dumpstate_found, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.no_dumpstate_found, Toast.LENGTH_SHORT).show();
return; return;
@@ -442,22 +450,57 @@ public class MainActivity extends AppCompatActivity {
uploadDumpstateButton.setEnabled(false); uploadDumpstateButton.setEnabled(false);
dumpstateStatusText.setText(getString(R.string.uploading_file, file.getName())); dumpstateStatusText.setText(getString(R.string.uploading_file, file.getName()));
// Start the upload service // Start the upload service with parameters
startUploadService(file); startUploadService(file, maxDownloads, expiryHours);
} }
/** /**
* Start the upload service with the given file * Start the upload service with the given file and parameters
* *
* @param file The file to upload * @param file The file to upload
* @param maxDownloads The maximum number of downloads
* @param expiryHours The expiry time in hours
*/ */
private void startUploadService(File file) { private void startUploadService(File file, int maxDownloads, int expiryHours) {
// Start the upload service // Start the upload service
Intent serviceIntent = new Intent(this, FileUploadForegroundService.class); Intent serviceIntent = new Intent(this, FileUploadForegroundService.class);
serviceIntent.putExtra(FileUploadForegroundService.EXTRA_FILE_PATH, file.getAbsolutePath()); serviceIntent.putExtra(FileUploadForegroundService.EXTRA_FILE_PATH, file.getAbsolutePath());
serviceIntent.putExtra(FileUploadForegroundService.EXTRA_MAX_DOWNLOADS, maxDownloads);
serviceIntent.putExtra(FileUploadForegroundService.EXTRA_EXPIRY_HOURS, expiryHours);
startService(serviceIntent); startService(serviceIntent);
} }
/**
* Show the upload parameters dialog
*
* @param file The file to upload
*/
private void showUploadParamsDialog(File file) {
new UploadParamsDialog(this, new UploadParamsDialog.UploadParamsListener() {
@Override
public void onUploadParamsSet(int maxDownloads, int expiryHours) {
// Update UI
progressBar.setVisibility(View.VISIBLE);
uploadSelectedFileButton.setEnabled(false);
if (file == selectedFile) {
selectedFileText.setText(getString(R.string.uploading_file, file.getName()));
} else if (file == dumpstateFile) {
dumpstateStatusText.setText(getString(R.string.uploading_file, file.getName()));
uploadDumpstateButton.setEnabled(false);
}
// Start the upload service with parameters
startUploadService(file, maxDownloads, expiryHours);
}
@Override
public void onUploadParamsCancelled() {
// User cancelled, do nothing
}
}).show();
}
/** /**
* Update the selected file UI * Update the selected file UI
* *

View File

@@ -10,6 +10,8 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@@ -73,6 +75,18 @@ public class UploadHistoryActivity extends AppCompatActivity implements UploadHi
} }
} }
}); });
// Observe the delete status to provide feedback
viewModel.getDeleteStatus().observe(this, deleteSuccess -> {
if (deleteSuccess != null) {
if (deleteSuccess) {
Toast.makeText(this, "File deleted from server successfully", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Failed to delete file from server, but removed from local history",
Toast.LENGTH_LONG).show();
}
}
});
} }
@Override @Override
@@ -83,8 +97,30 @@ public class UploadHistoryActivity extends AppCompatActivity implements UploadHi
finish(); finish();
return true; return true;
} else if (id == R.id.action_clear_history) { } else if (id == R.id.action_clear_history) {
viewModel.deleteAll(); // Show confirmation dialog
Toast.makeText(this, "History cleared", Toast.LENGTH_SHORT).show(); new AlertDialog.Builder(this)
.setTitle("Clear All History")
.setMessage("Do you want to delete all uploads from both local history and the server?")
.setPositiveButton("Yes", (dialog, which) -> {
// Show progress dialog
AlertDialog progressDialog = new AlertDialog.Builder(this)
.setTitle("Deleting...")
.setMessage("Attempting to delete all files from server and clear history.")
.setCancelable(false)
.create();
progressDialog.show();
// Attempt to delete all
viewModel.deleteAll()
.thenAccept(unused -> {
runOnUiThread(() -> {
progressDialog.dismiss();
// Toast message will be shown through the observer
});
});
})
.setNegativeButton("No", null)
.show();
return true; return true;
} }
@@ -98,7 +134,29 @@ public class UploadHistoryActivity extends AppCompatActivity implements UploadHi
@Override @Override
public void onDeleteClick(UploadHistory history) { public void onDeleteClick(UploadHistory history) {
viewModel.delete(history); // Show confirmation dialog
new AlertDialog.Builder(this)
.setTitle("Delete Upload")
.setMessage("Do you want to delete this upload from both local history and the server?")
.setPositiveButton("Yes", (dialog, which) -> {
// Show progress dialog
AlertDialog progressDialog = new AlertDialog.Builder(this)
.setTitle("Deleting...")
.setMessage("Attempting to delete from server and local history.")
.setCancelable(false)
.create();
progressDialog.show();
// Attempt to delete
viewModel.delete(history)
.thenAccept(unused -> {
runOnUiThread(() -> {
progressDialog.dismiss();
});
});
})
.setNegativeButton("No", null)
.show();
} }
/** /**

View File

@@ -0,0 +1,127 @@
package com.mattintech.simplelogupload.view;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.NonNull;
import com.mattintech.simplelogupload.R;
import com.mattintech.simplelogupload.constant.NetworkConstants;
/**
* Dialog for setting upload parameters
*/
public class UploadParamsDialog extends Dialog {
private EditText maxDownloadsInput;
private EditText expiryTimeInput;
private final UploadParamsListener listener;
/**
* Interface for upload parameter callbacks
*/
public interface UploadParamsListener {
void onUploadParamsSet(int maxDownloads, int expiryHours);
void onUploadParamsCancelled();
}
/**
* Constructor
*
* @param context The context
* @param listener Callback listener
*/
public UploadParamsDialog(@NonNull Context context, UploadParamsListener listener) {
super(context);
this.listener = listener;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.dialog_upload_params);
// Initialize views
maxDownloadsInput = findViewById(R.id.maxDownloadsInput);
expiryTimeInput = findViewById(R.id.expiryTimeInput);
// Set default values as hints
maxDownloadsInput.setHint("Default: " + NetworkConstants.DEFAULT_MAX_DOWNLOADS);
expiryTimeInput.setHint("Default: " + NetworkConstants.DEFAULT_EXPIRY_HOURS);
// Set up buttons
Button confirmButton = findViewById(R.id.confirmButton);
Button cancelButton = findViewById(R.id.cancelButton);
confirmButton.setOnClickListener(v -> {
int maxDownloads = getMaxDownloadsValue();
int expiryHours = getExpiryHoursValue();
// Validate expiry time
if (expiryHours > NetworkConstants.MAX_EXPIRY_HOURS) {
expiryTimeInput.setError("Maximum expiry time is " +
NetworkConstants.MAX_EXPIRY_HOURS + " hours");
return;
}
// Validate max downloads (must be non-negative)
if (maxDownloads < 0) {
maxDownloadsInput.setError("Downloads must be 0 or greater");
return;
}
if (listener != null) {
listener.onUploadParamsSet(maxDownloads, expiryHours);
}
dismiss();
});
cancelButton.setOnClickListener(v -> {
if (listener != null) {
listener.onUploadParamsCancelled();
}
dismiss();
});
}
/**
* Get the max downloads value from the input
*
* @return The max downloads value (default if not set)
*/
private int getMaxDownloadsValue() {
if (TextUtils.isEmpty(maxDownloadsInput.getText())) {
return NetworkConstants.DEFAULT_MAX_DOWNLOADS;
}
try {
return Integer.parseInt(maxDownloadsInput.getText().toString());
} catch (NumberFormatException e) {
return NetworkConstants.DEFAULT_MAX_DOWNLOADS;
}
}
/**
* Get the expiry hours value from the input
*
* @return The expiry hours value (default if not set)
*/
private int getExpiryHoursValue() {
if (TextUtils.isEmpty(expiryTimeInput.getText())) {
return NetworkConstants.DEFAULT_EXPIRY_HOURS;
}
try {
return Integer.parseInt(expiryTimeInput.getText().toString());
} catch (NumberFormatException e) {
return NetworkConstants.DEFAULT_EXPIRY_HOURS;
}
}
}

View File

@@ -5,11 +5,14 @@ import android.app.Application;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.mattintech.simplelogupload.controller.UploadController;
import com.mattintech.simplelogupload.db.UploadHistoryRepository; import com.mattintech.simplelogupload.db.UploadHistoryRepository;
import com.mattintech.simplelogupload.model.UploadHistory; import com.mattintech.simplelogupload.model.UploadHistory;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
/** /**
* ViewModel for upload history * ViewModel for upload history
@@ -17,7 +20,9 @@ import java.util.List;
public class UploadHistoryViewModel extends AndroidViewModel { public class UploadHistoryViewModel extends AndroidViewModel {
private final UploadHistoryRepository repository; private final UploadHistoryRepository repository;
private final UploadController uploadController;
private final LiveData<List<UploadHistory>> allHistory; private final LiveData<List<UploadHistory>> allHistory;
private final MutableLiveData<Boolean> deleteStatus = new MutableLiveData<>();
/** /**
* Constructor * Constructor
@@ -27,6 +32,7 @@ public class UploadHistoryViewModel extends AndroidViewModel {
public UploadHistoryViewModel(@NonNull Application application) { public UploadHistoryViewModel(@NonNull Application application) {
super(application); super(application);
repository = new UploadHistoryRepository(application); repository = new UploadHistoryRepository(application);
uploadController = new UploadController(application);
allHistory = repository.getAllHistory(); allHistory = repository.getAllHistory();
} }
@@ -49,12 +55,42 @@ public class UploadHistoryViewModel extends AndroidViewModel {
} }
/** /**
* Delete a history entry * Get the delete status LiveData
*
* @return LiveData containing the delete status
*/
public LiveData<Boolean> getDeleteStatus() {
return deleteStatus;
}
/**
* Delete a history entry and the associated file from the server
* *
* @param history The entry to delete * @param history The entry to delete
* @return CompletableFuture for the operation
*/ */
public void delete(UploadHistory history) { public CompletableFuture<Void> delete(UploadHistory history) {
repository.delete(history); CompletableFuture<Void> future = new CompletableFuture<>();
// First attempt to delete from server
uploadController.deleteFileFromServer(history.getUploadCode())
.thenAccept(success -> {
// Update the status
deleteStatus.postValue(success);
// Always delete from local database, even if server delete fails
repository.delete(history);
future.complete(null);
})
.exceptionally(ex -> {
// On exception, still delete from local database
deleteStatus.postValue(false);
repository.delete(history);
future.complete(null);
return null;
});
return future;
} }
/** /**
@@ -67,9 +103,76 @@ public class UploadHistoryViewModel extends AndroidViewModel {
} }
/** /**
* Delete all history entries * Delete all history entries and attempt to delete all files from server
*
* @return CompletableFuture for the operation
*/ */
public void deleteAll() { public CompletableFuture<Void> deleteAll() {
repository.deleteAll(); CompletableFuture<Void> future = new CompletableFuture<>();
// First get all history entries
repository.getAllHistoryOneTime()
.thenAccept(historyList -> {
if (historyList == null || historyList.isEmpty()) {
// No entries to delete
repository.deleteAll();
future.complete(null);
return;
}
// Counter for tracking completion
final int[] completedCount = {0};
final int totalCount = historyList.size();
final boolean[] anyServerSuccess = {false};
// Process each entry
for (UploadHistory history : historyList) {
uploadController.deleteFileFromServer(history.getUploadCode())
.thenAccept(success -> {
if (success) {
anyServerSuccess[0] = true;
}
// Increment counter
completedCount[0]++;
// Check if all operations are complete
if (completedCount[0] >= totalCount) {
// All server delete attempts completed, now delete local entries
repository.deleteAll();
// Update status based on overall success
deleteStatus.postValue(anyServerSuccess[0]);
future.complete(null);
}
})
.exceptionally(ex -> {
// Count failed operations too
completedCount[0]++;
// Check if all operations are complete
if (completedCount[0] >= totalCount) {
// All server delete attempts completed, now delete local entries
repository.deleteAll();
// Update status based on overall success
deleteStatus.postValue(anyServerSuccess[0]);
future.complete(null);
}
return null;
});
}
})
.exceptionally(ex -> {
// If we can't get the history, just delete local entries
repository.deleteAll();
deleteStatus.postValue(false);
future.complete(null);
return null;
});
return future;
} }
} }

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/upload_params_title"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/max_downloads_label"
android:textSize="16sp" />
<EditText
android:id="@+id/maxDownloadsInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number"
android:hint="@string/max_downloads_hint"
android:importantForAutofill="no" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/max_downloads_note"
android:textSize="12sp"
android:textStyle="italic"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/expiry_time_label"
android:textSize="16sp" />
<EditText
android:id="@+id/expiryTimeInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number"
android:hint="@string/expiry_time_hint"
android:importantForAutofill="no" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/expiry_time_note"
android:textSize="12sp"
android:textStyle="italic"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel_button"
style="@style/Widget.AppCompat.Button.Borderless" />
<Button
android:id="@+id/confirmButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/upload_button" />
</LinearLayout>
</LinearLayout>

View File

@@ -56,4 +56,15 @@
<string name="no_dumpstates_selected">No files selected</string> <string name="no_dumpstates_selected">No files selected</string>
<string name="preparing_multiple_files">Preparing %1$d files for upload…</string> <string name="preparing_multiple_files">Preparing %1$d files for upload…</string>
<string name="uploading_multiple_files">Uploading %1$d files as one package</string> <string name="uploading_multiple_files">Uploading %1$d files as one package</string>
<!-- Upload Parameters Dialog -->
<string name="upload_params_title">Upload Parameters</string>
<string name="max_downloads_label">Maximum Downloads:</string>
<string name="max_downloads_hint">Default: 1</string>
<string name="max_downloads_note">Set to 0 for unlimited downloads</string>
<string name="expiry_time_label">Expiry Time (hours):</string>
<string name="expiry_time_hint">Default: 24</string>
<string name="expiry_time_note">Maximum 24 hours (1 day)</string>
<string name="cancel_button">Cancel</string>
<string name="upload_button">Upload</string>
</resources> </resources>