From c78cb8a77dc5acf35e02e6a33929b06d9171a171 Mon Sep 17 00:00:00 2001 From: Helios Date: Tue, 24 Feb 2026 15:48:05 +0100 Subject: [PATCH] feat: add optional date field to schedule alarms on specific dates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 8 ++++ .../helios_alarm_clock/data/AlarmDatabase.kt | 10 ++++- .../helios_alarm_clock/data/AlarmEntity.kt | 5 ++- .../helios_alarm_clock/di/AppModule.kt | 4 +- .../helios_alarm_clock/service/KtorService.kt | 37 +++++++++++++++++-- .../helios_alarm_clock/ui/MainViewModel.kt | 14 +++++-- 6 files changed, 69 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7bb2fcb..e701c3a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/src/main/java/com/example/helios_alarm_clock/data/AlarmDatabase.kt b/app/src/main/java/com/example/helios_alarm_clock/data/AlarmDatabase.kt index 85f911c..551c82e 100644 --- a/app/src/main/java/com/example/helios_alarm_clock/data/AlarmDatabase.kt +++ b/app/src/main/java/com/example/helios_alarm_clock/data/AlarmDatabase.kt @@ -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") + } + } } } diff --git a/app/src/main/java/com/example/helios_alarm_clock/data/AlarmEntity.kt b/app/src/main/java/com/example/helios_alarm_clock/data/AlarmEntity.kt index 7adf2eb..b663c2b 100644 --- a/app/src/main/java/com/example/helios_alarm_clock/data/AlarmEntity.kt +++ b/app/src/main/java/com/example/helios_alarm_clock/data/AlarmEntity.kt @@ -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 ) diff --git a/app/src/main/java/com/example/helios_alarm_clock/di/AppModule.kt b/app/src/main/java/com/example/helios_alarm_clock/di/AppModule.kt index a21afb6..3d736a1 100644 --- a/app/src/main/java/com/example/helios_alarm_clock/di/AppModule.kt +++ b/app/src/main/java/com/example/helios_alarm_clock/di/AppModule.kt @@ -22,7 +22,9 @@ object AppModule { context, AlarmDatabase::class.java, AlarmDatabase.NAME - ).build() + ) + .addMigrations(AlarmDatabase.MIGRATION_1_2) + .build() @Provides @Singleton diff --git a/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt b/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt index a8693a7..28c830d 100644 --- a/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt +++ b/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt @@ -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) diff --git a/app/src/main/java/com/example/helios_alarm_clock/ui/MainViewModel.kt b/app/src/main/java/com/example/helios_alarm_clock/ui/MainViewModel.kt index cc870cc..09aab8f 100644 --- a/app/src/main/java/com/example/helios_alarm_clock/ui/MainViewModel.kt +++ b/app/src/main/java/com/example/helios_alarm_clock/ui/MainViewModel.kt @@ -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)