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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- 📱 **Kotlin Multiplatform**: Shared logic for Android and iOS.
- 💾 **Persistence**: Tasks are stored in a local SQLite database (via Room) and resumed after app restarts.
- 🔗 **Work Chaining**: Easily chain multiple tasks together with `then` operations.
- ⚙️ **Constraints**: Define requirements like `requiredNetwork` and `requireBatteryNotLow` for your tasks.
- ⚙️ **Constraints**: Define requirements like `requiredNetwork`, `requireBatteryNotLow` and `requireCharging` for your tasks.
- 🛠️ **DSL-based API**: Clean and intuitive DSL for initialization and task definition.
- 📊 **Monitoring**: Observe task status using Kotlin Flows.

Expand Down
90 changes: 90 additions & 0 deletions lorraine/schemas/io.dot.lorraine.db.LorraineDB/4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "82b30102d79eee44a0aab656a8fb67c6",
"entities": [
{
"tableName": "worker",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `queue_id` TEXT NOT NULL, `identifier` TEXT NOT NULL, `state` TEXT NOT NULL, `tags` TEXT NOT NULL, `worker_dependencies` TEXT NOT NULL, `input_data` TEXT, `output_data` TEXT, `constraints_require_network` INTEGER NOT NULL, `constraints_require_battery_not_low` INTEGER NOT NULL, `constraints_require_charging` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "queueId",
"columnName": "queue_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "identifier",
"columnName": "identifier",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "workerDependencies",
"columnName": "worker_dependencies",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "inputData",
"columnName": "input_data",
"affinity": "TEXT"
},
{
"fieldPath": "outputData",
"columnName": "output_data",
"affinity": "TEXT"
},
{
"fieldPath": "constraints.requireNetwork",
"columnName": "constraints_require_network",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "constraints.requireBatteryNotLow",
"columnName": "constraints_require_battery_not_low",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "constraints.requireCharging",
"columnName": "constraints_require_charging",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uuid"
]
}
}
],
"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, '82b30102d79eee44a0aab656a8fb67c6')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal fun LorraineConstraints.asWorkManagerConstraints(): Constraints {
} else {
NetworkType.NOT_REQUIRED
},
requiresBatteryNotLow = requireBatteryNotLow
requiresBatteryNotLow = requireBatteryNotLow,
requiresCharging = requireCharging
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.dot.lorraine.db

import androidx.room.AutoMigration
import androidx.room.ConstructedBy
import androidx.room.Database
import androidx.room.RoomDatabase
Expand All @@ -11,9 +12,13 @@ import io.dot.lorraine.db.dao.WorkerDao
import io.dot.lorraine.db.entity.WorkerEntity

@Database(
version = 3,
Comment thread
rteyssandier marked this conversation as resolved.
version = 4,
entities = [
WorkerEntity::class
],
exportSchema = true,
autoMigrations = [
AutoMigration(from = 3, to = 4)
]
)
@TypeConverters(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@ internal data class ConstraintEntity(
@ColumnInfo(name = "require_network")
val requireNetwork: Boolean,
@ColumnInfo(name = "require_battery_not_low")
val requireBatteryNotLow: Boolean
val requireBatteryNotLow: Boolean,
@ColumnInfo(name = "require_charging", defaultValue = "0")
val requireCharging: Boolean

)

internal fun ConstraintEntity.toDomain() = LorraineConstraints(
requireNetwork = requireNetwork,
requireBatteryNotLow = requireBatteryNotLow
requireBatteryNotLow = requireBatteryNotLow,
requireCharging = requireCharging
)

internal fun LorraineConstraints.toEntity() = ConstraintEntity(
requireNetwork = requireNetwork,
requireBatteryNotLow = requireBatteryNotLow
requireBatteryNotLow = requireBatteryNotLow,
requireCharging = requireCharging
)


Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@ package io.dot.lorraine.dsl
data class LorraineConstraints internal constructor(
val requireNetwork: Boolean,
val requireBatteryNotLow: Boolean,
val requireCharging: Boolean,
) {

companion object {
val NONE = LorraineConstraints(
requireNetwork = false,
requireBatteryNotLow = false
requireBatteryNotLow = false,
requireCharging = false
)
}
}

class LorraineConstraintsDefinition internal constructor() {
var requiredNetwork: Boolean = false
var requiredBatteryNotLow: Boolean = false
var requireCharging: Boolean = false


fun build() = LorraineConstraints(
requireNetwork = requiredNetwork,
requireBatteryNotLow = requiredBatteryNotLow
requireBatteryNotLow = requiredBatteryNotLow,
requireCharging = requireCharging
)

}
8 changes: 7 additions & 1 deletion lorraine/src/iosMain/kotlin/io/dot/lorraine/Platform.ios.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package io.dot.lorraine

import io.dot.lorraine.constraint.BatteryNotLowCheck
import io.dot.lorraine.constraint.ChargingCheck
import io.dot.lorraine.constraint.ConnectivityCheck
import io.dot.lorraine.constraint.ConstraintCheck
import io.dot.lorraine.constraint.match
Expand Down Expand Up @@ -47,6 +48,11 @@ internal class IOSPlatform(
scope = scope,
onChange = ::constraintChanged,
logger = logger
),
ChargingCheck(
scope = scope,
onChange = ::constraintChanged,
logger = logger
)
)

Expand Down Expand Up @@ -111,7 +117,7 @@ internal class IOSPlatform(
operation: LorraineOperation
) {
requireNotNull(operation.operations.firstOrNull()) {
"Operations shoud not be empty"
"Operations should not be empty"
}
val queue = queues.getOrElse(queueId) { createQueue(queueId) }
var previous: NSOperation? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.dot.lorraine.constraint

import io.dot.lorraine.dsl.LorraineConstraints
import io.dot.lorraine.logger.LorraineLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okio.Closeable
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSOperationQueue
import platform.UIKit.UIDevice
import platform.UIKit.UIDeviceBatteryState
import platform.UIKit.UIDeviceBatteryStateDidChangeNotification

internal class ChargingCheck(
scope: CoroutineScope,
onChange: () -> Unit,
logger: LorraineLogger
) : ConstraintCheck {

private val observer = AppleChargingObserver()

private val _value = MutableStateFlow(false)

init {
observer.setListener(
object : ChargingObserver.Listener {
override fun chargingChanged(isCharging: Boolean) {
_value.update { isCharging }
}
}
)

scope.launch {
_value.onEach { logger.info("ChargingCheck: $it") }.collect { onChange() }
}
}

override suspend fun match(constraints: LorraineConstraints): Boolean {
if (!constraints.requireCharging)
return true

return _value.value
}

}

internal class AppleChargingObserver : ChargingObserver, Closeable {
private var listener: ChargingObserver.Listener? = null
private var stateObserver: Any? = null

override fun setListener(listener: ChargingObserver.Listener) {
this.listener = listener
UIDevice.currentDevice.batteryMonitoringEnabled = true

val center = NSNotificationCenter.defaultCenter

stateObserver = center.addObserverForName(
UIDeviceBatteryStateDidChangeNotification,
null,
NSOperationQueue.mainQueue
) { _ -> notifyListener() }

notifyListener()
}

private fun notifyListener() {
listener?.chargingChanged(isCharging())
}

private fun isCharging(): Boolean {
val device = UIDevice.currentDevice
val batteryState = device.batteryState

return batteryState == UIDeviceBatteryState.UIDeviceBatteryStateCharging
}

override fun close() {
stateObserver?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
UIDevice.currentDevice.batteryMonitoringEnabled = false
}
}

internal interface ChargingObserver : Closeable {
/**
* Sets the listener
*
* Implementation must call [listener] shortly after [setListener] returns to let the callers know about the initial state.
*/
fun setListener(listener: Listener)

interface Listener {
fun chargingChanged(isCharging: Boolean)
}
}