From 44cbf409b7ea2ff2589bc10d8c273ada837f7f7b Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 7 Oct 2024 22:47:30 -0400 Subject: [PATCH] initial commit --- .gitignore | 61 +++++ app/build.gradle | 34 +++ app/src/main/AndroidManifest.xml | 36 +++ .../mattintech/simplelogupload/Constant.java | 5 + .../FileUploadForegroundService.java | 135 ++++++++++ .../simplelogupload/FileUploadJobService.java | 92 +++++++ .../simplelogupload/MainActivity.java | 144 ++++++++++ app/src/main/res/drawable/logupload.xml | 25 ++ app/src/main/res/layout/activity_main.xml | 31 +++ app/src/main/res/values/colors.xml | 13 + app/src/main/res/values/styles.xml | 14 + .../main/res/xml/network_security_config.xml | 12 + build.gradle | 18 ++ gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 ++++++++++++++++++ gradlew.bat | 92 +++++++ settings.gradle | 3 + 18 files changed, 971 insertions(+) create mode 100644 .gitignore create mode 100644 app/build.gradle create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/mattintech/simplelogupload/Constant.java create mode 100644 app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java create mode 100644 app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java create mode 100644 app/src/main/java/com/mattintech/simplelogupload/MainActivity.java create mode 100644 app/src/main/res/drawable/logupload.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..437f873 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Gradle files +.gradle/ +build/ + +# Local configuration file (SDK path, etc.) +local.properties + +# Log and Android Studio files +*.log +.idea/ +.DS_Store +*.iml +*.hprof + +# Ignore Gradle cache and wrapper files +.caches/ +.gradle/ +.gradle/caches/ +.gradle/gradle-app.cache +.gradle/gradle-wrapper.jar + +# Ignore Android build files +/build +/app/build +/**/build/ + +# Ignore APK, AAB and Bundle files in the root of directory +*.apk +*.ap_ +*.aab +*.jar +*.keystore + +# Ignore Google Services JSON or XML (API keys, etc.) +google-services.json +google-services.xml + +# Ignore Mac OS generated files +*.DS_Store + +# Ignore Linux temporary files +*~ + +# Ignore crashlytics specific files +crashlytics.properties +fabric.properties +*.mapping +*.symbols + +# Android Studio specific files +.idea/ +*.iml +*.ipr +*.iws + +# Miscellaneous +*.orig + +# Ignore the generated files in the root of the project +/android/app/build +/android/app/.externalNativeBuild diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..4235f43 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,34 @@ + +plugins { + id 'com.android.application' +} + +android { + namespace 'com.mattintech.simplelogupload' + compileSdkVersion 34 + defaultConfig { + applicationId "com.mattintech.simplelogupload" + minSdkVersion 30 + targetSdkVersion 33 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0d16fcd --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/simplelogupload/Constant.java b/app/src/main/java/com/mattintech/simplelogupload/Constant.java new file mode 100644 index 0000000..4540d1d --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/Constant.java @@ -0,0 +1,5 @@ +package com.mattintech.simplelogupload; + +public class Constant { + public static final String APP_TAG = "SLU::"; +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java b/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java new file mode 100644 index 0000000..8b07ce0 --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/FileUploadForegroundService.java @@ -0,0 +1,135 @@ +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 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."); + 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) { + OkHttpClient client = new OkHttpClient(); + Log.d(TAG, "Uploading file: " + file.getName() + ", Size: " + file.length()); + + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/zip")); + MultipartBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), fileBody) + .build(); + + Request request = new Request.Builder() + .url("http://172.16.30.69:7777/upload") // Replace with your server's URL + .post(requestBody) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + e.printStackTrace(); + Log.e(TAG, "Upload failed: " + e.getMessage()); + stopSelf(); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response.isSuccessful()) { + String tinyUrl = response.body().string(); // Assuming the server returns tinyURL in response + Log.d(TAG, "Upload successful! TinyURL: " + tinyUrl); + } else { + Log.e(TAG, "Upload failed, Response code: " + response.code()); + } + stopSelf(); + } + }); + } +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java b/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java new file mode 100644 index 0000000..f0150e1 --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/FileUploadJobService.java @@ -0,0 +1,92 @@ +package com.mattintech.simplelogupload; + +import android.app.job.JobParameters; +import android.app.job.JobService; +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 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(); + } + + 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) { + OkHttpClient client = new OkHttpClient(); + Log.d(TAG, "Uploading file: " + file.getName() + ", Size: " + file.length()); + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/zip")); + + MultipartBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), fileBody) + .build(); + + Request request = new Request.Builder() + .url("http://172.16.30.69:7777/upload") // Replace with your Flask server's URL + .post(requestBody) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + e.printStackTrace(); + Log.e(TAG, "Upload failed: " + e.getMessage()); + jobFinished(params, false); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response.isSuccessful()) { + String tinyUrl = response.body().string(); // Assuming the server returns tinyURL in response + Log.d(TAG, "Upload successful! TinyURL: " + tinyUrl); + + // Show tinyURL on the UI (using BroadcastReceiver or local storage to communicate with MainActivity) + } else { + Log.e(TAG, "Upload failed, Response code: " + response.code()); + } + jobFinished(params, false); + } + }); + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java b/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java new file mode 100644 index 0000000..bee8d19 --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/MainActivity.java @@ -0,0 +1,144 @@ + +package com.mattintech.simplelogupload; + +import android.Manifest; +import android.app.Activity; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.app.job.JobParameters; +import android.content.ComponentName; +import android.content.Intent; +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.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 File mostRecentDumpStateFile; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + resultView = findViewById(R.id.resultView); + uploadButton = findViewById(R.id.uploadButton); + + + // Check for All Files Access Permission + requestNotificationPermission(); + if (!hasAllFilesAccessPermission()) { + requestAllFilesAccessPermission(); + } else { + checkForDumpStateAndUpdateUI(); + } + + uploadButton.setOnClickListener(v -> { + if (mostRecentDumpStateFile != null) { + //scheduleJob(); + startFileUpload(); + } + }); + } + + 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()) { + scheduleJob(); // Now you can proceed if permission is granted + } 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); + } + + } + +} diff --git a/app/src/main/res/drawable/logupload.xml b/app/src/main/res/drawable/logupload.xml new file mode 100644 index 0000000..dd45d7c --- /dev/null +++ b/app/src/main/res/drawable/logupload.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c1e2df9 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,31 @@ + + + + + +