calendar: кэш событий по CTag (Этап 2, DAV ч.2)

listCalendars теперь забирает и cs:getctag (тот же PROPFIND — ноль лишних
запросов; DavCalendar.ctag). Новый CalendarEventsCache — дисковый кэш
СЫРЫХ REPORT-ответов per (аккаунт, календарь, диапазон), ключ — CTag:
не менялся → события парсятся с диска (боевым парсером CalDavClient),
REPORT-запросы в сеть не идут. Изменился → сеть + обновление кэша.
Хранение сырого XML вместо сериализации моделей — без хрупкого
JSON-маппинга ~20 полей события; чистка записей старше 30 дней.
CalDavClient.queryEventsInRange разделён на queryEventsRawXml +
parseEventsXml (поведение прежнее). Эффект: переключение месяцев,
периодический рефреш и возврат в календарь при неизменных календарях —
1 PROPFIND вместо 1+N REPORT. +5 unit-тестов кэша (JVM, TemporaryFolder).
This commit is contained in:
b-dev-mobile
2026-07-09 09:39:07 +00:00
parent 4c403e5ec2
commit fb57156ee9
6 changed files with 157 additions and 8 deletions
@@ -22,6 +22,9 @@ data class DavCalendar(
val href: String,
val displayName: String,
val color: String? = null,
// CTag коллекции (CalendarServer-расширение, Nextcloud поддерживает):
// меняется при любом изменении в календаре → ключ инкрементального кэша событий.
val ctag: String = "",
)
data class DavEvent(
@@ -103,7 +106,7 @@ object CalDavClient {
val body = """
<?xml version="1.0" encoding="utf-8"?>
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:ical="http://apple.com/ns/ical/">
<d:prop><d:displayname/><d:resourcetype/><ical:calendar-color/></d:prop>
<d:prop><d:displayname/><d:resourcetype/><ical:calendar-color/><cs:getctag/></d:prop>
</d:propfind>
""".trimIndent()
val xml = propfind(client, baseUrl, depth = 1, body)
@@ -129,7 +132,15 @@ object CalDavClient {
calendar: DavCalendar,
rangeStart: Instant,
rangeEnd: Instant,
): List<DavEvent> {
): List<DavEvent> = parseEventsXml(queryEventsRawXml(client, calendar, rangeStart, rangeEnd), calendar)
/** Сырой REPORT-ответ (для кэширования на диске с ключом по CTag). */
fun queryEventsRawXml(
client: OkHttpClient,
calendar: DavCalendar,
rangeStart: Instant,
rangeEnd: Instant,
): String {
val startStr = formatCalDavTime(rangeStart)
val endStr = formatCalDavTime(rangeEnd)
val body = """
@@ -146,10 +157,13 @@ object CalDavClient {
</c:calendar-query>
""".trimIndent()
val href = calendar.href.trimEnd('/') + "/"
val xml = report(client, href, body)
return parseCalendarQueryResponses(xml, calendar)
return report(client, href, body)
}
/** Парсинг REPORT-ответа (в т.ч. взятого из кэша). */
fun parseEventsXml(xml: String, calendar: DavCalendar): List<DavEvent> =
parseCalendarQueryResponses(xml, calendar)
fun createEvent(
client: OkHttpClient,
calendar: DavCalendar,
@@ -782,6 +796,7 @@ object CalDavClient {
var href = ""
var displayName = ""
var calendarColor: String? = null
var ctag = ""
var isCollection = false
while (parser.eventType != XmlPullParser.END_DOCUMENT) {
when (parser.eventType) {
@@ -791,11 +806,13 @@ object CalDavClient {
href = ""
displayName = ""
calendarColor = null
ctag = ""
isCollection = false
}
"collection" -> if (inResponse) isCollection = true
"displayname" -> if (inResponse) displayName = parser.readText().trim()
"calendar-color" -> if (inResponse) calendarColor = parser.readText().trim()
"getctag" -> if (inResponse) ctag = parser.readText().trim()
"href" -> if (inResponse) href = parser.readText().trim()
}
XmlPullParser.END_TAG -> if (parser.localTag() == "response" && inResponse) {
@@ -809,7 +826,7 @@ object CalDavClient {
val name = displayName.ifBlank {
fullPath.removePrefix(basePath).trim('/').substringAfterLast('/')
}
out += DavCalendar(href = full, displayName = name, color = calendarColor)
out += DavCalendar(href = full, displayName = name, color = calendarColor, ctag = ctag)
}
}
inResponse = false