From 5215c73223f0aa7d51e9784e78f295ffbd90b796 Mon Sep 17 00:00:00 2001 From: MDP43140 <68391650+MDP43140@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:43:12 +0700 Subject: [PATCH] Decent useful changes + Added crash reporting (Kudos mostly to Gramophone projects for Uncaught Exception Handler backends, definitely really important to catch bugs and crashes) + Blacklists filter + Move aggresive filters to blacklist filter, disabled by default + use updatePercentage function instead of changing MainActivity gui bindings through FileScanner (cleaner code, and reusable FileScanner code in other projects). + Fixed Dynamic Colors not applied properly (i have to put it in Application, not MainActivity). + Fixed crashing when importing data, then clicking import/export again. + Reuse existing App.prefs, instead of creating individual prefs or rely on MainActivity.prefs. + [.github/ISSUE_TEMPLATE/bug-report] convert to .yml, and change some stuff... + [build.gradle] Disable crunch png by default, because its no longer needed, since the PNGs are optimized in the first place before compiling + [build.gradle] remove `.toString()` from `JavaVersion.VERSION_21`. + [build.gradle] Don't include some metadata (https://gitlab.com/IzzyOnDroid/repo/-/issues/491) + [build.gradle] Dependency upgrade... + [AndroidManifest] Add BlacklistActivity and ErrorActivity. + [App.kt] Count how many times the app has been run (not used yet, probably useful for first-run related code in the future). + [BootReceiver.kt] replace context with ctx. + [BootReceiver.kt] remove unnecessary runCleanup function. + [BootReceiver.kt] Replace ScheduledWorker.Companion variables with Constants variables. + [MainActivity.kt] move some scan Thread calls to scan function directly to reduce duplicate codes. + [MainActivity.kt] cleanup addText function code. + [MainActivity.kt] remove unnecessary reset function code. + [MainActivity.kt] move MainActivity.convertSize to CommonFunctions.convertSize + [SettingsActivity.kt] use separate loadFragment function. + [SettingsActivity.kt] Simplify crashing code (by simply throwing Exception). + [WhitelistActivity.kt] Added ability to disable filters + [WhitelistActivity.kt] Mostly same codes as WhiteList, but for blacklists, and pressing add asks for path (can use Kotlin regex). + [FileScanner.kt] Import java.util.Locale instead of everything in java.util + [FileScanner.kt] Add ability to remove empty files + [FileScanner.kt] remove unnecessary gui variable + [FileScanner.kt] Simplify getListFiles logic + [FileScanner.kt] Simplify maxCycles code + [FileScanner.kt] remove aggresive filter related codes + [PanicResponderActivity.kt] Code cleanups... + [PanicResponderActivity.kt] Trying to make it run somewhat faster by using Thread... + [ScheduledWorker.kt] replace `controllers.MainActivity.Companion.convertSize` with `CommonFunctions.convertSize` + [ScheduledWorker.kt] move UNIQUE_WORK_NAME and WORK_TAG to Constants.BGCLEAN_WORK_NAME and Constants.BGCLEAN_WORK_TAG + [ScheduledWorker.kt] move makeStatusNotification to CommonFunctions + [preferences.xml] Add blacklist & whitelist button, remove aggressive filter, add empty file, replace "empty" key to "emptyFolder" (to avoid confusion, since we have new empty file option as well), add dynamic color toggle, and modify about section as well. + [README.md] remove Repobeats analytics image that links to old repo + Remove some ununsed translations (only default and in-rID, the rest are in next commit because there is wayy too many of these) --- .github/ISSUE_TEMPLATE/bug-report.md | 32 --- .github/ISSUE_TEMPLATE/bug-report.yml | 93 ++++++++ README.md | 6 +- app/build.gradle | 15 +- app/src/main/AndroidManifest.xml | 7 + .../java/theredspy15/ltecleanerfoss/App.kt | 34 +-- .../ltecleanerfoss/BootReceiver.kt | 18 +- .../ltecleanerfoss/CommonFunctions.kt | 103 +++++++++ .../theredspy15/ltecleanerfoss/Constants.kt | 60 ++++++ .../theredspy15/ltecleanerfoss/FileScanner.kt | 184 ++++++++-------- .../ltecleanerfoss/PanicResponderActivity.kt | 42 ++-- .../ltecleanerfoss/ScheduledWorker.kt | 76 ++----- .../controllers/BlacklistActivity.kt | 171 +++++++++++++++ .../controllers/ErrorActivity.kt | 84 ++++++++ .../controllers/MainActivity.kt | 201 ++++++++---------- .../controllers/SettingsActivity.kt | 138 +++++------- .../controllers/WhitelistActivity.kt | 72 +++++-- .../main/res/layout-land/activity_main.xml | 84 ++++---- .../main/res/layout/activity_blacklist.xml | 37 ++++ app/src/main/res/layout/activity_error.xml | 66 ++++++ app/src/main/res/layout/activity_main.xml | 24 ++- app/src/main/res/values-in-rID/strings.xml | 21 +- app/src/main/res/values/strings.xml | 51 ++--- app/src/main/res/xml/preferences.xml | 36 +++- 24 files changed, 1121 insertions(+), 534 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 app/src/main/java/theredspy15/ltecleanerfoss/controllers/BlacklistActivity.kt create mode 100644 app/src/main/java/theredspy15/ltecleanerfoss/controllers/ErrorActivity.kt create mode 100644 app/src/main/res/layout/activity_blacklist.xml create mode 100644 app/src/main/res/layout/activity_error.xml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 8e087488..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Smartphone (please complete the following information):** - - Device: [e.g. Pixel 8] - - OS: [e.g. Android 13] - - App Version: [e.g. 4.9.9] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..fdf70f3e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,93 @@ +name: Bug report +description: Create a bug report to help us improve +labels: [bug, needs triage] +body: + - type: checkboxes + id: checklist + attributes: + label: "Checklist" + options: + - label: "I am able to reproduce the bug with the latest version." + required: true + - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." + required: true + - label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise." + required: true + + - type: input + id: app-version + attributes: + label: Affected version + description: "In which version did you encounter the bug?" + placeholder: "x.x.x - Not required if you include the log" + + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce the bug + description: | + What did you do for the bug to show up? + placeholder: | + 1. Go to '...' + 2. Scroll down to '....' + 3. Click on '....' + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: | + What do you expect to happen? + + - type: textarea + id: actual-behavior + attributes: + label: Actual behavior + description: | + What happens with the steps given above? + + - type: textarea + id: screen-media + attributes: + label: Screenshots/Screen recordings + description: | + A picture or video is worth a thousand words. + + If applicable, add screenshots or a screen recording to help explain your problem. + GitHub supports uploading them directly in the text box. + If your file is too big for GitHub to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead. + + :heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE. + Instead, follow the instructions in the "Logs" section below. + + - type: textarea + id: logs + attributes: + label: Logs + description: | + If your bug includes a crash log, tap on "Share formatted log" at the bottom, click "Copy to clipboard", and paste it here. + + - type: input + id: device-os-info + attributes: + label: Affected Android/Custom ROM version + description: | + With what Operating System (+ version) did you encounter the bug? + placeholder: "eg. Android 13/LineageOS 20" + + - type: input + id: device-model-info + attributes: + label: Affected device model + description: | + On what device did you encounter the bug? + placeholder: "eg. Google Pixel 7/Samsung Galaxy S10" + + - type: textarea + id: additional-information + attributes: + label: Additional information + description: | + Any other information you'd like to include \ No newline at end of file diff --git a/README.md b/README.md index 92352e04..7e476ac8 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,6 @@ Despite hitting 100k users, he is no longer able to devote time to it. Thank you [![GitHub issues](https://img.shields.io/github/issues/mdp43140/LTECleanerFOSS)](/issues) [![License](https://img.shields.io/github/license/mdp43140/LTECleanerFOSS)](/blob/master/LICENSE) -![Alt](https://repobeats.axiom.co/api/embed/e57b4b0c0e47daffc4e7feb4cff54fa6a1bc4120.svg "Repobeats analytics image") - ***The last Android cleaner you will ever need!*** Tired of the abundance of phone cleaners on the Play Store? Tired of @@ -31,7 +29,8 @@ __LTE Cleaner is 100% free, open source, no ads, and deletes everything it claim ## Install [GitHub](https://github.com/MDP43140/LTECleanerFOSS/files/13996640/app-release.zip) [Build it yourself](#compiling-the-app) -[Original version (outdated)](https://github.com/theredspy15/LTECleanerFOSS) +[Original F-Droid (outdated)](https://f-droid.org/packages/theredspy15.ltecleanerfoss) +[Original source code (outdated)](https://github.com/theredspy15/LTECleanerFOSS) ## Features - Clipboard clearing @@ -46,6 +45,7 @@ Cleans: - Advertisement folders To do list (not guaranteed because i'm busy irl): +- Code cleanups - Clean SD card (has to support minimal Android 10+, hopefully we can use StorageAccessFramework to make this work, but it might be a huge work that can take days, not possible with my spare time) - Custom (regex) blacklist (In theory should be simple, just need to add additional rule matching to cleaning system, but implementing the separate Activity might be a bit painful) - Regex whilelist diff --git a/app/build.gradle b/app/build.gradle index a552a485..2c7f73f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,13 +34,13 @@ android { release { minifyEnabled true shrinkResources true - crunchPngs true + //crunchPngs true // no longer needed, since the PNGs are optimized in the first place before compiling debuggable false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') } } kotlinOptions { - jvmTarget = JavaVersion.VERSION_21.toString() + jvmTarget = JavaVersion.VERSION_21 } compileOptions { sourceCompatibility JavaVersion.VERSION_21 @@ -53,13 +53,18 @@ android { resources.excludes.add("assets/dexopt/*") resources.excludes.add("META-INF/*") } + dependenciesInfo { + // https://gitlab.com/IzzyOnDroid/repo/-/issues/491 + includeInApk = false + includeInBundle = false + } } dependencies { //implementation fileTree(dir: 'libs', include: ['*.jar']) //noinspection GradleCompatible - implementation "androidx.appcompat:appcompat:1.6.1" - implementation "com.google.android.material:material:1.11.0" + implementation "androidx.appcompat:appcompat:1.7.0-alpha03" + implementation "com.google.android.material:material:1.12.0-alpha03" implementation "androidx.gridlayout:gridlayout:1.1.0-beta01" testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.2" @@ -72,6 +77,6 @@ dependencies { //Preference implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.work:work-runtime-ktx:2.9.0" - implementation "androidx.core:core-ktx:1.12.0" + implementation "androidx.core:core-ktx:1.13.0-alpha05" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9b12a51f..605a5e2b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,10 @@ + + diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/App.kt b/app/src/main/java/theredspy15/ltecleanerfoss/App.kt index b3093dce..121b6494 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/App.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/App.kt @@ -4,26 +4,34 @@ */ package theredspy15.ltecleanerfoss import android.app.Application +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager import android.content.SharedPreferences -import androidx.appcompat.app.AppCompatDelegate +import android.graphics.drawable.Icon +import android.os.Build import androidx.preference.PreferenceManager +import com.google.android.material.color.DynamicColors +import theredspy15.ltecleanerfoss.controllers.MainActivity +import theredspy15.ltecleanerfoss.controllers.SettingsActivity import theredspy15.ltecleanerfoss.R +import theredspy15.ltecleanerfoss.CommonFunctions class App: Application(){ + var runCount = 0 override fun onCreate(){ super.onCreate() + // Catches bugs and crashes, and makes it easy to report the bug + Thread.setDefaultUncaughtExceptionHandler { _, paramThrowable -> + CommonFunctions.handleError(this, 3, paramThrowable) + } prefs = PreferenceManager.getDefaultSharedPreferences(this) - updateTheme() - } - protected fun updateTheme(){ - val auto = resources.getStringArray(R.array.themes)[0] - val light = resources.getStringArray(R.array.themes)[1] - val dark = resources.getStringArray(R.array.themes)[2] - val theme = prefs!!.getString("theme",auto) - AppCompatDelegate.setDefaultNightMode(when (theme) { - light -> AppCompatDelegate.MODE_NIGHT_NO - dark -> AppCompatDelegate.MODE_NIGHT_YES - else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - }) + // Stores how many times the app has been opened, + // Can also be used for first run related codes in the future, who knows... + runCount = prefs!!.getInt("runCount",0) + prefs!!.edit().putInt("runCount",runCount + 1).commit() + // Update theme and apply dynamic color + CommonFunctions.updateTheme(prefs) + if (prefs!!.getBoolean("dynamicColor",true)) DynamicColors.applyToActivitiesIfAvailable(this) } companion object { @JvmField var prefs:SharedPreferences? = null diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/BootReceiver.kt b/app/src/main/java/theredspy15/ltecleanerfoss/BootReceiver.kt index 91fe5e9e..454f1871 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/BootReceiver.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/BootReceiver.kt @@ -13,29 +13,27 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import java.util.concurrent.TimeUnit +import theredspy15.ltecleanerfoss.App +import theredspy15.ltecleanerfoss.Constants class BootReceiver: BroadcastReceiver() { - override fun onReceive(ctxt: Context, i: Intent) { - runCleanup(ctxt) - } - fun runCleanup(context: Context){ - val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + override fun onReceive(ctx: Context, i: Intent) { val constraints = Constraints.Builder() .setRequiresBatteryNotLow(true) .setRequiresDeviceIdle(true) .build() // Schedule the work hourly - ScheduledWorker.enqueueWork(context) + ScheduledWorker.enqueueWork(ctx) // Schedule the work at boot completed - if (prefs.getBoolean("bootedcleanup",false)){ + if (App.prefs!!.getBoolean("bootedcleanup",false)){ val myWork = OneTimeWorkRequestBuilder() - .addTag(ScheduledWorker.Companion.WORK_TAG) + .addTag(Constants.BGCLEAN_WORK_TAG) .setConstraints(constraints) .setInitialDelay(1, TimeUnit.MINUTES) .build() - WorkManager.getInstance(context).enqueueUniqueWork( - ScheduledWorker.Companion.UNIQUE_WORK_NAME, + WorkManager.getInstance(ctx).enqueueUniqueWork( + Constants.BGCLEAN_WORK_NAME, ExistingWorkPolicy.REPLACE, myWork ) diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/CommonFunctions.kt b/app/src/main/java/theredspy15/ltecleanerfoss/CommonFunctions.kt index 22f9cbf4..afea50b6 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/CommonFunctions.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/CommonFunctions.kt @@ -2,3 +2,106 @@ * (C) 2020-2023 Hunter J Drum * (C) 2024 MDP43140 */ +package theredspy15.ltecleanerfoss +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Build +import android.util.Log +import android.widget.Toast +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import theredspy15.ltecleanerfoss.Constants +import theredspy15.ltecleanerfoss.controllers.ErrorActivity +import java.text.DecimalFormat +import kotlin.system.exitProcess +object CommonFunctions { + fun makeStatusNotification(message: String?, ctx: Context): NotificationCompat.Builder { + + // Name of Notification Channel for verbose notifications of background work + val NOTIFICATION_TITLE: CharSequence = ctx.getString(R.string.notification_title) + + // Make a channel if necessary + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val VERBOSE_NOTIFICATION_CHANNEL_NAME: CharSequence = + ctx.getString(R.string.settings_notification_name) + val VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION = + ctx.getString(R.string.settings_notification_sum) + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + val channel = + NotificationChannel(Constants.NOTIFICATION_CHANNEL_SERVICE, VERBOSE_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + channel.description = VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION + + // Add the channel + val notificationManager = + ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + // Create the notification + val notification = NotificationCompat.Builder(ctx, Constants.NOTIFICATION_CHANNEL_SERVICE) + .setSmallIcon(R.drawable.ic_baseline_cleaning_services_24) + .setContentTitle(NOTIFICATION_TITLE) + .setContentText(message) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setVibrate(LongArray(0)) + return notification + } + fun sendNotification(ctx: Context, id: Int, notification: NotificationCompat.Builder){ + NotificationManagerCompat.from(ctx).notify(id, notification.build()) + } + fun updateTheme(prefs: SharedPreferences?){ + try { + // currently put within try-catch + // block cuz crash vv different value type + val theme = prefs!!.getInt("theme",AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + AppCompatDelegate.setDefaultNightMode(theme) + } catch (e: Exception){} + } + fun updateTheme(theme: Int){ + AppCompatDelegate.setDefaultNightMode(theme) + } + // ANTI-UNLUCK CHUNK + fun handleError(ctx: Context, severity: Byte?, paramThrowable: Throwable){ + // Severity level: + // 1: Can be ignored, print Log.e() + // 2: Toast + Notification with button (when pressed, opens ErrorActivity) + // 3: Critical error: App crashed, and ErrorActivity launched (default) + val exceptionMessage = Log.getStackTraceString(paramThrowable) + Log.e("ErrorActivity",exceptionMessage) +// if (severity != 1){ + val intent = Intent(ctx, ErrorActivity::class.java) + intent.putExtra("exception_message", exceptionMessage) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK +// if (severity == 2){ + // TODO: Toast + Notification with button +// } else { + ctx.startActivity(intent) + exitProcess(10) +// } +// } + } + @JvmStatic fun writeContentToUri(ctx: Context,uri: Uri, content: String){ + ctx.contentResolver.openOutputStream(uri)?.use { outputStream -> + outputStream.write(content.toByteArray()) + } + } + @JvmStatic fun convertSize(length: Long): String { + val format = DecimalFormat("#.##") + val kib:Long = 1024 + val mib:Long = 1048576 + return if (length > mib) { + format.format(length / mib) + " MB" + } else if (length > kib) { + format.format(length / kib) + " KB" + } else { + format.format(length) + " B" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/Constants.kt b/app/src/main/java/theredspy15/ltecleanerfoss/Constants.kt index 22f9cbf4..d664b5a6 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/Constants.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/Constants.kt @@ -2,3 +2,63 @@ * (C) 2020-2023 Hunter J Drum * (C) 2024 MDP43140 */ +package theredspy15.ltecleanerfoss +object Constants { + const val BGCLEAN_WORK_NAME = "scheduled_cleanup_work" + const val BGCLEAN_WORK_TAG = "cleanup_work_tag" + const val NOTIFICATION_ID_SERVICE = 1 + const val NOTIFICATION_CHANNEL_SERVICE = "CLEANUP_SERVICE" + val blacklistDefault: Set = setOf( + ".*\\.log", + ".*\\.tmp", + ".*/log", + ".*/Logs", + "/storage/emulated/0/.*albumthumbs\\?", + "/storage/emulated/0/Android/data/.*/files/tombstone_.*", + "/storage/emulated/0/Android/data/.*/files/il2cpp", + "/storage/emulated/0/Android/data/.*/files/.*UnityAdsVideoCache", + "/storage/emulated/0/Android/data/.*/files/.*mobvista", + "/storage/emulated/0/Android/data/.*/files/Unity/.*/Analytics", + "/storage/emulated/0/Android/data/.*/files/supersonicads", + "/storage/emulated/0/Android/data/.*/files/.*splashad", + "/storage/emulated/0/.*Analytics", + "/storage/emulated/0/.*Cache", + "/storage/emulated/0/.*cache", + "/storage/emulated/0/.*\\.exo", + "/storage/emulated/0/.*\\.thumb[0-9]", + "/storage/emulated/0/.*\\.thumbnails\\?", + "/storage/emulated/0/.*thumbs?\\.db", + "/storage/emulated/0/.*/\\.spotlight-V100", + "/storage/emulated/0/.*/\\.DS_Store", + "/storage/emulated/0/.*/\\.Trash", + "/storage/emulated/0/.*/bugreports", + "/storage/emulated/0/.*/Bugreport", + "/storage/emulated/0/.*/desktop.ini", + "/storage/emulated/0/.*/fseventd", + "/storage/emulated/0/.*/leakcanary", + "/storage/emulated/0/.*/LOST\\.DIR" + ) + val blacklistOnDefault: Set = setOf( + ".*\\.log", + ".*\\.tmp", + ".*/log", + ".*/Logs", + "/storage/emulated/0/.*Analytics", + "/storage/emulated/0/.*Cache", + "/storage/emulated/0/.*cache" + ) + val whitelistDefault: Set = setOf( + ".*/backup", + ".*/copy", + ".*/copies", + ".*/important", + ".*/do_not_edit" + ) + val whitelistOnDefault: Set = setOf( + ".*/backup", + ".*/copy", + ".*/copies", + ".*/important", + ".*/do_not_edit" + ) +} \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/FileScanner.kt b/app/src/main/java/theredspy15/ltecleanerfoss/FileScanner.kt index 9d8939f9..72370e04 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/FileScanner.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/FileScanner.kt @@ -12,19 +12,20 @@ import android.graphics.Color import android.widget.TextView import androidx.preference.PreferenceManager import theredspy15.ltecleanerfoss.controllers.MainActivity +import theredspy15.ltecleanerfoss.controllers.BlacklistActivity import theredspy15.ltecleanerfoss.controllers.WhitelistActivity -import theredspy15.ltecleanerfoss.databinding.ActivityMainBinding import java.io.File -import java.util.* +import java.util.Locale class FileScanner(private val path: File, context: Context){ // TODO: Ability to clean SD Card? Already tried SAF implementation, but its really hard, and soon i realized it has storage access restrictions: https://developer.android.com/training/data-storage/shared/documents-files#document-tree-access-restrictions private var prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - private var context: Context? = null - private var res: Resources? = null - private var gui: ActivityMainBinding? = null + private val context = context + private var res: Resources = context.resources + private var updateProgress: ((context: Context, percent: Double) -> Unit)? = null private var filesRemoved = 0 private var kilobytesTotal: Long = 0 private var delete = false + private var emptyFile = true private var emptyDir = false private var autoWhite = true private var corpse = false @@ -43,15 +44,13 @@ class FileScanner(private val path: File, context: Context){ val files = parentDirectory.listFiles() if (files != null) { for (file in files) { - if (file != null) { // hopefully to fix crashes on a very limited number of devices. - if (!isWhiteListed(file)) { // won't touch if whitelisted - if (file.isDirectory) { // folder - if (autoWhite) { // if auto whitelist enabled - if (!autoWhiteList(file)) inFiles.add(file) // if file is not in autowhitelist index, add it (TODO: Allow custom whitelist regexes (custom folder/file name)) - } else inFiles.add(file) // add folder itself - inFiles.addAll(getListFiles(file)) // add contents to returned list - } else inFiles.add(file) // add file - } + if (file != null && !isWhiteListed(file)) { // hopefully to fix crashes on a very limited number of devices && won't touch if whitelisted + if (file.isDirectory) { // folder + if (autoWhite) { // if auto whitelist enabled + if (!autoWhiteList(file)) inFiles.add(file) // if file is not in autowhitelist index, add it + } else inFiles.add(file) // add folder itself + inFiles.addAll(getListFiles(file)) // add contents to returned list + } else inFiles.add(file) // add file } } } @@ -59,15 +58,23 @@ class FileScanner(private val path: File, context: Context){ } /** - * Runs a for each loop through the white list, and compares the path of the file to each path in + * Runs a for each loop through the black/white list, and compares the path of the file to each path in * the list * @param file file to check if in the whitelist - * @return true if is the file is in the white list, false if not + * @return true if is the file is in the black/white list, false if not */ private fun isWhiteListed(file: File): Boolean { - for (path in WhitelistActivity.getWhiteList(prefs)) when { - path.equals(file.absolutePath, ignoreCase = true) || - path.equals(file.name, ignoreCase = true) -> return true + for (path in WhitelistActivity.getWhitelistOn(prefs)) when { + path.equals(file.absolutePath) || + path.equals(file.name, ignoreCase = true) -> return true + } + return false + } + private fun isBlackListed(file: File): Boolean { + for (path in BlacklistActivity.getBlacklistOn(prefs)){ + val pattern = path!!.toRegex() + if (file.absolutePath.matches(pattern) || + file.name.matches(pattern)) return true } return false } @@ -100,43 +107,44 @@ class FileScanner(private val path: File, context: Context){ /** * Runs as for each loop through the filter, and checks if the file matches any filters * @param file file to check - * @return true if the file's extension is in the filter, false otherwise + * @return true if the file matches certain rules, otherwise false */ - fun filter(file: File?): Boolean { - if (file != null) { - try { - if ( - // corpse checking - TODO: needs improvement! (Unsafe use of a nullable receiver of type File?) - // Android/Data/[file != .nomedia] - corpse && - file.parentFile != null && - file.parentFile.parentFile != null && - file.parentFile.name == "data" && - file.parentFile.parentFile.name == "Android" && - file.name != ".nomedia" && - !installedPackages.contains(file.name) || - // empty folder - emptyDir && - isDirectoryEmpty(file) - ) return true + fun filter(file: File): Boolean { + try { + if ( + // corpse checking + // Android/Data/[file != .nomedia] + corpse && + file.parentFile!!.name == "data" && + file.parentFile!!.parentFile!!.name == "Android" && + file.name != ".nomedia" && + !installedPackages.contains(file.name) || + // empty file + emptyFile && + isFileEmpty(file) || + // empty folder + emptyDir && + isDirectoryEmpty(file) || + // blacklist (targeted to get deleted) + isBlackListed(file) + ) return true - // file - val filterIterator = filters.iterator() - while (filterIterator.hasNext()) { - val filter = filterIterator.next() - if (file.absolutePath.lowercase(Locale.getDefault()).matches(filter.lowercase(Locale.getDefault()).toRegex())) - return true - } - } catch (e: NullPointerException) { - return false + // file + val filterIterator = filters.iterator() + while (filterIterator.hasNext()) { + val filter = filterIterator.next() + if (file.absolutePath.lowercase(Locale.getDefault()).matches(filter.lowercase(Locale.getDefault()).toRegex())) + return true } + } catch (e: NullPointerException) { + return false } return false // not empty folder or file in filter } private val installedPackages: List get() { - val pm = context!!.packageManager + val pm = context.packageManager val pkgs = pm.getInstalledApplications(PackageManager.GET_META_DATA) val pkgsStr: MutableList = ArrayList() for (pkg in pkgs) { @@ -152,7 +160,10 @@ class FileScanner(private val path: File, context: Context){ * @return true if empty, false if containing a file(s) */ private fun isDirectoryEmpty(directory: File): Boolean { - return directory.isDirectory && directory.list() != null && directory.list().isEmpty() + return directory.isDirectory && directory.list()!!.isEmpty() + } + private fun isFileEmpty(file: File): Boolean { + return !file.isDirectory && file.length() == 0L } /** @@ -160,17 +171,12 @@ class FileScanner(private val path: File, context: Context){ * 'generic', 'aggressive', and 'apk' should be assigned by calling preferences.getBoolean() */ @SuppressLint("ResourceType") - fun setUpFilters(generic: Boolean, aggressive: Boolean, apk: Boolean): FileScanner { + fun setUpFilters(generic: Boolean, apk: Boolean): FileScanner { val folders: MutableList = ArrayList() val files: MutableList = ArrayList() - setResources(context!!.resources) if (generic) { - folders.addAll(listOf(*res!!.getStringArray(R.array.generic_filter_folders))) - files.addAll(listOf(*res!!.getStringArray(R.array.generic_filter_files))) - } - if (aggressive) { - folders.addAll(listOf(*res!!.getStringArray(R.array.aggressive_filter_folders))) - files.addAll(listOf(*res!!.getStringArray(R.array.aggressive_filter_files))) + folders.addAll(listOf(*res.getStringArray(R.array.generic_filter_folders))) + files.addAll(listOf(*res.getStringArray(R.array.generic_filter_files))) } // filters @@ -181,7 +187,7 @@ class FileScanner(private val path: File, context: Context){ if (autoWhite){ // whitelist whitelist.clear() - whitelist.addAll(listOf(*res!!.getStringArray(R.array.autowhitelist_filter))) + whitelist.addAll(listOf(*res.getStringArray(R.array.autowhitelist_filter))) } // apk @@ -192,58 +198,47 @@ class FileScanner(private val path: File, context: Context){ fun startScan(): Long { isRunning = true var cycles: Byte = 0 - var maxCycles: Byte = 1 + var maxCycles: Byte = if (delete) prefs.getInt("multirun",1).toByte() else 1 var foundFiles: List - if (delete) maxCycles = prefs.getInt("multirun",1).toByte() // removes the need to 'clean' multiple times to get everything while (cycles < maxCycles) { // cycle indicator - if (gui != null) - (context as MainActivity?)!!.displayText( - "Running Cycle " + (cycles + 1) + "/" + maxCycles - ) + (context as MainActivity).addText( + "Running Cycle " + (cycles + 1) + "/" + maxCycles + ) // find/scan files foundFiles = listFiles // fetching this variable (List) triggers get function getListFiles(path) guiScanProgressMax = guiScanProgressMax + foundFiles.size // filter & delete - var tv: TextView? = null - for (file in foundFiles) { - if (filter(file)) { // filter - if (gui != null) tv = (context as MainActivity?)!!.displayDeletion(file) - + for (file in foundFiles){ + if (filter(file)){ // filter + val tv: TextView = (context as MainActivity?)!!.addText(file.absolutePath,"delete") kilobytesTotal += file.length() - if (delete) { + if (delete){ ++filesRemoved - // deletion - if (!file.delete() && tv != null) { // failed to remove file and the textView is visible (not null) - (context as MainActivity?)!!.runOnUiThread { - tv.setTextColor(Color.GRAY) // error effect - red looks too concerning + // failed to remove file and the textView is visible (not null) + if (!file.delete()) { + context.runOnUiThread { + // error effect - red looks too concerning + tv.setTextColor(Color.GRAY) } } } } - if (gui != null) { // progress - (context as MainActivity?)!!.runOnUiThread { - guiScanProgressProgress = guiScanProgressProgress + 1 - gui!!.statusTextView.text = String.format(Locale.US, "%s %.0f%%", - context!!.getString(R.string.status_running), - guiScanProgressProgress * 100.0 / guiScanProgressMax - ) // dont remove .0 part or crash - } - } + guiScanProgressProgress = guiScanProgressProgress + 1 + updateProgress!!.invoke(context,guiScanProgressProgress * 100.0 / guiScanProgressMax); } - - if (filesRemoved == 0) break // nothing found this run, no need to run again - filesRemoved = 0 // reset for next cycle + if (filesRemoved == 0) break + filesRemoved = 0 ++cycles } // cycle indicator - if (gui != null) (context as MainActivity?)!!.displayText("Finished!") + (context as MainActivity).addText("Finished!") isRunning = false return kilobytesTotal } @@ -256,16 +251,21 @@ class FileScanner(private val path: File, context: Context){ return ".+" + file.replace(".", "\\.") + "$" } - fun setGUI(gui: ActivityMainBinding?): FileScanner { - this.gui = gui + fun setUpdateProgress(updateProgress: ((context: Context, percent: Double) -> Unit)?): FileScanner { + this.updateProgress = updateProgress return this } - fun setResources(res: Resources?): FileScanner { + fun setResources(res: Resources): FileScanner { this.res = res return this } + fun setEmptyFile(emptyFile: Boolean): FileScanner { + this.emptyFile = emptyFile + return this + } + fun setEmptyDir(emptyDir: Boolean): FileScanner { this.emptyDir = emptyDir return this @@ -286,11 +286,6 @@ class FileScanner(private val path: File, context: Context){ return this } - fun setContext(context: Context?): FileScanner { - this.context = context - return this - } - companion object { // TODO remove local prefs objects, create setter for one instead @JvmField @@ -300,6 +295,7 @@ class FileScanner(private val path: File, context: Context){ } init { + BlacklistActivity.getBlackList(prefs) WhitelistActivity.getWhiteList(prefs) } } \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/PanicResponderActivity.kt b/app/src/main/java/theredspy15/ltecleanerfoss/PanicResponderActivity.kt index ecbeeaa6..4ed3425c 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/PanicResponderActivity.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/PanicResponderActivity.kt @@ -4,30 +4,34 @@ */ package theredspy15.ltecleanerfoss import android.app.Activity -import android.content.Intent import android.os.Bundle import android.os.Environment import androidx.preference.PreferenceManager class PanicResponderActivity: Activity(){ override fun onCreate(savedInstanceState:Bundle?) { + // TODO: when triggered, the ui lags and + // empty black screen until its done + // make it run in background super.onCreate(savedInstanceState) - val path = Environment.getExternalStorageDirectory() - val prefs = PreferenceManager.getDefaultSharedPreferences( - applicationContext - ) - FileScanner(path, applicationContext) - .setEmptyDir(prefs.getBoolean("empty",false)) - .setAutoWhite(prefs.getBoolean("auto_white",true)) - .setDelete(true) - .setCorpse(prefs.getBoolean("corpse", false)) - .setGUI(null) - .setContext(applicationContext) - .setUpFilters( - prefs.getBoolean("generic", true), - prefs.getBoolean("aggressive", false), - prefs.getBoolean("apk", false) - ) - .startScan() - finishAndRemoveTask() + Thread { + try { + val path = Environment.getExternalStorageDirectory() + val prefs = PreferenceManager.getDefaultSharedPreferences( + applicationContext + ) + // scanner setup + val fs = FileScanner(path, applicationContext) + .setEmptyFile(prefs.getBoolean("emptyFile", false)) + .setEmptyDir(prefs.getBoolean("emptyFolder", false)) + .setAutoWhite(prefs.getBoolean("auto_white", true)) + .setDelete(true) + .setCorpse(prefs.getBoolean("corpse", false)) + .setUpFilters( + prefs.getBoolean("generic", true), + prefs.getBoolean("apk", false) + ) + } catch (e: Exception) {} + finishAndRemoveTask() + }.start() } } \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/ScheduledWorker.kt b/app/src/main/java/theredspy15/ltecleanerfoss/ScheduledWorker.kt index 957d22d5..f6785929 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/ScheduledWorker.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/ScheduledWorker.kt @@ -3,31 +3,30 @@ * (C) 2024 MDP43140 */ package theredspy15.ltecleanerfoss -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context import android.os.Build import android.os.Environment -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import androidx.preference.PreferenceManager import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.WorkManager -import theredspy15.ltecleanerfoss.controllers.MainActivity.Companion.convertSize +import theredspy15.ltecleanerfoss.CommonFunctions.makeStatusNotification +import theredspy15.ltecleanerfoss.CommonFunctions.sendNotification +import theredspy15.ltecleanerfoss.CommonFunctions.convertSize +import theredspy15.ltecleanerfoss.Constants import android.content.SharedPreferences import java.util.concurrent.TimeUnit class ScheduledWorker(appContext: Context, workerParams: WorkerParameters): Worker(appContext, workerParams) { override fun doWork(): Result { try { - makeStatusNotification( + var notification = makeStatusNotification( applicationContext.getString(R.string.status_running), applicationContext ) + sendNotification(applicationContext, Constants.NOTIFICATION_ID_SERVICE, notification); val path = Environment.getExternalStorageDirectory() val prefs = PreferenceManager.getDefaultSharedPreferences( applicationContext @@ -35,15 +34,14 @@ class ScheduledWorker(appContext: Context, workerParams: WorkerParameters): Work // scanner setup val fs = FileScanner(path, applicationContext) - .setEmptyDir(prefs.getBoolean("empty", false)) + .setEmptyFile(prefs.getBoolean("emptyFile", false)) + .setEmptyDir(prefs.getBoolean("emptyFolder", false)) .setAutoWhite(prefs.getBoolean("auto_white", true)) .setDelete(true) .setCorpse(prefs.getBoolean("corpse", false)) - .setGUI(null) - .setContext(applicationContext) + .setUpdateProgress(::updatePercentage) .setUpFilters( prefs.getBoolean("generic", true), - prefs.getBoolean("aggressive", false), prefs.getBoolean("apk", false) ) @@ -53,16 +51,21 @@ class ScheduledWorker(appContext: Context, workerParams: WorkerParameters): Work applicationContext.getString(R.string.clean_notification) + " " + convertSize( kilobytesTotal ) - makeStatusNotification(title, applicationContext) + + notification = makeStatusNotification(title, applicationContext) + sendNotification(applicationContext, Constants.NOTIFICATION_ID_SERVICE, notification); return Result.success() } catch (e: Exception) { makeStatusNotification(e.toString(), applicationContext) return Result.failure() } } + private fun updatePercentage(context: Context, percent: Double){ + val notification = makeStatusNotification(context.getString(R.string.status_running), context) + notification.setProgress(100,percent.toInt(),false).setOnlyAlertOnce(true) + sendNotification(context, Constants.NOTIFICATION_ID_SERVICE, notification) + } companion object { - const val UNIQUE_WORK_NAME = "scheduled_cleanup_work" - const val WORK_TAG = "cleanup_work_tag" @JvmStatic fun enqueueWork(context: Context) { val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -71,60 +74,21 @@ class ScheduledWorker(appContext: Context, workerParams: WorkerParameters): Work .setRequiresBatteryNotLow(true) .setRequiresDeviceIdle(true) .build() - WorkManager.getInstance(context).cancelAllWorkByTag(UNIQUE_WORK_NAME) + WorkManager.getInstance(context).cancelAllWorkByTag(Constants.BGCLEAN_WORK_NAME) if (dailyCleanupInterval > 0){ val myPeriodicWork = PeriodicWorkRequestBuilder( dailyCleanupInterval, TimeUnit.HOURS, // Interval 15, TimeUnit.MINUTES // Flex interval for battery optimization ) - .addTag(WORK_TAG) + .addTag(Constants.BGCLEAN_WORK_TAG) .setConstraints(constraints) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( - UNIQUE_WORK_NAME, + Constants.BGCLEAN_WORK_NAME, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, myPeriodicWork ) } } - - fun makeStatusNotification(message: String?, context: Context) { - - // Name of Notification Channel for verbose notifications of background work - val VERBOSE_NOTIFICATION_CHANNEL_NAME: CharSequence = - context.getString(R.string.settings_notification_name) - val VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION = - context.getString(R.string.settings_notification_sum) - val NOTIFICATION_TITLE: CharSequence = context.getString(R.string.notification_title) - val CHANNEL_ID = "VERBOSE_NOTIFICATION" - val NOTIFICATION_ID = 1 - - // Make a channel if necessary - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - val importance = NotificationManager.IMPORTANCE_DEFAULT - val channel = - NotificationChannel(CHANNEL_ID, VERBOSE_NOTIFICATION_CHANNEL_NAME, importance) - channel.description = VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION - - // Add the channel - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - } - - // Create the notification - val builder = NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_baseline_cleaning_services_24) - .setContentTitle(NOTIFICATION_TITLE) - .setContentText(message) - .setAutoCancel(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setVibrate(LongArray(0)) - - // Show the notification - NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build()) - } } } \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/BlacklistActivity.kt b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/BlacklistActivity.kt new file mode 100644 index 00000000..e9d9c1a6 --- /dev/null +++ b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/BlacklistActivity.kt @@ -0,0 +1,171 @@ +/* + * (C) 2020-2023 Hunter J Drum + * (C) 2024 MDP43140 + */ +package theredspy15.ltecleanerfoss.controllers +import android.content.DialogInterface +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.CheckBox +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AlertDialog +import theredspy15.ltecleanerfoss.R +import theredspy15.ltecleanerfoss.App +import theredspy15.ltecleanerfoss.Constants.blacklistDefault +import theredspy15.ltecleanerfoss.Constants.blacklistOnDefault +import theredspy15.ltecleanerfoss.databinding.ActivityBlacklistBinding +class BlacklistActivity: AppCompatActivity(){ + lateinit var binding: ActivityBlacklistBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_blacklist) + binding = ActivityBlacklistBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.newButton.setOnClickListener { + val inputEditText = EditText(this) + val alertDialog = AlertDialog.Builder(this).create() + alertDialog.setTitle("Add filter") + alertDialog.setMessage("You can use Kotlin regular expression, such as \".*\"") + alertDialog.setView(inputEditText) + alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "OK"){ dialog:DialogInterface, _:Int -> + val userInput = inputEditText.text.toString().replace("^/sdcard/", "/storage/emulated/0") + if (userInput != "") addBlackList(App.prefs,userInput) + dialog.dismiss() + loadViews() + } + alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel)) { dialog:DialogInterface, _:Int -> + dialog.dismiss() + } + alertDialog.show() + } + getBlackList(App.prefs) + getBlacklistOn(App.prefs) + loadViews() + } + private fun loadViews() { + binding.pathsLayout.removeAllViews() + val layout = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + layout.setMargins(0,20,0,20) + if (blackList.isNullOrEmpty()) { + val textView = TextView(this) + textView.setText(R.string.empty_blacklist) + textView.textAlignment = View.TEXT_ALIGNMENT_CENTER + textView.textSize = 18f + runOnUiThread { binding.pathsLayout.addView(textView, layout) } + } else { + for (path in blackList) { + val horizontalLayout = LinearLayout(this) + val checkBox = CheckBox(this) + val button = Button(this) + button.text = path + button.textSize = 18f + button.isAllCaps = false + button.setPadding(0,0,0,0) + button.background = null + button.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + button.setOnClickListener { removeOrEditPattern(path) } + checkBox.isChecked = blackListOn.contains(path); + checkBox.setOnCheckedChangeListener { _, checked -> + setBlacklistOn(App.prefs,path,checked) + } + horizontalLayout.setBackgroundResource(R.drawable.rounded_view) + horizontalLayout.orientation = LinearLayout.HORIZONTAL + horizontalLayout.setPadding(12,12,12,12) + horizontalLayout.addView(checkBox) + horizontalLayout.addView(button) + runOnUiThread { binding.pathsLayout.addView(horizontalLayout, layout) } + } + } + } + private fun removeOrEditPattern(path: String?) { + val inputEditText = EditText(this) + inputEditText.setText(path!!) + val alertDialog = AlertDialog.Builder(this).create() + alertDialog.setTitle("Edit or remove filter") + alertDialog.setMessage("You can use Kotlin regular expression, such as \".*\"") + alertDialog.setView(inputEditText) + alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "OK"){ dialog:DialogInterface, _:Int -> + val userInput = inputEditText.text.toString().replace("^/sdcard/", "/storage/emulated/0") + rmBlackList(App.prefs,path) + if (userInput != "") addBlackList(App.prefs,userInput) + dialog.dismiss() + loadViews() + } + alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel)) { dialog:DialogInterface, _:Int -> + dialog.dismiss() + } + alertDialog.show() + } + + companion object { + private var blackList: ArrayList = ArrayList() + private var blackListOn: ArrayList = ArrayList() + fun getBlackList(prefs: SharedPreferences?): List { + if (blackList.isNullOrEmpty() && prefs != null) { + // Type mismatch: inferred type is + // (Mutable)Set? but + // (MutableCollection..Collection) was expected + blackList = ArrayList(prefs.getStringSet("blacklist",blacklistDefault)) + blackList.remove("[") + blackList.remove("]") + } + return blackList + } + fun addBlackList(prefs: SharedPreferences?, path: String) { + if (blackList.isNullOrEmpty()) getBlackList(prefs) + blackList.add(path) + blackList.distinct() + blackList.sort() + blackListOn.add(path) + prefs!! + .edit() + .putStringSet("blacklist", HashSet(blackList)) + .putStringSet("blacklistOn",HashSet(blackListOn)) + .apply() + } + fun rmBlackList(prefs: SharedPreferences?, path: String) { + if (blackList.isNullOrEmpty()) getBlackList(prefs) + blackList.remove(path) + blackListOn.remove(path) + prefs!! + .edit() + .putStringSet("blacklist", HashSet(blackList)) + .putStringSet("blacklistOn",HashSet(blackListOn)) + .apply() + } + fun getBlacklistOn(prefs: SharedPreferences?): List { + if (blackListOn.isNullOrEmpty() && prefs != null) { + val blackListOnSet = prefs.getStringSet("blacklistOn",blacklistOnDefault) + blackListOn = ArrayList(blackListOnSet) + for (path in blackListOnSet.orEmpty()) { + blackListOn.add(path) + } + } + return blackListOn + } + fun setBlacklistOn(prefs: SharedPreferences?, path: String, checked: Boolean) { + if (blackListOn.isNullOrEmpty()) getBlacklistOn(prefs) + if (checked) blackListOn.add(path) + else blackListOn.remove(path) + prefs!! + .edit() + .putStringSet("blacklistOn",HashSet(blackListOn)) + .apply() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/ErrorActivity.kt b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/ErrorActivity.kt new file mode 100644 index 00000000..fe42ae5a --- /dev/null +++ b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/ErrorActivity.kt @@ -0,0 +1,84 @@ +/* + * (C) 2024 Akane Foundation (https://github.com/AkaneTan/Gramophone/blob/beta/LICENSE) + * (C) 2024 MDP43140 + */ +package theredspy15.ltecleanerfoss.controllers +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Intent +import android.graphics.Typeface +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import java.time.format.DateTimeFormatter +import java.time.LocalDateTime +//import theredspy15.ltecleanerfoss.BuildConfig +import theredspy15.ltecleanerfoss.databinding.ActivityErrorBinding +import theredspy15.ltecleanerfoss.R +class ErrorActivity: AppCompatActivity(){ + private lateinit var binding: ActivityErrorBinding + override fun onCreate(savedInstanceState: Bundle?){ + super.onCreate(savedInstanceState) + binding = ActivityErrorBinding.inflate(layoutInflater) + setContentView(binding.root) + +// val appVersion: String = BuildConfig.VERSION_NAME + val appLang: String = "TODO" + val osVersion: String = (System.getProperty("os.name") ?: "Android") + + (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) " ${Build.VERSION.BASE_OS}" else "") + + " " + Build.VERSION.RELEASE + + " - " + Build.VERSION.SDK_INT + val exceptionMessage: String? = intent.getStringExtra("exception_message") + val formattedDateTime = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm") + .format(LocalDateTime.now()) + binding.error.typeface = Typeface.MONOSPACE + +// .append("App version: $appVersion").append('\n') + val text = StringBuilder() + .append("App language: $appLang") + .append("\nDevice brand: ${Build.BRAND}") + .append("\nDevice model: ${Build.MODEL}") + .append("\nOS Version : $osVersion") + .append("\nGMT Time : $formattedDateTime").append('\n').append('\n') + .append(exceptionMessage) + .toString() +// .append("* __App version:__ $appVersion").append('\n') + val formattedText = StringBuilder() + .append("## Exception") + .append("\n* __App language:__ $appLang") + .append("\n* __Device brand:__ ${Build.BRAND}") + .append("\n* __Device model:__ ${Build.MODEL}") + .append("\n* __OS Version :__ $osVersion") + .append("\n* __GMT Time :__ $formattedDateTime") + .append("\n
Crash log

") + .append("\n```\n") + .append(exceptionMessage) + .append("\n```") + .append("\n


") + .toString() + + binding.error.text = text + binding.shareLogBtn.setOnClickListener { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, text) + startActivity(Intent.createChooser(intent,"Share with")) + } + binding.shareFormattedLogBtn.setOnClickListener { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, formattedText) + startActivity(Intent.createChooser(intent,"Share with")) + } + binding.reportIssueGithubBtn.setOnClickListener { + startActivity(Intent( + Intent.ACTION_VIEW, + Uri.parse("https://github.com/mdp43140/LTECleanerFOSS/issues") + )) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/MainActivity.kt b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/MainActivity.kt index 45ff5b37..4cfa26b6 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/MainActivity.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/MainActivity.kt @@ -3,17 +3,16 @@ * (C) 2024 MDP43140 */ package theredspy15.ltecleanerfoss.controllers -import com.google.android.material.color.DynamicColors import android.Manifest import android.annotation.SuppressLint import android.app.ActivityManager import android.content.ClipData import android.content.ClipboardManager +import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.content.res.Configuration import android.graphics.Color import android.net.Uri import android.os.Build @@ -21,7 +20,6 @@ import android.os.Bundle import android.os.Environment import android.os.Looper import android.provider.Settings -import android.view.View import android.widget.ImageView import android.widget.ScrollView import android.widget.TextView @@ -29,14 +27,17 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityCompat -import androidx.preference.PreferenceManager import android.content.DialogInterface import theredspy15.ltecleanerfoss.App import theredspy15.ltecleanerfoss.FileScanner -import theredspy15.ltecleanerfoss.R +import theredspy15.ltecleanerfoss.CommonFunctions.convertSize +import theredspy15.ltecleanerfoss.CommonFunctions.makeStatusNotification +import theredspy15.ltecleanerfoss.CommonFunctions.sendNotification +import theredspy15.ltecleanerfoss.Constants import theredspy15.ltecleanerfoss.databinding.ActivityMainBinding +import theredspy15.ltecleanerfoss.R import java.io.File -import java.text.DecimalFormat +import java.util.Locale class MainActivity: AppCompatActivity(){ private lateinit var binding: ActivityMainBinding private lateinit var mDialogBuilder: AlertDialog.Builder @@ -48,10 +49,8 @@ class MainActivity: AppCompatActivity(){ binding.cleanBtn.setOnClickListener { clean() } binding.settingsBtn.setOnClickListener { settings() } binding.whitelistBtn.setOnClickListener { whitelist() } - WhitelistActivity.getWhiteList(prefs) + WhitelistActivity.getWhiteList(App.prefs) mDialogBuilder = AlertDialog.Builder(this) - if (prefs!!.getBoolean("dynamicColor",true)) DynamicColors.applyToActivityIfAvailable(this) - } private fun settings(){ startActivity(Intent(this,SettingsActivity::class.java)) @@ -63,7 +62,7 @@ class MainActivity: AppCompatActivity(){ private fun analyze(){ if (!FileScanner.isRunning){ requestWriteExternalPermission() - Thread { scan(false) }.start() + scan(false) } } @@ -73,9 +72,9 @@ class MainActivity: AppCompatActivity(){ private fun clean() { if (!FileScanner.isRunning) { requestWriteExternalPermission() - if (prefs == null) println("prefs is null!") - if (prefs!!.getBoolean("one_click",false)){ - Thread { scan(true) }.start() // one-click enabled + if (App.prefs == null) println("prefs is null!") + if (App.prefs!!.getBoolean("one_click",false)){ + scan(true) // one-click enabled } else { // one-click disabled val mDialog: AlertDialog = mDialogBuilder.create() mDialog.setTitle(getString(R.string.are_you_sure_deletion_title)) @@ -83,7 +82,7 @@ class MainActivity: AppCompatActivity(){ mDialog.setCancelable(false) mDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.clean)){ dialogInterface: DialogInterface, _: Int -> dialogInterface.dismiss() - Thread { scan(true) }.start() + scan(true) } mDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getString(R.string.cancel)){ dialogInterface: DialogInterface, _: Int -> dialogInterface.dismiss() } mDialog.show() @@ -116,106 +115,96 @@ class MainActivity: AppCompatActivity(){ * unless nothing is found to begin with */ @SuppressLint("SetTextI18n") - private fun scan(delete: Boolean) { - Looper.prepare() - runOnUiThread { - binding.cleanBtn.isEnabled = !FileScanner.isRunning - binding.analyzeBtn.isEnabled = !FileScanner.isRunning - binding.statusTextView.text = getString(R.string.status_running) - } - reset() - if (prefs!!.getBoolean("clipboard", false)) clearClipboard() - if (prefs!!.getBoolean("closebgapps", false)) { - val am = this.getSystemService("activity") as ActivityManager - for (pkg in getPackageManager().getInstalledApplications(8704)) { - am.killBackgroundProcesses(pkg.processName) + private fun scan(delete: Boolean){ + Thread { + Looper.prepare() + runOnUiThread { + binding.cleanBtn.isEnabled = !FileScanner.isRunning + binding.analyzeBtn.isEnabled = !FileScanner.isRunning + binding.statusTextView.text = getString(R.string.status_running) + binding.fileListView.removeAllViews() } - } - val path = Environment.getExternalStorageDirectory() + if (App.prefs!!.getBoolean("clipboard", false)) clearClipboard() + if (App.prefs!!.getBoolean("closebgapps", false)) stopBgApps() + val path = Environment.getExternalStorageDirectory() - // scanner setup - val fs = FileScanner(path,this) - .setEmptyDir(prefs!!.getBoolean("empty", false)) - .setAutoWhite(prefs!!.getBoolean("auto_white", true)) - .setDelete(delete) - .setCorpse(prefs!!.getBoolean("corpse", false)) - .setGUI(binding) - .setContext(this) - .setUpFilters( - prefs!!.getBoolean("generic",true), - prefs!!.getBoolean("aggressive",false), - prefs!!.getBoolean("apk",false) - ) + // scanner setup + val fs = FileScanner(path,this) + .setEmptyFile(App.prefs!!.getBoolean("emptyFile", false)) + .setEmptyDir(App.prefs!!.getBoolean("emptyFolder", false)) + .setAutoWhite(App.prefs!!.getBoolean("auto_white", true)) + .setDelete(delete) + .setCorpse(App.prefs!!.getBoolean("corpse", false)) + .setUpdateProgress(::updatePercentage) + .setUpFilters( + App.prefs!!.getBoolean("generic",true), + App.prefs!!.getBoolean("apk",false) + ) - // failed scan - if (path.listFiles() == null){ // is this needed? yes. - runOnUiThread { binding.fileListView.addView(printTextView(getString(R.string.failed_scan),Color.RED)) } + // failed scan + if (path.listFiles() == null){ // is this needed? yes. + addText(getString(R.string.failed_scan),Color.RED) + } + + // run the scan and put KBs found/freed text + val kilobytesTotal = fs.startScan() + runOnUiThread { + binding.statusTextView.text = + getString(if (delete) R.string.freed else R.string.found) + + " " + convertSize(kilobytesTotal) + binding.cleanBtn.isEnabled = !FileScanner.isRunning + binding.analyzeBtn.isEnabled = !FileScanner.isRunning + } + binding.fileScrollView.post { binding.fileScrollView.fullScroll(ScrollView.FOCUS_DOWN) } + Looper.loop() + }.start() + } + + private fun stopBgApps(){ + val am = this.getSystemService("activity") as ActivityManager + for (pkg in getPackageManager().getInstalledApplications(8704)) { + am.killBackgroundProcesses(pkg.processName) } + } - // run the scan and put KBs found/freed text - val kilobytesTotal = fs.startScan() - runOnUiThread { - binding.statusTextView.text = - getString(if (delete) R.string.freed else R.string.found) + - " " + convertSize(kilobytesTotal) - binding.cleanBtn.isEnabled = !FileScanner.isRunning - binding.analyzeBtn.isEnabled = !FileScanner.isRunning + /** + * Update GUI Percentage + */ + private fun updatePercentage(context: Context, percent: Double){ + (context as MainActivity?)!!.runOnUiThread { + binding.statusTextView.text = String.format( + Locale.US, "%s %.0f%%", + context.getString(R.string.status_running), + percent + ) // dont remove .0 part or crash } - binding.fileScrollView.post { binding.fileScrollView.fullScroll(ScrollView.FOCUS_DOWN) } - Looper.loop() } /** - * Convenience method to quickly create a textview - * @param text - text of textview - * @return - created textview + * Convenient method to + * quickly add a text + * @param text - text of textView + * @return - created textView */ - private fun printTextView(text: String, color: Int): TextView { + fun addText(text: String, color: Int): TextView { val textView = TextView(this@MainActivity) textView.setTextColor(color) textView.text = text textView.setPadding(3,3,3,3) - return textView - } - - /** - * Displaying text for files that have been removed - */ - fun displayDeletion(file: File): TextView { - // creating and adding a text view to the scroll view with path to file - val textView = printTextView(file.absolutePath, resources.getColor(R.color.colorAccent,resources.newTheme())) - // adding to scroll view runOnUiThread { binding.fileListView.addView(textView) } - // scroll to bottom binding.fileScrollView.post { binding.fileScrollView.fullScroll(ScrollView.FOCUS_DOWN) } return textView } - - /** - * Displays generic text - */ - fun displayText(text: String) { - // creating and adding a text view to the scroll view with path to file - val textView = printTextView(text, Color.YELLOW) - - // adding to scroll view - runOnUiThread { binding.fileListView.addView(textView) } - - // scroll to bottom - binding.fileScrollView.post { binding.fileScrollView.fullScroll(ScrollView.FOCUS_DOWN) } + fun addText(text: String, type: String): TextView{ + return addText(text, when (type){ + "delete" -> resources.getColor(R.color.colorAccent,resources.newTheme()) + else -> Color.YELLOW + }) } - - /** - * Removes all views present in fileListView (linear view), and sets found and removed - * files to 0 - */ - private fun reset() { - prefs = PreferenceManager.getDefaultSharedPreferences(this) - runOnUiThread { - binding.fileListView.removeAllViews() - } + fun addText(text: String): TextView{ + return addText(text,Color.YELLOW) } /** @@ -243,7 +232,9 @@ class MainActivity: AppCompatActivity(){ } /** - * Handles the whether the user grants permission. Shows an alert dialog asking the user to give storage permission. + * Handles whether the user grants permission. + * Shows an alert dialog asking + * user to give storage permission. */ override fun onRequestPermissionsResult( requestCode:Int, @@ -251,7 +242,10 @@ class MainActivity: AppCompatActivity(){ grantResults:IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == 1 && grantResults.isNotEmpty() && grantResults[0] != PackageManager.PERMISSION_GRANTED){ + if ( + requestCode == 1 && + grantResults.isNotEmpty() && + grantResults[0] != PackageManager.PERMISSION_GRANTED){ val mDialog: AlertDialog = mDialogBuilder.create() mDialog.setTitle("Grant a permission") mDialog.setMessage(getString(R.string.prompt_string)) @@ -264,21 +258,4 @@ class MainActivity: AppCompatActivity(){ mDialog.show() } } - - - companion object { - @JvmField var prefs:SharedPreferences? = App.prefs - @JvmStatic fun convertSize(length: Long): String { - val format = DecimalFormat("#.##") - val kib:Long = 1024 - val mib:Long = 1048576 - return if (length > mib) { - format.format(length / mib) + " MB" - } else if (length > kib) { - format.format(length / kib) + " KB" - } else { - format.format(length) + " B" - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/SettingsActivity.kt b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/SettingsActivity.kt index 0e051857..112ed27e 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/SettingsActivity.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/SettingsActivity.kt @@ -3,9 +3,12 @@ * (C) 2024 MDP43140 */ package theredspy15.ltecleanerfoss.controllers +import android.content.Context import android.content.DialogInterface -import android.os.Bundle +import android.content.Intent +import android.content.SharedPreferences import android.net.Uri +import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.ActivityResultLauncher @@ -17,76 +20,63 @@ import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager -import theredspy15.ltecleanerfoss.ScheduledWorker.Companion.enqueueWork -import theredspy15.ltecleanerfoss.R +//import org.acra.ACRA import org.json.JSONArray import org.json.JSONObject +import theredspy15.ltecleanerfoss.App +import theredspy15.ltecleanerfoss.CommonFunctions.writeContentToUri +import theredspy15.ltecleanerfoss.ScheduledWorker.Companion.enqueueWork +import theredspy15.ltecleanerfoss.R class SettingsActivity: AppCompatActivity(){ override fun onCreate(savedInstanceState:Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) - val prefs = PreferenceManager.getDefaultSharedPreferences(this) + loadFragment() + } + fun loadFragment(){ val preferenceFragment = MyPreferenceFragment() preferenceFragment.setImportFileLauncher(registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - try { - if (uri != null){ - val inputStream = contentResolver.openInputStream(uri) - val jsonBytes = inputStream?.readBytes() - inputStream?.close() - if (jsonBytes != null && prefs != null) { - val jsonString = jsonBytes.toString(Charsets.UTF_8) - val jsonObject = JSONObject(jsonString) - val prefsEditor = prefs.edit() - - for (key in jsonObject.keys()){ - val value = jsonObject.get(key) - when (value) { - is String -> prefsEditor.putString(key, value) - is Int -> prefsEditor.putInt(key, value) - is Boolean -> prefsEditor.putBoolean(key, value) - is JSONArray -> { - val stringArray = mutableListOf() - for (i in 0 until value.length()) stringArray.add(value.optString(i)) - prefsEditor.putStringSet(key, stringArray.toSet()) - } - else -> { - // Handle unsupported data type or provide a fallback - Toast.makeText(this, - String.format("Unsupported data type: %s = %s",key,value), - Toast.LENGTH_SHORT - ).show() - } - } + if (uri != null){ + val inputStream = contentResolver.openInputStream(uri) + val jsonBytes = inputStream?.readBytes() + inputStream?.close() + val jsonString = jsonBytes!!.toString(Charsets.UTF_8) + val jsonObject = JSONObject(jsonString) + val prefsEditor = App.prefs?.edit() + for (key in jsonObject.keys()){ + val value = jsonObject.get(key) + when (value){ + is String -> prefsEditor?.putString(key, value) + is Int -> prefsEditor?.putInt(key, value) + is Boolean -> prefsEditor?.putBoolean(key, value) + is JSONArray -> { + val stringArray = mutableListOf() + for (i in 0 until value.length()) stringArray.add(value.optString(i)) + prefsEditor?.putStringSet(key, stringArray.toSet()) + } + else -> { + // Handle unsupported data type or provide a fallback + Toast.makeText(this, "Unsupported data type: $key: $value", Toast.LENGTH_SHORT).show() } - - prefsEditor.apply() - Toast.makeText(this, "Settings imported successfully!", Toast.LENGTH_SHORT).show() - - supportFragmentManager.beginTransaction() - .replace(R.id.layout, MyPreferenceFragment()) - .commit() } } - } catch (e: Exception){Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()} + prefsEditor?.apply() + Toast.makeText(this, "Settings imported!", Toast.LENGTH_SHORT).show() + loadFragment() + } }) +// ANTI UNLUCK 69 // preferenceFragment.setExportFileLauncher(registerForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri -> - val jsonData: String = JSONObject(prefs?.all).toString() // TODO - warning - Type mismatch: inferred type is (Mutable)Map? but (MutableMap..Map<*, *>) was expected if (uri != null){ - writeContentToUri(uri, jsonData); - Toast.makeText(this, "Settings imported successfully!", Toast.LENGTH_SHORT).show() + val jsonData: String = JSONObject(App.prefs?.all).toString() // TODO - warning - Type mismatch: inferred type is (Mutable)Map? but (MutableMap..Map<*, *>) was expected + writeContentToUri(this, uri, jsonData); + Toast.makeText(this, "Settings exported!", Toast.LENGTH_SHORT).show() } }) supportFragmentManager.beginTransaction() .replace(R.id.layout, preferenceFragment) .commit() } - - private fun writeContentToUri(uri: Uri, content: String) { - this.contentResolver.openOutputStream(uri)?.use { outputStream -> - outputStream.write(content.toByteArray()) - } - } - class MyPreferenceFragment: PreferenceFragmentCompat() { private lateinit var importFileLauncher: ActivityResultLauncher> private lateinit var exportFileLauncher: ActivityResultLauncher @@ -94,20 +84,15 @@ class SettingsActivity: AppCompatActivity(){ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) - findPreference("aggressive")!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _:Preference, value:Any? -> - val isChecked = (value as Boolean) - if (isChecked){ - val filtersFiles = resources.getStringArray(R.array.aggressive_filter_folders) - val alertDialog = AlertDialog.Builder(requireContext()).create() - alertDialog.setTitle(getString(R.string.aggressive_filter_what_title)) - alertDialog.setMessage( - getString(R.string.adds_the_following) + " " + filtersFiles.contentToString() - ) - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE,"OK"){ dialog:DialogInterface, _:Int -> dialog.dismiss() } - alertDialog.show() - } - true + findPreference("blacklist")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _ -> + startActivity(Intent(requireContext(), BlacklistActivity::class.java)) + false + } + findPreference("whitelist")!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { _ -> + startActivity(Intent(requireContext(), WhitelistActivity::class.java)) + false } findPreference("cleanevery")!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _:Preference, _:Any? -> @@ -116,31 +101,26 @@ class SettingsActivity: AppCompatActivity(){ } findPreference("theme")!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _:Preference, value:Any? -> - val light = resources.getStringArray(R.array.themes)[1] - val dark = resources.getStringArray(R.array.themes)[2] - if (value == dark) { // dark - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - } else if (value == light) { // light - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } else { // auto - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - } + AppCompatDelegate.setDefaultNightMode(when (value){ + resources.getStringArray(R.array.themes)[1] -> AppCompatDelegate.MODE_NIGHT_NO + resources.getStringArray(R.array.themes)[2] -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + }) true } findPreference("dataImport")!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> importFileLauncher.launch(arrayOf("application/json")) - true + false } findPreference("dataExport")!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> exportFileLauncher.launch("LTECleaner_settings.json") - true + false } - findPreference("crash")!!.onPreferenceClickListener = + findPreference("__doomedBruhhh")!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> - val result = 10 / 0 - result == 0 + throw IllegalAccessException("Get doomed haha >:)") } } diff --git a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/WhitelistActivity.kt b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/WhitelistActivity.kt index b5efb2fd..a2bd611b 100644 --- a/app/src/main/java/theredspy15/ltecleanerfoss/controllers/WhitelistActivity.kt +++ b/app/src/main/java/theredspy15/ltecleanerfoss/controllers/WhitelistActivity.kt @@ -5,20 +5,22 @@ package theredspy15.ltecleanerfoss.controllers import android.content.DialogInterface import android.content.SharedPreferences -import android.graphics.Color -import android.graphics.drawable.GradientDrawable import android.net.Uri import android.os.Bundle import android.os.Environment import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.CheckBox import android.widget.LinearLayout import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AlertDialog import theredspy15.ltecleanerfoss.R +import theredspy15.ltecleanerfoss.App +import theredspy15.ltecleanerfoss.Constants.whitelistDefault +import theredspy15.ltecleanerfoss.Constants.whitelistOnDefault import theredspy15.ltecleanerfoss.databinding.ActivityWhitelistBinding class WhitelistActivity: AppCompatActivity(){ lateinit var binding: ActivityWhitelistBinding @@ -31,18 +33,17 @@ class WhitelistActivity: AppCompatActivity(){ // Creates a dialog asking for a file/folder name to add to the whitelist mGetContent.launch(Uri.fromFile(Environment.getDataDirectory())) } - getWhiteList(MainActivity.prefs) + getWhiteList(App.prefs) + getWhitelistOn(App.prefs) loadViews() } - private fun loadViews() { binding.pathsLayout.removeAllViews() val layout = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) - layout.setMargins(0, 20, 0, 20) - + layout.setMargins(0,20,0,20) if (whiteList.isNullOrEmpty()) { val textView = TextView(this) textView.setText(R.string.empty_whitelist) @@ -51,24 +52,38 @@ class WhitelistActivity: AppCompatActivity(){ runOnUiThread { binding.pathsLayout.addView(textView, layout) } } else { for (path in whiteList) { + val horizontalLayout = LinearLayout(this) + val checkBox = CheckBox(this) val button = Button(this) button.text = path button.textSize = 18f button.isAllCaps = false + button.setPadding(0,0,0,0) + button.background = null + button.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) button.setOnClickListener { removePath(path, button) } - button.setPadding(24, 24, 24, 24) - button.setBackgroundResource(R.drawable.rounded_view) - runOnUiThread { binding.pathsLayout.addView(button, layout) } + checkBox.isChecked = whiteListOn.contains(path); + checkBox.setOnCheckedChangeListener { _, checked -> + setWhitelistOn(App.prefs,path,checked) + } + horizontalLayout.setBackgroundResource(R.drawable.rounded_view) + horizontalLayout.orientation = LinearLayout.HORIZONTAL + horizontalLayout.setPadding(12,12,12,12) + horizontalLayout.addView(checkBox) + horizontalLayout.addView(button) + runOnUiThread { binding.pathsLayout.addView(horizontalLayout, layout) } } } } - private fun removePath(path: String?, button: Button?) { val alertDialog = AlertDialog.Builder(this).create() alertDialog.setTitle(getString(R.string.remove_from_whitelist)) alertDialog.setMessage(path!!) alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, getString(R.string.delete)){ dialogInterface:DialogInterface, _:Int -> - rmWhiteList(MainActivity.prefs,path); + rmWhiteList(App.prefs,path) dialogInterface.dismiss() binding.pathsLayout.removeView(button) } @@ -80,41 +95,66 @@ class WhitelistActivity: AppCompatActivity(){ * Prepare a dialog that asks for a file/folder name to add to the whitelist */ private var mGetContent = registerForActivityResult(OpenDocumentTree()) { uri: Uri? -> - if (uri != null) { + if (uri != null){ val path: String = uri.path!!.substring(uri.path!!.indexOf(":") + 1) - addWhiteList(MainActivity.prefs, path) + addWhiteList(App.prefs, "/storage/emulated/0/" + path) + loadViews() } - loadViews() } companion object { private var whiteList: ArrayList = ArrayList() + private var whiteListOn: ArrayList = ArrayList() fun getWhiteList(prefs: SharedPreferences?): List { if (whiteList.isNullOrEmpty() && prefs != null) { // Type mismatch:inferred type is // (Mutable)Set? but // (MutableCollection..Collection) was expected - whiteList = ArrayList(prefs.getStringSet("whitelist", emptySet())) + whiteList = ArrayList(prefs.getStringSet("whitelist",whitelistDefault)) whiteList.remove("[") whiteList.remove("]") } return whiteList } fun addWhiteList(prefs: SharedPreferences?, path: String) { - // TODO: check for duplicates first before adding it if (whiteList.isNullOrEmpty()) getWhiteList(prefs) whiteList.add(path) + whiteList.distinct() + whiteList.sort() + whiteListOn.add(path) prefs!! .edit() .putStringSet("whitelist", HashSet(whiteList)) + .putStringSet("whitelistOn", HashSet(whiteListOn)) .apply() } fun rmWhiteList(prefs: SharedPreferences?, path: String) { if (whiteList.isNullOrEmpty()) getWhiteList(prefs) whiteList.remove(path) + whiteListOn.remove(path) prefs!! .edit() .putStringSet("whitelist", HashSet(whiteList)) + .putStringSet("whitelistOn", HashSet(whiteListOn)) + .apply() + } + fun getWhitelistOn(prefs: SharedPreferences?): List { + if (whiteListOn.isNullOrEmpty() && prefs != null) { + val whiteListOnSet = prefs.getStringSet("whitelistOn",whitelistOnDefault) + whiteListOn = ArrayList(whiteListOnSet) + for (path in whiteListOnSet.orEmpty()) { + whiteListOn.add(path) + } + } + return whiteListOn + } + fun setWhitelistOn(prefs: SharedPreferences?, path: String, checked: Boolean) { + if (whiteListOn.isNullOrEmpty()) getWhitelistOn(prefs) + if (checked) whiteListOn.add(path) + else whiteListOn.remove(path) + prefs!! + .edit() + .putStringSet("whitelistOn",HashSet(whiteListOn)) .apply() } } diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index be0bc30e..ec4b294f 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -8,57 +8,61 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" - android:weightSum="4" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".controllers.MainActivity" android:id="@+id/main_layout"> - + android:layout_marginStart="16dp" + android:layout_marginEnd="12dp" + android:layout_weight="2"> + + + + + - - + android:layout_gravity="center" + android:layout_marginTop="10dp" + android:layout_marginStart="30dp" + android:layout_marginEnd="30dp" + android:layout_marginBottom="10dp" + android:fontFamily="sans-serif-condensed-medium" + android:textAlignment="center" + android:textColor="?attr/Accents" + android:textSize="20sp" /> + - - - + android:layout_gravity="center" + app:columnCount="1" + app:rowCount="4">