Unsubscribe push from unsynced collections (#1011)

* Unsubscribe push from unsynced collections

* Remove subscription from DB, too

* Subscription: catch HTTP errors
This commit is contained in:
Ricki Hirner 2024-09-10 12:07:04 +02:00 committed by GitHub
parent 0581417bba
commit 0b9d4cd3b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 73 additions and 19 deletions

View file

@ -72,6 +72,9 @@ interface CollectionDao {
@Query("SELECT * FROM collection WHERE sync AND supportsWebPush AND pushTopic IS NOT NULL")
suspend fun getPushCapableSyncCollections(): List<Collection>
@Query("SELECT * FROM collection WHERE pushSubscription IS NOT NULL AND NOT sync")
suspend fun getPushRegisteredAndNotSyncable(): List<Collection>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(collection: Collection): Long
@ -85,7 +88,7 @@ interface CollectionDao {
suspend fun updateForceReadOnly(id: Long, forceReadOnly: Boolean)
@Query("UPDATE collection SET pushSubscription=:pushSubscription, pushSubscriptionCreated=:updatedAt WHERE id=:id")
suspend fun updatePushSubscription(id: Long, pushSubscription: String, updatedAt: Long = System.currentTimeMillis())
fun updatePushSubscription(id: Long, pushSubscription: String?, updatedAt: Long = System.currentTimeMillis())
@Query("UPDATE collection SET sync=:sync WHERE id=:id")
suspend fun updateSync(id: Long, sync: Boolean)

View file

@ -19,6 +19,7 @@ import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.dav4jvm.XmlUtils.insertTag
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.property.push.NS_WEBDAV_PUSH
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
@ -35,11 +36,13 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.StringWriter
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
@ -48,7 +51,8 @@ import javax.inject.Inject
* To be run as soon as a collection that supports push is changed (selected for sync status
* changes, or collection is created, deleted, etc).
*
* TODO Should run periodically, too. Not required for a first demonstration version.
* TODO Should run periodically, too (to refresh registrations that are about to expire).
* Not required for a first demonstration version.
*/
@Suppress("unused")
@HiltWorker
@ -85,7 +89,16 @@ class PushRegistrationWorker @AssistedInject constructor(
}
private suspend fun requestPushRegistration(collection: Collection, account: Account, endpoint: String) {
override suspend fun doWork(): Result {
logger.info("Running push registration worker")
registerSyncable()
unregisterNotSyncable()
return Result.success()
}
private suspend fun registerPushSubscription(collection: Collection, account: Account, endpoint: String) {
val settings = accountSettingsFactory.forAccount(account)
runInterruptible {
@ -112,7 +125,7 @@ class PushRegistrationWorker @AssistedInject constructor(
val xml = writer.toString().toRequestBody(DavResource.MIME_XML)
DavCollection(httpClient, collection.url).post(xml) { response ->
if (response.isSuccessful) runBlocking {
if (response.isSuccessful) {
response.header("Location")?.let { subscriptionUrl ->
collectionRepository.updatePushSubscription(collection.id, subscriptionUrl)
}
@ -123,23 +136,61 @@ class PushRegistrationWorker @AssistedInject constructor(
}
}
override suspend fun doWork(): Result {
logger.info("Running push registration worker")
private suspend fun registerSyncable() {
val endpoint = preferenceRepository.unifiedPushEndpoint()
// register push subscription for syncable collections
if (endpoint != null)
for (collection in collectionRepository.getSyncableAndPushCapable()) {
for (collection in collectionRepository.getPushCapableAndSyncable()) {
logger.info("Registering push for ${collection.url}")
val service = serviceRepository.get(collection.serviceId) ?: continue
val account = Account(service.accountName, applicationContext.getString(R.string.account_type))
requestPushRegistration(collection, account, endpoint)
serviceRepository.get(collection.serviceId)?.let { service ->
val account = Account(service.accountName, applicationContext.getString(R.string.account_type))
try {
registerPushSubscription(collection, account, endpoint)
} catch (e: DavException) {
// catch possible per-collection exception so that all collections can be processed
logger.log(Level.WARNING, "Couldn't register push for ${collection.url}", e)
}
}
}
else
logger.info("No UnifiedPush endpoint configured")
}
return Result.success()
private suspend fun unregisterPushSubscription(collection: Collection, account: Account, url: HttpUrl) {
val settings = accountSettingsFactory.forAccount(account)
runInterruptible {
HttpClient.Builder(applicationContext, settings)
.setForeground(true)
.build()
.use { client ->
val httpClient = client.okHttpClient
try {
DavResource(httpClient, url).delete {
// deleted
}
} catch (e: DavException) {
logger.log(Level.WARNING, "Couldn't unregister push for ${collection.url}", e)
}
// remove registration URL from DB in any case
collectionRepository.updatePushSubscription(collection.id, null)
}
}
}
private suspend fun unregisterNotSyncable() {
for (collection in collectionRepository.getPushRegisteredAndNotSyncable()) {
logger.info("Unregistering push for ${collection.url}")
collection.pushSubscription?.toHttpUrlOrNull()?.let { url ->
serviceRepository.get(collection.serviceId)?.let { service ->
val account = Account(service.accountName, applicationContext.getString(R.string.account_type))
unregisterPushSubscription(collection, account, url)
}
}
}
}

View file

@ -6,8 +6,6 @@ package at.bitfire.davdroid.repository
import android.accounts.Account
import android.content.Context
import android.provider.CalendarContract
import android.provider.ContactsContract
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.dav4jvm.XmlUtils.insertTag
@ -29,7 +27,6 @@ import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.util.DateUtils
import dagger.Module
import dagger.hilt.InstallIn
@ -196,9 +193,12 @@ class DavCollectionRepository @Inject constructor(
fun getSyncTaskLists(serviceId: Long) = dao.getSyncTaskLists(serviceId)
/** Returns all collections that are both selected for synchronization and push-capable. */
suspend fun getSyncableAndPushCapable(): List<Collection> =
suspend fun getPushCapableAndSyncable(): List<Collection> =
dao.getPushCapableSyncCollections()
suspend fun getPushRegisteredAndNotSyncable(): List<Collection> =
dao.getPushRegisteredAndNotSyncable()
/**
* Inserts or updates the collection. On update it will not update flag values ([Collection.sync],
* [Collection.forceReadOnly]), but use the values of the already existing collection.
@ -246,7 +246,7 @@ class DavCollectionRepository @Inject constructor(
notifyOnChangeListeners()
}
suspend fun updatePushSubscription(id: Long, subscriptionUrl: String) {
fun updatePushSubscription(id: Long, subscriptionUrl: String?) {
dao.updatePushSubscription(id, subscriptionUrl)
}