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:
parent
5793af5772
commit
c78cb8a77d
6 changed files with 69 additions and 9 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue