mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2025-12-23 23:17:50 -05:00
Don't subclass AndroidEvent / AndroidCalendar populate / build methods anymore (#1544)
* Fix tests * Update synctools; use AndroidCalendar SyncState * Update synctools; move companion objects to end of class declarations
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import androidx.test.filters.SdkSuppress
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
@@ -29,6 +30,7 @@ class OkhttpClientTest {
|
||||
|
||||
|
||||
@Test
|
||||
@SdkSuppress(maxSdkVersion = 34)
|
||||
fun testIcloudWithSettings() {
|
||||
httpClientBuilder.build().use { client ->
|
||||
client.okHttpClient
|
||||
|
||||
@@ -19,7 +19,7 @@ class Android10ResolverTest {
|
||||
val FQDN_DAVX5 = "www.davx5.com"
|
||||
|
||||
@Test
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q, maxSdkVersion = 34)
|
||||
fun testResolveA() {
|
||||
val www = InetAddress.getAllByName(FQDN_DAVX5).filterIsInstance<Inet4Address>().first()
|
||||
|
||||
|
||||
@@ -204,10 +204,12 @@ open class LocalAddressBook @AssistedInject constructor(
|
||||
.newUpdate(groupsSyncUri())
|
||||
.withSelection(Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?", arrayOf(oldAccount.name, oldAccount.type))
|
||||
.withValue(Groups.ACCOUNT_NAME, newAccount.name)
|
||||
.withValue(Groups.ACCOUNT_TYPE, newAccount.type)
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(rawContactsSyncUri())
|
||||
.withSelection(RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?", arrayOf(oldAccount.name, oldAccount.type))
|
||||
.withValue(RawContacts.ACCOUNT_NAME, newAccount.name)
|
||||
.withValue(RawContacts.ACCOUNT_TYPE, newAccount.type)
|
||||
batch.commit()
|
||||
|
||||
// update AndroidAddressBook.account
|
||||
|
||||
@@ -7,13 +7,13 @@ package at.bitfire.davdroid.resource
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.core.content.contentValuesOf
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidCalendarFactory
|
||||
import at.bitfire.ical4android.AndroidEvent
|
||||
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
|
||||
import at.bitfire.synctools.storage.BatchOperation
|
||||
import at.bitfire.synctools.storage.CalendarBatchOperation
|
||||
@@ -32,14 +32,9 @@ class LocalCalendar private constructor(
|
||||
id: Long
|
||||
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
|
||||
|
||||
companion object {
|
||||
private val logger: Logger
|
||||
get() = Logger.getLogger(javaClass.name)
|
||||
|
||||
private const val COLUMN_SYNC_STATE = Calendars.CAL_SYNC1
|
||||
|
||||
private val logger: Logger
|
||||
get() = Logger.getGlobal()
|
||||
|
||||
}
|
||||
|
||||
override val dbCollectionId: Long?
|
||||
get() = syncId?.toLongOrNull()
|
||||
@@ -50,29 +45,16 @@ class LocalCalendar private constructor(
|
||||
override val title: String
|
||||
get() = displayName ?: id.toString()
|
||||
|
||||
private var accessLevel: Int = Calendars.CAL_ACCESS_OWNER // assume full access if not specified
|
||||
override val readOnly
|
||||
get() = accessLevel <= Calendars.CAL_ACCESS_READ
|
||||
get() = accessLevel?.let { it <= Calendars.CAL_ACCESS_READ } ?: false
|
||||
|
||||
override var lastSyncState: SyncState?
|
||||
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return SyncState.fromString(cursor.getString(0))
|
||||
else
|
||||
null
|
||||
}
|
||||
get() = readSyncState()?.let { SyncState.fromString(it) }
|
||||
set(state) {
|
||||
val values = contentValuesOf(COLUMN_SYNC_STATE to state.toString())
|
||||
provider.update(calendarSyncURI(), values, null, null)
|
||||
writeSyncState(state.toString())
|
||||
}
|
||||
|
||||
|
||||
override fun populate(info: ContentValues) {
|
||||
super.populate(info)
|
||||
accessLevel = info.getAsInteger(Calendars.CALENDAR_ACCESS_LEVEL) ?: Calendars.CAL_ACCESS_OWNER
|
||||
}
|
||||
|
||||
|
||||
override fun findDeleted() =
|
||||
queryEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null)
|
||||
|
||||
@@ -112,7 +94,7 @@ class LocalCalendar private constructor(
|
||||
|
||||
|
||||
override fun markNotDirty(flags: Int): Int {
|
||||
val values = contentValuesOf(LocalEvent.COLUMN_FLAGS to flags)
|
||||
val values = contentValuesOf(AndroidEvent.COLUMN_FLAGS to flags)
|
||||
return provider.update(Events.CONTENT_URI.asSyncAdapter(account), values,
|
||||
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL",
|
||||
arrayOf(id.toString()))
|
||||
@@ -122,7 +104,7 @@ class LocalCalendar private constructor(
|
||||
var deleted = 0
|
||||
// list all non-dirty events with the given flags and delete every row + its exceptions
|
||||
provider.query(Events.CONTENT_URI.asSyncAdapter(account), arrayOf(Events._ID),
|
||||
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${LocalEvent.COLUMN_FLAGS}=?",
|
||||
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${AndroidEvent.COLUMN_FLAGS}=?",
|
||||
arrayOf(id.toString(), flags.toString()), null)?.use { cursor ->
|
||||
val batch = CalendarBatchOperation(provider)
|
||||
while (cursor.moveToNext()) {
|
||||
@@ -138,7 +120,7 @@ class LocalCalendar private constructor(
|
||||
}
|
||||
|
||||
override fun forgetETags() {
|
||||
val values = contentValuesOf(LocalEvent.COLUMN_ETAG to null)
|
||||
val values = contentValuesOf(AndroidEvent.COLUMN_ETAG to null)
|
||||
provider.update(Events.CONTENT_URI.asSyncAdapter(account), values, "${Events.CALENDAR_ID}=?",
|
||||
arrayOf(id.toString()))
|
||||
}
|
||||
@@ -149,7 +131,7 @@ class LocalCalendar private constructor(
|
||||
logger.info("Processing deleted exceptions")
|
||||
provider.query(
|
||||
Events.CONTENT_URI.asSyncAdapter(account),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(id.toString()), null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
@@ -162,7 +144,7 @@ class LocalCalendar private constructor(
|
||||
// get original event's SEQUENCE
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(account),
|
||||
arrayOf(LocalEvent.COLUMN_SEQUENCE),
|
||||
arrayOf(AndroidEvent.COLUMN_SEQUENCE),
|
||||
null, null, null)?.use { cursor2 ->
|
||||
if (cursor2.moveToNext()) {
|
||||
// original event is available
|
||||
@@ -171,7 +153,7 @@ class LocalCalendar private constructor(
|
||||
// re-schedule original event and set it to DIRTY
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(account))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.withValue(AndroidEvent.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.withValue(Events.DIRTY, 1)
|
||||
}
|
||||
}
|
||||
@@ -186,7 +168,7 @@ class LocalCalendar private constructor(
|
||||
logger.info("Processing dirty exceptions")
|
||||
provider.query(
|
||||
Events.CONTENT_URI.asSyncAdapter(account),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(id.toString()), null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
@@ -203,7 +185,7 @@ class LocalCalendar private constructor(
|
||||
// increase SEQUENCE and set DIRTY to 0
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(account))
|
||||
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(AndroidEvent.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
@@ -20,22 +20,103 @@ import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.ICalendar
|
||||
import at.bitfire.ical4android.ical4jVersion
|
||||
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
|
||||
import at.bitfire.synctools.storage.BatchOperation
|
||||
import net.fortuna.ical4j.model.property.ProdId
|
||||
import java.util.UUID
|
||||
|
||||
class LocalEvent: AndroidEvent, LocalResource<Event> {
|
||||
class LocalEvent : AndroidEvent, LocalResource<Event> {
|
||||
|
||||
override var fileName: String?
|
||||
get() = syncId
|
||||
private set(value) {
|
||||
syncId = value
|
||||
}
|
||||
|
||||
val weAreOrganizer
|
||||
get() = event!!.isOrganizer == true
|
||||
|
||||
|
||||
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int)
|
||||
: super(calendar, event, fileName, eTag, scheduleTag, flags)
|
||||
|
||||
private constructor(calendar: AndroidCalendar<*>, values: ContentValues)
|
||||
: super(calendar, values)
|
||||
|
||||
|
||||
/**
|
||||
* Creates and sets a new UID in the calendar provider, if no UID is already set.
|
||||
* It also returns the desired file name for the event for further processing in the sync algorithm.
|
||||
*
|
||||
* @return file name to use at upload
|
||||
*/
|
||||
override fun prepareForUpload(): String {
|
||||
// make sure that UID is set
|
||||
val uid: String = event!!.uid ?: run {
|
||||
// generate new UID
|
||||
val newUid = UUID.randomUUID().toString()
|
||||
|
||||
// update in calendar provider
|
||||
val values = contentValuesOf(Events.UID_2445 to newUid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
// update this event
|
||||
event?.uid = newUid
|
||||
|
||||
newUid
|
||||
}
|
||||
|
||||
val uidIsGoodFilename = uid.all { char ->
|
||||
// see RFC 2396 2.2
|
||||
char.isLetterOrDigit() || arrayOf( // allow letters and digits
|
||||
';', ':', '@', '&', '=', '+', '$', ',', // allow reserved characters except '/' and '?'
|
||||
'-', '_', '.', '!', '~', '*', '\'', '(', ')' // allow unreserved characters
|
||||
).contains(char)
|
||||
}
|
||||
return if (uidIsGoodFilename)
|
||||
"$uid.ics" // use UID as file name
|
||||
else
|
||||
"${UUID.randomUUID()}.ics" // UID would be dangerous as file name, use random UUID instead
|
||||
}
|
||||
|
||||
|
||||
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
|
||||
val values = ContentValues(5)
|
||||
if (fileName != null)
|
||||
values.put(Events._SYNC_ID, fileName)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
values.put(COLUMN_SCHEDULE_TAG, scheduleTag)
|
||||
values.put(COLUMN_SEQUENCE, event!!.sequence)
|
||||
values.put(Events.DIRTY, 0)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
if (fileName != null)
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
this.scheduleTag = scheduleTag
|
||||
}
|
||||
|
||||
override fun updateFlags(flags: Int) {
|
||||
val values = contentValuesOf(COLUMN_FLAGS to flags)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
val values = contentValuesOf(Events.DELETED to 0)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
object Factory : AndroidEventFactory<LocalEvent> {
|
||||
override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) =
|
||||
LocalEvent(calendar, values)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
init {
|
||||
ICalendar.prodId = ProdId("DAVx5/${BuildConfig.VERSION_NAME} ical4j/" + ical4jVersion)
|
||||
}
|
||||
|
||||
const val COLUMN_ETAG = Events.SYNC_DATA1
|
||||
const val COLUMN_FLAGS = Events.SYNC_DATA2
|
||||
const val COLUMN_SEQUENCE = Events.SYNC_DATA3
|
||||
const val COLUMN_SCHEDULE_TAG = Events.SYNC_DATA4
|
||||
|
||||
/**
|
||||
* Marks the event as deleted
|
||||
* @param eventID
|
||||
@@ -126,7 +207,7 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
|
||||
val exceptionInstances = numDirectInstances(provider, account, exceptionEventID)
|
||||
|
||||
if (exceptionInstances == null)
|
||||
// number of instances of exception can't be determined; so the total number of instances is also unclear
|
||||
// number of instances of exception can't be determined; so the total number of instances is also unclear
|
||||
return null
|
||||
|
||||
numInstances += exceptionInstances
|
||||
@@ -136,134 +217,4 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override var fileName: String? = null
|
||||
private set
|
||||
|
||||
override var eTag: String? = null
|
||||
override var scheduleTag: String? = null
|
||||
|
||||
override var flags: Int = 0
|
||||
private set
|
||||
|
||||
var weAreOrganizer = false
|
||||
private set
|
||||
|
||||
|
||||
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int): super(calendar, event) {
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
this.scheduleTag = scheduleTag
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
private constructor(calendar: AndroidCalendar<*>, values: ContentValues): super(calendar, values) {
|
||||
fileName = values.getAsString(Events._SYNC_ID)
|
||||
eTag = values.getAsString(COLUMN_ETAG)
|
||||
scheduleTag = values.getAsString(COLUMN_SCHEDULE_TAG)
|
||||
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
|
||||
}
|
||||
|
||||
override fun populateEvent(row: ContentValues, groupScheduled: Boolean) {
|
||||
val event = requireNotNull(event)
|
||||
event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
|
||||
|
||||
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
|
||||
weAreOrganizer = isOrganizer != null && isOrganizer != 0
|
||||
|
||||
super.populateEvent(row, groupScheduled)
|
||||
}
|
||||
|
||||
override fun buildEvent(recurrence: Event?, builder: BatchOperation.CpoBuilder) {
|
||||
val event = requireNotNull(event)
|
||||
|
||||
val buildException = recurrence != null
|
||||
val eventToBuild = recurrence ?: event
|
||||
|
||||
builder .withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
.withValue(Events.DELETED, 0)
|
||||
.withValue(COLUMN_FLAGS, flags)
|
||||
|
||||
if (buildException)
|
||||
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
|
||||
else
|
||||
builder .withValue(Events._SYNC_ID, fileName)
|
||||
.withValue(COLUMN_ETAG, eTag)
|
||||
.withValue(COLUMN_SCHEDULE_TAG, scheduleTag)
|
||||
|
||||
super.buildEvent(recurrence, builder)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates and sets a new UID in the calendar provider, if no UID is already set.
|
||||
* It also returns the desired file name for the event for further processing in the sync algorithm.
|
||||
*
|
||||
* @return file name to use at upload
|
||||
*/
|
||||
override fun prepareForUpload(): String {
|
||||
// make sure that UID is set
|
||||
val uid: String = event!!.uid ?: run {
|
||||
// generate new UID
|
||||
val newUid = UUID.randomUUID().toString()
|
||||
|
||||
// update in calendar provider
|
||||
val values = contentValuesOf(Events.UID_2445 to newUid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
// update this event
|
||||
event?.uid = newUid
|
||||
|
||||
newUid
|
||||
}
|
||||
|
||||
val uidIsGoodFilename = uid.all { char ->
|
||||
// see RFC 2396 2.2
|
||||
char.isLetterOrDigit() || arrayOf( // allow letters and digits
|
||||
';',':','@','&','=','+','$',',', // allow reserved characters except '/' and '?'
|
||||
'-','_','.','!','~','*','\'','(',')' // allow unreserved characters
|
||||
).contains(char)
|
||||
}
|
||||
return if (uidIsGoodFilename)
|
||||
"$uid.ics" // use UID as file name
|
||||
else
|
||||
"${UUID.randomUUID()}.ics" // UID would be dangerous as file name, use random UUID instead
|
||||
}
|
||||
|
||||
|
||||
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
|
||||
val values = ContentValues(5)
|
||||
if (fileName != null)
|
||||
values.put(Events._SYNC_ID, fileName)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
values.put(COLUMN_SCHEDULE_TAG, scheduleTag)
|
||||
values.put(COLUMN_SEQUENCE, event!!.sequence)
|
||||
values.put(Events.DIRTY, 0)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
if (fileName != null)
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
this.scheduleTag = scheduleTag
|
||||
}
|
||||
|
||||
override fun updateFlags(flags: Int) {
|
||||
val values = contentValuesOf(COLUMN_FLAGS to flags)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
val values = contentValuesOf(Events.DELETED to 0)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
object Factory: AndroidEventFactory<LocalEvent> {
|
||||
override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) =
|
||||
LocalEvent(calendar, values)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -110,7 +110,7 @@ class LocalTaskList private constructor(
|
||||
arrayOf(id.toString(), flags.toString()))
|
||||
|
||||
override fun forgetETags() {
|
||||
val values = contentValuesOf(LocalEvent.COLUMN_ETAG to null)
|
||||
val values = contentValuesOf(LocalTask.COLUMN_ETAG to null)
|
||||
provider.update(tasksSyncUri(), values, "${Tasks.LIST_ID}=?",
|
||||
arrayOf(id.toString()))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ androidx-test-junit = "1.2.1"
|
||||
androidx-work = "2.10.2"
|
||||
bitfire-cert4android = "b67ba86d31"
|
||||
bitfire-dav4jvm = "05fb8ecda6"
|
||||
bitfire-synctools = "366184ea7b"
|
||||
bitfire-synctools = "78cacf8eba"
|
||||
compose-accompanist = "0.37.3"
|
||||
compose-bom = "2025.06.01"
|
||||
dnsjava = "3.6.3"
|
||||
|
||||
Reference in New Issue
Block a user