Skip to content

Commit c3ead2d

Browse files
authored
feat: Let user choose execution time and frequency (#21)
2 parents 696b099 + 94acc37 commit c3ead2d

File tree

35 files changed

+1148
-293
lines changed

35 files changed

+1148
-293
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ FileFlow scans your files periodically and organizes them according to your rule
1919
- **Actions** - Choose what to do to your files ⚙
2020
1. Copy, move, or rename files 📁
2121
2. Delete stale files 🗑
22+
- **Schedule** - Choose when and how often rules run ⏰
2223
- **History** - Recent executions are stored (locally) ⏳
2324
- **Free, open-source & private**
2425
- No ads, subscriptions, or in-app purchases 🆓

app/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ android {
2222
applicationId = "co.adityarajput.fileflow"
2323
minSdk = 29
2424
targetSdk = 36
25-
versionCode = 5
26-
versionName = "1.3.0"
25+
versionCode = 6
26+
versionName = "1.4.0"
2727

2828
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2929
}
@@ -93,6 +93,7 @@ dependencies {
9393
ksp(libs.androidx.room.compiler)
9494
implementation(libs.androidx.room.ktx)
9595
implementation(libs.aboutlibraries.compose)
96+
implementation(libs.cron.utils)
9697
testImplementation(libs.junit)
9798
androidTestImplementation(libs.androidx.junit)
9899
androidTestImplementation(libs.androidx.espresso.core)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 4,
5+
"identityHash": "fe2af7f88a055f816417fe0ebace9f2a",
6+
"entities": [
7+
{
8+
"tableName": "rules",
9+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`action` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `executions` INTEGER NOT NULL, `interval` INTEGER DEFAULT 3600000, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
10+
"fields": [
11+
{
12+
"fieldPath": "action",
13+
"columnName": "action",
14+
"affinity": "TEXT",
15+
"notNull": true
16+
},
17+
{
18+
"fieldPath": "enabled",
19+
"columnName": "enabled",
20+
"affinity": "INTEGER",
21+
"notNull": true
22+
},
23+
{
24+
"fieldPath": "executions",
25+
"columnName": "executions",
26+
"affinity": "INTEGER",
27+
"notNull": true
28+
},
29+
{
30+
"fieldPath": "interval",
31+
"columnName": "interval",
32+
"affinity": "INTEGER",
33+
"defaultValue": "3600000"
34+
},
35+
{
36+
"fieldPath": "id",
37+
"columnName": "id",
38+
"affinity": "INTEGER",
39+
"notNull": true
40+
}
41+
],
42+
"primaryKey": {
43+
"autoGenerate": true,
44+
"columnNames": [
45+
"id"
46+
]
47+
}
48+
},
49+
{
50+
"tableName": "executions",
51+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileName` TEXT NOT NULL, `verb` TEXT NOT NULL DEFAULT 'MOVE', `timestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
52+
"fields": [
53+
{
54+
"fieldPath": "fileName",
55+
"columnName": "fileName",
56+
"affinity": "TEXT",
57+
"notNull": true
58+
},
59+
{
60+
"fieldPath": "verb",
61+
"columnName": "verb",
62+
"affinity": "TEXT",
63+
"notNull": true,
64+
"defaultValue": "'MOVE'"
65+
},
66+
{
67+
"fieldPath": "timestamp",
68+
"columnName": "timestamp",
69+
"affinity": "INTEGER",
70+
"notNull": true
71+
},
72+
{
73+
"fieldPath": "id",
74+
"columnName": "id",
75+
"affinity": "INTEGER",
76+
"notNull": true
77+
}
78+
],
79+
"primaryKey": {
80+
"autoGenerate": true,
81+
"columnNames": [
82+
"id"
83+
]
84+
}
85+
}
86+
],
87+
"setupQueries": [
88+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
89+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe2af7f88a055f816417fe0ebace9f2a')"
90+
]
91+
}
92+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 5,
5+
"identityHash": "5fa205cd80eec0e4d09c32130309c044",
6+
"entities": [
7+
{
8+
"tableName": "rules",
9+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`action` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `executions` INTEGER NOT NULL, `interval` INTEGER DEFAULT 3600000, `cronString` TEXT DEFAULT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
10+
"fields": [
11+
{
12+
"fieldPath": "action",
13+
"columnName": "action",
14+
"affinity": "TEXT",
15+
"notNull": true
16+
},
17+
{
18+
"fieldPath": "enabled",
19+
"columnName": "enabled",
20+
"affinity": "INTEGER",
21+
"notNull": true
22+
},
23+
{
24+
"fieldPath": "executions",
25+
"columnName": "executions",
26+
"affinity": "INTEGER",
27+
"notNull": true
28+
},
29+
{
30+
"fieldPath": "interval",
31+
"columnName": "interval",
32+
"affinity": "INTEGER",
33+
"defaultValue": "3600000"
34+
},
35+
{
36+
"fieldPath": "cronString",
37+
"columnName": "cronString",
38+
"affinity": "TEXT",
39+
"defaultValue": "NULL"
40+
},
41+
{
42+
"fieldPath": "id",
43+
"columnName": "id",
44+
"affinity": "INTEGER",
45+
"notNull": true
46+
}
47+
],
48+
"primaryKey": {
49+
"autoGenerate": true,
50+
"columnNames": [
51+
"id"
52+
]
53+
}
54+
},
55+
{
56+
"tableName": "executions",
57+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileName` TEXT NOT NULL, `verb` TEXT NOT NULL DEFAULT 'MOVE', `timestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
58+
"fields": [
59+
{
60+
"fieldPath": "fileName",
61+
"columnName": "fileName",
62+
"affinity": "TEXT",
63+
"notNull": true
64+
},
65+
{
66+
"fieldPath": "verb",
67+
"columnName": "verb",
68+
"affinity": "TEXT",
69+
"notNull": true,
70+
"defaultValue": "'MOVE'"
71+
},
72+
{
73+
"fieldPath": "timestamp",
74+
"columnName": "timestamp",
75+
"affinity": "INTEGER",
76+
"notNull": true
77+
},
78+
{
79+
"fieldPath": "id",
80+
"columnName": "id",
81+
"affinity": "INTEGER",
82+
"notNull": true
83+
}
84+
],
85+
"primaryKey": {
86+
"autoGenerate": true,
87+
"columnNames": [
88+
"id"
89+
]
90+
}
91+
}
92+
],
93+
"setupQueries": [
94+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
95+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5fa205cd80eec0e4d09c32130309c044')"
96+
]
97+
}
98+
}

app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
android:allowBackup="${allowBackup}"
2121
android:dataExtractionRules="@xml/data_extraction_rules"
2222
android:fullBackupContent="@xml/backup_rules"
23-
android:requestLegacyExternalStorage="true"
2423
android:icon="@mipmap/ic_launcher"
2524
android:label="@string/app_name_launcher"
25+
android:requestLegacyExternalStorage="true"
2626
android:roundIcon="@mipmap/ic_launcher_round"
2727
android:supportsRtl="true"
2828
android:theme="@style/Theme.FileFlow">
@@ -35,5 +35,12 @@
3535
<category android:name="android.intent.category.LAUNCHER" />
3636
</intent-filter>
3737
</activity>
38+
<receiver
39+
android:name=".AlarmReceiver"
40+
android:exported="false">
41+
<intent-filter>
42+
<action android:name="co.adityarajput.fileflow.EXECUTE_RULE" />
43+
</intent-filter>
44+
</receiver>
3845
</application>
3946
</manifest>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package co.adityarajput.fileflow
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import co.adityarajput.fileflow.data.AppContainer
7+
import co.adityarajput.fileflow.data.models.Execution
8+
import co.adityarajput.fileflow.utils.Logger
9+
import kotlinx.coroutines.CoroutineScope
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.launch
12+
13+
class AlarmReceiver : BroadcastReceiver() {
14+
override fun onReceive(context: Context, intent: Intent) {
15+
Logger.d("AlarmReceiver", "Received intent with action: ${intent.action}")
16+
17+
if (intent.action == Constants.ACTION_EXECUTE_RULE) {
18+
CoroutineScope(Dispatchers.IO).launch {
19+
val repository = AppContainer(context).repository
20+
val rule = repository.rule(intent.getIntExtra(Constants.EXTRA_RULE_ID, -1))
21+
22+
if (rule == null || !rule.enabled)
23+
return@launch
24+
25+
Logger.d("Worker", "Executing $rule")
26+
rule.action.execute(context) {
27+
repository.registerExecution(
28+
rule,
29+
Execution(it, rule.action.verb),
30+
)
31+
}
32+
}
33+
}
34+
}
35+
}

app/src/main/java/co/adityarajput/fileflow/Constants.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ object Constants {
88
const val BRIGHTNESS = "brightness"
99

1010
const val WORKER_NAME = "fileflow_worker"
11+
const val ACTION_EXECUTE_RULE = "co.adityarajput.fileflow.EXECUTE_RULE"
12+
const val EXTRA_RULE_ID = "extra_rule_id"
13+
const val MAX_CRON_EXECUTIONS_PER_HOUR = 4
1114

1215
const val LOG_SIZE = 100
16+
17+
const val ONE_HOUR_IN_MILLIS = 3_600_000L
1318
}
Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package co.adityarajput.fileflow
22

33
import android.app.Application
4-
import androidx.work.ExistingPeriodicWorkPolicy
5-
import androidx.work.PeriodicWorkRequestBuilder
6-
import androidx.work.WorkManager
74
import co.adityarajput.fileflow.data.AppContainer
8-
import co.adityarajput.fileflow.services.Worker
95
import co.adityarajput.fileflow.utils.isDebugBuild
10-
import java.util.concurrent.TimeUnit
6+
import co.adityarajput.fileflow.utils.scheduleWork
7+
import kotlinx.coroutines.CoroutineScope
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.launch
1110

1211
class FileFlowApplication : Application() {
1312
lateinit var container: AppContainer
@@ -22,14 +21,8 @@ class FileFlowApplication : Application() {
2221
container.seedDemoData()
2322
}
2423

25-
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
26-
Constants.WORKER_NAME,
27-
ExistingPeriodicWorkPolicy.KEEP,
28-
PeriodicWorkRequestBuilder<Worker>(
29-
// INFO: While debugging, use a shorter interval
30-
if (isDebugBuild()) 15 else 60,
31-
TimeUnit.MINUTES,
32-
).build(),
33-
)
24+
CoroutineScope(Dispatchers.IO).launch {
25+
scheduleWork()
26+
}
3427
}
3528
}

app/src/main/java/co/adityarajput/fileflow/data/AppContainer.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ class AppContainer(private val context: Context) {
1717

1818
fun seedDemoData() {
1919
runBlocking {
20-
if (
21-
repository.rules().first().isEmpty() &&
22-
repository.executions().first().isEmpty()
23-
) {
20+
if (repository.rules().first().isEmpty()) {
2421
repository.upsert(
2522
Rule(
2623
Action.MOVE(
@@ -31,6 +28,8 @@ class AppContainer(private val context: Context) {
3128
overwriteExisting = true,
3229
),
3330
executions = 2,
31+
interval = null,
32+
cronString = "00 10 * * 0",
3433
),
3534
Rule(
3635
Action.MOVE(
@@ -42,6 +41,7 @@ class AppContainer(private val context: Context) {
4241
overwriteExisting = true,
4342
),
4443
executions = 3,
44+
interval = null,
4545
),
4646
Rule(
4747
Action.DELETE_STALE(
@@ -50,6 +50,7 @@ class AppContainer(private val context: Context) {
5050
scanSubdirectories = true,
5151
),
5252
enabled = false,
53+
interval = 86_400_000,
5354
),
5455
)
5556
repository.upsert(

app/src/main/java/co/adityarajput/fileflow/data/FileFlowDatabase.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import co.adityarajput.fileflow.data.models.Rule
88

99
@Database(
1010
[Rule::class, Execution::class],
11-
version = 3,
11+
version = 5,
1212
autoMigrations = [
1313
AutoMigration(1, 2),
1414
AutoMigration(2, 3, FileFlowDatabase.DeleteEColumnAV::class),
15+
AutoMigration(3, 4),
16+
AutoMigration(4, 5),
1517
],
1618
)
1719
@TypeConverters(Converters::class)

0 commit comments

Comments
 (0)