feat: add optional date field to schedule alarms on specific dates

Add YYYY-MM-DD date parameter to /set endpoint. When provided, the alarm
fires at the exact date instead of the next occurrence of hour:minute.

- AlarmEntity: add nullable date column
- AlarmDatabase: bump to version 2, add migration 1→2
- AppModule: register migration
- KtorService: parse date, validate it's not in the past
- MainViewModel: accept optional date in createAlarm()
- README: document new date parameter
This commit is contained in:
Helios 2026-02-24 15:48:05 +01:00
parent 5793af5772
commit c78cb8a77d
No known key found for this signature in database
GPG key ID: C8259547CD8309B5
6 changed files with 69 additions and 9 deletions

View file

@ -31,6 +31,14 @@ Response: `201 Created`
`hour` must be 0-23, `minute` must be 0-59. `label` is optional (defaults to empty).
To set an alarm for a specific future date, add an optional `date` field in `YYYY-MM-DD` format. Without `date`, the alarm fires at the next occurrence of the given time (today or tomorrow).
```bash
curl -X POST http://PHONE_IP:8080/set \
-H "Content-Type: application/json" \
-d '{"hour": 8, "minute": 0, "label": "Vacation start", "date": "2026-10-08"}'
```
### Remove an alarm
```bash

View file

@ -2,12 +2,20 @@ package com.example.helios_alarm_clock.data
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(entities = [AlarmEntity::class], version = 1, exportSchema = true)
@Database(entities = [AlarmEntity::class], version = 2, exportSchema = true)
abstract class AlarmDatabase : RoomDatabase() {
abstract fun alarmDao(): AlarmDao
companion object {
const val NAME = "helios_alarms.db"
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE alarms ADD COLUMN date TEXT DEFAULT NULL")
}
}
}
}

View file

@ -1,5 +1,6 @@
package com.example.helios_alarm_clock.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@ -12,5 +13,7 @@ data class AlarmEntity(
val hour: Int,
val minute: Int,
val label: String,
val triggerTimeMillis: Long
val triggerTimeMillis: Long,
@ColumnInfo(defaultValue = "")
val date: String? = null
)

View file

@ -22,7 +22,9 @@ object AppModule {
context,
AlarmDatabase::class.java,
AlarmDatabase.NAME
).build()
)
.addMigrations(AlarmDatabase.MIGRATION_1_2)
.build()
@Provides
@Singleton

View file

@ -36,6 +36,9 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.Calendar
import java.util.UUID
import javax.inject.Inject
@ -126,13 +129,40 @@ class KtorService : Service() {
}
val id = UUID.randomUUID().toString()
// Parse date outside apply block so return@post works at the correct scope
val parsedDate: LocalDate? = if (req.date != null) {
try {
LocalDate.parse(req.date, DateTimeFormatter.ISO_LOCAL_DATE)
} catch (e: DateTimeParseException) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse("date must be in YYYY-MM-DD format (e.g. 2026-10-08)")
)
return@post
}
} else null
val now = Calendar.getInstance()
val trigger = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, req.hour)
set(Calendar.MINUTE, req.minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
if (before(now)) add(Calendar.DAY_OF_YEAR, 1)
if (parsedDate != null) {
set(Calendar.YEAR, parsedDate.year)
set(Calendar.MONTH, parsedDate.monthValue - 1)
set(Calendar.DAY_OF_MONTH, parsedDate.dayOfMonth)
} else {
if (before(now)) add(Calendar.DAY_OF_YEAR, 1)
}
}
if (parsedDate != null && trigger.before(now)) {
call.respond(
HttpStatusCode.BadRequest,
ErrorResponse("Alarm time is in the past")
)
return@post
}
val entity = AlarmEntity(
@ -140,7 +170,8 @@ class KtorService : Service() {
hour = req.hour,
minute = req.minute,
label = req.label,
triggerTimeMillis = trigger.timeInMillis
triggerTimeMillis = trigger.timeInMillis,
date = req.date
)
alarmDao.insert(entity)
@ -241,7 +272,7 @@ class KtorService : Service() {
}
@Serializable
data class SetAlarmRequest(val hour: Int, val minute: Int, val label: String = "")
data class SetAlarmRequest(val hour: Int, val minute: Int, val label: String = "", val date: String? = null)
@Serializable
data class RemoveAlarmRequest(val id: String)

View file

@ -30,7 +30,7 @@ class MainViewModel @Inject constructor(
val port: Int = KtorService.PORT
fun createAlarm(hour: Int, minute: Int, label: String) {
fun createAlarm(hour: Int, minute: Int, label: String, date: String? = null) {
viewModelScope.launch {
val now = Calendar.getInstance()
val trigger = Calendar.getInstance().apply {
@ -38,14 +38,22 @@ class MainViewModel @Inject constructor(
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
if (before(now)) add(Calendar.DAY_OF_YEAR, 1)
if (date != null) {
val parsedDate = java.time.LocalDate.parse(date)
set(Calendar.YEAR, parsedDate.year)
set(Calendar.MONTH, parsedDate.monthValue - 1)
set(Calendar.DAY_OF_MONTH, parsedDate.dayOfMonth)
} else {
if (before(now)) add(Calendar.DAY_OF_YEAR, 1)
}
}
val entity = AlarmEntity(
id = UUID.randomUUID().toString(),
hour = hour,
minute = minute,
label = label,
triggerTimeMillis = trigger.timeInMillis
triggerTimeMillis = trigger.timeInMillis,
date = date
)
alarmDao.insert(entity)
alarmScheduler.schedule(entity)