Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

Commit c198606

Browse files
authored
Extract sub-row builders and refactor DmfsTaskBuilder to Entity-based approach (#366)
* Merge main into 340-5-subrow-entity-refactor * Add note about PARENT_ID alteration in RelationsBuilder * Remove return value from addRows in DmfsTaskBuilder * Extract CreatedBuilder and LastModifiedBuilder from DmfsTaskBuilder * Add logging for task update operation * Add null check for unknown property value in UnknownPropertiesBuilder
1 parent 22c9dd3 commit c198606

19 files changed

Lines changed: 884 additions & 168 deletions

lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,7 @@ class DmfsTask(
145145

146146
val requiredTask = requireNotNull(task)
147147
val builder = DmfsTaskBuilder(taskList, requiredTask, id, syncId, eTag, flags)
148-
val idxTask = builder.addRows(batch)
149-
builder.insertProperties(batch, idxTask)
148+
builder.addRows(batch)
150149

151150
batch.commit()
152151

@@ -180,9 +179,6 @@ class DmfsTask(
180179
val builder = DmfsTaskBuilder(taskList, task, id, syncId, eTag, flags)
181180
builder.updateRows(batch)
182181

183-
// insert task properties again
184-
builder.insertProperties(batch, null)
185-
186182
batch.commit()
187183
return ContentUris.withAppendedId(Tasks.getContentUri(taskList.providerName.authority), existingId)
188184
}

lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt

Lines changed: 61 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,48 @@ package at.bitfire.synctools.mapping.tasks
99
import android.content.ContentValues
1010
import android.content.Entity
1111
import at.bitfire.ical4android.Task
12-
import at.bitfire.ical4android.UnknownProperty
12+
import at.bitfire.synctools.mapping.tasks.builder.AlarmsBuilder
1313
import at.bitfire.synctools.mapping.tasks.builder.AllDayBuilder
14+
import at.bitfire.synctools.mapping.tasks.builder.CategoriesBuilder
1415
import at.bitfire.synctools.mapping.tasks.builder.ClassificationBuilder
1516
import at.bitfire.synctools.mapping.tasks.builder.ColorBuilder
17+
import at.bitfire.synctools.mapping.tasks.builder.CommentsBuilder
1618
import at.bitfire.synctools.mapping.tasks.builder.CompletedBuilder
19+
import at.bitfire.synctools.mapping.tasks.builder.CreatedBuilder
1720
import at.bitfire.synctools.mapping.tasks.builder.DescriptionBuilder
1821
import at.bitfire.synctools.mapping.tasks.builder.DirtyBuilder
1922
import at.bitfire.synctools.mapping.tasks.builder.DmfsTaskFieldBuilder
2023
import at.bitfire.synctools.mapping.tasks.builder.DueBuilder
2124
import at.bitfire.synctools.mapping.tasks.builder.DurationBuilder
2225
import at.bitfire.synctools.mapping.tasks.builder.ETagBuilder
2326
import at.bitfire.synctools.mapping.tasks.builder.GeoBuilder
27+
import at.bitfire.synctools.mapping.tasks.builder.LastModifiedBuilder
28+
import at.bitfire.synctools.mapping.tasks.builder.ListIdBuilder
2429
import at.bitfire.synctools.mapping.tasks.builder.LocationBuilder
2530
import at.bitfire.synctools.mapping.tasks.builder.OrganizerBuilder
2631
import at.bitfire.synctools.mapping.tasks.builder.PercentCompleteBuilder
2732
import at.bitfire.synctools.mapping.tasks.builder.PriorityBuilder
2833
import at.bitfire.synctools.mapping.tasks.builder.RecurrenceFieldsBuilder
34+
import at.bitfire.synctools.mapping.tasks.builder.RelationsBuilder
2935
import at.bitfire.synctools.mapping.tasks.builder.SequenceBuilder
3036
import at.bitfire.synctools.mapping.tasks.builder.StartTimeBuilder
3137
import at.bitfire.synctools.mapping.tasks.builder.StatusBuilder
3238
import at.bitfire.synctools.mapping.tasks.builder.SyncFlagsBuilder
3339
import at.bitfire.synctools.mapping.tasks.builder.SyncIdBuilder
3440
import at.bitfire.synctools.mapping.tasks.builder.TitleBuilder
3541
import at.bitfire.synctools.mapping.tasks.builder.UidBuilder
42+
import at.bitfire.synctools.mapping.tasks.builder.UnknownPropertiesBuilder
3643
import at.bitfire.synctools.mapping.tasks.builder.UrlBuilder
3744
import at.bitfire.synctools.storage.BatchOperation.CpoBuilder
38-
import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA
3945
import at.bitfire.synctools.storage.tasks.DmfsTaskList
4046
import at.bitfire.synctools.storage.tasks.TasksBatchOperation
41-
import at.bitfire.synctools.util.AlarmTriggerCalculator
42-
import net.fortuna.ical4j.model.Parameter
43-
import net.fortuna.ical4j.model.Property
44-
import net.fortuna.ical4j.model.parameter.RelType
45-
import net.fortuna.ical4j.model.parameter.Related
46-
import net.fortuna.ical4j.model.property.Action
47-
import net.fortuna.ical4j.model.property.immutable.ImmutableAction
4847
import org.dmfs.tasks.contract.TaskContract.Properties
49-
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
50-
import org.dmfs.tasks.contract.TaskContract.Property.Category
51-
import org.dmfs.tasks.contract.TaskContract.Property.Comment
52-
import org.dmfs.tasks.contract.TaskContract.Property.Relation
5348
import org.dmfs.tasks.contract.TaskContract.Tasks
54-
import java.util.Locale
5549
import java.util.logging.Level
5650
import java.util.logging.Logger
57-
import kotlin.jvm.optionals.getOrNull
5851

5952
/**
60-
* Writes [at.bitfire.ical4android.Task] to dmfs task provider data rows
61-
* (former DmfsTask "build..." methods).
53+
* Writes [at.bitfire.ical4android.Task] to dmfs task provider data rows.
6254
*/
6355
class DmfsTaskBuilder(
6456
private val taskList: DmfsTaskList,
@@ -71,20 +63,20 @@ class DmfsTaskBuilder(
7163
private val flags: Int,
7264
) {
7365

66+
private val logger
67+
get() = Logger.getLogger(javaClass.name)
68+
7469
private val fieldBuilders: Array<DmfsTaskFieldBuilder> = arrayOf(
7570
// main task row fields
7671
UidBuilder(),
7772
SyncIdBuilder(syncId),
7873
ETagBuilder(eTag),
7974
SyncFlagsBuilder(flags),
8075
SequenceBuilder(),
76+
ListIdBuilder(taskList.id),
8177
DirtyBuilder(),
82-
// status fields
83-
PriorityBuilder(),
84-
ClassificationBuilder(),
85-
StatusBuilder(),
86-
CompletedBuilder(),
87-
PercentCompleteBuilder(),
78+
CreatedBuilder(),
79+
LastModifiedBuilder(),
8880
// content fields
8981
TitleBuilder(),
9082
DescriptionBuilder(),
@@ -93,162 +85,73 @@ class DmfsTaskBuilder(
9385
ColorBuilder(),
9486
UrlBuilder(),
9587
OrganizerBuilder(),
96-
// time fields and recurrence
88+
// status fields
89+
PriorityBuilder(),
90+
ClassificationBuilder(),
91+
StatusBuilder(),
92+
CompletedBuilder(),
93+
PercentCompleteBuilder(),
94+
// time fields
9795
AllDayBuilder(),
9896
StartTimeBuilder(),
9997
DueBuilder(),
10098
DurationBuilder(),
99+
// recurrence
101100
RecurrenceFieldsBuilder(),
102-
// property sub-rows (still inline below via insertProperties)
101+
// property sub-rows
102+
AlarmsBuilder(taskList),
103+
CategoriesBuilder(taskList),
104+
CommentsBuilder(taskList),
105+
RelationsBuilder(taskList),
106+
UnknownPropertiesBuilder(taskList),
103107
)
104108

105-
private val logger
106-
get() = Logger.getLogger(javaClass.name)
107-
108-
fun addRows(batch: TasksBatchOperation): Int {
109-
val builder = CpoBuilder.newInsert(taskList.tasksUri())
110-
buildTask(builder, false)
111-
val idxTask = batch.nextBackrefIdx() // Get nextBackrefIdx BEFORE adding builder to batch
112-
batch += builder
113-
return idxTask
114-
}
115-
116-
fun updateRows(batch: TasksBatchOperation) {
117-
val id = requireNotNull(id)
118-
val builder = CpoBuilder.newUpdate(taskList.taskUri(id))
119-
buildTask(builder, true)
120-
batch += builder
121-
}
122-
123-
private fun buildTask(builder: CpoBuilder, update: Boolean) {
124-
if (!update)
125-
builder .withValue(Tasks.LIST_ID, taskList.id)
126109

127-
// new builders
110+
fun addRows(batch: TasksBatchOperation) {
111+
val entity = buildTask()
128112

129-
val entity = Entity(ContentValues())
130-
for (fieldBuilder in fieldBuilders)
131-
fieldBuilder.build(task, entity)
132-
builder.withValues(entity.entityValues)
133-
134-
// old builders
135-
136-
// parent_id will be re-calculated when the relation row is inserted (if there is any)
137-
.withValue(Tasks.PARENT_ID, null)
113+
val mainBuilder = CpoBuilder.newInsert(taskList.tasksUri())
114+
.withValues(entity.entityValues)
115+
val idxTask = batch.nextBackrefIdx() // Get nextBackrefIdx BEFORE adding builder to batch
116+
batch += mainBuilder
138117

139-
builder
140-
.withValue(Tasks.CREATED, task.createdAt)
141-
.withValue(Tasks.LAST_MODIFIED, task.lastModified)
118+
for (subValue in entity.subValues)
119+
batch += CpoBuilder.newInsert(subValue.uri)
120+
.withValues(subValue.values)
121+
.withValueBackReference(Properties.TASK_ID, idxTask)
142122

143-
logger.log(Level.FINE, "Built task object", builder.build())
123+
logger.log(Level.FINE, "Added task", mainBuilder.build())
144124
}
145125

146-
fun insertProperties(batch: TasksBatchOperation, idxTask: Int?) {
147-
insertAlarms(batch, idxTask)
148-
insertCategories(batch, idxTask)
149-
insertComment(batch, idxTask)
150-
insertRelatedTo(batch, idxTask)
151-
insertUnknownProperties(batch, idxTask)
152-
}
126+
fun updateRows(batch: TasksBatchOperation) {
127+
val existingId = requireNotNull(id)
128+
val entity = buildTask()
153129

154-
private fun insertAlarms(batch: TasksBatchOperation, idxTask: Int?) {
155-
for (alarm in task.alarms) {
156-
val (alarmRef, minutes) = AlarmTriggerCalculator.alarmTriggerToMinutes(
157-
alarm = alarm,
158-
refStart = task.dtStart,
159-
refEnd = task.end,
160-
allowRelEnd = true
161-
) ?: continue
162-
val ref = when (alarmRef) {
163-
Related.END ->
164-
Alarm.ALARM_REFERENCE_DUE_DATE
165-
else /* Related.START is the default value */ ->
166-
Alarm.ALARM_REFERENCE_START_DATE
167-
}
168-
169-
val alarmType = when (
170-
alarm.getProperty<Action>(Property.ACTION).getOrNull()?.value?.uppercase(Locale.ROOT)
171-
) {
172-
ImmutableAction.VALUE_AUDIO -> Alarm.ALARM_TYPE_SOUND
173-
ImmutableAction.VALUE_DISPLAY -> Alarm.ALARM_TYPE_MESSAGE
174-
ImmutableAction.VALUE_EMAIL -> Alarm.ALARM_TYPE_EMAIL
175-
else -> Alarm.ALARM_TYPE_NOTHING
176-
}
177-
178-
val builder = CpoBuilder
179-
.newInsert(taskList.tasksPropertiesUri())
180-
.withTaskId(Alarm.TASK_ID, idxTask)
181-
.withValue(Alarm.MIMETYPE, Alarm.CONTENT_ITEM_TYPE)
182-
.withValue(Alarm.MINUTES_BEFORE, minutes)
183-
.withValue(Alarm.REFERENCE, ref)
184-
.withValue(Alarm.MESSAGE, alarm.description?.value ?: alarm.summary)
185-
.withValue(Alarm.ALARM_TYPE, alarmType)
186-
187-
logger.log(Level.FINE, "Inserting alarm", builder.build())
188-
batch += builder
130+
val mainValues = ContentValues(entity.entityValues).apply {
131+
// LIST_ID must not be updated (it doesn't change for updates, and setting it would cause issues)
132+
remove(Tasks.LIST_ID)
189133
}
190-
}
134+
val mainBuilder = CpoBuilder.newUpdate(taskList.taskUri(existingId))
135+
.withValues(mainValues)
136+
batch += mainBuilder
191137

192-
private fun insertCategories(batch: TasksBatchOperation, idxTask: Int?) {
193-
for (category in task.categories) {
194-
val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri())
195-
.withTaskId(Category.TASK_ID, idxTask)
196-
.withValue(Category.MIMETYPE, Category.CONTENT_ITEM_TYPE)
197-
.withValue(Category.CATEGORY_NAME, category)
198-
logger.log(Level.FINE, "Inserting category", builder.build())
199-
batch += builder
200-
}
201-
}
138+
for (subValue in entity.subValues)
139+
batch += CpoBuilder.newInsert(subValue.uri)
140+
.withValues(ContentValues(subValue.values).apply {
141+
put(Properties.TASK_ID, existingId)
142+
})
202143

203-
private fun insertComment(batch: TasksBatchOperation, idxTask: Int?) {
204-
val comment = task.comment ?: return
205-
val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri())
206-
.withTaskId(Comment.TASK_ID, idxTask)
207-
.withValue(Comment.MIMETYPE, Comment.CONTENT_ITEM_TYPE)
208-
.withValue(Comment.COMMENT, comment)
209-
logger.log(Level.FINE, "Inserting comment", builder.build())
210-
batch += builder
144+
logger.log(Level.FINE, "Updated task", mainBuilder.build())
211145
}
212146

213-
private fun insertRelatedTo(batch: TasksBatchOperation, idxTask: Int?) {
214-
for (relatedTo in task.relatedTo) {
215-
val relType = when ((relatedTo.getParameter<RelType>(Parameter.RELTYPE)).getOrNull()) {
216-
RelType.CHILD -> Relation.RELTYPE_CHILD
217-
RelType.SIBLING -> Relation.RELTYPE_SIBLING
218-
else /* RelType.PARENT, default value */ -> Relation.RELTYPE_PARENT
219-
}
220-
val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri())
221-
.withTaskId(Relation.TASK_ID, idxTask)
222-
.withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE)
223-
.withValue(Relation.RELATED_UID, relatedTo.value)
224-
.withValue(Relation.RELATED_TYPE, relType)
225-
logger.log(Level.FINE, "Inserting relation", builder.build())
226-
batch += builder
227-
}
228-
}
147+
private fun buildTask(): Entity {
148+
val entity = Entity(ContentValues())
229149

230-
private fun insertUnknownProperties(batch: TasksBatchOperation, idxTask: Int?) {
231-
for (property in task.unknownProperties) {
232-
if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) {
233-
logger.warning("Ignoring unknown property with ${property.value.length} octets (too long)")
234-
return
235-
}
236-
237-
val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri())
238-
.withTaskId(Properties.TASK_ID, idxTask)
239-
.withValue(Properties.MIMETYPE, UnknownProperty.CONTENT_ITEM_TYPE)
240-
.withValue(UNKNOWN_PROPERTY_DATA, UnknownProperty.toJsonString(property))
241-
logger.log(Level.FINE, "Inserting unknown property", builder.build())
242-
batch += builder
243-
}
244-
}
150+
for (fieldBuilder in fieldBuilders)
151+
fieldBuilder.build(task, entity)
245152

246-
private fun CpoBuilder.withTaskId(column: String, idxTask: Int?): CpoBuilder {
247-
if (idxTask != null)
248-
withValueBackReference(column, idxTask)
249-
else
250-
withValue(column, requireNotNull(id))
251-
return this
153+
logger.log(Level.FINE, "Built task", entity.entityValues)
154+
return entity
252155
}
253156

254157
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.tasks.builder
8+
9+
import android.content.Entity
10+
import androidx.core.content.contentValuesOf
11+
import at.bitfire.ical4android.Task
12+
import at.bitfire.synctools.storage.tasks.DmfsTaskList
13+
import at.bitfire.synctools.util.AlarmTriggerCalculator
14+
import net.fortuna.ical4j.model.Property
15+
import net.fortuna.ical4j.model.parameter.Related
16+
import net.fortuna.ical4j.model.property.Action
17+
import net.fortuna.ical4j.model.property.immutable.ImmutableAction
18+
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
19+
import java.util.Locale
20+
import kotlin.jvm.optionals.getOrNull
21+
22+
class AlarmsBuilder(
23+
private val taskList: DmfsTaskList
24+
) : DmfsTaskFieldBuilder {
25+
26+
override fun build(from: Task, to: Entity) {
27+
for (alarm in from.alarms) {
28+
val (alarmRef, minutes) = AlarmTriggerCalculator.alarmTriggerToMinutes(
29+
alarm = alarm,
30+
refStart = from.dtStart,
31+
refEnd = from.end,
32+
allowRelEnd = true
33+
) ?: continue
34+
35+
val ref = when (alarmRef) {
36+
Related.END ->
37+
Alarm.ALARM_REFERENCE_DUE_DATE
38+
else /* Related.START is the default value */ ->
39+
Alarm.ALARM_REFERENCE_START_DATE
40+
}
41+
42+
val alarmType = when (
43+
alarm.getProperty<Action>(Property.ACTION).getOrNull()?.value?.uppercase(Locale.ROOT)
44+
) {
45+
ImmutableAction.VALUE_AUDIO -> Alarm.ALARM_TYPE_SOUND
46+
ImmutableAction.VALUE_DISPLAY -> Alarm.ALARM_TYPE_MESSAGE
47+
ImmutableAction.VALUE_EMAIL -> Alarm.ALARM_TYPE_EMAIL
48+
else -> Alarm.ALARM_TYPE_NOTHING
49+
}
50+
51+
to.addSubValue(
52+
taskList.tasksPropertiesUri(),
53+
contentValuesOf(
54+
Alarm.MIMETYPE to Alarm.CONTENT_ITEM_TYPE,
55+
Alarm.MINUTES_BEFORE to minutes,
56+
Alarm.REFERENCE to ref,
57+
Alarm.MESSAGE to (alarm.description?.value ?: alarm.summary),
58+
Alarm.ALARM_TYPE to alarmType
59+
)
60+
)
61+
}
62+
}
63+
64+
}

0 commit comments

Comments
 (0)