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). `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 ### Remove an alarm
```bash ```bash

View file

@ -2,12 +2,20 @@ package com.example.helios_alarm_clock.data
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase 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 class AlarmDatabase : RoomDatabase() {
abstract fun alarmDao(): AlarmDao abstract fun alarmDao(): AlarmDao
companion object { companion object {
const val NAME = "helios_alarms.db" 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 package com.example.helios_alarm_clock.data
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -12,5 +13,7 @@ data class AlarmEntity(
val hour: Int, val hour: Int,
val minute: Int, val minute: Int,
val label: String, val label: String,
val triggerTimeMillis: Long val triggerTimeMillis: Long,
@ColumnInfo(defaultValue = "")
val date: String? = null
) )

View file

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

View file

@ -36,6 +36,9 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json 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.Calendar
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -126,13 +129,40 @@ class KtorService : Service() {
} }
val id = UUID.randomUUID().toString() 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 now = Calendar.getInstance()
val trigger = Calendar.getInstance().apply { val trigger = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, req.hour) set(Calendar.HOUR_OF_DAY, req.hour)
set(Calendar.MINUTE, req.minute) set(Calendar.MINUTE, req.minute)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 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( val entity = AlarmEntity(
@ -140,7 +170,8 @@ class KtorService : Service() {
hour = req.hour, hour = req.hour,
minute = req.minute, minute = req.minute,
label = req.label, label = req.label,
triggerTimeMillis = trigger.timeInMillis triggerTimeMillis = trigger.timeInMillis,
date = req.date
) )
alarmDao.insert(entity) alarmDao.insert(entity)
@ -241,7 +272,7 @@ class KtorService : Service() {
} }
@Serializable @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 @Serializable
data class RemoveAlarmRequest(val id: String) data class RemoveAlarmRequest(val id: String)

View file

@ -30,7 +30,7 @@ class MainViewModel @Inject constructor(
val port: Int = KtorService.PORT 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 { viewModelScope.launch {
val now = Calendar.getInstance() val now = Calendar.getInstance()
val trigger = Calendar.getInstance().apply { val trigger = Calendar.getInstance().apply {
@ -38,14 +38,22 @@ class MainViewModel @Inject constructor(
set(Calendar.MINUTE, minute) set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 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( val entity = AlarmEntity(
id = UUID.randomUUID().toString(), id = UUID.randomUUID().toString(),
hour = hour, hour = hour,
minute = minute, minute = minute,
label = label, label = label,
triggerTimeMillis = trigger.timeInMillis triggerTimeMillis = trigger.timeInMillis,
date = date
) )
alarmDao.insert(entity) alarmDao.insert(entity)
alarmScheduler.schedule(entity) alarmScheduler.schedule(entity)