diff --git a/app/build.gradle b/app/build.gradle
index 4235f43..43f6656 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,15 +1,17 @@
-
plugins {
id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'kotlin-kapt'
}
android {
namespace 'com.mattintech.simplelogupload'
- compileSdkVersion 34
+ compileSdkVersion 36
+
defaultConfig {
applicationId "com.mattintech.simplelogupload"
minSdkVersion 30
- targetSdkVersion 33
+ targetSdkVersion 36
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -21,14 +23,69 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
+ // Optional: explicitly specify Java toolchain
+ java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(17))
+ }
+ }
}
dependencies {
+ // Kotlin
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+ // Core Android UI
implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'com.google.android.material:material:1.8.0'
+ implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
+
+ // Networking
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
+ implementation 'com.android.support:support-annotations:28.0.0'
+ implementation 'androidx.palette:palette:1.0.0'
+
+ // Room
+ def room_version = "2.6.1"
+ implementation "androidx.room:room-runtime:$room_version"
+ kapt "androidx.room:room-compiler:$room_version"
+
+ // Lifecycle
+ def lifecycle_version = "2.6.2"
+ implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
+
+ // UI Components
+ implementation "androidx.recyclerview:recyclerview:1.3.2"
+ implementation "androidx.cardview:cardview:1.0.0"
+
+ // Coroutines
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
+
+ // Testing
testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
+
+ // Kotlin compatibility
+ constraints {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") {
+ because("Merged into kotlin-stdlib")
+ }
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") {
+ because("Merged into kotlin-stdlib")
+ }
+ }
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 57dec3b..a05985e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+ android:name=".service.FileUploadForegroundService"
+ android:enabled="true"
+ android:exported="false"
+ android:foregroundServiceType="dataSync" />
\ No newline at end of file
diff --git a/app/src/main/java/com/mattintech/simplelogupload/Constant.java b/app/src/main/java/com/mattintech/simplelogupload/Constant.java
deleted file mode 100644
index c4ca731..0000000
--- a/app/src/main/java/com/mattintech/simplelogupload/Constant.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.mattintech.simplelogupload;
-
-public class Constant {
- public static final String APP_TAG = "SLU::";
-
- // Server constants
- public static final String SERVER_URL = "http://172.16.100.167:7777/upload"; // HTTP protocol explicitly specified
-
- // File size limits
- public static final long MAX_FILE_SIZE = 2L * 1024 * 1024 * 1024; // 2GB
-
- // Upload parameters
- public static final String PARAM_FILE = "file";
- public static final String PARAM_MAX_DOWNLOADS = "max_downloads";
- public static final String PARAM_EXPIRY_HOURS = "expiry_hours";
-
- // Default values
- public static final int DEFAULT_MAX_DOWNLOADS = 1;
- public static final int DEFAULT_EXPIRY_HOURS = 24;
- public static final int MAX_EXPIRY_HOURS = 24;
-
- // Broadcast actions
- public static final String ACTION_UPLOAD_COMPLETE = "com.mattintech.simplelogupload.UPLOAD_COMPLETE";
- public static final String ACTION_UPLOAD_ERROR = "com.mattintech.simplelogupload.UPLOAD_ERROR";
-
- // Intent extras
- public static final String EXTRA_UPLOAD_CODE = "upload_code";
- public static final String EXTRA_UPLOAD_ERROR = "upload_error";
-}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java b/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java
deleted file mode 100644
index cb640b1..0000000
--- a/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java
+++ /dev/null
@@ -1,198 +0,0 @@
-package com.mattintech.simplelogupload;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Environment;
-import android.os.IBinder;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
-
-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;
-import okhttp3.MediaType;
-import okhttp3.MultipartBody;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-
-public class FileUploadForegroundService extends Service {
-
- private static final String TAG = Constant.APP_TAG + "FileUploadService";
- private static final String CHANNEL_ID = "FileUploadChannel";
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- Log.d(TAG, "Foreground Service started.");
-
- // Create a notification and run the service in the foreground
- createNotificationChannel();
- Notification notification = createNotification();
- startForeground(1, notification);
-
- Log.d(TAG, "Notification created.");
-
- // Start file upload process
- File dir = new File(Environment.getExternalStorageDirectory(), "log");
- File mostRecentFile = findMostRecentFile(dir);
- if (mostRecentFile != null) {
- uploadFile(mostRecentFile);
- } else {
- Log.e(TAG, "No DUMPSTATE file found.");
- sendErrorBroadcast("No DUMPSTATE file found");
- stopSelf();
- }
-
- return START_NOT_STICKY;
- }
-
-
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- private Notification createNotification() {
- Intent notificationIntent = new Intent(this, MainActivity.class);
- PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
-
- return new NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle("File Upload")
- .setContentText("Uploading file in progress...")
- .setSmallIcon(R.drawable.logupload)
- .setContentIntent(pendingIntent)
- .build();
- }
-
- private void createNotificationChannel() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- CharSequence name = "File Upload Channel";
- String description = "Notification channel for file upload";
- int importance = NotificationManager.IMPORTANCE_HIGH; // Change to HIGH to ensure visibility
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
- channel.setDescription(description);
-
- NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.createNotificationChannel(channel);
- }
- }
-
- private File findMostRecentFile(File dir) {
- File[] files = dir.listFiles((d, name) -> name.matches("dumpState_.*\\.zip"));
- return (files != null && files.length > 0) ? Arrays.stream(files)
- .max(Comparator.comparingLong(File::lastModified))
- .orElse(null) : null;
- }
-
- private void uploadFile(File file) {
- // 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(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(Constant.SERVER_URL)
- .post(requestBody)
- .build();
-
- client.newCall(request).enqueue(new Callback() {
- @Override
- public void onFailure(Call call, IOException e) {
- e.printStackTrace();
- 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()) {
- 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 {
- String errorBody = response.body().string();
- String errorMsg = "Upload failed, Response code: " + response.code();
- Log.e(TAG, errorMsg + ", Body: " + errorBody);
-
- try {
- JSONObject errorJson = new JSONObject(errorBody);
- if (errorJson.has("error")) {
- errorMsg = errorJson.getString("error");
- }
- } catch (JSONException e) {
- // If parsing fails, use the default error message
- }
-
- sendErrorBroadcast(errorMsg);
- }
- stopSelf();
- }
- });
- }
-
- private void sendSuccessBroadcast(String uploadCode) {
- Intent intent = new Intent(Constant.ACTION_UPLOAD_COMPLETE);
- intent.putExtra(Constant.EXTRA_UPLOAD_CODE, uploadCode);
- sendBroadcast(intent);
- }
-
- private void sendErrorBroadcast(String errorMessage) {
- Intent intent = new Intent(Constant.ACTION_UPLOAD_ERROR);
- intent.putExtra(Constant.EXTRA_UPLOAD_ERROR, errorMessage);
- sendBroadcast(intent);
- }
-}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java b/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java
deleted file mode 100644
index ba74603..0000000
--- a/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java
+++ /dev/null
@@ -1,151 +0,0 @@
-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;
-
-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;
-import okhttp3.MediaType;
-import okhttp3.MultipartBody;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-
-public class FileUploadJobService extends JobService {
- private static final String TAG = Constant.APP_TAG + "FileUploadJobService:";
-
- @Override
- public boolean onStartJob(JobParameters params) {
- File dir = new File(Environment.getExternalStorageDirectory(), "log");
- File mostRecentFile = findMostRecentFile(dir);
-
- if (mostRecentFile != null) {
- Toast.makeText(this, "Starting Upload", Toast.LENGTH_SHORT).show();
- 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;
- }
-
- @Override
- public boolean onStopJob(JobParameters params) {
- return false;
- }
-
- private File findMostRecentFile(File dir) {
- File[] files = dir.listFiles((d, name) -> name.matches("dumpState_.*\\.zip"));
- return (files != null && files.length > 0) ? Arrays.stream(files)
- .max(Comparator.comparingLong(File::lastModified))
- .orElse(null) : null;
- }
-
- private void uploadFile(File file, JobParameters params) {
- // 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"));
- MultipartBody requestBody = new MultipartBody.Builder()
- .setType(MultipartBody.FORM)
- .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(Constant.SERVER_URL)
- .post(requestBody)
- .build();
-
- client.newCall(request).enqueue(new Callback() {
- @Override
- public void onFailure(Call call, IOException e) {
- e.printStackTrace();
- 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()) {
- 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 {
- String errorBody = response.body().string();
- String errorMsg = "Upload failed, Response code: " + response.code();
- Log.e(TAG, errorMsg + ", Body: " + errorBody);
-
- try {
- JSONObject errorJson = new JSONObject(errorBody);
- if (errorJson.has("error")) {
- errorMsg = errorJson.getString("error");
- }
- } catch (JSONException e) {
- // If parsing fails, use the default error message
- }
-
- sendErrorBroadcast(errorMsg);
- }
- jobFinished(params, false);
- }
- });
- }
-
- private void sendSuccessBroadcast(String uploadCode) {
- Intent intent = new Intent(Constant.ACTION_UPLOAD_COMPLETE);
- intent.putExtra(Constant.EXTRA_UPLOAD_CODE, uploadCode);
- sendBroadcast(intent);
- }
-
- private void sendErrorBroadcast(String errorMessage) {
- Intent intent = new Intent(Constant.ACTION_UPLOAD_ERROR);
- intent.putExtra(Constant.EXTRA_UPLOAD_ERROR, errorMessage);
- sendBroadcast(intent);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java b/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java
deleted file mode 100644
index a2ff657..0000000
--- a/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java
+++ /dev/null
@@ -1,195 +0,0 @@
-
-package com.mattintech.simplelogupload;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-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;
-import android.os.Bundle;
-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;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
-import java.io.File;
-import java.util.Arrays;
-import java.util.Comparator;
-
-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) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- resultView = findViewById(R.id.resultView);
- uploadButton = findViewById(R.id.uploadButton);
- progressBar = findViewById(R.id.progressBar);
-
- // Check for All Files Access Permission
- requestNotificationPermission();
- if (!hasAllFilesAccessPermission()) {
- requestAllFilesAccessPermission();
- } else {
- checkForDumpStateAndUpdateUI();
- }
-
- uploadButton.setOnClickListener(v -> {
- 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() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
- != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this,
- new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
- }
- }
- }
-
- private void checkForDumpStateAndUpdateUI() {
- // Find the most recent *DUMPSTATE*.zip file
- File dir = new File(Environment.getExternalStorageDirectory(), "log");
- mostRecentDumpStateFile = findMostRecentFile(dir);
-
- if (mostRecentDumpStateFile != null) {
- resultView.setText("Dumpstate found: " + mostRecentDumpStateFile.getName());
- uploadButton.setVisibility(View.VISIBLE); // Show the button
- } else {
- resultView.setText("No dumpstate file found.");
- uploadButton.setVisibility(View.GONE); // Hide the button if no file found
- }
- }
-
- private File findMostRecentFile(File dir) {
- File[] files = dir.listFiles((d, name) -> name.matches("dumpState_.*\\.zip")); // Regex to match dumpstate
- return (files != null && files.length > 0) ? Arrays.stream(files)
- .max(Comparator.comparingLong(File::lastModified))
- .orElse(null) : null;
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_MANAGE_EXTERNAL_STORAGE) {
- if (hasAllFilesAccessPermission()) {
- checkForDumpStateAndUpdateUI();
- } else {
- Toast.makeText(this, "Permission not granted!", Toast.LENGTH_SHORT).show();
- }
- }
- }
-
- private boolean hasAllFilesAccessPermission() {
- return Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager();
- }
-
- private void requestAllFilesAccessPermission() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- try {
- Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
- intent.setData(Uri.parse("package:" + getPackageName()));
- startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
- } catch (Exception e) {
- // Fallback in case the direct intent fails (should not normally happen)
- Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
- startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
- }
- }
- }
-
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- private void scheduleJob() {
- ComponentName componentName = new ComponentName(this, FileUploadJobService.class);
- JobInfo info = new JobInfo.Builder(123, componentName)
- .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
- .setPersisted(true)
- .build();
-
- JobScheduler scheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
- scheduler.schedule(info);
- }
-
- private void startFileUpload() {
- Intent serviceIntent = new Intent(this, FileUploadForegroundService.class);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- startForegroundService(serviceIntent);
- } else {
- startService(serviceIntent);
- }
- }
-
- private void showUploadSuccessDialog(String uploadCode) {
- UploadResultDialog dialog = new UploadResultDialog(this, uploadCode);
- dialog.show();
- }
-}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/UploadResultDialog.java b/app/src/main/java/com/mattintech/simplelogupload/UploadResultDialog.java
deleted file mode 100644
index dda752a..0000000
--- a/app/src/main/java/com/mattintech/simplelogupload/UploadResultDialog.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.mattintech.simplelogupload;
-
-import android.app.Dialog;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.View;
-import android.view.Window;
-import android.widget.Button;
-import android.widget.TextView;
-import android.widget.Toast;
-
-public class UploadResultDialog extends Dialog {
- private String uploadCode;
-
- public UploadResultDialog(Context context, String uploadCode) {
- super(context);
- this.uploadCode = uploadCode;
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- requestWindowFeature(Window.FEATURE_NO_TITLE);
- setContentView(R.layout.upload_result_dialog);
-
- TextView codeTextView = findViewById(R.id.uploadCode);
- codeTextView.setText(uploadCode);
-
- Button copyButton = findViewById(R.id.copyButton);
- copyButton.setOnClickListener(v -> {
- ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("Upload Code", uploadCode);
- clipboard.setPrimaryClip(clip);
- Toast.makeText(getContext(), "Code copied to clipboard", Toast.LENGTH_SHORT).show();
- });
- }
-}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/constant/AppConstants.java b/app/src/main/java/com/mattintech/simplelogupload/constant/AppConstants.java
new file mode 100644
index 0000000..7f51c77
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/constant/AppConstants.java
@@ -0,0 +1,22 @@
+package com.mattintech.simplelogupload.constant;
+
+/**
+ * General application constants
+ */
+public class AppConstants {
+ public static final String APP_TAG = "SLU::";
+
+ // 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";
+
+ // File patterns
+ public static final String DUMPSTATE_FILE_PATTERN = "dumpState_.*\\.zip";
+
+ // Notification
+ public static final String NOTIFICATION_CHANNEL_ID = "FileUploadChannel";
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/constant/FileConstants.java b/app/src/main/java/com/mattintech/simplelogupload/constant/FileConstants.java
new file mode 100644
index 0000000..b9aad4e
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/constant/FileConstants.java
@@ -0,0 +1,15 @@
+package com.mattintech.simplelogupload.constant;
+
+/**
+ * File-related constants
+ */
+public class FileConstants {
+ // File size limits
+ public static final long MAX_FILE_SIZE = 2L * 1024 * 1024 * 1024; // 2GB
+
+ // File types
+ public static final String ZIP_EXTENSION = ".zip";
+
+ // File paths
+ public static final String LOG_DIRECTORY = "log";
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/constant/NetworkConstants.java b/app/src/main/java/com/mattintech/simplelogupload/constant/NetworkConstants.java
new file mode 100644
index 0000000..7068444
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/constant/NetworkConstants.java
@@ -0,0 +1,24 @@
+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";
+
+ // 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;
+
+ // Timeouts (in seconds)
+ public static final int CONNECTION_TIMEOUT = 60;
+ public static final int WRITE_TIMEOUT = 180;
+ public static final int READ_TIMEOUT = 60;
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/controller/FileController.java b/app/src/main/java/com/mattintech/simplelogupload/controller/FileController.java
new file mode 100644
index 0000000..466a687
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/controller/FileController.java
@@ -0,0 +1,190 @@
+package com.mattintech.simplelogupload.controller;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.mattintech.simplelogupload.constant.AppConstants;
+import com.mattintech.simplelogupload.constant.FileConstants;
+import com.mattintech.simplelogupload.util.FileUtil;
+import com.mattintech.simplelogupload.util.ZipUtil;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+
+/**
+ * Controller for file operations
+ */
+public class FileController {
+ private static final String TAG = AppConstants.APP_TAG + "FileController";
+
+ /**
+ * Interface for file preparation callbacks
+ */
+ public interface FilePrepareCallback {
+ void onSuccess(File preparedFile);
+ void onProgress(int progress);
+ void onError(Exception e);
+ }
+
+ /**
+ * Find the most recent dumpstate file
+ *
+ * @return The most recent dumpstate file or null if none found
+ */
+ public static File getMostRecentDumpStateFile() {
+ return FileUtil.findMostRecentDumpStateFile();
+ }
+
+ /**
+ * Prepare a file for upload - ensures the file is zipped
+ *
+ * @param file The file to prepare
+ * @param callback Callback for progress updates and completion
+ */
+ public static void prepareFileForUpload(File file, FilePrepareCallback callback) {
+ if (file == null || !file.exists()) {
+ callback.onError(new IOException("File does not exist"));
+ return;
+ }
+
+ // Check file size
+ if (!FileUtil.isValidFileSize(file)) {
+ callback.onError(new IOException("File size exceeds limit of " +
+ FileConstants.MAX_FILE_SIZE + " bytes"));
+ return;
+ }
+
+ // If already a zip file, just return it
+ if (FileUtil.isZipFile(file)) {
+ callback.onSuccess(file);
+ return;
+ }
+
+ // Otherwise, compress it
+ try {
+ ZipUtil.zipFile(file, new ZipUtil.ZipProgressListener() {
+ @Override
+ public void onProgress(long bytesProcessed, long totalBytes) {
+ int progress = (int) ((bytesProcessed * 100) / totalBytes);
+ callback.onProgress(progress);
+ }
+
+ @Override
+ public void onComplete(File zipFile) {
+ callback.onSuccess(zipFile);
+ }
+
+ @Override
+ public void onError(Exception e) {
+ callback.onError(e);
+ }
+ });
+ } catch (IOException e) {
+ Log.e(TAG, "Error preparing file for upload", e);
+ callback.onError(e);
+ }
+ }
+
+ /**
+ * Prepare a file for upload asynchronously
+ *
+ * @param file The file to prepare
+ * @return CompletableFuture containing the prepared file
+ */
+ public static CompletableFuture prepareFileForUploadAsync(File file) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ if (file == null || !file.exists()) {
+ throw new IOException("File does not exist");
+ }
+
+ // Check file size
+ if (!FileUtil.isValidFileSize(file)) {
+ throw new IOException("File size exceeds limit of " +
+ FileConstants.MAX_FILE_SIZE + " bytes");
+ }
+
+ // If already a zip file, just return it
+ if (FileUtil.isZipFile(file)) {
+ return file;
+ }
+
+ // Otherwise, compress it
+ return ZipUtil.zipFile(file);
+ } catch (IOException e) {
+ Log.e(TAG, "Error preparing file for upload", e);
+ throw new RuntimeException(e);
+ }
+ }, Executors.newSingleThreadExecutor());
+ }
+
+ /**
+ * Save a content URI to a temporary file
+ *
+ * @param context The context
+ * @param uri The content URI
+ * @param filename The desired filename
+ * @return The saved file
+ * @throws IOException If saving fails
+ */
+ public static File saveContentUriToFile(Context context, Uri uri, String filename) throws IOException {
+ if (context == null || uri == null) {
+ throw new IOException("Invalid context or URI");
+ }
+
+ // Create a temporary file
+ File outputFile = new File(context.getCacheDir(), filename);
+
+ try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
+ FileOutputStream outputStream = new FileOutputStream(outputFile)) {
+
+ if (inputStream == null) {
+ throw new IOException("Could not open input stream for URI: " + uri);
+ }
+
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ }
+
+ return outputFile;
+ }
+
+ /**
+ * Validates if the file size is within the allowable limits
+ *
+ * @param file The file to validate
+ * @return true if the file size is valid, false otherwise
+ */
+ public static boolean isValidFileSize(File file) {
+ return FileUtil.isValidFileSize(file);
+ }
+
+ /**
+ * Save a content URI to a temporary file asynchronously
+ *
+ * @param context The context
+ * @param uri The content URI
+ * @param filename The desired filename
+ * @return CompletableFuture containing the saved file
+ */
+ public static CompletableFuture saveContentUriToFileAsync(
+ Context context, Uri uri, String filename) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return saveContentUriToFile(context, uri, filename);
+ } catch (IOException e) {
+ Log.e(TAG, "Error saving content URI to file", e);
+ throw new RuntimeException(e);
+ }
+ }, Executors.newSingleThreadExecutor());
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/controller/ShareIntentHandler.java b/app/src/main/java/com/mattintech/simplelogupload/controller/ShareIntentHandler.java
new file mode 100644
index 0000000..7c2fa83
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/controller/ShareIntentHandler.java
@@ -0,0 +1,132 @@
+package com.mattintech.simplelogupload.controller;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.mattintech.simplelogupload.constant.AppConstants;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Controller for handling share intents
+ */
+public class ShareIntentHandler {
+ private static final String TAG = AppConstants.APP_TAG + "ShareIntentHandler";
+
+ /**
+ * Check if an intent is a share intent
+ *
+ * @param intent The intent to check
+ * @return true if it's a share intent, false otherwise
+ */
+ public static boolean isShareIntent(Intent intent) {
+ if (intent == null) {
+ return false;
+ }
+
+ String action = intent.getAction();
+ return Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action);
+ }
+
+ /**
+ * Extract shared URIs from an intent
+ *
+ * @param intent The share intent
+ * @return List of URIs or an empty list if none found
+ */
+ public static List extractSharedUris(Intent intent) {
+ List uris = new ArrayList<>();
+
+ if (intent == null) {
+ return uris;
+ }
+
+ String action = intent.getAction();
+ String type = intent.getType();
+
+ if (Intent.ACTION_SEND.equals(action) && type != null) {
+ Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ uris.add(uri);
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) {
+ ArrayList intentUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (intentUris != null) {
+ uris.addAll(intentUris);
+ }
+ }
+
+ return uris;
+ }
+
+ /**
+ * Process a shared URI and prepare it for upload
+ *
+ * @param context The context
+ * @param uri The URI to process
+ * @return CompletableFuture containing the prepared file
+ */
+ public static CompletableFuture processSharedUri(Context context, Uri uri) {
+ if (context == null || uri == null) {
+ CompletableFuture future = new CompletableFuture<>();
+ future.completeExceptionally(new IllegalArgumentException("Invalid context or URI"));
+ return future;
+ }
+
+ String filename = getFileName(context, uri);
+
+ return FileController.saveContentUriToFileAsync(context, uri, filename)
+ .thenCompose(FileController::prepareFileForUploadAsync);
+ }
+
+ /**
+ * Get the filename from a URI
+ *
+ * @param context The context
+ * @param uri The URI
+ * @return The filename or a default name if none can be determined
+ */
+ private static String getFileName(Context context, Uri uri) {
+ String result = null;
+
+ if ("content".equals(uri.getScheme())) {
+ ContentResolver contentResolver = context.getContentResolver();
+ android.database.Cursor cursor = contentResolver.query(uri, null, null, null, null);
+
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ int nameIndex = cursor.getColumnIndex("_display_name");
+ if (nameIndex != -1) {
+ result = cursor.getString(nameIndex);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ if (result == null) {
+ // If no name found, use the last path segment with an extension based on mime type
+ result = uri.getLastPathSegment();
+
+ // If still null, generate a default name
+ if (result == null) {
+ String mimeType = context.getContentResolver().getType(uri);
+ String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ result = "shared_file" + (extension != null ? "." + extension : "");
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/controller/UploadController.java b/app/src/main/java/com/mattintech/simplelogupload/controller/UploadController.java
new file mode 100644
index 0000000..420518e
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/controller/UploadController.java
@@ -0,0 +1,190 @@
+package com.mattintech.simplelogupload.controller;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.mattintech.simplelogupload.constant.AppConstants;
+import com.mattintech.simplelogupload.constant.NetworkConstants;
+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 java.io.File;
+import java.io.IOException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Controller for handling file uploads
+ */
+public class UploadController {
+ private static final String TAG = AppConstants.APP_TAG + "UploadController";
+
+ private final Context context;
+ private final UploadHistoryRepository repository;
+
+ /**
+ * Constructor
+ *
+ * @param context The application context
+ */
+ public UploadController(Context context) {
+ this.context = context.getApplicationContext();
+ this.repository = new UploadHistoryRepository((Application) this.context);
+ }
+
+ /**
+ * Start an upload using the foreground service
+ *
+ * @param file The file to upload
+ */
+ public void startUploadService(File file) {
+ if (file == null || !file.exists()) {
+ Log.e(TAG, "Cannot start upload service with null or non-existent file");
+ return;
+ }
+
+ Intent serviceIntent = new Intent(context, FileUploadForegroundService.class);
+ serviceIntent.putExtra(FileUploadForegroundService.EXTRA_FILE_PATH, file.getAbsolutePath());
+ serviceIntent.putExtra(FileUploadForegroundService.EXTRA_MAX_DOWNLOADS, NetworkConstants.DEFAULT_MAX_DOWNLOADS);
+ serviceIntent.putExtra(FileUploadForegroundService.EXTRA_EXPIRY_HOURS, NetworkConstants.DEFAULT_EXPIRY_HOURS);
+
+ context.startService(serviceIntent);
+ }
+
+ /**
+ * Upload a file directly and return a future with the result
+ *
+ * @param file The file to upload
+ * @param maxDownloads Maximum number of downloads
+ * @param expiryHours Expiry time in hours
+ * @return CompletableFuture containing the upload result
+ */
+ public CompletableFuture uploadFile(
+ File file, int maxDownloads, int expiryHours) {
+ CompletableFuture future = new CompletableFuture<>();
+
+ if (file == null || !file.exists()) {
+ future.complete(new UploadResult("File does not exist"));
+ return future;
+ }
+
+ // Create OkHttpClient with timeout configuration
+ OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(NetworkConstants.CONNECTION_TIMEOUT, TimeUnit.SECONDS)
+ .writeTimeout(NetworkConstants.WRITE_TIMEOUT, TimeUnit.SECONDS)
+ .readTimeout(NetworkConstants.READ_TIMEOUT, TimeUnit.SECONDS)
+ .retryOnConnectionFailure(true)
+ .build();
+
+ Log.d(TAG, "Uploading file: " + file.getName() + ", Size: " + file.length());
+
+ RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream"));
+ MultipartBody requestBody = new MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart(NetworkConstants.PARAM_FILE, file.getName(), fileBody)
+ .addFormDataPart(NetworkConstants.PARAM_MAX_DOWNLOADS, String.valueOf(maxDownloads))
+ .addFormDataPart(NetworkConstants.PARAM_EXPIRY_HOURS, String.valueOf(expiryHours))
+ .build();
+
+ Request request = new Request.Builder()
+ .url(NetworkConstants.SERVER_URL)
+ .post(requestBody)
+ .build();
+
+ client.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ Log.e(TAG, "Upload failed", e);
+ future.complete(new UploadResult("Upload failed: " + e.getMessage()));
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ if (response.isSuccessful()) {
+ 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);
+
+ // Create the result
+ UploadResult result = new UploadResult(
+ uploadCode, file.getName(), maxDownloads, expiryHours);
+
+ // Save to history
+ saveToHistory(result);
+
+ future.complete(result);
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to parse server response", e);
+ future.complete(new UploadResult("Failed to parse server response"));
+ }
+ } else {
+ 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
+ }
+
+ future.complete(new UploadResult(errorMsg));
+ }
+ }
+ });
+
+ return future;
+ }
+
+ /**
+ * Save an upload result to the history database
+ *
+ * @param result The upload result
+ */
+ private void saveToHistory(UploadResult result) {
+ if (result == null || !result.isSuccess()) {
+ return;
+ }
+
+ UploadHistory historyEntry = new UploadHistory(
+ result.getFileName(),
+ System.currentTimeMillis(),
+ result.getCode(),
+ result.getMaxDownloads(),
+ result.getExpiryHours()
+ );
+
+ repository.insert(historyEntry)
+ .thenAccept(id -> Log.d(TAG, "Saved to history with ID: " + id))
+ .exceptionally(ex -> {
+ Log.e(TAG, "Error saving to history", ex);
+ return null;
+ });
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/db/AppDatabase.java b/app/src/main/java/com/mattintech/simplelogupload/db/AppDatabase.java
new file mode 100644
index 0000000..7e147f3
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/db/AppDatabase.java
@@ -0,0 +1,68 @@
+package com.mattintech.simplelogupload.db;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import com.mattintech.simplelogupload.model.UploadHistory;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Room database for the application
+ */
+@Database(entities = {UploadHistory.class}, version = 1, exportSchema = false)
+public abstract class AppDatabase extends RoomDatabase {
+
+ public abstract UploadHistoryDao uploadHistoryDao();
+
+ private static volatile AppDatabase INSTANCE;
+ private static final int NUMBER_OF_THREADS = 4;
+
+ /**
+ * Executor service for database operations
+ */
+ public static final ExecutorService databaseWriteExecutor =
+ Executors.newFixedThreadPool(NUMBER_OF_THREADS);
+
+ /**
+ * Get the database instance
+ *
+ * @param context The application context
+ * @return The database instance
+ */
+ public static AppDatabase getDatabase(final Context context) {
+ if (INSTANCE == null) {
+ synchronized (AppDatabase.class) {
+ if (INSTANCE == null) {
+ INSTANCE = Room.databaseBuilder(
+ context.getApplicationContext(),
+ AppDatabase.class,
+ "app_database")
+ .fallbackToDestructiveMigration()
+ .addCallback(sRoomDatabaseCallback)
+ .build();
+ }
+ }
+ }
+ return INSTANCE;
+ }
+
+ /**
+ * Callback for database creation/opening
+ */
+ private static final RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() {
+ @Override
+ public void onCreate(@NonNull SupportSQLiteDatabase db) {
+ super.onCreate(db);
+
+ // If you want to populate the database with initial data,
+ // you can do it here using databaseWriteExecutor
+ }
+ };
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryDao.java b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryDao.java
new file mode 100644
index 0000000..f80006a
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryDao.java
@@ -0,0 +1,65 @@
+package com.mattintech.simplelogupload.db;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+
+import com.mattintech.simplelogupload.model.UploadHistory;
+
+import java.util.List;
+
+/**
+ * Data Access Object for upload history
+ */
+@Dao
+public interface UploadHistoryDao {
+ /**
+ * Insert a new history entry
+ *
+ * @param entry The entry to insert
+ * @return The row ID of the inserted entry
+ */
+ @Insert
+ long insert(UploadHistory entry);
+
+ /**
+ * Get all history entries ordered by upload time (most recent first)
+ *
+ * @return LiveData list of history entries
+ */
+ @Query("SELECT * FROM upload_history ORDER BY upload_time DESC")
+ LiveData> getAllHistory();
+
+ /**
+ * Get a history entry by ID
+ *
+ * @param id The ID of the entry
+ * @return The history entry
+ */
+ @Query("SELECT * FROM upload_history WHERE id = :id")
+ UploadHistory getEntryById(int id);
+
+ /**
+ * Delete a history entry
+ *
+ * @param entry The entry to delete
+ */
+ @Delete
+ void delete(UploadHistory entry);
+
+ /**
+ * Delete a history entry by ID
+ *
+ * @param id The ID of the entry to delete
+ */
+ @Query("DELETE FROM upload_history WHERE id = :id")
+ void deleteById(int id);
+
+ /**
+ * Delete all history entries
+ */
+ @Query("DELETE FROM upload_history")
+ void deleteAll();
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryRepository.java b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryRepository.java
new file mode 100644
index 0000000..9d02fc8
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/db/UploadHistoryRepository.java
@@ -0,0 +1,90 @@
+package com.mattintech.simplelogupload.db;
+
+import android.app.Application;
+
+import androidx.lifecycle.LiveData;
+
+import com.mattintech.simplelogupload.model.UploadHistory;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Repository for handling upload history data operations
+ */
+public class UploadHistoryRepository {
+ private final UploadHistoryDao mUploadHistoryDao;
+ private final LiveData> mAllHistory;
+
+ /**
+ * Constructor
+ *
+ * @param application The application
+ */
+ public UploadHistoryRepository(Application application) {
+ AppDatabase db = AppDatabase.getDatabase(application);
+ mUploadHistoryDao = db.uploadHistoryDao();
+ mAllHistory = mUploadHistoryDao.getAllHistory();
+ }
+
+ /**
+ * Get all history entries
+ *
+ * @return LiveData list of all history entries
+ */
+ public LiveData> getAllHistory() {
+ return mAllHistory;
+ }
+
+ /**
+ * Insert a new history entry asynchronously
+ *
+ * @param entry The entry to insert
+ * @return CompletableFuture containing the inserted row ID
+ */
+ public CompletableFuture insert(final UploadHistory entry) {
+ return CompletableFuture.supplyAsync(() ->
+ mUploadHistoryDao.insert(entry),
+ AppDatabase.databaseWriteExecutor);
+ }
+
+ /**
+ * Delete a history entry asynchronously
+ *
+ * @param entry The entry to delete
+ */
+ public void delete(final UploadHistory entry) {
+ AppDatabase.databaseWriteExecutor.execute(() ->
+ mUploadHistoryDao.delete(entry));
+ }
+
+ /**
+ * Delete a history entry by ID asynchronously
+ *
+ * @param id The ID of the entry to delete
+ */
+ public void deleteById(final int id) {
+ AppDatabase.databaseWriteExecutor.execute(() ->
+ mUploadHistoryDao.deleteById(id));
+ }
+
+ /**
+ * Delete all history entries asynchronously
+ */
+ public void deleteAll() {
+ AppDatabase.databaseWriteExecutor.execute(
+ mUploadHistoryDao::deleteAll);
+ }
+
+ /**
+ * Get a history entry by ID asynchronously
+ *
+ * @param id The ID of the entry
+ * @return CompletableFuture containing the history entry
+ */
+ public CompletableFuture getEntryById(final int id) {
+ return CompletableFuture.supplyAsync(() ->
+ mUploadHistoryDao.getEntryById(id),
+ AppDatabase.databaseWriteExecutor);
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/model/UploadHistory.java b/app/src/main/java/com/mattintech/simplelogupload/model/UploadHistory.java
new file mode 100644
index 0000000..1604c6d
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/model/UploadHistory.java
@@ -0,0 +1,109 @@
+package com.mattintech.simplelogupload.model;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+/**
+ * Entity class for upload history
+ */
+@Entity(tableName = "upload_history")
+public class UploadHistory {
+ @PrimaryKey(autoGenerate = true)
+ private int id;
+
+ @ColumnInfo(name = "file_name")
+ private String fileName;
+
+ @ColumnInfo(name = "upload_time")
+ private long uploadTime;
+
+ @ColumnInfo(name = "upload_code")
+ private String uploadCode;
+
+ @ColumnInfo(name = "max_downloads")
+ private int maxDownloads;
+
+ @ColumnInfo(name = "expiry_hours")
+ private int expiryHours;
+
+ /**
+ * Constructor
+ *
+ * @param fileName The name of the uploaded file
+ * @param uploadTime The time of upload (in milliseconds)
+ * @param uploadCode The server-provided upload code
+ * @param maxDownloads The maximum number of downloads allowed
+ * @param expiryHours The expiry time in hours
+ */
+ public UploadHistory(String fileName, long uploadTime, String uploadCode, int maxDownloads, int expiryHours) {
+ this.fileName = fileName;
+ this.uploadTime = uploadTime;
+ this.uploadCode = uploadCode;
+ this.maxDownloads = maxDownloads;
+ this.expiryHours = expiryHours;
+ }
+
+ // Getters and setters
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public long getUploadTime() {
+ return uploadTime;
+ }
+
+ public void setUploadTime(long uploadTime) {
+ this.uploadTime = uploadTime;
+ }
+
+ public String getUploadCode() {
+ return uploadCode;
+ }
+
+ public void setUploadCode(String uploadCode) {
+ this.uploadCode = uploadCode;
+ }
+
+ public int getMaxDownloads() {
+ return maxDownloads;
+ }
+
+ public void setMaxDownloads(int maxDownloads) {
+ this.maxDownloads = maxDownloads;
+ }
+
+ public int getExpiryHours() {
+ return expiryHours;
+ }
+
+ public void setExpiryHours(int expiryHours) {
+ this.expiryHours = expiryHours;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "UploadHistory{" +
+ "id=" + id +
+ ", fileName='" + fileName + '\'' +
+ ", uploadTime=" + uploadTime +
+ ", uploadCode='" + uploadCode + '\'' +
+ ", maxDownloads=" + maxDownloads +
+ ", expiryHours=" + expiryHours +
+ '}';
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java b/app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java
index cb58801..b5c82fb 100644
--- a/app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java
+++ b/app/src/main/java/com/mattintech/simplelogupload/model/UploadResult.java
@@ -1,17 +1,45 @@
package com.mattintech.simplelogupload.model;
+/**
+ * Model class for upload results
+ */
public class UploadResult {
- private String code;
- private String error;
-
- public UploadResult(String code) {
+ private final String code;
+ private final String error;
+ private final boolean success;
+ private final String fileName;
+ private final int maxDownloads;
+ private final int expiryHours;
+
+ /**
+ * Constructor for successful uploads
+ *
+ * @param code The server-provided upload code
+ * @param fileName The name of the uploaded file
+ * @param maxDownloads The maximum number of downloads
+ * @param expiryHours The expiry time in hours
+ */
+ public UploadResult(String code, String fileName, int maxDownloads, int expiryHours) {
this.code = code;
this.error = null;
+ this.success = true;
+ this.fileName = fileName;
+ this.maxDownloads = maxDownloads;
+ this.expiryHours = expiryHours;
}
- public UploadResult(String error, boolean isError) {
+ /**
+ * Constructor for failed uploads
+ *
+ * @param error The error message
+ */
+ public UploadResult(String error) {
this.code = null;
this.error = error;
+ this.success = false;
+ this.fileName = null;
+ this.maxDownloads = 0;
+ this.expiryHours = 0;
}
public String getCode() {
@@ -23,6 +51,18 @@ public class UploadResult {
}
public boolean isSuccess() {
- return code != null && !code.isEmpty();
+ return success;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public int getMaxDownloads() {
+ return maxDownloads;
+ }
+
+ public int getExpiryHours() {
+ return expiryHours;
}
}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.java b/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.java
new file mode 100644
index 0000000..5fe1145
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/service/FileUploadForegroundService.java
@@ -0,0 +1,241 @@
+package com.mattintech.simplelogupload.service;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import com.mattintech.simplelogupload.R;
+import com.mattintech.simplelogupload.constant.AppConstants;
+import com.mattintech.simplelogupload.constant.FileConstants;
+import com.mattintech.simplelogupload.constant.NetworkConstants;
+import com.mattintech.simplelogupload.controller.FileController;
+import com.mattintech.simplelogupload.controller.UploadController;
+import com.mattintech.simplelogupload.model.UploadResult;
+import com.mattintech.simplelogupload.view.MainActivity;
+
+import java.io.File;
+
+/**
+ * Foreground service for file uploads
+ */
+public class FileUploadForegroundService extends Service {
+ private static final String TAG = AppConstants.APP_TAG + "UploadService";
+ private static final int NOTIFICATION_ID = 1;
+
+ // Intent extras
+ public static final String EXTRA_FILE_PATH = "extra_file_path";
+ public static final String EXTRA_MAX_DOWNLOADS = "extra_max_downloads";
+ public static final String EXTRA_EXPIRY_HOURS = "extra_expiry_hours";
+
+ private UploadController uploadController;
+ private NotificationManager notificationManager;
+ private NotificationCompat.Builder notificationBuilder;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ uploadController = new UploadController(this);
+ notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ createNotificationChannel();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.d(TAG, "Foreground Service started.");
+
+ // Get file path from intent
+ String filePath = intent.getStringExtra(EXTRA_FILE_PATH);
+ int maxDownloads = intent.getIntExtra(EXTRA_MAX_DOWNLOADS, NetworkConstants.DEFAULT_MAX_DOWNLOADS);
+ int expiryHours = intent.getIntExtra(EXTRA_EXPIRY_HOURS, NetworkConstants.DEFAULT_EXPIRY_HOURS);
+
+ if (filePath == null) {
+ Log.e(TAG, "No file path provided");
+ // Try to find the most recent dump state file
+ File mostRecentFile = FileController.getMostRecentDumpStateFile();
+ if (mostRecentFile != null) {
+ filePath = mostRecentFile.getAbsolutePath();
+ } else {
+ Log.e(TAG, "No file to upload");
+ sendErrorBroadcast("No file to upload");
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ }
+
+ File file = new File(filePath);
+ if (!file.exists()) {
+ Log.e(TAG, "File not found: " + filePath);
+ sendErrorBroadcast("File not found");
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ // Create a notification and run the service in the foreground
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // For Android 14+ (API 34+)
+ startForeground(NOTIFICATION_ID, createInitialNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
+ } else {
+ // For Android 13 and below
+ startForeground(NOTIFICATION_ID, createInitialNotification());
+ }
+
+ // Prepare file for upload
+ FileController.prepareFileForUpload(file, new FileController.FilePrepareCallback() {
+ @Override
+ public void onSuccess(File preparedFile) {
+ Log.d(TAG, "File prepared for upload: " + preparedFile.getName());
+ uploadFile(preparedFile, maxDownloads, expiryHours);
+ }
+
+ @Override
+ public void onProgress(int progress) {
+ updateNotification("Preparing file: " + progress + "%");
+ }
+
+ @Override
+ public void onError(Exception e) {
+ Log.e(TAG, "Error preparing file", e);
+ sendErrorBroadcast("Error preparing file: " + e.getMessage());
+ stopSelf();
+ }
+ });
+
+ return START_NOT_STICKY;
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ /**
+ * Upload a file using the UploadController
+ *
+ * @param file The file to upload
+ * @param maxDownloads Maximum number of downloads
+ * @param expiryHours Expiry time in hours
+ */
+ private void uploadFile(File file, int maxDownloads, int expiryHours) {
+ // Validate file size before upload
+ if (!FileController.isValidFileSize(file)) {
+ Log.e(TAG, "File too large: " + file.length() + " bytes (max: " + FileConstants.MAX_FILE_SIZE + " bytes)");
+ sendErrorBroadcast("File size exceeds the maximum allowed size");
+ stopSelf();
+ return;
+ }
+
+ // Update notification to show uploading
+ updateNotification("Uploading " + file.getName() + "...");
+
+ // Upload the file
+ uploadController.uploadFile(file, maxDownloads, expiryHours)
+ .thenAccept(result -> {
+ if (result.isSuccess()) {
+ Log.d(TAG, "Upload successful! Code: " + result.getCode());
+ sendSuccessBroadcast(result.getCode());
+ } else {
+ Log.e(TAG, "Upload failed: " + result.getError());
+ sendErrorBroadcast(result.getError());
+ }
+ stopSelf();
+ })
+ .exceptionally(ex -> {
+ Log.e(TAG, "Exception during upload", ex);
+ sendErrorBroadcast("Error during upload: " + ex.getMessage());
+ stopSelf();
+ return null;
+ });
+ }
+
+ /**
+ * Create the initial notification
+ *
+ * @return The notification
+ */
+ private Notification createInitialNotification() {
+ Intent notificationIntent = new Intent(this, MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
+ notificationIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ notificationBuilder = new NotificationCompat.Builder(this, AppConstants.NOTIFICATION_CHANNEL_ID)
+ .setContentTitle("File Upload")
+ .setContentText("Preparing file...")
+ .setSmallIcon(R.drawable.logupload)
+ .setContentIntent(pendingIntent)
+ .setProgress(100, 0, true)
+ .setOngoing(true);
+
+ return notificationBuilder.build();
+ }
+
+ /**
+ * Update the notification with a new status message
+ *
+ * @param status The status message
+ */
+ private void updateNotification(String status) {
+ if (notificationBuilder != null) {
+ notificationBuilder.setContentText(status);
+ notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
+ }
+ }
+
+ /**
+ * Create the notification channel (required for Android 8.0+)
+ */
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ CharSequence name = "File Upload Channel";
+ String description = "Notification channel for file upload";
+ int importance = NotificationManager.IMPORTANCE_DEFAULT;
+ NotificationChannel channel = new NotificationChannel(
+ AppConstants.NOTIFICATION_CHANNEL_ID, name, importance);
+ channel.setDescription(description);
+
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ /**
+ * Send a broadcast with the upload success result
+ *
+ * @param uploadCode The upload code from the server
+ */
+ private void sendSuccessBroadcast(String uploadCode) {
+ Log.d(TAG, "Sending success broadcast with code: " + uploadCode);
+ Intent intent = new Intent(AppConstants.ACTION_UPLOAD_COMPLETE);
+ intent.putExtra(AppConstants.EXTRA_UPLOAD_CODE, uploadCode);
+
+ // Send both a regular broadcast and a local broadcast to ensure delivery
+ sendBroadcast(intent);
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ }
+
+ /**
+ * Send a broadcast with the upload error result
+ *
+ * @param errorMessage The error message
+ */
+ private void sendErrorBroadcast(String errorMessage) {
+ Log.e(TAG, "Sending error broadcast: " + errorMessage);
+ Intent intent = new Intent(AppConstants.ACTION_UPLOAD_ERROR);
+ intent.putExtra(AppConstants.EXTRA_UPLOAD_ERROR, errorMessage);
+
+ // Send both a regular broadcast and a local broadcast to ensure delivery
+ sendBroadcast(intent);
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/util/FileUtil.java b/app/src/main/java/com/mattintech/simplelogupload/util/FileUtil.java
new file mode 100644
index 0000000..c8fba47
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/util/FileUtil.java
@@ -0,0 +1,102 @@
+package com.mattintech.simplelogupload.util;
+
+import android.os.Environment;
+import android.util.Log;
+
+import com.mattintech.simplelogupload.constant.AppConstants;
+import com.mattintech.simplelogupload.constant.FileConstants;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * Utility class for file operations
+ */
+public class FileUtil {
+ private static final String TAG = AppConstants.APP_TAG + "FileUtil";
+
+ /**
+ * Finds the most recent dumpstate file in the logs directory
+ * @return The most recent dumpstate file or null if none found
+ */
+ public static File findMostRecentDumpStateFile() {
+ File dir = new File(Environment.getExternalStorageDirectory(), FileConstants.LOG_DIRECTORY);
+ return findMostRecentFileByPattern(dir, AppConstants.DUMPSTATE_FILE_PATTERN);
+ }
+
+ /**
+ * Finds the most recent file in a directory matching the given pattern
+ *
+ * @param dir The directory to search
+ * @param pattern The regex pattern to match filenames
+ * @return The most recent matching file or null if none found
+ */
+ public static File findMostRecentFileByPattern(File dir, String pattern) {
+ if (dir == null || !dir.exists() || !dir.isDirectory()) {
+ Log.e(TAG, "Invalid directory: " + (dir != null ? dir.getAbsolutePath() : "null"));
+ return null;
+ }
+
+ File[] files = dir.listFiles((d, name) -> name.matches(pattern));
+
+ if (files == null || files.length == 0) {
+ Log.d(TAG, "No files matching pattern '" + pattern + "' found in " + dir.getAbsolutePath());
+ return null;
+ }
+
+ return Arrays.stream(files)
+ .max(Comparator.comparingLong(File::lastModified))
+ .orElse(null);
+ }
+
+ /**
+ * Checks if a file needs to be zipped
+ * @param file The file to check
+ * @return true if the file is already a zip, false otherwise
+ */
+ public static boolean isZipFile(File file) {
+ if (file == null) {
+ return false;
+ }
+ return file.getName().toLowerCase().endsWith(FileConstants.ZIP_EXTENSION);
+ }
+
+ /**
+ * Validates if the file size is within the allowable limits
+ * @param file The file to validate
+ * @return true if the file size is valid, false otherwise
+ */
+ public static boolean isValidFileSize(File file) {
+ if (file == null || !file.exists()) {
+ return false;
+ }
+
+ boolean isValid = file.length() <= FileConstants.MAX_FILE_SIZE;
+
+ if (!isValid) {
+ Log.w(TAG, "File size check failed: " + file.length() + " bytes exceeds limit of "
+ + FileConstants.MAX_FILE_SIZE + " bytes");
+ }
+
+ return isValid;
+ }
+
+ /**
+ * Gets the file extension from a filename
+ * @param filename The filename
+ * @return The file extension or an empty string if none
+ */
+ public static String getFileExtension(String filename) {
+ if (filename == null || filename.isEmpty()) {
+ return "";
+ }
+
+ int lastDotIndex = filename.lastIndexOf('.');
+ if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
+ return "";
+ }
+
+ return filename.substring(lastDotIndex);
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/util/PermissionUtil.java b/app/src/main/java/com/mattintech/simplelogupload/util/PermissionUtil.java
new file mode 100644
index 0000000..bae034a
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/util/PermissionUtil.java
@@ -0,0 +1,96 @@
+package com.mattintech.simplelogupload.util;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.mattintech.simplelogupload.constant.AppConstants;
+
+/**
+ * Utility class for handling permissions
+ */
+public class PermissionUtil {
+ private static final String TAG = AppConstants.APP_TAG + "PermissionUtil";
+
+ public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
+ public static final int REQUEST_NOTIFICATION_PERMISSION = 102;
+
+ /**
+ * Checks if the app has notification permission
+ *
+ * @param context The context
+ * @return true if the app has notification permission, false otherwise
+ */
+ public static boolean hasNotificationPermission(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ // Permission is implicitly granted on older Android versions
+ return true;
+ }
+
+ /**
+ * Requests notification permission
+ *
+ * @param activity The activity
+ */
+ public static void requestNotificationPermission(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (!hasNotificationPermission(activity)) {
+ Log.d(TAG, "Requesting notification permission");
+ ActivityCompat.requestPermissions(
+ activity,
+ new String[]{Manifest.permission.POST_NOTIFICATIONS},
+ REQUEST_NOTIFICATION_PERMISSION
+ );
+ }
+ }
+ }
+
+ /**
+ * Checks if the app has all files access permission
+ *
+ * @return true if the app has all files access permission, false otherwise
+ */
+ public static boolean hasAllFilesAccessPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return Environment.isExternalStorageManager();
+ }
+ // Permission is handled differently on older Android versions
+ return true;
+ }
+
+ /**
+ * Requests all files access permission
+ *
+ * @param activity The activity
+ */
+ public static void requestAllFilesAccessPermission(Activity activity) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ if (!hasAllFilesAccessPermission()) {
+ Log.d(TAG, "Requesting all files access permission");
+ try {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
+ intent.setData(Uri.parse("package:" + activity.getPackageName()));
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
+ } catch (Exception e) {
+ Log.e(TAG, "Error requesting all files access permission", e);
+ // Fallback in case the direct intent fails
+ Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
+ activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/util/ZipUtil.java b/app/src/main/java/com/mattintech/simplelogupload/util/ZipUtil.java
new file mode 100644
index 0000000..b138581
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/util/ZipUtil.java
@@ -0,0 +1,124 @@
+package com.mattintech.simplelogupload.util;
+
+import android.util.Log;
+
+import com.mattintech.simplelogupload.constant.AppConstants;
+import com.mattintech.simplelogupload.constant.FileConstants;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Utility class for zipping files
+ */
+public class ZipUtil {
+ private static final String TAG = AppConstants.APP_TAG + "ZipUtil";
+ private static final int BUFFER_SIZE = 8192; // 8KB buffer
+
+ /**
+ * Interface for tracking zip progress
+ */
+ public interface ZipProgressListener {
+ void onProgress(long bytesProcessed, long totalBytes);
+ void onComplete(File zipFile);
+ void onError(Exception e);
+ }
+
+ /**
+ * Compresses a file into a zip archive
+ *
+ * @param sourceFile The file to compress
+ * @return The compressed zip file
+ * @throws IOException If compression fails
+ */
+ public static File zipFile(File sourceFile) throws IOException {
+ return zipFile(sourceFile, null);
+ }
+
+ /**
+ * Compresses a file into a zip archive with progress tracking
+ *
+ * @param sourceFile The file to compress
+ * @param listener A listener for tracking progress (can be null)
+ * @return The compressed zip file
+ * @throws IOException If compression fails
+ */
+ public static File zipFile(File sourceFile, ZipProgressListener listener) throws IOException {
+ if (sourceFile == null || !sourceFile.exists()) {
+ String msg = "Source file does not exist: " + (sourceFile != null ? sourceFile.getAbsolutePath() : "null");
+ Log.e(TAG, msg);
+ throw new IOException(msg);
+ }
+
+ // If already a zip file, just return it
+ if (FileUtil.isZipFile(sourceFile)) {
+ Log.d(TAG, "File is already a zip file: " + sourceFile.getName());
+ if (listener != null) {
+ listener.onComplete(sourceFile);
+ }
+ return sourceFile;
+ }
+
+ // Create a zip file in the same directory
+ String zipFileName = sourceFile.getName() + FileConstants.ZIP_EXTENSION;
+ File zipFile = new File(sourceFile.getParentFile(), zipFileName);
+
+ Log.d(TAG, "Creating zip file: " + zipFile.getAbsolutePath());
+
+ try (FileOutputStream fos = new FileOutputStream(zipFile);
+ BufferedOutputStream bos = new BufferedOutputStream(fos);
+ ZipOutputStream zos = new ZipOutputStream(bos);
+ FileInputStream fis = new FileInputStream(sourceFile);
+ BufferedInputStream bis = new BufferedInputStream(fis)) {
+
+ ZipEntry zipEntry = new ZipEntry(sourceFile.getName());
+ zipEntry.setSize(sourceFile.length());
+ zos.putNextEntry(zipEntry);
+
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int len;
+ long bytesProcessed = 0;
+ long totalBytes = sourceFile.length();
+
+ while ((len = bis.read(buffer)) > 0) {
+ zos.write(buffer, 0, len);
+ bytesProcessed += len;
+
+ if (listener != null) {
+ listener.onProgress(bytesProcessed, totalBytes);
+ }
+ }
+
+ zos.closeEntry();
+
+ if (listener != null) {
+ listener.onComplete(zipFile);
+ }
+
+ Log.d(TAG, "Successfully created zip file: " + zipFile.getName());
+ return zipFile;
+ } catch (IOException e) {
+ Log.e(TAG, "Error creating zip file", e);
+
+ // Clean up the partial zip file if it exists
+ if (zipFile.exists()) {
+ boolean deleted = zipFile.delete();
+ if (!deleted) {
+ Log.w(TAG, "Failed to delete partial zip file: " + zipFile.getAbsolutePath());
+ }
+ }
+
+ if (listener != null) {
+ listener.onError(e);
+ }
+
+ throw e;
+ }
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java b/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java
new file mode 100644
index 0000000..f2c91ed
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java
@@ -0,0 +1,312 @@
+package com.mattintech.simplelogupload.view;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.mattintech.simplelogupload.R;
+import com.mattintech.simplelogupload.constant.AppConstants;
+import com.mattintech.simplelogupload.controller.FileController;
+import com.mattintech.simplelogupload.controller.ShareIntentHandler;
+import com.mattintech.simplelogupload.controller.UploadController;
+import com.mattintech.simplelogupload.service.FileUploadForegroundService;
+import com.mattintech.simplelogupload.util.PermissionUtil;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Main activity for the application
+ */
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = AppConstants.APP_TAG + "MainActivity";
+
+ private TextView resultView;
+ private Button uploadButton;
+ private ProgressBar progressBar;
+ private File selectedFile;
+
+ private UploadController uploadController;
+
+ // BroadcastReceiver to listen for upload results
+ private BroadcastReceiver uploadReceiver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ uploadController = new UploadController(this);
+
+ // Initialize UI elements
+ resultView = findViewById(R.id.resultView);
+ uploadButton = findViewById(R.id.uploadButton);
+ progressBar = findViewById(R.id.progressBar);
+
+ // Set up button click listeners
+ uploadButton.setOnClickListener(v -> startUpload());
+
+ // Check for necessary permissions
+ checkAndRequestPermissions();
+
+ // Initialize broadcast receiver
+ setupBroadcastReceiver();
+
+ // Handle intent (e.g., from share)
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ handleIntent(intent);
+ }
+
+ /**
+ * Handle the intent, particularly for share intents
+ *
+ * @param intent The intent to handle
+ */
+ private void handleIntent(Intent intent) {
+ if (intent == null) {
+ return;
+ }
+
+ // Check if this is a share intent
+ if (ShareIntentHandler.isShareIntent(intent)) {
+ handleShareIntent(intent);
+ }
+ }
+
+ /**
+ * Handle a share intent
+ *
+ * @param intent The share intent
+ */
+ private void handleShareIntent(Intent intent) {
+ List sharedUris = ShareIntentHandler.extractSharedUris(intent);
+
+ if (sharedUris.isEmpty()) {
+ Toast.makeText(this, "No files shared", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // For now, just handle the first URI
+ Uri sharedUri = sharedUris.get(0);
+
+ resultView.setText("Processing shared file...");
+ progressBar.setVisibility(View.VISIBLE);
+ uploadButton.setEnabled(false);
+
+ // Process the shared URI
+ ShareIntentHandler.processSharedUri(this, sharedUri)
+ .thenAccept(file -> {
+ // Save the file for upload
+ selectedFile = file;
+ runOnUiThread(() -> {
+ resultView.setText("File ready: " + file.getName());
+ uploadButton.setEnabled(true);
+ uploadButton.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.GONE);
+ });
+ })
+ .exceptionally(ex -> {
+ Log.e(TAG, "Error processing shared file", ex);
+ runOnUiThread(() -> {
+ Toast.makeText(MainActivity.this,
+ "Error processing file: " + ex.getMessage(),
+ Toast.LENGTH_LONG).show();
+ resultView.setText("Error processing file");
+ progressBar.setVisibility(View.GONE);
+ });
+ return null;
+ });
+ }
+
+ /**
+ * Set up the broadcast receiver for upload results
+ */
+ private void setupBroadcastReceiver() {
+ uploadReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Received broadcast: " + intent.getAction());
+ progressBar.setVisibility(View.GONE);
+ uploadButton.setEnabled(true);
+
+ if (intent.getAction().equals(AppConstants.ACTION_UPLOAD_COMPLETE)) {
+ String uploadCode = intent.getStringExtra(AppConstants.EXTRA_UPLOAD_CODE);
+ Log.d(TAG, "Upload complete with code: " + uploadCode);
+ showUploadSuccessDialog(uploadCode);
+ } else if (intent.getAction().equals(AppConstants.ACTION_UPLOAD_ERROR)) {
+ String errorMessage = intent.getStringExtra(AppConstants.EXTRA_UPLOAD_ERROR);
+ Log.e(TAG, "Upload error: " + errorMessage);
+ Toast.makeText(MainActivity.this, "Upload Error: " + errorMessage, Toast.LENGTH_LONG).show();
+ resultView.setText("Upload failed: " + errorMessage);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Check for file if we have permissions
+ if (PermissionUtil.hasAllFilesAccessPermission()) {
+ checkForDumpStateAndUpdateUI();
+ }
+
+ // Register the broadcast receiver
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(AppConstants.ACTION_UPLOAD_COMPLETE);
+ filter.addAction(AppConstants.ACTION_UPLOAD_ERROR);
+
+ // Register with both regular and local broadcast managers to ensure delivery
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(uploadReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
+ } else {
+ registerReceiver(uploadReceiver, filter);
+ }
+
+ // Also register with LocalBroadcastManager for reliability
+ LocalBroadcastManager.getInstance(this).registerReceiver(uploadReceiver, filter);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ // Unregister the broadcast receiver
+ try {
+ unregisterReceiver(uploadReceiver);
+ } catch (IllegalArgumentException e) {
+ // Receiver not registered, ignore
+ }
+
+ // Also unregister from LocalBroadcastManager
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(uploadReceiver);
+ }
+
+ /**
+ * Check for and request necessary permissions
+ */
+ private void checkAndRequestPermissions() {
+ // Request notification permission
+ PermissionUtil.requestNotificationPermission(this);
+
+ // Check and request all files access permission
+ if (!PermissionUtil.hasAllFilesAccessPermission()) {
+ PermissionUtil.requestAllFilesAccessPermission(this);
+ }
+ }
+
+ /**
+ * Check for dumpstate files and update the UI
+ */
+ private void checkForDumpStateAndUpdateUI() {
+ // If we already have a selected file (e.g., from share), use that
+ if (selectedFile != null && selectedFile.exists()) {
+ resultView.setText("File selected: " + selectedFile.getName());
+ uploadButton.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ // Otherwise, look for dumpstate files
+ File mostRecentDumpStateFile = FileController.getMostRecentDumpStateFile();
+
+ if (mostRecentDumpStateFile != null) {
+ selectedFile = mostRecentDumpStateFile;
+ resultView.setText("Dumpstate found: " + mostRecentDumpStateFile.getName());
+ uploadButton.setVisibility(View.VISIBLE);
+ } else {
+ resultView.setText("No dumpstate file found.");
+ uploadButton.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == PermissionUtil.REQUEST_MANAGE_EXTERNAL_STORAGE) {
+ if (PermissionUtil.hasAllFilesAccessPermission()) {
+ checkForDumpStateAndUpdateUI();
+ } else {
+ Toast.makeText(this, "Storage permission not granted!", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ /**
+ * Start the upload process
+ */
+ private void startUpload() {
+ if (selectedFile == null || !selectedFile.exists()) {
+ Toast.makeText(this, "No file selected", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // Update UI
+ progressBar.setVisibility(View.VISIBLE);
+ uploadButton.setEnabled(false);
+ resultView.setText("Uploading file...");
+
+ // Start the upload service
+ Intent serviceIntent = new Intent(this, FileUploadForegroundService.class);
+ serviceIntent.putExtra(FileUploadForegroundService.EXTRA_FILE_PATH, selectedFile.getAbsolutePath());
+ startService(serviceIntent);
+ }
+
+ /**
+ * Show a dialog with the upload success result
+ *
+ * @param uploadCode The upload code
+ */
+ private void showUploadSuccessDialog(String uploadCode) {
+ Log.d(TAG, "Opening upload success dialog with code: " + uploadCode);
+ runOnUiThread(() -> {
+ try {
+ UploadResultDialog dialog = new UploadResultDialog(this, uploadCode);
+ dialog.show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error showing upload result dialog", e);
+ Toast.makeText(this, "Upload successful! Code: " + uploadCode, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.action_history) {
+ // Open upload history activity
+ startActivity(new Intent(this, UploadHistoryActivity.class));
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/UploadHistoryActivity.java b/app/src/main/java/com/mattintech/simplelogupload/view/UploadHistoryActivity.java
new file mode 100644
index 0000000..399e7c0
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/view/UploadHistoryActivity.java
@@ -0,0 +1,120 @@
+package com.mattintech.simplelogupload.view;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.mattintech.simplelogupload.R;
+import com.mattintech.simplelogupload.constant.AppConstants;
+import com.mattintech.simplelogupload.model.UploadHistory;
+import com.mattintech.simplelogupload.view.adapter.UploadHistoryAdapter;
+import com.mattintech.simplelogupload.viewmodel.UploadHistoryViewModel;
+
+import java.util.List;
+
+/**
+ * Activity for displaying upload history
+ */
+public class UploadHistoryActivity extends AppCompatActivity implements UploadHistoryAdapter.OnHistoryItemClickListener {
+ private static final String TAG = AppConstants.APP_TAG + "HistoryActivity";
+
+ private UploadHistoryViewModel viewModel;
+ private UploadHistoryAdapter adapter;
+ private RecyclerView recyclerView;
+ private TextView emptyView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_upload_history);
+
+ // Enable "up" navigation
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ // Initialize views
+ recyclerView = findViewById(R.id.historyRecyclerView);
+ emptyView = findViewById(R.id.emptyView);
+
+ // Set up the RecyclerView
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ adapter = new UploadHistoryAdapter(this);
+ recyclerView.setAdapter(adapter);
+
+ // Initialize the ViewModel
+ viewModel = new ViewModelProvider.AndroidViewModelFactory(getApplication())
+ .create(UploadHistoryViewModel.class);
+
+ // Observe the history data
+ viewModel.getAllHistory().observe(this, new Observer>() {
+ @Override
+ public void onChanged(List history) {
+ // Update the cached copy of the history in the adapter
+ adapter.setHistory(history);
+
+ // Show/hide empty view
+ if (history == null || history.isEmpty()) {
+ recyclerView.setVisibility(View.GONE);
+ emptyView.setVisibility(View.VISIBLE);
+ } else {
+ recyclerView.setVisibility(View.VISIBLE);
+ emptyView.setVisibility(View.GONE);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == android.R.id.home) {
+ finish();
+ return true;
+ } else if (id == R.id.action_clear_history) {
+ viewModel.deleteAll();
+ Toast.makeText(this, "History cleared", Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCopyCodeClick(UploadHistory history) {
+ copyToClipboard(history.getUploadCode());
+ }
+
+ @Override
+ public void onDeleteClick(UploadHistory history) {
+ viewModel.delete(history);
+ }
+
+ /**
+ * Copy text to clipboard
+ *
+ * @param text The text to copy
+ */
+ private void copyToClipboard(String text) {
+ try {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Upload Code", text);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(this, "Code copied to clipboard", Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ Log.e(TAG, "Error copying to clipboard", e);
+ Toast.makeText(this, "Failed to copy code", Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/UploadResultDialog.java b/app/src/main/java/com/mattintech/simplelogupload/view/UploadResultDialog.java
new file mode 100644
index 0000000..8962735
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/view/UploadResultDialog.java
@@ -0,0 +1,70 @@
+package com.mattintech.simplelogupload.view;
+
+import android.app.Dialog;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.mattintech.simplelogupload.R;
+import com.mattintech.simplelogupload.constant.AppConstants;
+
+/**
+ * Dialog to display upload result
+ */
+public class UploadResultDialog extends Dialog {
+ private static final String TAG = AppConstants.APP_TAG + "ResultDialog";
+
+ private final String uploadCode;
+
+ /**
+ * Constructor
+ *
+ * @param context The context
+ * @param uploadCode The upload code
+ */
+ 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 -> copyCodeToClipboard());
+
+ Button closeButton = findViewById(R.id.closeButton);
+ closeButton.setOnClickListener(v -> dismiss());
+ }
+
+ /**
+ * Copy the upload code to the clipboard
+ */
+ private void copyCodeToClipboard() {
+ try {
+ 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();
+ } catch (Exception e) {
+ Log.e(TAG, "Error copying to clipboard", e);
+ Toast.makeText(getContext(), "Failed to copy code", Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/adapter/UploadHistoryAdapter.java b/app/src/main/java/com/mattintech/simplelogupload/view/adapter/UploadHistoryAdapter.java
new file mode 100644
index 0000000..ba191d7
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/view/adapter/UploadHistoryAdapter.java
@@ -0,0 +1,123 @@
+package com.mattintech.simplelogupload.view.adapter;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.mattintech.simplelogupload.R;
+import com.mattintech.simplelogupload.model.UploadHistory;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Adapter for displaying upload history items
+ */
+public class UploadHistoryAdapter extends RecyclerView.Adapter {
+
+ private List history = new ArrayList<>();
+ private final OnHistoryItemClickListener listener;
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault());
+
+ /**
+ * Interface for history item click events
+ */
+ public interface OnHistoryItemClickListener {
+ void onCopyCodeClick(UploadHistory history);
+ void onDeleteClick(UploadHistory history);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param listener The click listener
+ */
+ public UploadHistoryAdapter(OnHistoryItemClickListener listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Update the history data
+ *
+ * @param history The new history data
+ */
+ public void setHistory(List history) {
+ this.history = history;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public HistoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View itemView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_upload_history, parent, false);
+ return new HistoryViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull HistoryViewHolder holder, int position) {
+ UploadHistory current = history.get(position);
+
+ holder.fileNameTextView.setText(current.getFileName());
+ holder.uploadCodeTextView.setText(current.getUploadCode());
+
+ // Format date
+ String formattedDate = dateFormat.format(new Date(current.getUploadTime()));
+ holder.uploadTimeTextView.setText(formattedDate);
+
+ // Set details text
+ String details = String.format(Locale.getDefault(),
+ "Max downloads: %d • Expires in %d hours",
+ current.getMaxDownloads(),
+ current.getExpiryHours());
+ holder.detailsTextView.setText(details);
+
+ // Set click listeners
+ holder.copyButton.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onCopyCodeClick(current);
+ }
+ });
+
+ holder.deleteButton.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onDeleteClick(current);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return history.size();
+ }
+
+ /**
+ * ViewHolder for history items
+ */
+ static class HistoryViewHolder extends RecyclerView.ViewHolder {
+ private final TextView fileNameTextView;
+ private final TextView uploadCodeTextView;
+ private final TextView uploadTimeTextView;
+ private final TextView detailsTextView;
+ private final Button copyButton;
+ private final Button deleteButton;
+
+ HistoryViewHolder(View itemView) {
+ super(itemView);
+ fileNameTextView = itemView.findViewById(R.id.fileNameText);
+ uploadCodeTextView = itemView.findViewById(R.id.uploadCodeText);
+ uploadTimeTextView = itemView.findViewById(R.id.uploadTimeText);
+ detailsTextView = itemView.findViewById(R.id.detailsText);
+ copyButton = itemView.findViewById(R.id.copyButton);
+ deleteButton = itemView.findViewById(R.id.deleteButton);
+ }
+ }
+}
diff --git a/app/src/main/java/com/mattintech/simplelogupload/viewmodel/UploadHistoryViewModel.java b/app/src/main/java/com/mattintech/simplelogupload/viewmodel/UploadHistoryViewModel.java
new file mode 100644
index 0000000..a1e046b
--- /dev/null
+++ b/app/src/main/java/com/mattintech/simplelogupload/viewmodel/UploadHistoryViewModel.java
@@ -0,0 +1,75 @@
+package com.mattintech.simplelogupload.viewmodel;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.mattintech.simplelogupload.db.UploadHistoryRepository;
+import com.mattintech.simplelogupload.model.UploadHistory;
+
+import java.util.List;
+
+/**
+ * ViewModel for upload history
+ */
+public class UploadHistoryViewModel extends AndroidViewModel {
+
+ private final UploadHistoryRepository repository;
+ private final LiveData> allHistory;
+
+ /**
+ * Constructor
+ *
+ * @param application The application
+ */
+ public UploadHistoryViewModel(@NonNull Application application) {
+ super(application);
+ repository = new UploadHistoryRepository(application);
+ allHistory = repository.getAllHistory();
+ }
+
+ /**
+ * Get all history entries
+ *
+ * @return LiveData containing all history entries
+ */
+ public LiveData> getAllHistory() {
+ return allHistory;
+ }
+
+ /**
+ * Insert a new history entry
+ *
+ * @param history The entry to insert
+ */
+ public void insert(UploadHistory history) {
+ repository.insert(history);
+ }
+
+ /**
+ * Delete a history entry
+ *
+ * @param history The entry to delete
+ */
+ public void delete(UploadHistory history) {
+ repository.delete(history);
+ }
+
+ /**
+ * Delete a history entry by ID
+ *
+ * @param id The ID of the entry to delete
+ */
+ public void deleteById(int id) {
+ repository.deleteById(id);
+ }
+
+ /**
+ * Delete all history entries
+ */
+ public void deleteAll() {
+ repository.deleteAll();
+ }
+}
diff --git a/app/src/main/res/layout/activity_upload_history.xml b/app/src/main/res/layout/activity_upload_history.xml
new file mode 100644
index 0000000..45d057c
--- /dev/null
+++ b/app/src/main/res/layout/activity_upload_history.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_upload_history.xml b/app/src/main/res/layout/item_upload_history.xml
new file mode 100644
index 0000000..72af004
--- /dev/null
+++ b/app/src/main/res/layout/item_upload_history.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/upload_result_dialog.xml b/app/src/main/res/layout/upload_result_dialog.xml
index ba55853..41f0afd 100644
--- a/app/src/main/res/layout/upload_result_dialog.xml
+++ b/app/src/main/res/layout/upload_result_dialog.xml
@@ -44,12 +44,27 @@
+ app:layout_constraintEnd_toStartOf="@id/closeButton"
+ app:layout_constraintHorizontal_weight="1" />
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/history_menu.xml b/app/src/main/res/menu/history_menu.xml
new file mode 100644
index 0000000..16d4f60
--- /dev/null
+++ b/app/src/main/res/menu/history_menu.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml
new file mode 100644
index 0000000..118cbee
--- /dev/null
+++ b/app/src/main/res/menu/main_menu.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/build.gradle b/build.gradle
index 1119dd1..a7c62c6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
+ ext.kotlin_version = '1.8.0' // Updated to be compatible with Gradle 8.x
repositories {
google()
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.3.1' // Updated to 8.0.0
+ classpath 'com.android.tools.build:gradle:8.9.1' // More stable version
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -15,4 +17,17 @@ allprojects {
google()
mavenCentral()
}
+ // Apply to all projects
+ configurations.all {
+ resolutionStrategy {
+ // Force a specific version of Kotlin stdlib
+ force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+ }
+ }
+}
+
+tasks.register('clean', Delete) {
+ delete rootProject.buildDir
}
diff --git a/gradle.properties b/gradle.properties
index 2d8d1e4..ac94947 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1,15 @@
-android.useAndroidX=true
\ No newline at end of file
+android.suppressUnsupportedCompileSdk=36
+android.useAndroidX=true
+
+# JVM arguments for kapt with JDK 9+
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 \
+ --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
+ --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
+ --add-opens=jdk.compiler/com.sun.tools.javac.treescanner=ALL-UNNAMED
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 3fa8f86..e2847c8 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME