Skip to content

Commit 5f801a0

Browse files
committed
Add coroutine dispatchers for push registration and unregistration
1 parent 5b68505 commit 5f801a0

11 files changed

Lines changed: 294 additions & 224 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
*/
4+
5+
package at.bitfire.davdroid.push
6+
7+
import dagger.hilt.android.testing.HiltAndroidRule
8+
import dagger.hilt.android.testing.HiltAndroidTest
9+
import org.junit.Assert
10+
import org.junit.Before
11+
import org.junit.Rule
12+
import org.junit.Test
13+
import javax.inject.Inject
14+
15+
@HiltAndroidTest
16+
class PushMessageHandlerTest {
17+
18+
@get:Rule
19+
val hiltRule = HiltAndroidRule(this)
20+
21+
@Inject
22+
lateinit var handler: PushMessageHandler
23+
24+
@Before
25+
fun setUp() {
26+
hiltRule.inject()
27+
}
28+
29+
30+
@Test
31+
fun testParse_InvalidXml() {
32+
Assert.assertNull(handler.parse("Non-XML content"))
33+
}
34+
35+
@Test
36+
fun testParse_WithXmlDeclAndTopic() {
37+
val topic = handler.parse(
38+
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
39+
"<P:push-message xmlns:D=\"DAV:\" xmlns:P=\"https://bitfire.at/webdav-push\">" +
40+
" <P:topic>O7M1nQ7cKkKTKsoS_j6Z3w</P:topic>" +
41+
"</P:push-message>"
42+
)
43+
Assert.assertEquals("O7M1nQ7cKkKTKsoS_j6Z3w", topic)
44+
}
45+
46+
}

app/src/main/kotlin/at/bitfire/davdroid/db/CollectionDao.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ interface CollectionDao {
3333
fun getByServiceAndType(serviceId: Long, @CollectionType type: String): List<Collection>
3434

3535
@Query("SELECT * FROM collection WHERE pushTopic=:topic AND sync")
36-
fun getSyncableByPushTopic(topic: String): Collection?
36+
suspend fun getSyncableByPushTopic(topic: String): Collection?
3737

3838
@Query("SELECT pushVapidKey FROM collection WHERE serviceId=:serviceId AND pushVapidKey IS NOT NULL LIMIT 1")
3939
suspend fun getFirstVapidKey(serviceId: Long): String?

app/src/main/kotlin/at/bitfire/davdroid/db/ServiceDao.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ interface ServiceDao {
2525
@Query("SELECT * FROM service WHERE id=:id")
2626
fun get(id: Long): Service?
2727

28+
@Query("SELECT * FROM service WHERE id=:id")
29+
suspend fun getAsync(id: Long): Service?
30+
2831
@Query("SELECT * FROM service")
2932
suspend fun getAll(): List<Service>
3033

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
*/
4+
5+
package at.bitfire.davdroid.push
6+
7+
import androidx.annotation.VisibleForTesting
8+
import at.bitfire.dav4jvm.XmlReader
9+
import at.bitfire.dav4jvm.XmlUtils
10+
import at.bitfire.davdroid.db.Collection.Companion.TYPE_ADDRESSBOOK
11+
import at.bitfire.davdroid.repository.AccountRepository
12+
import at.bitfire.davdroid.repository.DavCollectionRepository
13+
import at.bitfire.davdroid.repository.DavServiceRepository
14+
import at.bitfire.davdroid.sync.SyncDataType
15+
import at.bitfire.davdroid.sync.TasksAppManager
16+
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
17+
import dagger.Lazy
18+
import org.unifiedpush.android.connector.data.PushMessage
19+
import org.xmlpull.v1.XmlPullParserException
20+
import java.io.StringReader
21+
import java.util.logging.Level
22+
import java.util.logging.Logger
23+
import javax.inject.Inject
24+
import at.bitfire.dav4jvm.property.push.PushMessage as DavPushMessage
25+
26+
/**
27+
* Handles incoming WebDAV-Push messages.
28+
*/
29+
class PushMessageHandler @Inject constructor(
30+
private val accountRepository: AccountRepository,
31+
private val collectionRepository: DavCollectionRepository,
32+
private val logger: Logger,
33+
private val serviceRepository: DavServiceRepository,
34+
private val syncWorkerManager: SyncWorkerManager,
35+
private val tasksAppManager: Lazy<TasksAppManager>
36+
) {
37+
38+
suspend fun processMessage(message: PushMessage, instance: String) {
39+
if (!message.decrypted) {
40+
logger.severe("Received a push message that could not be decrypted.")
41+
return
42+
}
43+
val messageXml = message.content.toString(Charsets.UTF_8)
44+
logger.log(Level.INFO, "Received push message", messageXml)
45+
46+
// parse push notification
47+
val topic = parse(messageXml)
48+
49+
// sync affected collection
50+
if (topic != null) {
51+
logger.info("Got push notification for topic $topic")
52+
53+
// Sync all authorities of account that the collection belongs to
54+
// Later: only sync affected collection and authorities
55+
collectionRepository.getSyncableByTopic(topic)?.let { collection ->
56+
serviceRepository.getAsync(collection.serviceId)?.let { service ->
57+
val syncDataTypes = mutableSetOf<SyncDataType>()
58+
// If the type is an address book, add the contacts type
59+
if (collection.type == TYPE_ADDRESSBOOK)
60+
syncDataTypes += SyncDataType.CONTACTS
61+
62+
// If the collection supports events, add the events type
63+
if (collection.supportsVEVENT != false)
64+
syncDataTypes += SyncDataType.EVENTS
65+
66+
// If the collection supports tasks, make sure there's a provider installed,
67+
// and add the tasks type
68+
if (collection.supportsVJOURNAL != false || collection.supportsVTODO != false)
69+
if (tasksAppManager.get().currentProvider() != null)
70+
syncDataTypes += SyncDataType.TASKS
71+
72+
// Schedule sync for all the types identified
73+
val account = accountRepository.fromName(service.accountName)
74+
for (syncDataType in syncDataTypes)
75+
syncWorkerManager.enqueueOneTime(account, syncDataType, fromPush = true)
76+
}
77+
}
78+
79+
} else {
80+
// fallback when no known topic is present (shouldn't happen)
81+
val service = instance.toLongOrNull()?.let { serviceRepository.get(it) }
82+
if (service != null) {
83+
logger.warning("Got push message without topic and service, syncing all accounts")
84+
val account = accountRepository.fromName(service.accountName)
85+
syncWorkerManager.enqueueOneTimeAllAuthorities(account, fromPush = true)
86+
87+
} else {
88+
logger.warning("Got push message without topic, syncing all accounts")
89+
for (account in accountRepository.getAll())
90+
syncWorkerManager.enqueueOneTimeAllAuthorities(account, fromPush = true)
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Parses a WebDAV-Push message and returns the `topic` that the message is about.
97+
*
98+
* @return topic of the modified collection, or `null` if the topic couldn't be determined
99+
*/
100+
@VisibleForTesting
101+
internal fun parse(message: String): String? {
102+
var topic: String? = null
103+
104+
val parser = XmlUtils.newPullParser()
105+
try {
106+
parser.setInput(StringReader(message))
107+
108+
XmlReader(parser).processTag(DavPushMessage.NAME) {
109+
val pushMessage = DavPushMessage.Factory.create(parser)
110+
topic = pushMessage.topic?.topic
111+
}
112+
} catch (e: XmlPullParserException) {
113+
logger.log(Level.WARNING, "Couldn't parse push message", e)
114+
}
115+
116+
return topic
117+
}
118+
119+
}

app/src/main/kotlin/at/bitfire/davdroid/push/PushMessageParser.kt

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)