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: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">
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_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>
|
||||||
|
|||||||
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"?>
|
<?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>
|
||||||
|
|||||||
Reference in New Issue
Block a user