diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt index a109dd6b..3b42f8a3 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt @@ -11,12 +11,18 @@ import android.content.Entity import at.bitfire.ical4android.Task import at.bitfire.ical4android.UnknownProperty import at.bitfire.synctools.mapping.tasks.builder.AllDayBuilder +import at.bitfire.synctools.mapping.tasks.builder.ColorBuilder +import at.bitfire.synctools.mapping.tasks.builder.DescriptionBuilder import at.bitfire.synctools.mapping.tasks.builder.DmfsTaskFieldBuilder import at.bitfire.synctools.mapping.tasks.builder.DueBuilder import at.bitfire.synctools.mapping.tasks.builder.DurationBuilder +import at.bitfire.synctools.mapping.tasks.builder.GeoBuilder +import at.bitfire.synctools.mapping.tasks.builder.LocationBuilder +import at.bitfire.synctools.mapping.tasks.builder.OrganizerBuilder import at.bitfire.synctools.mapping.tasks.builder.RecurrenceFieldsBuilder import at.bitfire.synctools.mapping.tasks.builder.StartTimeBuilder import at.bitfire.synctools.mapping.tasks.builder.TitleBuilder +import at.bitfire.synctools.mapping.tasks.builder.UrlBuilder import at.bitfire.synctools.storage.BatchOperation.CpoBuilder import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.COLUMN_ETAG import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.COLUMN_FLAGS @@ -26,7 +32,6 @@ import at.bitfire.synctools.storage.tasks.TasksBatchOperation import at.bitfire.synctools.util.AlarmTriggerCalculator import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.Related import net.fortuna.ical4j.model.property.Action @@ -60,6 +65,15 @@ class DmfsTaskBuilder( ) { private val fieldBuilders: Array = arrayOf( + // content fields + TitleBuilder(), + DescriptionBuilder(), + LocationBuilder(), + GeoBuilder(), + ColorBuilder(), + UrlBuilder(), + OrganizerBuilder(), + // status fields (still inline below) // time fields and recurrence TitleBuilder(), AllDayBuilder(), @@ -104,12 +118,6 @@ class DmfsTaskBuilder( builder .withValue(Tasks._UID, task.uid) .withValue(Tasks._DIRTY, 0) .withValue(Tasks.SYNC_VERSION, task.sequence) - .withValue(Tasks.LOCATION, task.location) - .withValue(Tasks.GEO, task.geoPosition?.let { "${it.longitude},${it.latitude}" }) - .withValue(Tasks.DESCRIPTION, task.description) - .withValue(Tasks.TASK_COLOR, task.color) - .withValue(Tasks.URL, task.url) - .withValue(Tasks._SYNC_ID, syncId) .withValue(COLUMN_FLAGS, flags) .withValue(COLUMN_ETAG, eTag) @@ -117,21 +125,6 @@ class DmfsTaskBuilder( // parent_id will be re-calculated when the relation row is inserted (if there is any) .withValue(Tasks.PARENT_ID, null) - // organizer - // Note: big method – maybe split? Depends on how we want to proceed with refactoring. - - task.organizer?.let { organizer -> - val uri = organizer.calAddress - val email = if (uri.scheme.equals("mailto", true)) - uri.schemeSpecificPart - else - organizer.getParameter(Parameter.EMAIL).getOrNull()?.value - if (email != null) - builder.withValue(Tasks.ORGANIZER, email) - else - logger.warning("Ignoring ORGANIZER without email address (not supported by Android)") - } - // Priority, classification builder .withValue(Tasks.PRIORITY, task.priority) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/ColorBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/ColorBuilder.kt new file mode 100644 index 00000000..03676561 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/ColorBuilder.kt @@ -0,0 +1,19 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.Entity +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks + +class ColorBuilder : DmfsTaskFieldBuilder { + + override fun build(from: Task, to: Entity) { + to.entityValues.put(Tasks.TASK_COLOR, from.color) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/DescriptionBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/DescriptionBuilder.kt new file mode 100644 index 00000000..8777f031 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/DescriptionBuilder.kt @@ -0,0 +1,20 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.Entity +import at.bitfire.ical4android.Task +import at.bitfire.vcard4android.Utils.trimToNull +import org.dmfs.tasks.contract.TaskContract.Tasks + +class DescriptionBuilder : DmfsTaskFieldBuilder { + + override fun build(from: Task, to: Entity) { + to.entityValues.put(Tasks.DESCRIPTION, from.description.trimToNull()) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/GeoBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/GeoBuilder.kt new file mode 100644 index 00000000..9638d6d7 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/GeoBuilder.kt @@ -0,0 +1,19 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.Entity +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks + +class GeoBuilder : DmfsTaskFieldBuilder { + + override fun build(from: Task, to: Entity) { + to.entityValues.put(Tasks.GEO, from.geoPosition?.let { "${it.longitude},${it.latitude}" }) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/LocationBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/LocationBuilder.kt new file mode 100644 index 00000000..93ce9d20 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/LocationBuilder.kt @@ -0,0 +1,20 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.Entity +import at.bitfire.ical4android.Task +import at.bitfire.vcard4android.Utils.trimToNull +import org.dmfs.tasks.contract.TaskContract.Tasks + +class LocationBuilder : DmfsTaskFieldBuilder { + + override fun build(from: Task, to: Entity) { + to.entityValues.put(Tasks.LOCATION, from.location.trimToNull()) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/OrganizerBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/OrganizerBuilder.kt new file mode 100644 index 00000000..4fee61be --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/OrganizerBuilder.kt @@ -0,0 +1,44 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.Entity +import at.bitfire.ical4android.Task +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.parameter.Email +import org.dmfs.tasks.contract.TaskContract.Tasks +import java.util.logging.Level +import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull + +class OrganizerBuilder : DmfsTaskFieldBuilder { + + private val logger + get() = Logger.getLogger(javaClass.name) + + override fun build(from: Task, to: Entity) { + val organizer = from.organizer + if (organizer == null) { + to.entityValues.putNull(Tasks.ORGANIZER) + return + } + + val uri = organizer.calAddress + val email = if (uri.scheme.equals("mailto", true)) + uri.schemeSpecificPart + else + organizer.getParameter(Parameter.EMAIL).getOrNull()?.value + + if (email != null) + to.entityValues.put(Tasks.ORGANIZER, email) + else { + logger.log(Level.WARNING, "Ignoring ORGANIZER without email address (not supported by Android)", organizer) + to.entityValues.putNull(Tasks.ORGANIZER) + } + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/UrlBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/UrlBuilder.kt new file mode 100644 index 00000000..a84c213f --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/UrlBuilder.kt @@ -0,0 +1,19 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.Entity +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks + +class UrlBuilder : DmfsTaskFieldBuilder { + + override fun build(from: Task, to: Entity) { + to.entityValues.put(Tasks.URL, from.url) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/ColorBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/ColorBuilderTest.kt new file mode 100644 index 00000000..340c9225 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/ColorBuilderTest.kt @@ -0,0 +1,48 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.ContentValues +import android.content.Entity +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import at.bitfire.synctools.test.assertContentValuesEqual +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ColorBuilderTest { + + private val builder = ColorBuilder() + + @Test + fun `No COLOR`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(), + to = result + ) + assertContentValuesEqual(contentValuesOf( + Tasks.TASK_COLOR to null + ), result.entityValues) + } + + @Test + fun `COLOR is set`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(color = 0xFF112233.toInt()), + to = result + ) + assertContentValuesEqual(contentValuesOf( + Tasks.TASK_COLOR to 0xFF112233.toInt() + ), result.entityValues) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/DescriptionBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/DescriptionBuilderTest.kt new file mode 100644 index 00000000..d5dfb2dd --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/DescriptionBuilderTest.kt @@ -0,0 +1,57 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.ContentValues +import android.content.Entity +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DescriptionBuilderTest { + + private val builder = DescriptionBuilder() + + @Test + fun `No DESCRIPTION`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(), + to = result + ) + assertTrue(result.entityValues.containsKey(Tasks.DESCRIPTION)) + assertNull(result.entityValues.get(Tasks.DESCRIPTION)) + } + + @Test + fun `DESCRIPTION is blank`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(description = ""), + to = result + ) + assertTrue(result.entityValues.containsKey(Tasks.DESCRIPTION)) + assertNull(result.entityValues.get(Tasks.DESCRIPTION)) + } + + @Test + fun `DESCRIPTION is text`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(description = "Task Details"), + to = result + ) + assertEquals("Task Details", result.entityValues.getAsString(Tasks.DESCRIPTION)) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/GeoBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/GeoBuilderTest.kt new file mode 100644 index 00000000..9dceef69 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/GeoBuilderTest.kt @@ -0,0 +1,47 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.ContentValues +import android.content.Entity +import at.bitfire.ical4android.Task +import net.fortuna.ical4j.model.property.Geo +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class GeoBuilderTest { + + private val builder = GeoBuilder() + + @Test + fun `No GEO`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(), + to = result + ) + assertTrue(result.entityValues.containsKey(Tasks.GEO)) + assertNull(result.entityValues.get(Tasks.GEO)) + } + + @Test + fun `GEO is set`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(geoPosition = Geo(48.2.toBigDecimal(), 16.3.toBigDecimal())), + to = result + ) + assertEquals("16.3,48.2", result.entityValues.getAsString(Tasks.GEO)) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/LocationBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/LocationBuilderTest.kt new file mode 100644 index 00000000..42f6edc1 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/LocationBuilderTest.kt @@ -0,0 +1,57 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.ContentValues +import android.content.Entity +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LocationBuilderTest { + + private val builder = LocationBuilder() + + @Test + fun `No LOCATION`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(), + to = result + ) + assertTrue(result.entityValues.containsKey(Tasks.LOCATION)) + assertNull(result.entityValues.get(Tasks.LOCATION)) + } + + @Test + fun `LOCATION is blank`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(location = ""), + to = result + ) + assertTrue(result.entityValues.containsKey(Tasks.LOCATION)) + assertNull(result.entityValues.get(Tasks.LOCATION)) + } + + @Test + fun `LOCATION is text`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(location = "Task Location"), + to = result + ) + assertEquals("Task Location", result.entityValues.getAsString(Tasks.LOCATION)) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/OrganizerBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/OrganizerBuilderTest.kt new file mode 100644 index 00000000..1b50dcea --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/OrganizerBuilderTest.kt @@ -0,0 +1,75 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.ContentValues +import android.content.Entity +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import at.bitfire.synctools.test.assertContentValuesEqual +import net.fortuna.ical4j.model.parameter.Email +import net.fortuna.ical4j.model.property.Organizer +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class OrganizerBuilderTest { + + private val builder = OrganizerBuilder() + + @Test + fun `No ORGANIZER`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(), + to = result + ) + assertContentValuesEqual(contentValuesOf( + Tasks.ORGANIZER to null + ), result.entityValues) + } + + @Test + fun `ORGANIZER is email address`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(organizer = Organizer("mailto:organizer@example.com")), + to = result + ) + assertContentValuesEqual(contentValuesOf( + Tasks.ORGANIZER to "organizer@example.com" + ), result.entityValues) + } + + @Test + fun `ORGANIZER is custom URI without email`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(organizer = Organizer("local-id:user")), + to = result + ) + // Custom URI without email → null (tasks have no ownerAccount fallback) + assertContentValuesEqual(contentValuesOf( + Tasks.ORGANIZER to null + ), result.entityValues) + } + + @Test + fun `ORGANIZER is custom URI with email parameter`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(organizer = Organizer("local-id:user").add(Email("organizer@example.com"))), + to = result + ) + assertContentValuesEqual(contentValuesOf( + Tasks.ORGANIZER to "organizer@example.com" + ), result.entityValues) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/UrlBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/UrlBuilderTest.kt new file mode 100644 index 00000000..e0e88722 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/UrlBuilderTest.kt @@ -0,0 +1,48 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks.builder + +import android.content.ContentValues +import android.content.Entity +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import at.bitfire.synctools.test.assertContentValuesEqual +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UrlBuilderTest { + + private val builder = UrlBuilder() + + @Test + fun `No URL`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(), + to = result + ) + assertContentValuesEqual(contentValuesOf( + Tasks.URL to null + ), result.entityValues) + } + + @Test + fun `URL is set`() { + val result = Entity(ContentValues()) + builder.build( + from = Task(url = "https://example.com"), + to = result + ) + assertContentValuesEqual(contentValuesOf( + Tasks.URL to "https://example.com" + ), result.entityValues) + } + +}