Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1925b714 authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Tasks: support saving/restoring unknown properties

parent 4c871398
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import at.bitfire.ical4android.impl.TestEvent
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateList
import net.fortuna.ical4j.model.Dur
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.Value
import net.fortuna.ical4j.model.property.*
@@ -115,6 +116,10 @@ class AndroidEventTest {
        // add EXDATE
        event.exDates += ExDate(DateList("20150502T120000", Value.DATE_TIME, tzVienna))

        // add special properties
        event.unknownProperties.add(Categories("CAT1,CAT2"))
        event.unknownProperties.add(XProperty("X-NAME", "X-Value"))

        // add to calendar
        val uri = TestEvent(calendar, event).add()
        assertNotNull(uri)
@@ -172,6 +177,9 @@ class AndroidEventTest {
            // compare EXDATE
            assertEquals(1, event2.exDates.size)
            assertEquals(event.exDates.first, event2.exDates.first)

            // compare unknown properties
            assertArrayEquals(event.unknownProperties.toArray(), event2.unknownProperties.toArray())
        } finally {
            testEvent.delete()
        }
+7 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import net.fortuna.ical4j.model.TimeZone
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.Due
import net.fortuna.ical4j.model.property.Organizer
import net.fortuna.ical4j.model.property.XProperty
import org.dmfs.tasks.contract.TaskContract
import org.junit.After
import org.junit.Assert.*
@@ -77,6 +78,10 @@ class AndroidTaskTest {
        task.organizer = Organizer("mailto:organizer@example.com")
        assertFalse(task.isAllDay())

        // extended properties
        task.categories.addAll(arrayOf("Cat1", "Cat2"))
        task.unknownProperties += XProperty("X-UNKNOWN-PROP", "Unknown Value")

        // add to task list
        val uri = TestTask(taskList!!, task).add()
        assertNotNull("Couldn't add task", uri)
@@ -93,6 +98,8 @@ class AndroidTaskTest {
            assertEquals(task.description, task2.description)
            assertEquals(task.location, task2.location)
            assertEquals(task.dtStart, task2.dtStart)
            assertEquals(task.categories, task2.categories)
            assertEquals(task.unknownProperties, task2.unknownProperties)
        } finally {
            testTask.delete()
        }
+7 −7
Original line number Diff line number Diff line
@@ -14,8 +14,8 @@ class UnknownPropertyTest {

    @Test
    @SmallTest
    fun testFromExtendedProperty() {
        val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"UID\", \"PropValue\" ]")
    fun testFromJsonString() {
        val prop = UnknownProperty.fromJsonString("[ \"UID\", \"PropValue\" ]")
        assertTrue(prop is Uid)
        assertEquals("UID", prop.name)
        assertEquals("PropValue", prop.value)
@@ -23,8 +23,8 @@ class UnknownPropertyTest {

    @Test
    @SmallTest
    fun testFromExtendedPropertyWithParameters() {
        val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"ATTENDEE\", \"PropValue\", { \"x-param1\": \"value1\", \"x-param2\": \"value2\" } ]")
    fun testFromJsonStringWithParameters() {
        val prop = UnknownProperty.fromJsonString("[ \"ATTENDEE\", \"PropValue\", { \"x-param1\": \"value1\", \"x-param2\": \"value2\" } ]")
        assertTrue(prop is Attendee)
        assertEquals("ATTENDEE", prop.name)
        assertEquals("PropValue", prop.value)
@@ -35,14 +35,14 @@ class UnknownPropertyTest {

    @Test(expected = JSONException::class)
    @SmallTest
    fun testFromInvalidExtendedProperty() {
        AndroidEvent.UnknownProperty.fromExtendedProperty("This isn't JSON")
    fun testFromInvalidJsonString() {
        UnknownProperty.fromJsonString("This isn't JSON")
    }


    @Test
    @SmallTest
    fun testToExtendedProperty() {
    fun testToJsonString() {
        val attendee = Attendee("mailto:test@test.at")
        assertEquals(
                "ATTENDEE:mailto:test@test.at",
+11 −71
Original line number Diff line number Diff line
@@ -25,8 +25,6 @@ import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.*
import net.fortuna.ical4j.model.property.*
import net.fortuna.ical4j.util.TimeZones
import org.json.JSONArray
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.FileNotFoundException
import java.io.ObjectInputStream
@@ -52,13 +50,15 @@ abstract class AndroidEvent(

    companion object {

        /** [ExtendedProperties.NAME] for unknown iCal properties */
        @Deprecated("New serialization format", ReplaceWith("EXT_UNKNOWN_PROPERTY2"))
        const val EXT_UNKNOWN_PROPERTY = "unknown-property"

        @Deprecated("New content item MIME type", ReplaceWith("UnknownProperty.CONTENT_ITEM_TYPE"))
        const val EXT_UNKNOWN_PROPERTY2 = "unknown-property.v2"
        const val MAX_UNKNOWN_PROPERTY_SIZE = 25000

        // not declared in ical4j Parameters class yet
        /**
         * EMAIL parameter name (as used for ORGANIZER). Not declared in ical4j Parameters class yet.
         */
        private const val PARAMETER_EMAIL = "EMAIL"

    }
@@ -329,17 +329,15 @@ abstract class AndroidEvent(
        try {
            when (row.getAsString(ExtendedProperties.NAME)) {
                EXT_UNKNOWN_PROPERTY -> {
                    // deserialize unknown property v1 (deprecated)
                    // deserialize unknown property (deprecated format)
                    val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP))
                    ObjectInputStream(stream).use {
                        event.unknownProperties += it.readObject() as Property
                    }
                }

                EXT_UNKNOWN_PROPERTY2 -> {
                    // deserialize unknown property v2
                    event.unknownProperties += UnknownProperty.fromExtendedProperty(row.getAsString(ExtendedProperties.VALUE))
                }
                EXT_UNKNOWN_PROPERTY2, UnknownProperty.CONTENT_ITEM_TYPE ->
                    event.unknownProperties += UnknownProperty.fromJsonString(row.getAsString(ExtendedProperties.VALUE))
            }
        } catch(e: Exception) {
            Constants.log.log(Level.WARNING, "Couldn't parse extended property", e)
@@ -694,14 +692,14 @@ abstract class AndroidEvent(
    }

    protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int, property: Property) {
        if (property.value.length > MAX_UNKNOWN_PROPERTY_SIZE) {
        if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) {
            Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)")
            return
        }

        val builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(ExtendedProperties.CONTENT_URI))
        builder .withValue(ExtendedProperties.NAME, EXT_UNKNOWN_PROPERTY2)
                .withValue(ExtendedProperties.VALUE, UnknownProperty.toExtendedProperty(property))
                .withValue(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE)
                .withValue(ExtendedProperties.VALUE, UnknownProperty.toJsonString(property))

        batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent))
    }
@@ -735,62 +733,4 @@ abstract class AndroidEvent(
    override fun toString() = MiscUtils.reflectionToString(this)


    /**
     * Helpers to (de)serialize unknown properties as JSON to store it in an Android ExtendedProperty row.
     *
     * Format: `{ propertyName, propertyValue, { param1Name: param1Value, ... } }`, with the third
     * array (parameters) being optional.
     */
    object UnknownProperty {

        private val parameterFactory = ParameterFactoryRegistry()
        private val propertyFactory = PropertyFactoryRegistry()

        /**
         * Deserializes a JSON string from an ExtendedProperty value to an ical4j property.
         *
         * @param jsonString JSON representation of an ical4j property
         * @return ical4j property, generated from [jsonString]
         * @throws org.json.JSONException when the input value can't be parsed
         */
        fun fromExtendedProperty(jsonString: String): Property {
            val json = JSONArray(jsonString)
            val name = json.getString(0)
            val value = json.getString(1)

            val params = ParameterList()
            json.optJSONObject(2)?.let { jsonParams ->
                for (paramName in jsonParams.keys())
                    params.add(parameterFactory.createParameter(
                            paramName,
                            jsonParams.getString(paramName)
                    ))
            }

            return propertyFactory.createProperty(name, params, value)
        }

        /**
         * Serializes an ical4j property to a JSON string that can be stored in an ExtendedProperty.
         *
         * @param prop property to serialize as JSON
         * @return JSON representation of [prop]
         */
        fun toExtendedProperty(prop: Property): String {
            val json = JSONArray()
            json.put(prop.name)
            json.put(prop.value)

            if (!prop.parameters.isEmpty) {
                val jsonParams = JSONObject()
                for (param in prop.parameters)
                    jsonParams.put(param.name, param.value)
                json.put(jsonParams)
            }

            return json.toString()
        }

    }

}
+35 −14
Original line number Diff line number Diff line
@@ -46,6 +46,10 @@ abstract class AndroidTask(
        val taskList: AndroidTaskList<AndroidTask>
) {

    companion object {
        const val UNKNOWN_PROPERTY_DATA = Properties.DATA0
    }

    var id: Long? = null


@@ -84,11 +88,8 @@ abstract class AndroidTask(
                        client.query(taskList.tasksPropertiesSyncUri(), null,
                                "${Properties.TASK_ID}=?", arrayOf(id.toString()),
                                null)?.use { propCursor ->
                            while (propCursor.moveToNext()) {
                                val propValues = propCursor.toValues(true)
                                Constants.log.log(Level.FINER, "Found property", propValues)
                                populateProperty(propValues)
                            }
                            while (propCursor.moveToNext())
                                populateProperty(propCursor.toValues(true))
                        }

                    return task
@@ -180,12 +181,16 @@ abstract class AndroidTask(
    }

    protected open fun populateProperty(row: ContentValues) {
        Constants.log.log(Level.FINER, "Found property", row)

        val task = requireNotNull(task)
        when (val type = row.getAsString(Properties.MIMETYPE)) {
            Alarm.CONTENT_ITEM_TYPE ->
                populateAlarm(row)
            Category.CONTENT_ITEM_TYPE ->
                task.categories += row.getAsString(Category.CATEGORY_NAME)
            UnknownProperty.CONTENT_ITEM_TYPE ->
                task.unknownProperties += UnknownProperty.fromJsonString(row.getAsString(UNKNOWN_PROPERTY_DATA))
            else ->
                Constants.log.warning("Found unknown property of type $type")
        }
@@ -259,6 +264,7 @@ abstract class AndroidTask(
    private fun insertProperties(batch: BatchOperation) {
        insertAlarms(batch)
        insertCategories(batch)
        insertUnknownProperties(batch)
    }

    private fun insertAlarms(batch: BatchOperation) {
@@ -282,7 +288,7 @@ abstract class AndroidTask(
            }

            val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri())
            builder .withValue(Alarm.TASK_ID, id)
                    .withValue(Alarm.TASK_ID, id)
                    .withValue(Alarm.MIMETYPE, Alarm.CONTENT_ITEM_TYPE)
                    .withValue(Alarm.MINUTES_BEFORE, ICalendar.alarmMinBefore(alarm))
                    .withValue(Alarm.REFERENCE, alarmRef)
@@ -297,7 +303,7 @@ abstract class AndroidTask(
    private fun insertCategories(batch: BatchOperation) {
        for (category in requireNotNull(task).categories) {
            val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri())
            builder .withValue(Category.TASK_ID, id)
                    .withValue(Category.TASK_ID, id)
                    .withValue(Category.MIMETYPE, Category.CONTENT_ITEM_TYPE)
                    .withValue(Category.CATEGORY_NAME, category)
            Constants.log.log(Level.FINE, "Inserting category", builder.build())
@@ -305,6 +311,22 @@ abstract class AndroidTask(
        }
    }

    private fun insertUnknownProperties(batch: BatchOperation) {
        for (property in requireNotNull(task).unknownProperties) {
            if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) {
                Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)")
                return
            }

            val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri())
                    .withValue(Properties.TASK_ID, id)
                    .withValue(Properties.MIMETYPE, UnknownProperty.CONTENT_ITEM_TYPE)
                    .withValue(UNKNOWN_PROPERTY_DATA, UnknownProperty.toJsonString(property))
            Constants.log.log(Level.FINE, "Inserting unknown property", builder.build())
            batch.enqueue(BatchOperation.Operation(builder))
        }
    }

    fun delete(): Int {
        try {
            return taskList.provider.client.delete(taskSyncURI(), null, null)
@@ -318,16 +340,15 @@ abstract class AndroidTask(
            builder .withValue(Tasks.LIST_ID, taskList.id)

        val task = requireNotNull(task)
        builder
                .withValue(Tasks._UID, task.uid)
        builder .withValue(Tasks._UID, task.uid)
                .withValue(Tasks._DIRTY, 0)
                .withValue(Tasks.SYNC_VERSION, task.sequence)
                .withValue(Tasks.TITLE, task.summary)
                .withValue(Tasks.LOCATION, task.location)

        builder .withValue(Tasks.GEO, task.geoPosition?.value)
                .withValue(Tasks.GEO, task.geoPosition?.value)

        builder .withValue(Tasks.DESCRIPTION, task.description)
                .withValue(Tasks.DESCRIPTION, task.description)
                .withValue(Tasks.TASK_COLOR, task.color)
                .withValue(Tasks.URL, task.url)

@@ -379,14 +400,14 @@ abstract class AndroidTask(
        builder .withValue(Tasks.CREATED, task.createdAt)
                .withValue(Tasks.LAST_MODIFIED, task.lastModified)

        builder .withValue(Tasks.DTSTART, task.dtStart?.date?.time)
                .withValue(Tasks.DTSTART, task.dtStart?.date?.time)
                .withValue(Tasks.DUE, task.due?.date?.time)
                .withValue(Tasks.DURATION, task.duration?.value)

        builder .withValue(Tasks.RDATE, if (task.rDates.isEmpty()) null else DateUtils.recurrenceSetsToAndroidString(task.rDates, allDay))
                .withValue(Tasks.RDATE, if (task.rDates.isEmpty()) null else DateUtils.recurrenceSetsToAndroidString(task.rDates, allDay))
                .withValue(Tasks.RRULE, task.rRule?.value)

        builder .withValue(Tasks.EXDATE, if (task.exDates.isEmpty()) null else DateUtils.recurrenceSetsToAndroidString(task.exDates, allDay))
                .withValue(Tasks.EXDATE, if (task.exDates.isEmpty()) null else DateUtils.recurrenceSetsToAndroidString(task.exDates, allDay))
        Constants.log.log(Level.FINE, "Built task object", builder.build())
    }

Loading