diff --git a/README.md b/README.md index ac48b39..e60584b 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,12 @@ Note - this requires that you leverage SimpleFileUpload-Server - Copy codes for easy sharing - Support for Android 14+ with proper foreground service implementation - MVC architecture for clean code separation +- Custom upload parameters (expiry time, download limit) +- Server-side file deletion from history ## 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 - Implemented zipping multiple files into a single package for upload - 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 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. -3. **Multi-select Mode**: When multiple dumpstate files are detected, tap "Show Files" to view and select multiple files for upload. -4. **Select Files**: Check multiple files to upload them as a single package. -5. **View History**: Access your upload history from the menu to find previously uploaded file codes. +2. **Set Upload Parameters**: When uploading, set custom parameters such as maximum downloads allowed and expiry time. +3. **Upload a Dumpstate**: The app automatically detects Samsung dumpstate files on your device. +4. **Multi-select Mode**: When multiple dumpstate files are detected, tap "Show Files" to view and select multiple files for upload. +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 - Progress tracking for large files - Dark mode support - Upload history filtering and search - 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/ +``` + +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 diff --git a/app/src/main/java/com/mattintech/simplelogupload/controller/UploadController.java b/app/src/main/java/com/mattintech/simplelogupload/controller/UploadController.java index c1ed0b7..7206737 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/controller/UploadController.java +++ b/app/src/main/java/com/mattintech/simplelogupload/controller/UploadController.java @@ -208,4 +208,64 @@ public class UploadController { 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 deleteFileFromServer(String uploadCode) { + CompletableFuture 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; + } } diff --git a/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryDao.java b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryDao.java index f80006a..b414b18 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryDao.java +++ b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryDao.java @@ -32,6 +32,14 @@ public interface UploadHistoryDao { @Query("SELECT * FROM upload_history ORDER BY upload_time DESC") LiveData> 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 getAllHistoryDirect(); + /** * Get a history entry by ID * diff --git a/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryRepository.java b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryRepository.java index 9d02fc8..975fd0e 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryRepository.java +++ b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryRepository.java @@ -36,6 +36,17 @@ public class UploadHistoryRepository { return mAllHistory; } + /** + * Get all history entries as a one-time operation + * + * @return CompletableFuture containing a list of all history entries + */ + public CompletableFuture> getAllHistoryOneTime() { + return CompletableFuture.supplyAsync(() -> + mUploadHistoryDao.getAllHistoryDirect(), + AppDatabase.databaseWriteExecutor); + } + /** * Insert a new history entry asynchronously * diff --git a/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.java b/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.java index cc8eb35..84d3804 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.java +++ b/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.java @@ -60,6 +60,15 @@ public class FileUploadForegroundService extends Service { int maxDownloads = intent.getIntExtra(EXTRA_MAX_DOWNLOADS, NetworkConstants.DEFAULT_MAX_DOWNLOADS); 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) { Log.e(TAG, "No file path provided"); // Try to find the most recent dump state file @@ -91,12 +100,16 @@ public class FileUploadForegroundService extends Service { startForeground(NOTIFICATION_ID, createInitialNotification()); } + // Create final copies for use in the callback + final int finalMaxDownloads = maxDownloads; + final int finalExpiryHours = expiryHours; + // Prepare file for upload FileController.prepareFileForUpload(file, new FileController.FilePrepareCallback() { @Override public void onSuccess(File preparedFile) { Log.d(TAG, "File prepared for upload: " + preparedFile.getName()); - uploadFile(preparedFile, maxDownloads, expiryHours); + uploadFile(preparedFile, finalMaxDownloads, finalExpiryHours); } @Override 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 3e29578..1ed2c54 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java +++ b/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java @@ -91,6 +91,18 @@ public class PreferencesUtil { 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) * @@ -98,9 +110,7 @@ public class PreferencesUtil { * @return The complete server URL */ public static String getServerUrl(Context context) { - String address = getServerAddress(context); - int port = getServerPort(context); - return "http://" + address + ":" + port + "/upload"; + return getServerBaseUrl(context) + "/upload"; } /** diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java b/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java index b699224..f0e6a35 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java +++ b/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java @@ -252,34 +252,45 @@ public class MainActivity extends AppCompatActivity { List selectedFiles = dumpstateAdapter.getSelectedFiles(); int fileCount = selectedFiles.size(); - // Update UI - progressBar.setVisibility(View.VISIBLE); - uploadDumpstateButton.setEnabled(false); - dumpstateStatusText.setText(getString(R.string.preparing_multiple_files, fileCount)); - - // Prepare and upload multiple files - FileController.prepareMultipleFilesForUploadAsync(selectedFiles, this) - .thenAccept(zipFile -> { - Log.d(TAG, "Multiple files zipped successfully: " + zipFile.getAbsolutePath()); - - runOnUiThread(() -> { - dumpstateStatusText.setText(getString(R.string.uploading_multiple_files, fileCount)); - }); - - // Upload the zip file - startUploadService(zipFile); - }) - .exceptionally(ex -> { - Log.e(TAG, "Error preparing multiple files", ex); - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - uploadDumpstateButton.setEnabled(true); - Toast.makeText(MainActivity.this, - getString(R.string.file_error, ex.getMessage()), - Toast.LENGTH_LONG).show(); - }); - return null; - }); + // Show upload parameters dialog first + new UploadParamsDialog(this, new UploadParamsDialog.UploadParamsListener() { + @Override + public void onUploadParamsSet(int maxDownloads, int expiryHours) { + // Update UI + progressBar.setVisibility(View.VISIBLE); + uploadDumpstateButton.setEnabled(false); + dumpstateStatusText.setText(getString(R.string.preparing_multiple_files, fileCount)); + + // Prepare and upload multiple files + FileController.prepareMultipleFilesForUploadAsync(selectedFiles, MainActivity.this) + .thenAccept(zipFile -> { + Log.d(TAG, "Multiple files zipped successfully: " + zipFile.getAbsolutePath()); + + runOnUiThread(() -> { + dumpstateStatusText.setText(getString(R.string.uploading_multiple_files, fileCount)); + }); + + // Upload the zip file with parameters + startUploadService(zipFile, maxDownloads, expiryHours); + }) + .exceptionally(ex -> { + Log.e(TAG, "Error preparing multiple files", ex); + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + uploadDumpstateButton.setEnabled(true); + 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 @@ -389,13 +400,8 @@ public class MainActivity extends AppCompatActivity { return; } - // Update UI - progressBar.setVisibility(View.VISIBLE); - uploadSelectedFileButton.setEnabled(false); - selectedFileText.setText(getString(R.string.uploading_file, selectedFile.getName())); - - // Start the upload service - startUploadService(selectedFile); + // Show upload parameters dialog + showUploadParamsDialog(selectedFile); } /** @@ -413,7 +419,7 @@ public class MainActivity extends AppCompatActivity { if (dumpstateFiles.length == 1) { // Only one dumpstate file, use it directly dumpstateFile = dumpstateFiles[0]; - startDumpstateUpload(dumpstateFile); + showUploadParamsDialog(dumpstateFile); } else { // Multiple dumpstate files are available, suggest expanding the list if (!isDumpstateListExpanded) { @@ -421,7 +427,7 @@ public class MainActivity extends AppCompatActivity { } else { // If list is already expanded, use the first file dumpstateFile = dumpstateFiles[0]; - startDumpstateUpload(dumpstateFile); + showUploadParamsDialog(dumpstateFile); } } } @@ -430,8 +436,10 @@ public class MainActivity extends AppCompatActivity { * Start uploading the selected dumpstate file * * @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()) { Toast.makeText(this, R.string.no_dumpstate_found, Toast.LENGTH_SHORT).show(); return; @@ -442,22 +450,57 @@ public class MainActivity extends AppCompatActivity { uploadDumpstateButton.setEnabled(false); dumpstateStatusText.setText(getString(R.string.uploading_file, file.getName())); - // Start the upload service - startUploadService(file); + // Start the upload service with parameters + 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 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 Intent serviceIntent = new Intent(this, FileUploadForegroundService.class); serviceIntent.putExtra(FileUploadForegroundService.EXTRA_FILE_PATH, file.getAbsolutePath()); + serviceIntent.putExtra(FileUploadForegroundService.EXTRA_MAX_DOWNLOADS, maxDownloads); + serviceIntent.putExtra(FileUploadForegroundService.EXTRA_EXPIRY_HOURS, expiryHours); 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 * diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/UploadHistoryActivity.java b/app/src/main/java/com/mattintech/simplelogupload/view/UploadHistoryActivity.java index 399e7c0..652189f 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/view/UploadHistoryActivity.java +++ b/app/src/main/java/com/mattintech/simplelogupload/view/UploadHistoryActivity.java @@ -10,6 +10,8 @@ import android.view.View; import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; + import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; 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 @@ -83,8 +97,30 @@ public class UploadHistoryActivity extends AppCompatActivity implements UploadHi finish(); return true; } else if (id == R.id.action_clear_history) { - viewModel.deleteAll(); - Toast.makeText(this, "History cleared", Toast.LENGTH_SHORT).show(); + // Show confirmation dialog + 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; } @@ -98,7 +134,29 @@ public class UploadHistoryActivity extends AppCompatActivity implements UploadHi @Override 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(); } /** diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/UploadParamsDialog.java b/app/src/main/java/com/mattintech/simplelogupload/view/UploadParamsDialog.java new file mode 100644 index 0000000..ee5609a --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/view/UploadParamsDialog.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/viewmodel/UploadHistoryViewModel.java b/app/src/main/java/com/mattintech/simplelogupload/viewmodel/UploadHistoryViewModel.java index a1e046b..1bd370a 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/viewmodel/UploadHistoryViewModel.java +++ b/app/src/main/java/com/mattintech/simplelogupload/viewmodel/UploadHistoryViewModel.java @@ -5,11 +5,14 @@ import android.app.Application; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import com.mattintech.simplelogupload.controller.UploadController; import com.mattintech.simplelogupload.db.UploadHistoryRepository; import com.mattintech.simplelogupload.model.UploadHistory; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * ViewModel for upload history @@ -17,7 +20,9 @@ import java.util.List; public class UploadHistoryViewModel extends AndroidViewModel { private final UploadHistoryRepository repository; + private final UploadController uploadController; private final LiveData> allHistory; + private final MutableLiveData deleteStatus = new MutableLiveData<>(); /** * Constructor @@ -27,6 +32,7 @@ public class UploadHistoryViewModel extends AndroidViewModel { public UploadHistoryViewModel(@NonNull Application application) { super(application); repository = new UploadHistoryRepository(application); + uploadController = new UploadController(application); 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 getDeleteStatus() { + return deleteStatus; + } + + /** + * Delete a history entry and the associated file from the server * * @param history The entry to delete + * @return CompletableFuture for the operation */ - public void delete(UploadHistory history) { - repository.delete(history); + public CompletableFuture delete(UploadHistory history) { + CompletableFuture 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() { - repository.deleteAll(); + public CompletableFuture deleteAll() { + CompletableFuture 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; } } diff --git a/app/src/main/res/layout/dialog_upload_params.xml b/app/src/main/res/layout/dialog_upload_params.xml new file mode 100644 index 0000000..bb4eea6 --- /dev/null +++ b/app/src/main/res/layout/dialog_upload_params.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +