adding settings screen

This commit is contained in:
2025-04-15 09:52:20 -04:00
parent 0a4f7871bc
commit e36d0616d5
10 changed files with 442 additions and 210 deletions

205
MOBILE.md
View File

@@ -1,205 +0,0 @@
# 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

View File

@@ -55,6 +55,17 @@
android:value=".view.MainActivity" />
</activity>
<!-- Settings Activity -->
<activity
android:name=".view.SettingsActivity"
android:label="Settings"
android:parentActivityName=".view.MainActivity"
android:exported="false">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".view.MainActivity" />
</activity>
<!-- File Upload Foreground Service -->
<service
android:name=".service.FileUploadForegroundService"

View File

@@ -4,14 +4,18 @@ package com.mattintech.simplelogupload.constant;
* Network-related constants
*/
public class NetworkConstants {
// Server constants
public static final String SERVER_URL = "http://172.16.100.167:7777/upload";
// These URL constants are now dynamically set in getServerUrl()
// via PreferencesUtil to allow for customization
public static final String SERVER_URL_BASE = "/upload";
// 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";
// Authentication
public static final String CLIENT_KEY_HEADER = "client-key";
// Default values
public static final int DEFAULT_MAX_DOWNLOADS = 1;
public static final int DEFAULT_EXPIRY_HOURS = 24;

View File

@@ -11,6 +11,7 @@ import com.mattintech.simplelogupload.db.UploadHistoryRepository;
import com.mattintech.simplelogupload.model.UploadHistory;
import com.mattintech.simplelogupload.model.UploadResult;
import com.mattintech.simplelogupload.service.FileUploadForegroundService;
import com.mattintech.simplelogupload.util.PreferencesUtil;
import java.io.File;
import java.io.IOException;
@@ -103,11 +104,21 @@ public class UploadController {
.addFormDataPart(NetworkConstants.PARAM_EXPIRY_HOURS, String.valueOf(expiryHours))
.build();
// Get server URL and client key from preferences
String serverUrl = PreferencesUtil.getServerUrl(context);
String clientKey = PreferencesUtil.getClientKey(context);
Log.d(TAG, "Using server URL: " + serverUrl);
// Build request with client-key header
Request request = new Request.Builder()
.url(NetworkConstants.SERVER_URL)
.url(serverUrl)
.header(NetworkConstants.CLIENT_KEY_HEADER, clientKey)
.post(requestBody)
.build();
Log.d(TAG, "Sending upload request with " + NetworkConstants.CLIENT_KEY_HEADER + " header");
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
@@ -142,8 +153,16 @@ public class UploadController {
}
} else {
String errorBody = response.body().string();
String errorMsg = "Upload failed, Response code: " + response.code();
Log.e(TAG, errorMsg + ", Body: " + errorBody);
String errorMsg;
// Check for authentication errors
if (response.code() == 401 || response.code() == 403) {
errorMsg = "Authentication failed. Please check client key.";
Log.e(TAG, "Authentication error: " + response.code() + ", Body: " + errorBody);
} else {
errorMsg = "Upload failed, Response code: " + response.code();
Log.e(TAG, errorMsg + ", Body: " + errorBody);
}
try {
JSONObject errorJson = new JSONObject(errorBody);

View File

@@ -0,0 +1,116 @@
package com.mattintech.simplelogupload.util;
import android.content.Context;
import android.content.SharedPreferences;
/**
* Utility class for handling application preferences
*/
public class PreferencesUtil {
private static final String PREFERENCES_NAME = "SimpleLogUploadPrefs";
// Preference keys
public static final String KEY_SERVER_ADDRESS = "server_address";
public static final String KEY_SERVER_PORT = "server_port";
public static final String KEY_CLIENT_KEY = "client_key";
// Default values
private static final String DEFAULT_SERVER_ADDRESS = "172.16.100.167";
private static final int DEFAULT_SERVER_PORT = 7777;
private static final String DEFAULT_CLIENT_KEY = "default-secret-key";
/**
* Get the shared preferences instance
*
* @param context The application context
* @return The SharedPreferences instance
*/
private static SharedPreferences getPreferences(Context context) {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
}
/**
* Get the server address from preferences
*
* @param context The application context
* @return The server address
*/
public static String getServerAddress(Context context) {
return getPreferences(context).getString(KEY_SERVER_ADDRESS, DEFAULT_SERVER_ADDRESS);
}
/**
* Set the server address in preferences
*
* @param context The application context
* @param serverAddress The server address to save
*/
public static void setServerAddress(Context context, String serverAddress) {
getPreferences(context).edit().putString(KEY_SERVER_ADDRESS, serverAddress).apply();
}
/**
* Get the server port from preferences
*
* @param context The application context
* @return The server port
*/
public static int getServerPort(Context context) {
return getPreferences(context).getInt(KEY_SERVER_PORT, DEFAULT_SERVER_PORT);
}
/**
* Set the server port in preferences
*
* @param context The application context
* @param serverPort The server port to save
*/
public static void setServerPort(Context context, int serverPort) {
getPreferences(context).edit().putInt(KEY_SERVER_PORT, serverPort).apply();
}
/**
* Get the client key from preferences
*
* @param context The application context
* @return The client key
*/
public static String getClientKey(Context context) {
return getPreferences(context).getString(KEY_CLIENT_KEY, DEFAULT_CLIENT_KEY);
}
/**
* Set the client key in preferences
*
* @param context The application context
* @param clientKey The client key to save
*/
public static void setClientKey(Context context, String clientKey) {
getPreferences(context).edit().putString(KEY_CLIENT_KEY, clientKey).apply();
}
/**
* Get the complete server URL (address + port + path)
*
* @param context The application context
* @return The complete server URL
*/
public static String getServerUrl(Context context) {
String address = getServerAddress(context);
int port = getServerPort(context);
return "http://" + address + ":" + port + "/upload";
}
/**
* Reset all settings to default values
*
* @param context The application context
*/
public static void resetToDefaults(Context context) {
SharedPreferences.Editor editor = getPreferences(context).edit();
editor.putString(KEY_SERVER_ADDRESS, DEFAULT_SERVER_ADDRESS);
editor.putInt(KEY_SERVER_PORT, DEFAULT_SERVER_PORT);
editor.putString(KEY_CLIENT_KEY, DEFAULT_CLIENT_KEY);
editor.apply();
}
}

View File

@@ -305,6 +305,10 @@ public class MainActivity extends AppCompatActivity {
// Open upload history activity
startActivity(new Intent(this, UploadHistoryActivity.class));
return true;
} else if (id == R.id.action_settings) {
// Open settings activity
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);

View File

@@ -0,0 +1,128 @@
package com.mattintech.simplelogupload.view;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputEditText;
import com.mattintech.simplelogupload.R;
import com.mattintech.simplelogupload.constant.AppConstants;
import com.mattintech.simplelogupload.util.PreferencesUtil;
/**
* Activity for application settings
*/
public class SettingsActivity extends AppCompatActivity {
private static final String TAG = AppConstants.APP_TAG + "SettingsActivity";
private TextInputEditText serverAddressInput;
private TextInputEditText serverPortInput;
private TextInputEditText clientKeyInput;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
// Initialize UI components
serverAddressInput = findViewById(R.id.serverAddressInput);
serverPortInput = findViewById(R.id.serverPortInput);
clientKeyInput = findViewById(R.id.clientKeyInput);
Button saveButton = findViewById(R.id.saveSettingsButton);
Button resetButton = findViewById(R.id.resetSettingsButton);
// Set up action bar
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle("Settings");
}
// Load current settings
loadSettings();
// Set up button click listeners
saveButton.setOnClickListener(v -> saveSettings());
resetButton.setOnClickListener(v -> resetSettings());
}
/**
* Load current settings from SharedPreferences
*/
private void loadSettings() {
serverAddressInput.setText(PreferencesUtil.getServerAddress(this));
serverPortInput.setText(String.valueOf(PreferencesUtil.getServerPort(this)));
clientKeyInput.setText(PreferencesUtil.getClientKey(this));
}
/**
* Save settings to SharedPreferences
*/
private void saveSettings() {
try {
// Get values from input fields
String serverAddress = serverAddressInput.getText().toString().trim();
String serverPortStr = serverPortInput.getText().toString().trim();
String clientKey = clientKeyInput.getText().toString().trim();
// Validate input
if (TextUtils.isEmpty(serverAddress)) {
serverAddressInput.setError("Server address is required");
return;
}
if (TextUtils.isEmpty(serverPortStr)) {
serverPortInput.setError("Server port is required");
return;
}
int serverPort;
try {
serverPort = Integer.parseInt(serverPortStr);
if (serverPort <= 0 || serverPort > 65535) {
serverPortInput.setError("Port must be between 1 and 65535");
return;
}
} catch (NumberFormatException e) {
serverPortInput.setError("Invalid port number");
return;
}
if (TextUtils.isEmpty(clientKey)) {
clientKeyInput.setError("Client key is required");
return;
}
// Save settings
PreferencesUtil.setServerAddress(this, serverAddress);
PreferencesUtil.setServerPort(this, serverPort);
PreferencesUtil.setClientKey(this, clientKey);
Log.d(TAG, "Settings saved successfully");
Toast.makeText(this, "Settings saved successfully", Toast.LENGTH_SHORT).show();
finish();
} catch (Exception e) {
Log.e(TAG, "Error saving settings", e);
Toast.makeText(this, "Error saving settings: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
/**
* Reset settings to default values
*/
private void resetSettings() {
PreferencesUtil.resetToDefaults(this);
loadSettings();
Toast.makeText(this, "Settings reset to defaults", Toast.LENGTH_SHORT).show();
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
}

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Server Configuration"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Server Address">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/serverAddressInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Server Port">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/serverPortInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Authentication"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Client Key"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/clientKeyInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/saveSettingsButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Settings"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/resetSettingsButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Reset to Defaults"
android:layout_marginTop="8dp"
style="@style/Widget.AppCompat.Button.Borderless.Colored" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,64 @@
<?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="match_parent"
android:padding="24dp">
<ImageView
android:id="@+id/logoImage"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="32dp"
android:src="@android:drawable/ic_menu_upload"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/welcomeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Welcome to SimpleLogUpload"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logoImage" />
<TextView
android:id="@+id/welcomeDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Before you can use this app, you need to configure it to connect to your SimpleLogUpload server instance."
android:textAlignment="center"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/welcomeTitle" />
<TextView
android:id="@+id/welcomeDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="The app requires:\n\n• Server Address - IP or hostname of your server\n• Server Port - Port your server is running on\n• Client Key - Authentication key for secure uploads\n\nYour administrator should have provided these details."
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/welcomeDescription" />
<Button
android:id="@+id/nextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Continue to Setup"
android:padding="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -5,4 +5,9 @@
android:title="Upload History"
android:orderInCategory="100"
android:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:title="Settings"
android:orderInCategory="200"
android:showAsAction="never" />
</menu>