removed tinyurl logic added support for new short code logic

This commit is contained in:
2025-04-14 21:37:18 -04:00
parent efa32f2831
commit 45dc412cb7
12 changed files with 653 additions and 33 deletions

205
MOBILE.md Normal file
View File

@@ -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

78
TASK.md Normal file
View File

@@ -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 `<intent-filter>` 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

View File

@@ -14,7 +14,10 @@
android:label="SimpleLogUpload" android:label="SimpleLogUpload"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"> <!-- Add this line --> android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true">

View File

@@ -2,4 +2,28 @@ package com.mattintech.simplelogupload;
public class Constant { public class Constant {
public static final String APP_TAG = "SLU::"; 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";
} }

View File

@@ -19,6 +19,12 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; 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.Call;
import okhttp3.Callback; import okhttp3.Callback;
@@ -52,6 +58,7 @@ public class FileUploadForegroundService extends Service {
uploadFile(mostRecentFile); uploadFile(mostRecentFile);
} else { } else {
Log.e(TAG, "No DUMPSTATE file found."); Log.e(TAG, "No DUMPSTATE file found.");
sendErrorBroadcast("No DUMPSTATE file found");
stopSelf(); stopSelf();
} }
@@ -98,17 +105,34 @@ public class FileUploadForegroundService extends Service {
} }
private void uploadFile(File file) { 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()); 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() MultipartBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM) .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(); .build();
Request request = new Request.Builder() 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) .post(requestBody)
.build(); .build();
@@ -116,20 +140,59 @@ public class FileUploadForegroundService extends Service {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
e.printStackTrace(); e.printStackTrace();
Log.e(TAG, "Upload failed: " + e.getMessage()); String errorMsg = "Upload failed: " + e.getMessage();
Log.e(TAG, errorMsg);
sendErrorBroadcast(errorMsg);
stopSelf(); stopSelf();
} }
@Override @Override
public void onResponse(Call call, Response response) throws IOException { public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String tinyUrl = response.body().string(); // Assuming the server returns tinyURL in response try {
Log.d(TAG, "Upload successful! TinyURL: " + tinyUrl); 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 { } 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(); 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);
}
} }

View File

@@ -2,6 +2,7 @@ package com.mattintech.simplelogupload;
import android.app.job.JobParameters; import android.app.job.JobParameters;
import android.app.job.JobService; import android.app.job.JobService;
import android.content.Intent;
import android.os.Environment; import android.os.Environment;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@@ -10,6 +11,10 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import org.json.JSONException;
import org.json.JSONObject;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
@@ -33,6 +38,8 @@ public class FileUploadJobService extends JobService {
uploadFile(mostRecentFile, params); uploadFile(mostRecentFile, params);
} else { } else {
Toast.makeText(getApplicationContext(), "No DUMPSTATE file found.", Toast.LENGTH_SHORT).show(); Toast.makeText(getApplicationContext(), "No DUMPSTATE file found.", Toast.LENGTH_SHORT).show();
sendErrorBroadcast("No DUMPSTATE file found");
jobFinished(params, false);
} }
return true; return true;
@@ -49,18 +56,36 @@ public class FileUploadJobService extends JobService {
.max(Comparator.comparingLong(File::lastModified)) .max(Comparator.comparingLong(File::lastModified))
.orElse(null) : null; .orElse(null) : null;
} }
private void uploadFile(File file, JobParameters params) { 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()); 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() MultipartBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM) .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(); .build();
Request request = new Request.Builder() 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) .post(requestBody)
.build(); .build();
@@ -68,25 +93,59 @@ public class FileUploadJobService extends JobService {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
e.printStackTrace(); e.printStackTrace();
Log.e(TAG, "Upload failed: " + e.getMessage()); String errorMsg = "Upload failed: " + e.getMessage();
Log.e(TAG, errorMsg);
sendErrorBroadcast(errorMsg);
jobFinished(params, false); jobFinished(params, false);
} }
@Override @Override
public void onResponse(Call call, Response response) throws IOException { public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String tinyUrl = response.body().string(); // Assuming the server returns tinyURL in response try {
Log.d(TAG, "Upload successful! TinyURL: " + tinyUrl); String responseBody = response.body().string();
Log.d(TAG, "Server response: " + responseBody);
// Show tinyURL on the UI (using BroadcastReceiver or local storage to communicate with MainActivity)
// 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 { } 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); 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);
}
} }

View File

@@ -5,10 +5,11 @@ import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.app.job.JobInfo; import android.app.job.JobInfo;
import android.app.job.JobScheduler; import android.app.job.JobScheduler;
import android.app.job.JobService; import android.content.BroadcastReceiver;
import android.app.job.JobParameters;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@@ -17,6 +18,7 @@ import android.os.Environment;
import android.provider.Settings; import android.provider.Settings;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
@@ -31,7 +33,11 @@ public class MainActivity extends Activity {
private static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101; private static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
private TextView resultView; private TextView resultView;
private Button uploadButton; private Button uploadButton;
private ProgressBar progressBar;
private File mostRecentDumpStateFile; private File mostRecentDumpStateFile;
// BroadcastReceiver to listen for upload results
private BroadcastReceiver uploadReceiver;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -40,7 +46,7 @@ public class MainActivity extends Activity {
resultView = findViewById(R.id.resultView); resultView = findViewById(R.id.resultView);
uploadButton = findViewById(R.id.uploadButton); uploadButton = findViewById(R.id.uploadButton);
progressBar = findViewById(R.id.progressBar);
// Check for All Files Access Permission // Check for All Files Access Permission
requestNotificationPermission(); requestNotificationPermission();
@@ -54,8 +60,50 @@ public class MainActivity extends Activity {
if (mostRecentDumpStateFile != null) { if (mostRecentDumpStateFile != null) {
//scheduleJob(); //scheduleJob();
startFileUpload(); 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() { private void requestNotificationPermission() {
@@ -94,7 +142,7 @@ public class MainActivity extends Activity {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_MANAGE_EXTERNAL_STORAGE) { if (requestCode == REQUEST_MANAGE_EXTERNAL_STORAGE) {
if (hasAllFilesAccessPermission()) { if (hasAllFilesAccessPermission()) {
scheduleJob(); // Now you can proceed if permission is granted checkForDumpStateAndUpdateUI();
} else { } else {
Toast.makeText(this, "Permission not granted!", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Permission not granted!", Toast.LENGTH_SHORT).show();
} }
@@ -138,7 +186,10 @@ public class MainActivity extends Activity {
} else { } else {
startService(serviceIntent); startService(serviceIntent);
} }
} }
private void showUploadSuccessDialog(String uploadCode) {
UploadResultDialog dialog = new UploadResultDialog(this, uploadCode);
dialog.show();
}
} }

View File

@@ -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();
});
}
}

View File

@@ -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();
}
}

View File

@@ -27,5 +27,15 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/> android:layout_marginTop="16dp"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/uploadButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/dialogTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Upload Successful"
android:textAlignment="center"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/codeLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Your upload code:"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@id/dialogTitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/uploadCode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#007BFF"
app:layout_constraintTop_toBottomOf="@id/codeLabel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/copyButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Copy Code"
app:layout_constraintTop_toBottomOf="@id/uploadCode"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config>
<!-- Allow cleartext traffic to specific server IP -->
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">172.16.30.69</domain> <!-- Replace with your server --> <domain includeSubdomains="true">172.16.100.167</domain>
</domain-config> <!-- Add any other specific servers below if needed -->
<!-- OR: To allow for all domains (for testing purposes only) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain> <domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">192.168.1.1</domain> <domain includeSubdomains="true">10.0.2.2</domain> <!-- Android emulator localhost -->
<domain includeSubdomains="true">*.*</domain>
</domain-config> </domain-config>
<!-- Base config for all other domains -->
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config> </network-security-config>