contacts: инкрементальная синхронизация по CTag (Этап 2, DAV)
CardDavClient.collectionSignature() — дешёвый PROPFIND depth:1 за CTag книг (расширение CalendarServer, Nextcloud; фолбэк sync-token), без address-data. ContactsRepository.syncContacts: перед полной закачкой всех vCard сверяет подпись коллекций; не менялось → отдаём кэш (Room), не качаем. Ускоряет pull-to-refresh (раньше — полный PROPFIND ~500 vCard каждый раз). +4 unit-теста на parseCollectionSignature (regex-парсер, без Android XmlPull).
This commit is contained in:
@@ -69,6 +69,53 @@ object CardDavClient {
|
||||
return out.distinctBy { "${it.uid}|${it.email}" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Дешёвая «подпись» состояния адресных книг — CTag коллекций (расширение
|
||||
* CalendarServer, поддерживается Nextcloud), с фолбэком на sync-token.
|
||||
* Один PROPFIND depth:1 без address-data. Если подпись не изменилась с прошлой
|
||||
* синхронизации — контакты качать не нужно. null при ошибке/неподдержке → полный sync.
|
||||
*/
|
||||
fun collectionSignature(
|
||||
client: OkHttpClient,
|
||||
serverUrl: String,
|
||||
userId: String,
|
||||
): String? = runCatching {
|
||||
val base = davAddressBooksBaseUrl(serverUrl, userId)
|
||||
val body = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
|
||||
<d:prop><cs:getctag/><d:sync-token/></d:prop>
|
||||
</d:propfind>
|
||||
""".trimIndent()
|
||||
val xml = propfind(client, base, depth = 1, body)
|
||||
parseCollectionSignature(xml).ifBlank { null }
|
||||
}.getOrNull()
|
||||
|
||||
/**
|
||||
* Парсит PROPFIND-ответ в стабильную подпись «href=ctag» (по книгам, отсортировано).
|
||||
* Regex-парсинг (как ICS в проекте) — чистая функция, тестируется без Android XmlPull.
|
||||
*/
|
||||
internal fun parseCollectionSignature(xml: String): String {
|
||||
val entries = sortedSetOf<String>()
|
||||
for (m in responseBlockPattern.findAll(xml)) {
|
||||
val block = m.value
|
||||
val href = hrefTagPattern.find(block)?.groupValues?.get(1)?.trim().orEmpty()
|
||||
val tag = (
|
||||
getctagPattern.find(block)?.groupValues?.get(1)
|
||||
?: syncTokenPattern.find(block)?.groupValues?.get(1)
|
||||
)?.trim().orEmpty()
|
||||
if (href.isNotBlank() && tag.isNotBlank()) {
|
||||
entries += "$href=$tag"
|
||||
}
|
||||
}
|
||||
return entries.joinToString("\n")
|
||||
}
|
||||
|
||||
private val responseBlockPattern = Regex("(?is)<(?:\\w+:)?response\\b.*?</(?:\\w+:)?response>")
|
||||
private val hrefTagPattern = Regex("(?is)<(?:\\w+:)?href>(.*?)</(?:\\w+:)?href>")
|
||||
private val getctagPattern = Regex("(?is)<(?:\\w+:)?getctag>(.*?)</(?:\\w+:)?getctag>")
|
||||
private val syncTokenPattern = Regex("(?is)<(?:\\w+:)?sync-token>(.*?)</(?:\\w+:)?sync-token>")
|
||||
|
||||
fun createContact(
|
||||
client: OkHttpClient,
|
||||
serverUrl: String,
|
||||
|
||||
Reference in New Issue
Block a user