Skip to content

Commit 4363ff3

Browse files
committed
fix: normalize Android all-day calendar events
1 parent c78b0d5 commit 4363ff3

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

apps/android/app/src/main/java/ai/openclaw/app/node/CalendarHandler.kt

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ internal data class CalendarAddRequest(
3737
val startMs: Long,
3838
val endMs: Long,
3939
val isAllDay: Boolean,
40+
val timeZoneId: String,
4041
val location: String?,
4142
val notes: String?,
4243
val calendarId: Long?,
4344
val calendarTitle: String?,
4445
)
4546

47+
private data class CalendarAddRange(
48+
val start: Instant,
49+
val end: Instant,
50+
)
51+
4652
/**
4753
* Normalized calendar event returned through gateway calendar commands.
4854
*/
@@ -148,7 +154,7 @@ private object SystemCalendarDataSource : CalendarDataSource {
148154
put(CalendarContract.Events.DTSTART, request.startMs)
149155
put(CalendarContract.Events.DTEND, request.endMs)
150156
put(CalendarContract.Events.ALL_DAY, if (request.isAllDay) 1 else 0)
151-
put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
157+
put(CalendarContract.Events.EVENT_TIMEZONE, request.timeZoneId)
152158
request.location?.let { put(CalendarContract.Events.EVENT_LOCATION, it) }
153159
request.notes?.let { put(CalendarContract.Events.DESCRIPTION, it) }
154160
}
@@ -393,18 +399,36 @@ class CalendarHandler private constructor(
393399
val end =
394400
parseISO((params["endISO"] as? JsonPrimitive)?.content)
395401
?: return null
402+
val isAllDay = (params["isAllDay"] as? JsonPrimitive)?.content?.toBooleanStrictOrNull() ?: false
403+
val addRange = normalizeAddRange(start, end, isAllDay)
396404
return CalendarAddRequest(
397405
title = (params["title"] as? JsonPrimitive)?.content?.trim().orEmpty(),
398-
startMs = start.toEpochMilli(),
399-
endMs = end.toEpochMilli(),
400-
isAllDay = (params["isAllDay"] as? JsonPrimitive)?.content?.toBooleanStrictOrNull() ?: false,
406+
startMs = addRange.start.toEpochMilli(),
407+
endMs = addRange.end.toEpochMilli(),
408+
isAllDay = isAllDay,
409+
timeZoneId = if (isAllDay) "UTC" else TimeZone.getDefault().id,
401410
location = (params["location"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
402411
notes = (params["notes"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
403412
calendarId = (params["calendarId"] as? JsonPrimitive)?.content?.toLongOrNull(),
404413
calendarTitle = (params["calendarTitle"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
405414
)
406415
}
407416

417+
private fun normalizeAddRange(
418+
start: Instant,
419+
end: Instant,
420+
isAllDay: Boolean,
421+
): CalendarAddRange {
422+
if (!isAllDay) return CalendarAddRange(start = start, end = end)
423+
if (end <= start) return CalendarAddRange(start = start, end = end)
424+
val dayStart = start.truncatedTo(ChronoUnit.DAYS)
425+
val dayEnd = end.truncatedTo(ChronoUnit.DAYS)
426+
return CalendarAddRange(
427+
start = dayStart,
428+
end = if (dayEnd > dayStart) dayEnd else dayStart.plus(1, ChronoUnit.DAYS),
429+
)
430+
}
431+
408432
private fun parseISO(raw: String?): Instant? {
409433
val value = raw?.trim().orEmpty()
410434
if (value.isEmpty()) return null

apps/android/app/src/test/java/ai/openclaw/app/node/CalendarHandlerTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.junit.Assert.assertEquals
99
import org.junit.Assert.assertFalse
1010
import org.junit.Assert.assertTrue
1111
import org.junit.Test
12+
import java.time.Instant
1213

1314
class CalendarHandlerTest : NodeHandlerRobolectricTest() {
1415
@Test
@@ -86,6 +87,24 @@ class CalendarHandlerTest : NodeHandlerRobolectricTest() {
8687
assertFalse(result.ok)
8788
assertEquals("CALENDAR_NOT_FOUND", result.error?.code)
8889
}
90+
91+
@Test
92+
fun handleCalendarAdd_normalizesAllDayEventForAndroidProvider() {
93+
val source = FakeCalendarDataSource(canRead = true, canWrite = true)
94+
val handler = CalendarHandler.forTesting(appContext(), source)
95+
96+
val result =
97+
handler.handleCalendarAdd(
98+
"""{"title":"X","startISO":"2026-07-05T09:00:00Z","endISO":"2026-07-06T09:00:00Z","isAllDay":true}""",
99+
)
100+
101+
assertTrue(result.ok)
102+
val request = source.addedRequest ?: error("missing add request")
103+
assertTrue(request.isAllDay)
104+
assertEquals("UTC", request.timeZoneId)
105+
assertEquals(Instant.parse("2026-07-05T00:00:00Z").toEpochMilli(), request.startMs)
106+
assertEquals(Instant.parse("2026-07-06T00:00:00Z").toEpochMilli(), request.endMs)
107+
}
89108
}
90109

91110
private class FakeCalendarDataSource(
@@ -104,6 +123,9 @@ private class FakeCalendarDataSource(
104123
),
105124
private val addError: Throwable? = null,
106125
) : CalendarDataSource {
126+
var addedRequest: CalendarAddRequest? = null
127+
private set
128+
107129
override fun hasReadPermission(context: Context): Boolean = canRead
108130

109131
override fun hasWritePermission(context: Context): Boolean = canWrite
@@ -118,6 +140,7 @@ private class FakeCalendarDataSource(
118140
request: CalendarAddRequest,
119141
): CalendarEventRecord {
120142
addError?.let { throw it }
143+
addedRequest = request
121144
return addResult
122145
}
123146
}

0 commit comments

Comments
 (0)