Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ FileFlow scans your files periodically and organizes them according to your rule
- **Actions** - Choose what to do to your files ⚙
1. Copy, move, or rename files 📁
2. Delete stale files 🗑
3. Create zip archives 🗜
- **Schedule** - Choose when and how often rules run ⏰
- **History** - Recent executions are stored (locally) ⏳
- **Shortcuts** - Execute groups of rules from your homescreen 🚀
- **Free, open-source & private**
- No ads, subscriptions, or in-app purchases 🆓
- Licensed under the [GPLv3](https://github.com/BURG3R5/FileFlow/blob/dev/LICENSE) 📃
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ android {
applicationId = "co.adityarajput.fileflow"
minSdk = 29
targetSdk = 36
versionCode = 6
versionName = "1.4.0"
versionCode = 7
versionName = "1.5.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
128 changes: 128 additions & 0 deletions app/schemas/co.adityarajput.fileflow.data.FileFlowDatabase/6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "b0e045c438054bcca59175062168639b",
"entities": [
{
"tableName": "rules",
"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)",
"fields": [
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "executions",
"columnName": "executions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "interval",
"columnName": "interval",
"affinity": "INTEGER",
"defaultValue": "3600000"
},
{
"fieldPath": "cronString",
"columnName": "cronString",
"affinity": "TEXT",
"defaultValue": "NULL"
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "executions",
"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)",
"fields": [
{
"fieldPath": "fileName",
"columnName": "fileName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "verb",
"columnName": "verb",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'MOVE'"
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `ruleIds` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ruleIds",
"columnName": "ruleIds",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b0e045c438054bcca59175062168639b')"
]
}
}
15 changes: 15 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,27 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ShortcutActivity"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay" />

<receiver
android:name=".AlarmReceiver"
android:exported="false">
<intent-filter>
<action android:name="co.adityarajput.fileflow.EXECUTE_RULE" />
</intent-filter>
</receiver>
<receiver
android:name=".ShortcutReceiver"
android:exported="false">
<intent-filter>
<action android:name="co.adityarajput.fileflow.EXECUTE_GROUP" />
</intent-filter>
</receiver>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class AlarmReceiver : BroadcastReceiver() {
if (rule == null || !rule.enabled)
return@launch

Logger.d("Worker", "Executing $rule")
Logger.d("AlarmReceiver", "Executing $rule")
rule.action.execute(context) {
repository.registerExecution(
rule,
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/co/adityarajput/fileflow/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ object Constants {
const val EXTRA_RULE_ID = "extra_rule_id"
const val MAX_CRON_EXECUTIONS_PER_HOUR = 4

const val ACTION_EXECUTE_GROUP = "co.adityarajput.fileflow.EXECUTE_GROUP"
const val EXTRA_GROUP_ID = "extra_group_id"

const val LOG_SIZE = 100

const val ONE_HOUR_IN_MILLIS = 3_600_000L

/**
* Max amount of app shortcuts visible when launcher app icon is long-pressed.
*
* There *is* a `ShortcutManagerCompat.getMaxShortcutCountPerActivity` method, but it *lies*.
*/
const val MAX_SHORTCUTS = 4
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Application
import co.adityarajput.fileflow.data.AppContainer
import co.adityarajput.fileflow.utils.isDebugBuild
import co.adityarajput.fileflow.utils.scheduleWork
import co.adityarajput.fileflow.utils.upsertShortcuts
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand All @@ -23,6 +24,7 @@ class FileFlowApplication : Application() {

CoroutineScope(Dispatchers.IO).launch {
scheduleWork()
upsertShortcuts()
}
}
}
18 changes: 18 additions & 0 deletions app/src/main/java/co/adityarajput/fileflow/ShortcutActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package co.adityarajput.fileflow

import android.app.Activity
import android.content.Intent
import android.os.Bundle

class ShortcutActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sendBroadcast(
Intent(Constants.ACTION_EXECUTE_GROUP).apply {
setPackage(packageName)
putExtra(Constants.EXTRA_GROUP_ID, intent.getIntExtra(Constants.EXTRA_GROUP_ID, -1))
},
)
finish()
}
}
40 changes: 40 additions & 0 deletions app/src/main/java/co/adityarajput/fileflow/ShortcutReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package co.adityarajput.fileflow

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import co.adityarajput.fileflow.data.AppContainer
import co.adityarajput.fileflow.data.models.Execution
import co.adityarajput.fileflow.utils.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class ShortcutReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Logger.d("ShortcutReceiver", "Received intent with action: ${intent.action}")

if (intent.action != Constants.ACTION_EXECUTE_GROUP)
return

CoroutineScope(Dispatchers.IO).launch {
val repository = AppContainer(context).repository
val groupId = intent.getIntExtra(Constants.EXTRA_GROUP_ID, -1)
val (group, rules) = repository.group(groupId)

if (group == null)
return@launch

Logger.d("ShortcutReceiver", "Executing $group")
for (rule in rules) {
Logger.d("ShortcutReceiver", "Executing $rule")
rule.action.execute(context) {
repository.registerExecution(
rule,
Execution(it, rule.action.verb),
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class AppContainer(private val context: Context) {
Repository(
FileFlowDatabase.getDatabase(context).ruleDao(),
FileFlowDatabase.getDatabase(context).executionDao(),
FileFlowDatabase.getDatabase(context).groupDao(),
)
}

Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/co/adityarajput/fileflow/data/Converters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ class Converters {
Action.entries[0]
}
}

@TypeConverter
fun fromIntList(list: List<Int>) = list.joinToString(",")

@TypeConverter
fun toIntList(value: String) = value.split(",").mapNotNull { it.toIntOrNull() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,25 @@ import android.content.Context
import androidx.room.*
import androidx.room.migration.AutoMigrationSpec
import co.adityarajput.fileflow.data.models.Execution
import co.adityarajput.fileflow.data.models.Group
import co.adityarajput.fileflow.data.models.Rule

@Database(
[Rule::class, Execution::class],
version = 5,
[Rule::class, Execution::class, Group::class],
version = 6,
autoMigrations = [
AutoMigration(1, 2),
AutoMigration(2, 3, FileFlowDatabase.DeleteEColumnAV::class),
AutoMigration(3, 4),
AutoMigration(4, 5),
AutoMigration(5, 6),
],
)
@TypeConverters(Converters::class)
abstract class FileFlowDatabase : RoomDatabase() {
abstract fun ruleDao(): RuleDao
abstract fun executionDao(): ExecutionDao
abstract fun groupDao(): GroupDao

@DeleteColumn("executions", "actionVerb")
class DeleteEColumnAV : AutoMigrationSpec
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/co/adityarajput/fileflow/data/GroupDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package co.adityarajput.fileflow.data

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Query
import androidx.room.Upsert
import co.adityarajput.fileflow.data.models.Group
import kotlinx.coroutines.flow.Flow

@Dao
interface GroupDao {
@Upsert
suspend fun upsert(vararg groups: Group)

@Query("SELECT * from `groups` ORDER BY id ASC")
fun list(): Flow<List<Group>>

@Query("SELECT * from `groups` WHERE id = :id")
fun get(id: Int): Group?

@Delete
suspend fun delete(group: Group)
}
Loading
Loading