refactoring and adding a history
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -18,22 +19,47 @@
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar">
|
||||
|
||||
<!-- Main Activity -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".view.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share intent filter for files -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share intent filter for multiple files -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Upload History Activity -->
|
||||
<activity
|
||||
android:name=".view.UploadHistoryActivity"
|
||||
android:label="Upload History"
|
||||
android:parentActivityName=".view.MainActivity"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".view.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<!-- Register the JobService -->
|
||||
<!-- File Upload Foreground Service -->
|
||||
<service
|
||||
android:name=".FileUploadJobService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".FileUploadForegroundService"
|
||||
android:permission="android.permission.FOREGROUND_SERVICE" />
|
||||
android:name=".service.FileUploadForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<File> 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<File> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<Uri> extractSharedUris(Intent intent) {
|
||||
List<Uri> 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<Uri> 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<File> processSharedUri(Context context, Uri uri) {
|
||||
if (context == null || uri == null) {
|
||||
CompletableFuture<File> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<UploadResult> uploadFile(
|
||||
File file, int maxDownloads, int expiryHours) {
|
||||
CompletableFuture<UploadResult> 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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<List<UploadHistory>> 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();
|
||||
}
|
||||
@@ -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<List<UploadHistory>> 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<List<UploadHistory>> getAllHistory() {
|
||||
return mAllHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new history entry asynchronously
|
||||
*
|
||||
* @param entry The entry to insert
|
||||
* @return CompletableFuture containing the inserted row ID
|
||||
*/
|
||||
public CompletableFuture<Long> 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<UploadHistory> getEntryById(final int id) {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
mUploadHistoryDao.getEntryById(id),
|
||||
AppDatabase.databaseWriteExecutor);
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Uri> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<List<UploadHistory>>() {
|
||||
@Override
|
||||
public void onChanged(List<UploadHistory> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<UploadHistoryAdapter.HistoryViewHolder> {
|
||||
|
||||
private List<UploadHistory> 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<UploadHistory> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<List<UploadHistory>> 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<List<UploadHistory>> 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();
|
||||
}
|
||||
}
|
||||
25
app/src/main/res/layout/activity_upload_history.xml
Normal file
25
app/src/main/res/layout/activity_upload_history.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".view.UploadHistoryActivity">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/historyRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
android:padding="8dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="No upload history"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
90
app/src/main/res/layout/item_upload_history.xml
Normal file
90
app/src/main/res/layout/item_upload_history.xml
Normal file
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fileNameText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textSize="16sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/uploadTimeText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="#757575"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Code: "
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/uploadCodeText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textIsSelectable="true"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#0066CC" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/detailsText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Copy Code"
|
||||
style="@style/Widget.AppCompat.Button.Colored" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/deleteButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Delete"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -44,12 +44,27 @@
|
||||
|
||||
<Button
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="Copy Code"
|
||||
app:layout_constraintTop_toBottomOf="@id/uploadCode"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintEnd_toStartOf="@id/closeButton"
|
||||
app:layout_constraintHorizontal_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="Close"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
app:layout_constraintTop_toBottomOf="@id/uploadCode"
|
||||
app:layout_constraintStart_toEndOf="@id/copyButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_weight="1" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
8
app/src/main/res/menu/history_menu.xml
Normal file
8
app/src/main/res/menu/history_menu.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_clear_history"
|
||||
android:title="Clear History"
|
||||
android:orderInCategory="100"
|
||||
android:showAsAction="never" />
|
||||
</menu>
|
||||
8
app/src/main/res/menu/main_menu.xml
Normal file
8
app/src/main/res/menu/main_menu.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_history"
|
||||
android:title="Upload History"
|
||||
android:orderInCategory="100"
|
||||
android:showAsAction="never" />
|
||||
</menu>
|
||||
Reference in New Issue
Block a user