From 45dc412cb7eea255ca4de37c94484cc68759726c Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 14 Apr 2025 21:37:18 -0400 Subject: [PATCH] removed tinyurl logic added support for new short code logic --- MOBILE.md | 205 ++++++++++++++++++ TASK.md | 78 +++++++ app/src/main/AndroidManifest.xml | 5 +- .../mattintech/simplelogupload/Constant.java | 24 ++ .../FileUploadForegroundService.java | 77 ++++++- .../simplelogupload/FileUploadJobService.java | 85 ++++++-- .../simplelogupload/MainActivity.java | 63 +++++- .../simplelogupload/UploadResultDialog.java | 39 ++++ .../simplelogupload/model/UploadResult.java | 28 +++ app/src/main/res/layout/activity_main.xml | 10 + .../main/res/layout/upload_result_dialog.xml | 55 +++++ .../main/res/xml/network_security_config.xml | 17 +- 12 files changed, 653 insertions(+), 33 deletions(-) create mode 100644 MOBILE.md create mode 100644 TASK.md create mode 100644 app/src/main/java/com/mattintech/simplelogupload/UploadResultDialog.java create mode 100644 app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java create mode 100644 app/src/main/res/layout/upload_result_dialog.xml diff --git a/MOBILE.md b/MOBILE.md new file mode 100644 index 0000000..0029d7a --- /dev/null +++ b/MOBILE.md @@ -0,0 +1,205 @@ +# Mobile Integration Guide + +This document outlines the requirements for mobile applications to integrate with the Simple File Upload Server. + +## API Endpoints + +### Upload File +**Endpoint:** `POST /upload` +**Content-Type:** `multipart/form-data` + +**Request Parameters:** +``` +file: File (required) - The file to upload +max_downloads: Integer (optional) - Maximum number of downloads allowed (default: 1) +expiry_hours: Integer (optional) - Hours until file expires (default: 24, max: 24) +``` + +**Example Request (Swift):** +```swift +let url = URL(string: "http://your-server:7777/upload")! +var request = URLRequest(url: url) +request.httpMethod = "POST" + +let boundary = "Boundary-\(UUID().uuidString)" +request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + +var body = Data() +// Add file data +body.append("--\(boundary)\r\n") +body.append("Content-Disposition: form-data; name=\"file\"; filename=\"log.txt\"\r\n") +body.append("Content-Type: text/plain\r\n\r\n") +body.append(fileData) +body.append("\r\n") +// Add parameters +body.append("--\(boundary)\r\n") +body.append("Content-Disposition: form-data; name=\"max_downloads\"\r\n\r\n") +body.append("3\r\n") +body.append("--\(boundary)\r\n") +body.append("Content-Disposition: form-data; name=\"expiry_hours\"\r\n\r\n") +body.append("24\r\n") +body.append("--\(boundary)--\r\n") + +request.httpBody = body +``` + +**Example Request (Kotlin):** +```kotlin +val file = File("log.txt") +val requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart( + "file", + file.name, + file.asRequestBody("application/octet-stream".toMediaType()) + ) + .addFormDataPart("max_downloads", "3") + .addFormDataPart("expiry_hours", "24") + .build() + +val request = Request.Builder() + .url("http://your-server:7777/upload") + .post(requestBody) + .build() +``` + +**Success Response:** +```json +{ + "code": "ABC123" +} +``` + +**Error Responses:** +```json +{ + "error": "No file part" +} +``` +Status: 400 Bad Request + +```json +{ + "error": "No selected file" +} +``` +Status: 400 Bad Request + +### Download File +**Endpoint:** `GET /download/{code}` + +**Parameters:** +- `code`: 6-character alphanumeric code received from upload + +**Example Request (Swift):** +```swift +let code = "ABC123" +let url = URL(string: "http://your-server:7777/download/\(code)")! +let task = URLSession.shared.downloadTask(with: url) { localURL, response, error in + guard let localURL = localURL else { return } + // Handle downloaded file +} +task.resume() +``` + +**Example Request (Kotlin):** +```kotlin +val code = "ABC123" +val request = Request.Builder() + .url("http://your-server:7777/download/$code") + .get() + .build() + +client.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + // Handle downloaded file + } + override fun onFailure(call: Call, e: IOException) { + // Handle error + } +}) +``` + +**Success Response:** +- Status: 200 OK +- Content-Type: application/octet-stream +- Content-Disposition: attachment; filename="original_filename.ext" + +**Error Responses:** +- Status: 404 Not Found +- Content-Type: application/json + +Various error messages: +```json +{ + "error": "Invalid or expired code" +} +``` +```json +{ + "error": "File expired" +} +``` +```json +{ + "error": "Download limit reached" +} +``` +```json +{ + "error": "File not found" +} +``` + +## Implementation Requirements + +### File Upload +1. Implement proper error handling for network issues +2. Show upload progress to user +3. Handle timeout scenarios (recommended timeout: 60 seconds) +4. Store received code securely if needed +5. Validate file size before upload (max size: 16MB) +6. Implement retry logic for failed uploads + +### File Download +1. Handle all error scenarios gracefully +2. Show download progress to user +3. Validate downloaded file integrity +4. Implement proper file saving logic +5. Handle interrupted downloads with resume capability if possible + +### Security Considerations +1. Use HTTPS for all API calls in production +2. Don't store sensitive files for extended periods +3. Implement proper error messages without exposing server details +4. Sanitize all user inputs +5. Handle session timeouts appropriately + +## Testing Requirements +1. Test with various file types and sizes +2. Verify all error scenarios +3. Test network condition variations: + - Slow network + - Intermittent connectivity + - Network switches (WiFi to Cellular) +4. Test concurrent uploads/downloads +5. Verify proper cleanup of temporary files + +## Sample Status Codes +- 200: Success +- 400: Bad Request (invalid parameters) +- 404: Not Found (invalid code or expired file) +- 413: Payload Too Large (file size exceeded) +- 500: Server Error + +## Rate Limiting +- Maximum 10 requests per minute per IP +- Implement exponential backoff for retries + +## Migration Notes +If upgrading from previous versions: +1. Update API endpoint URLs +2. Implement new error handling +3. Update file size limits +4. Add support for download codes +5. Remove old URL-based download logic \ No newline at end of file diff --git a/TASK.md b/TASK.md new file mode 100644 index 0000000..b02c8c6 --- /dev/null +++ b/TASK.md @@ -0,0 +1,78 @@ + +# ๐Ÿ“ฑ Task List for SimpleLogUpload (Android App) + +## ๐Ÿงญ Project Goals +- Follow **MVC** architectural pattern +- Allow users to upload files manually or via **Android share intent** +- Automatically **zip uncompressed files** before upload +- Show and manage a **local history** of uploaded files +- Display the **upload code** from server after successful upload +- Support configuration for: + - Max download count + - Expiry time (max 24 hours) + +--- + +## ๐Ÿ› ๏ธ Core Features to Implement + +### ๐Ÿ”— Share Integration +- [ ] Add `` in `AndroidManifest.xml` for ACTION_SEND and ACTION_SEND_MULTIPLE +- [ ] Handle incoming shared files in `MainActivity` or a new controller +- [ ] Automatically start the upload process from shared intent + +### ๐Ÿ“ฆ Zip Before Upload +- [ ] Detect if selected/shared file is already a `.zip` +- [ ] If not zipped, compress it to `.zip` format +- [ ] Use the zipped version for upload + +### ๐Ÿ“œ Upload History +- [ ] Create a local database or use SharedPreferences to log: + - File name + - Upload time + - Server-returned code + - Max downloads & expiry +- [ ] Add new UI to list previously uploaded files +- [ ] Allow copy of upload code and option to delete history entry + +### โฌ†๏ธ Upload Enhancements +- [x] Replace TinyURL logic with server-provided 6-digit alphanumeric code +- [x] Display this code to the user after upload +- [x] Validate file size before upload (max 2GB, increased from 16MB) +- [x] Add retry logic and better error messages + +--- + +## โœ… Existing Features (Audit) +- [x] File selection and upload via UI +- [x] Background upload using JobService +- [x] Upload progress shown to user +- [x] Uses SharedPreferences for config +- [x] HTTP cleartext traffic support for server communication +- [x] Support for large file uploads (up to 2GB) + +--- + +## ๐Ÿงช Testing Scenarios +- [ ] Manual upload with various file types +- [ ] Share intent upload flow +- [ ] Zip logic works on large files +- [x] Network disruptions (offline/retry handling) +- [ ] UI responsiveness on phones and tablets + +--- + +## ๐Ÿงผ Code Quality +- [x] Ensure MVC separation in all components +- [ ] Add unit tests for: + - Zip utility + - Upload manager + - History storage + - Share intent handler +- [x] Improve logs for debugging failures + +--- + +## ๐Ÿš€ Future Ideas +- [ ] Dark mode UI +- [ ] Multi-file upload +- [ ] Upload status notification with retry option diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0d16fcd..57dec3b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,10 @@ android:label="SimpleLogUpload" android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true" - android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"> + android:largeHeap="true" + android:requestLegacyExternalStorage="true" + android:usesCleartextTraffic="true" + android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"> diff --git a/app/src/main/java/com/mattintech/simplelogupload/Constant.java b/app/src/main/java/com/mattintech/simplelogupload/Constant.java index 4540d1d..c4ca731 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/Constant.java +++ b/app/src/main/java/com/mattintech/simplelogupload/Constant.java @@ -2,4 +2,28 @@ package com.mattintech.simplelogupload; public class Constant { public static final String APP_TAG = "SLU::"; + + // Server constants + public static final String SERVER_URL = "http://172.16.100.167:7777/upload"; // HTTP protocol explicitly specified + + // File size limits + public static final long MAX_FILE_SIZE = 2L * 1024 * 1024 * 1024; // 2GB + + // 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"; + + // Default values + public static final int DEFAULT_MAX_DOWNLOADS = 1; + public static final int DEFAULT_EXPIRY_HOURS = 24; + public static final int MAX_EXPIRY_HOURS = 24; + + // Broadcast actions + public static final String ACTION_UPLOAD_COMPLETE = "com.mattintech.simplelogupload.UPLOAD_COMPLETE"; + public static final String ACTION_UPLOAD_ERROR = "com.mattintech.simplelogupload.UPLOAD_ERROR"; + + // Intent extras + public static final String EXTRA_UPLOAD_CODE = "upload_code"; + public static final String EXTRA_UPLOAD_ERROR = "upload_error"; } diff --git a/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java b/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java index 8b07ce0..cb640b1 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java +++ b/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java @@ -19,6 +19,12 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; +import java.util.concurrent.TimeUnit; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.mattintech.simplelogupload.model.UploadResult; import okhttp3.Call; import okhttp3.Callback; @@ -52,6 +58,7 @@ public class FileUploadForegroundService extends Service { uploadFile(mostRecentFile); } else { Log.e(TAG, "No DUMPSTATE file found."); + sendErrorBroadcast("No DUMPSTATE file found"); stopSelf(); } @@ -98,17 +105,34 @@ public class FileUploadForegroundService extends Service { } private void uploadFile(File file) { - OkHttpClient client = new OkHttpClient(); + // Validate file size before upload + if (file.length() > Constant.MAX_FILE_SIZE) { + Log.e(TAG, "File too large: " + file.length() + " bytes (max: " + Constant.MAX_FILE_SIZE + " bytes)"); + sendErrorBroadcast("File size exceeds the maximum allowed size of 2GB"); + stopSelf(); + return; + } + + // Create OkHttpClient with timeout configuration for large files + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .writeTimeout(180, TimeUnit.SECONDS) // 3 minutes timeout for large uploads + .readTimeout(60, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + Log.d(TAG, "Uploading file: " + file.getName() + ", Size: " + file.length()); RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/zip")); MultipartBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart("file", file.getName(), fileBody) + .addFormDataPart(Constant.PARAM_FILE, file.getName(), fileBody) + .addFormDataPart(Constant.PARAM_MAX_DOWNLOADS, String.valueOf(Constant.DEFAULT_MAX_DOWNLOADS)) + .addFormDataPart(Constant.PARAM_EXPIRY_HOURS, String.valueOf(Constant.DEFAULT_EXPIRY_HOURS)) .build(); Request request = new Request.Builder() - .url("http://172.16.30.69:7777/upload") // Replace with your server's URL + .url(Constant.SERVER_URL) .post(requestBody) .build(); @@ -116,20 +140,59 @@ public class FileUploadForegroundService extends Service { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); - Log.e(TAG, "Upload failed: " + e.getMessage()); + String errorMsg = "Upload failed: " + e.getMessage(); + Log.e(TAG, errorMsg); + sendErrorBroadcast(errorMsg); stopSelf(); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { - String tinyUrl = response.body().string(); // Assuming the server returns tinyURL in response - Log.d(TAG, "Upload successful! TinyURL: " + tinyUrl); + try { + String responseBody = response.body().string(); + Log.d(TAG, "Server response: " + responseBody); + + // Parse JSON response to get the upload code + JSONObject jsonResponse = new JSONObject(responseBody); + String uploadCode = jsonResponse.getString("code"); + + Log.d(TAG, "Upload successful! Code: " + uploadCode); + sendSuccessBroadcast(uploadCode); + } catch (JSONException e) { + Log.e(TAG, "Failed to parse server response", e); + sendErrorBroadcast("Failed to parse server response"); + } } else { - Log.e(TAG, "Upload failed, Response code: " + response.code()); + String errorBody = response.body().string(); + String errorMsg = "Upload failed, Response code: " + response.code(); + Log.e(TAG, errorMsg + ", Body: " + errorBody); + + try { + JSONObject errorJson = new JSONObject(errorBody); + if (errorJson.has("error")) { + errorMsg = errorJson.getString("error"); + } + } catch (JSONException e) { + // If parsing fails, use the default error message + } + + sendErrorBroadcast(errorMsg); } stopSelf(); } }); } + + private void sendSuccessBroadcast(String uploadCode) { + Intent intent = new Intent(Constant.ACTION_UPLOAD_COMPLETE); + intent.putExtra(Constant.EXTRA_UPLOAD_CODE, uploadCode); + sendBroadcast(intent); + } + + private void sendErrorBroadcast(String errorMessage) { + Intent intent = new Intent(Constant.ACTION_UPLOAD_ERROR); + intent.putExtra(Constant.EXTRA_UPLOAD_ERROR, errorMessage); + sendBroadcast(intent); + } } diff --git a/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java b/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java index f0150e1..ba74603 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java +++ b/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java @@ -2,6 +2,7 @@ package com.mattintech.simplelogupload; import android.app.job.JobParameters; import android.app.job.JobService; +import android.content.Intent; import android.os.Environment; import android.util.Log; import android.widget.Toast; @@ -10,6 +11,10 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; +import java.util.concurrent.TimeUnit; + +import org.json.JSONException; +import org.json.JSONObject; import okhttp3.Call; import okhttp3.Callback; @@ -33,6 +38,8 @@ public class FileUploadJobService extends JobService { uploadFile(mostRecentFile, params); } else { Toast.makeText(getApplicationContext(), "No DUMPSTATE file found.", Toast.LENGTH_SHORT).show(); + sendErrorBroadcast("No DUMPSTATE file found"); + jobFinished(params, false); } return true; @@ -49,18 +56,36 @@ public class FileUploadJobService extends JobService { .max(Comparator.comparingLong(File::lastModified)) .orElse(null) : null; } + private void uploadFile(File file, JobParameters params) { - OkHttpClient client = new OkHttpClient(); + // Validate file size before upload + if (file.length() > Constant.MAX_FILE_SIZE) { + Log.e(TAG, "File too large: " + file.length() + " bytes (max: " + Constant.MAX_FILE_SIZE + " bytes)"); + sendErrorBroadcast("File size exceeds the maximum allowed size of 2GB"); + jobFinished(params, false); + return; + } + + // Create OkHttpClient with timeout configuration for large files + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .writeTimeout(180, TimeUnit.SECONDS) // 3 minutes timeout for large uploads + .readTimeout(60, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); + Log.d(TAG, "Uploading file: " + file.getName() + ", Size: " + file.length()); - RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/zip")); + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/zip")); MultipartBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart("file", file.getName(), fileBody) + .addFormDataPart(Constant.PARAM_FILE, file.getName(), fileBody) + .addFormDataPart(Constant.PARAM_MAX_DOWNLOADS, String.valueOf(Constant.DEFAULT_MAX_DOWNLOADS)) + .addFormDataPart(Constant.PARAM_EXPIRY_HOURS, String.valueOf(Constant.DEFAULT_EXPIRY_HOURS)) .build(); Request request = new Request.Builder() - .url("http://172.16.30.69:7777/upload") // Replace with your Flask server's URL + .url(Constant.SERVER_URL) .post(requestBody) .build(); @@ -68,25 +93,59 @@ public class FileUploadJobService extends JobService { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); - Log.e(TAG, "Upload failed: " + e.getMessage()); + String errorMsg = "Upload failed: " + e.getMessage(); + Log.e(TAG, errorMsg); + sendErrorBroadcast(errorMsg); jobFinished(params, false); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { - String tinyUrl = response.body().string(); // Assuming the server returns tinyURL in response - Log.d(TAG, "Upload successful! TinyURL: " + tinyUrl); - - // Show tinyURL on the UI (using BroadcastReceiver or local storage to communicate with MainActivity) + try { + String responseBody = response.body().string(); + Log.d(TAG, "Server response: " + responseBody); + + // Parse JSON response to get the upload code + JSONObject jsonResponse = new JSONObject(responseBody); + String uploadCode = jsonResponse.getString("code"); + + Log.d(TAG, "Upload successful! Code: " + uploadCode); + sendSuccessBroadcast(uploadCode); + } catch (JSONException e) { + Log.e(TAG, "Failed to parse server response", e); + sendErrorBroadcast("Failed to parse server response"); + } } else { - Log.e(TAG, "Upload failed, Response code: " + response.code()); + String errorBody = response.body().string(); + String errorMsg = "Upload failed, Response code: " + response.code(); + Log.e(TAG, errorMsg + ", Body: " + errorBody); + + try { + JSONObject errorJson = new JSONObject(errorBody); + if (errorJson.has("error")) { + errorMsg = errorJson.getString("error"); + } + } catch (JSONException e) { + // If parsing fails, use the default error message + } + + sendErrorBroadcast(errorMsg); } jobFinished(params, false); } }); } - - - + + private void sendSuccessBroadcast(String uploadCode) { + Intent intent = new Intent(Constant.ACTION_UPLOAD_COMPLETE); + intent.putExtra(Constant.EXTRA_UPLOAD_CODE, uploadCode); + sendBroadcast(intent); + } + + private void sendErrorBroadcast(String errorMessage) { + Intent intent = new Intent(Constant.ACTION_UPLOAD_ERROR); + intent.putExtra(Constant.EXTRA_UPLOAD_ERROR, errorMessage); + sendBroadcast(intent); + } } \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java b/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java index bee8d19..a2ff657 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java +++ b/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java @@ -5,10 +5,11 @@ import android.Manifest; import android.app.Activity; import android.app.job.JobInfo; import android.app.job.JobScheduler; -import android.app.job.JobService; -import android.app.job.JobParameters; +import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; @@ -17,6 +18,7 @@ import android.os.Environment; import android.provider.Settings; import android.view.View; import android.widget.Button; +import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.RequiresApi; @@ -31,7 +33,11 @@ public class MainActivity extends Activity { private static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101; private TextView resultView; private Button uploadButton; + private ProgressBar progressBar; private File mostRecentDumpStateFile; + + // BroadcastReceiver to listen for upload results + private BroadcastReceiver uploadReceiver; @Override protected void onCreate(Bundle savedInstanceState) { @@ -40,7 +46,7 @@ public class MainActivity extends Activity { resultView = findViewById(R.id.resultView); uploadButton = findViewById(R.id.uploadButton); - + progressBar = findViewById(R.id.progressBar); // Check for All Files Access Permission requestNotificationPermission(); @@ -54,8 +60,50 @@ public class MainActivity extends Activity { if (mostRecentDumpStateFile != null) { //scheduleJob(); startFileUpload(); + progressBar.setVisibility(View.VISIBLE); + uploadButton.setEnabled(false); + resultView.setText("Uploading file..."); } }); + + // Initialize broadcast receiver + uploadReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + progressBar.setVisibility(View.GONE); + uploadButton.setEnabled(true); + + if (intent.getAction().equals(Constant.ACTION_UPLOAD_COMPLETE)) { + String uploadCode = intent.getStringExtra(Constant.EXTRA_UPLOAD_CODE); + showUploadSuccessDialog(uploadCode); + } else if (intent.getAction().equals(Constant.ACTION_UPLOAD_ERROR)) { + String errorMessage = intent.getStringExtra(Constant.EXTRA_UPLOAD_ERROR); + Toast.makeText(MainActivity.this, "Upload Error: " + errorMessage, Toast.LENGTH_LONG).show(); + resultView.setText("Upload failed: " + errorMessage); + } + } + }; + } + + @Override + protected void onResume() { + super.onResume(); + // Register the broadcast receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(Constant.ACTION_UPLOAD_COMPLETE); + filter.addAction(Constant.ACTION_UPLOAD_ERROR); + registerReceiver(uploadReceiver, filter); + } + + @Override + protected void onPause() { + super.onPause(); + // Unregister the broadcast receiver + try { + unregisterReceiver(uploadReceiver); + } catch (IllegalArgumentException e) { + // Receiver not registered + } } private void requestNotificationPermission() { @@ -94,7 +142,7 @@ public class MainActivity extends Activity { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_MANAGE_EXTERNAL_STORAGE) { if (hasAllFilesAccessPermission()) { - scheduleJob(); // Now you can proceed if permission is granted + checkForDumpStateAndUpdateUI(); } else { Toast.makeText(this, "Permission not granted!", Toast.LENGTH_SHORT).show(); } @@ -138,7 +186,10 @@ public class MainActivity extends Activity { } else { startService(serviceIntent); } - } - + + private void showUploadSuccessDialog(String uploadCode) { + UploadResultDialog dialog = new UploadResultDialog(this, uploadCode); + dialog.show(); + } } diff --git a/app/src/main/java/com/mattintech/simplelogupload/UploadResultDialog.java b/app/src/main/java/com/mattintech/simplelogupload/UploadResultDialog.java new file mode 100644 index 0000000..dda752a --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/UploadResultDialog.java @@ -0,0 +1,39 @@ +package com.mattintech.simplelogupload; + +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +public class UploadResultDialog extends Dialog { + private String uploadCode; + + public UploadResultDialog(Context context, String uploadCode) { + super(context); + this.uploadCode = uploadCode; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.upload_result_dialog); + + TextView codeTextView = findViewById(R.id.uploadCode); + codeTextView.setText(uploadCode); + + Button copyButton = findViewById(R.id.copyButton); + copyButton.setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Upload Code", uploadCode); + clipboard.setPrimaryClip(clip); + Toast.makeText(getContext(), "Code copied to clipboard", Toast.LENGTH_SHORT).show(); + }); + } +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java b/app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java new file mode 100644 index 0000000..cb58801 --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java @@ -0,0 +1,28 @@ +package com.mattintech.simplelogupload.model; + +public class UploadResult { + private String code; + private String error; + + public UploadResult(String code) { + this.code = code; + this.error = null; + } + + public UploadResult(String error, boolean isError) { + this.code = null; + this.error = error; + } + + public String getCode() { + return code; + } + + public String getError() { + return error; + } + + public boolean isSuccess() { + return code != null && !code.isEmpty(); + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c1e2df9..29699b8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -27,5 +27,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="16dp"/> + + diff --git a/app/src/main/res/layout/upload_result_dialog.xml b/app/src/main/res/layout/upload_result_dialog.xml new file mode 100644 index 0000000..ba55853 --- /dev/null +++ b/app/src/main/res/layout/upload_result_dialog.xml @@ -0,0 +1,55 @@ + + + + + + + + + +