From 6279eeda8140a8c3f3b49e6828831faddbc9ae14 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Sat, 10 Oct 2026 02:29:27 +0530 Subject: [PATCH 1/6] Prevent NPE when getting relative paths --- .../main/java/co/adityarajput/fileflow/data/models/Action.kt | 2 +- .../java/co/adityarajput/fileflow/data/models/RemoteAction.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt b/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt index c1a0afe..a10dfd7 100644 --- a/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt +++ b/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt @@ -155,7 +155,7 @@ sealed class Action { val srcFileName = srcFile.name ?: continue var destFileName = getDestFileName(srcFile) - val relativePath = srcFile.parent!!.pathRelativeTo(src) + val relativePath = srcFile.parent?.pathRelativeTo(src) val destSubDir = if (!preserveStructure || relativePath == null) destDir else destDir.createDirectory(relativePath) diff --git a/app/src/main/java/co/adityarajput/fileflow/data/models/RemoteAction.kt b/app/src/main/java/co/adityarajput/fileflow/data/models/RemoteAction.kt index 1572945..8d279d2 100644 --- a/app/src/main/java/co/adityarajput/fileflow/data/models/RemoteAction.kt +++ b/app/src/main/java/co/adityarajput/fileflow/data/models/RemoteAction.kt @@ -180,7 +180,7 @@ sealed class RemoteAction : Action() { for ((srcFile, _) in srcFiles) { val srcFileName = srcFile.name ?: continue var destFileName = getDestFileName(srcFile) - val relativePath = srcFile.parent!!.pathRelativeTo(src) + val relativePath = srcFile.parent?.pathRelativeTo(src) if (destServer == null) { val destSubDir = From 899499c23ae97e1509b0a57d91b7a76a3a62a69c Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Sat, 10 Oct 2026 13:35:54 +0530 Subject: [PATCH 2/6] Display spinner when executing rule --- .../fileflow/viewmodels/RulesViewModel.kt | 37 ++++++++++++------- .../fileflow/views/screens/RulesScreen.kt | 20 +++++----- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt b/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt index 52086fb..fa2124c 100644 --- a/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt +++ b/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt @@ -31,24 +31,35 @@ class RulesViewModel(private val repository: Repository) : ViewModel() { var selectedRule by mutableStateOf(null) + var executingRuleIds by mutableStateOf(emptySet()) + private set + fun executeRule(context: Context, showToast: (String) -> Unit) { + val rule = selectedRule ?: return + if (rule.id in executingRuleIds) return + executingRuleIds += rule.id + viewModelScope.launch { - val latestLogBeforeExecution = Logger.logs.lastOrNull() + try { + val latestLogBeforeExecution = Logger.logs.lastOrNull() - withContext(Dispatchers.IO) { - selectedRule!!.action.execute(context) { - repository.registerExecution( - selectedRule!!, - Execution(it, selectedRule!!.action.verb), - ) + withContext(Dispatchers.IO) { + rule.action.execute(context) { + repository.registerExecution( + rule, + Execution(it, rule.action.verb), + ) + } } - } - val recentErrorLog = Logger.logs - .dropWhile { it != latestLogBeforeExecution }.drop(1) - .firstOrNull { it.contains("[ERROR]") } - if (recentErrorLog != null) { - showToast("Error:" + recentErrorLog.substringAfter("[ERROR]")) + val recentErrorLog = Logger.logs + .dropWhile { it != latestLogBeforeExecution }.drop(1) + .firstOrNull { it.contains("[ERROR]") } + if (recentErrorLog != null) { + showToast("Error:" + recentErrorLog.substringAfter("[ERROR]")) + } + } finally { + executingRuleIds -= rule.id } } } diff --git a/app/src/main/java/co/adityarajput/fileflow/views/screens/RulesScreen.kt b/app/src/main/java/co/adityarajput/fileflow/views/screens/RulesScreen.kt index 0784de8..35b6b55 100644 --- a/app/src/main/java/co/adityarajput/fileflow/views/screens/RulesScreen.kt +++ b/app/src/main/java/co/adityarajput/fileflow/views/screens/RulesScreen.kt @@ -1,9 +1,6 @@ package co.adityarajput.fileflow.views.screens -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.* @@ -97,11 +94,16 @@ fun RulesScreen( else viewModel.selectedRule = it }, ) { - IconButton({ viewModel.dialogState = DialogState.EXECUTE }) { - Icon( - painterResource(R.drawable.play_circle), - stringResource(R.string.execute_rule), - ) + IconButton( + { viewModel.dialogState = DialogState.EXECUTE }, + enabled = it.id !in viewModel.executingRuleIds, + ) { + if (it.id !in viewModel.executingRuleIds) + Icon( + painterResource(R.drawable.play_circle), + stringResource(R.string.execute_rule), + ) + else CircularProgressIndicator(Modifier.size(24.dp)) } IconButton({ viewModel.dialogState = DialogState.TOGGLE_RULE }) { Icon( From 3ce059a9f135b1529c8879f43b96b0d69092a336 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Sat, 10 Oct 2026 17:20:22 +0530 Subject: [PATCH 3/6] Add unit tests --- .../fileflow/data/ConvertersTest.kt | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 app/src/test/kotlin/co/adityarajput/fileflow/data/ConvertersTest.kt diff --git a/app/src/test/kotlin/co/adityarajput/fileflow/data/ConvertersTest.kt b/app/src/test/kotlin/co/adityarajput/fileflow/data/ConvertersTest.kt new file mode 100644 index 0000000..d6ce278 --- /dev/null +++ b/app/src/test/kotlin/co/adityarajput/fileflow/data/ConvertersTest.kt @@ -0,0 +1,139 @@ +package co.adityarajput.fileflow.data + +import co.adityarajput.fileflow.data.models.Action +import co.adityarajput.fileflow.data.models.RemoteAction +import co.adityarajput.fileflow.data.models.Server +import co.adityarajput.fileflow.utils.FileSuperlative +import org.junit.Assert.assertEquals +import org.junit.Test + +class ConvertersTest { + @Test + fun Converters_Action() { + val actions = Action.entries + RemoteAction.entries + listOf( + Action.MOVE( + "/storage/emulated/0/AntennaPod", + "AntennaPodBackup-\\d{4}-\\d{2}-\\d{2}\\.db", + "/storage/emulated/0/Backups", + "AntennaPod.db", + scanSubdirectories = true, + overwriteExisting = true, + preserveStructure = false, + ), + Action.MOVE( + "/storage/emulated/0/Backups", + "PipePipeData-\\d{8}_\\d{6}\\.zip", + "/storage/emulated/0/Backups", + $$"PipePipe-${uuid}.zip", + keepOriginal = false, + superlative = FileSuperlative.LARGEST, + deleteEmptySrcSubdirectories = true, + ), + Action.DELETE_STALE( + "/storage/emulated/0/Download", + "(NotiFilter|Alarmetrics|FileFlow|MinCal)_v[\\d\\.]+\\.apk", + 90, + scanSubdirectories = true, + deleteEmptySrcSubdirectories = true, + ), + Action.ZIP( + "/storage/emulated/0/Documents/Notes", + "(.*)\\.md", + "/storage/emulated/0/Backups", + "Notes.zip", + scanSubdirectories = true, + overwriteExisting = true, + preserveStructure = false, + ), + Action.EMIT_CHANGES( + "/storage/emulated/0/Movies", + ".*\\.mp4", + "org.amoradi.syncopoli.SYNC_PROFILE", + "org.amoradi.syncopoli", + """{"profile_name": "Movies Backup"}""", + true, + 3_600_000L * 3, + ), + RemoteAction.MOVE( + Server( + "192.168.1.12", + 3456, + "admin", + "hunter2", + null, + 1, + ), + "backups/navidrome", + "navidrome_backup_[\\d_\\.]{19}\\.db", + Server( + "192.168.1.78", + 9012, + "sftp", + null, + "passkey", + 2, + ), + "backups/glacial", + "navidrome.db", + overwriteExisting = true, + ), + ) + + val serializedActions = listOf( + """{"type":"co.adityarajput.fileflow.data.models.Action.MOVE","src":"","srcFileNamePattern":"","dest":"","destFileNameTemplate":""}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.DELETE_STALE","src":"","srcFileNamePattern":""}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.ZIP","src":"","srcFileNamePattern":"","dest":"","destFileNameTemplate":""}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.EMIT_CHANGES","src":"","srcFileNamePattern":"","intent":"","packageName":""}""", + """{"type":"co.adityarajput.fileflow.data.models.RemoteAction.MOVE","srcServer":null,"src":"","srcFileNamePattern":"","destServer":null,"dest":"","destFileNameTemplate":""}""", + """{"type":"co.adityarajput.fileflow.data.models.RemoteAction.DELETE_STALE","srcServer":{"host":"","port":0,"username":"","encryptedPassword":null,"encryptedPrivateKey":null,"id":0},"src":"","srcFileNamePattern":""}""", + """{"type":"co.adityarajput.fileflow.data.models.RemoteAction.ZIP","srcServer":null,"src":"","srcFileNamePattern":"","destServer":null,"dest":"","destFileNameTemplate":""}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.EMIT_CHANGES","src":"","srcFileNamePattern":"","intent":"","packageName":""}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.MOVE","src":"/storage/emulated/0/AntennaPod","srcFileNamePattern":"AntennaPodBackup-\\d{4}-\\d{2}-\\d{2}\\.db","dest":"/storage/emulated/0/Backups","destFileNameTemplate":"AntennaPod.db","scanSubdirectories":true,"overwriteExisting":true,"preserveStructure":false}""", + $$"""{"type":"co.adityarajput.fileflow.data.models.Action.MOVE","src":"/storage/emulated/0/Backups","srcFileNamePattern":"PipePipeData-\\d{8}_\\d{6}\\.zip","dest":"/storage/emulated/0/Backups","destFileNameTemplate":"PipePipe-${uuid}.zip","keepOriginal":false,"superlative":"LARGEST","deleteEmptySrcSubdirectories":true}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.DELETE_STALE","src":"/storage/emulated/0/Download","srcFileNamePattern":"(NotiFilter|Alarmetrics|FileFlow|MinCal)_v[\\d\\.]+\\.apk","retentionDays":90,"scanSubdirectories":true,"deleteEmptySrcSubdirectories":true}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.ZIP","src":"/storage/emulated/0/Documents/Notes","srcFileNamePattern":"(.*)\\.md","dest":"/storage/emulated/0/Backups","destFileNameTemplate":"Notes.zip","scanSubdirectories":true,"overwriteExisting":true,"preserveStructure":false}""", + """{"type":"co.adityarajput.fileflow.data.models.Action.EMIT_CHANGES","src":"/storage/emulated/0/Movies","srcFileNamePattern":".*\\.mp4","intent":"org.amoradi.syncopoli.SYNC_PROFILE","packageName":"org.amoradi.syncopoli","extras":"{\"profile_name\": \"Movies Backup\"}","scanSubdirectories":true,"modifiedWithin":10800000}""", + """{"type":"co.adityarajput.fileflow.data.models.RemoteAction.MOVE","srcServer":{"host":"192.168.1.12","port":3456,"username":"admin","encryptedPassword":"hunter2","encryptedPrivateKey":null,"id":1},"src":"backups/navidrome","srcFileNamePattern":"navidrome_backup_[\\d_\\.]{19}\\.db","destServer":{"host":"192.168.1.78","port":9012,"username":"sftp","encryptedPassword":null,"encryptedPrivateKey":"passkey","id":2},"dest":"backups/glacial","destFileNameTemplate":"navidrome.db","overwriteExisting":true}""", + ) + + val incorrectSerializations = + listOf("", "{", "}", "{}", serializedActions[0].substring(0, 10)) + + Converters().run { + for (i in actions.indices) { + assertEquals(serializedActions[i], fromAction(actions[i])) + assertEquals(actions[i], toAction(serializedActions[i])) + assertEquals( + actions[i], + toAction(serializedActions[i].replaceFirst("{", """{"title": "deprecated",""")), + ) + } + for (string in incorrectSerializations) { + assertEquals(Action.MOVE("", "", "", ""), toAction(string)) + } + } + } + + @Test + fun Converters_IntList() { + val intLists = listOf( + listOf(1, 2), listOf(2, 1), listOf(2, 4, 6), listOf(2, 3, 5), + listOf(0, 1, 2, 3, 4, 5, 6), + ) + + val incorrectSerializations = + listOf("", "null", "null,") + + Converters().run { + for (list in intLists) { + assertEquals(list, toIntList(fromIntList(list))) + } + + for (string in incorrectSerializations) { + assertEquals(emptyList(), toIntList(string)) + } + + assertEquals(listOf(1), toIntList("1,null")) + } + } +} From 7da69af65461cfb791b97d42a97298185e0593e1 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Sat, 10 Oct 2026 17:21:03 +0530 Subject: [PATCH 4/6] Add run configurations --- .gitignore | 1 + .idea/runConfigurations/app.xml | 68 ++++++++++++++++++++++++++++++++ .idea/runConfigurations/test.xml | 35 ++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 .idea/runConfigurations/app.xml create mode 100644 .idea/runConfigurations/test.xml diff --git a/.gitignore b/.gitignore index 8d10870..5bc82a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .gradle /local.properties .idea/* +!.idea/runConfigurations !.idea/icon.png .DS_Store /build diff --git a/.idea/runConfigurations/app.xml b/.idea/runConfigurations/app.xml new file mode 100644 index 0000000..4162a33 --- /dev/null +++ b/.idea/runConfigurations/app.xml @@ -0,0 +1,68 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/test.xml b/.idea/runConfigurations/test.xml new file mode 100644 index 0000000..0a71e64 --- /dev/null +++ b/.idea/runConfigurations/test.xml @@ -0,0 +1,35 @@ + + + + + + + true + true + false + + + + + + + false + true + false + false + + + \ No newline at end of file From d50d477c2e5cc87222c97807c4d0121f07b478d5 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Sat, 10 Oct 2026 17:22:03 +0530 Subject: [PATCH 5/6] Add tests CI --- .github/workflows/test.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..6b41cd7 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - dev + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup JDK + uses: actions/setup-java@v6 + with: + java-version: 17 + distribution: "temurin" + cache: "gradle" + + - name: Run tests + run: ./gradlew test From 16df2ce8de6b14bd8b5a1067a5bc5717905d14cd Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Sat, 10 Oct 2026 17:59:53 +0530 Subject: [PATCH 6/6] Update version --- app/build.gradle.kts | 4 ++-- metadata/en-US/changelogs/12.txt | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 metadata/en-US/changelogs/12.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 77bf2b5..6479d93 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { applicationId = "co.adityarajput.fileflow" minSdk = 29 targetSdk = 36 - versionCode = 11 - versionName = "1.9.0" + versionCode = 12 + versionName = "1.10.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/metadata/en-US/changelogs/12.txt b/metadata/en-US/changelogs/12.txt new file mode 100644 index 0000000..0350a07 --- /dev/null +++ b/metadata/en-US/changelogs/12.txt @@ -0,0 +1,5 @@ +• fix: Fail gracefully for issues with encryption +• fix: Compare files in chunks to prevent OutOfMemoryError +• feat: Create subfolders if destination file name contains path separators +• feat: Let users choose to override OS-suggested extensions +• feat: Display spinner when executing rule