removed tinyurl logic added support for new short code logic
This commit is contained in:
205
MOBILE.md
Normal file
205
MOBILE.md
Normal 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
78
TASK.md
Normal 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
|
||||
@@ -14,7 +14,10 @@
|
||||
android:label="SimpleLogUpload"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,15 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
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>
|
||||
|
||||
55
app/src/main/res/layout/upload_result_dialog.xml
Normal file
55
app/src/main/res/layout/upload_result_dialog.xml
Normal 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>
|
||||
@@ -1,12 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- Allow cleartext traffic to specific server IP -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">172.16.30.69</domain> <!-- Replace with your server -->
|
||||
</domain-config>
|
||||
<!-- OR: To allow for all domains (for testing purposes only) -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">172.16.100.167</domain>
|
||||
<!-- Add any other specific servers below if needed -->
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">192.168.1.1</domain>
|
||||
<domain includeSubdomains="true">*.*</domain>
|
||||
<domain includeSubdomains="true">10.0.2.2</domain> <!-- Android emulator localhost -->
|
||||
</domain-config>
|
||||
|
||||
<!-- Base config for all other domains -->
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
|
||||
Reference in New Issue
Block a user