diff options
author | Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> | 2022-01-11 14:32:39 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-11 14:32:39 +0300 |
commit | e31cefd42f5bc299ba5cf16c9ad3710cc9a2a84f (patch) | |
tree | f031d4289ab178ea00d0cbce5d4ebf083ebbd78e /src | |
parent | 7fd764d2a86199a3e09e24d0f438651c2239f5c4 (diff) |
jtx Board support (see bitfireAT/davx5#38)
Initial support for jtx Board
Diffstat (limited to 'src')
79 files changed, 5048 insertions, 243 deletions
diff --git a/src/androidTest/java/at/bitfire/ical4android/AbstractTasksTest.kt b/src/androidTest/java/at/bitfire/ical4android/AbstractTasksTest.kt index fe5425f..16431eb 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AbstractTasksTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AbstractTasksTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import androidx.test.platform.app.InstrumentationRegistry @@ -16,7 +20,7 @@ abstract class AbstractTasksTest( companion object { @Parameterized.Parameters(name="{0}") @JvmStatic - fun taskProviders() = TaskProvider.ProviderName.values() + fun taskProviders() = listOf(TaskProvider.ProviderName.OpenTasks,TaskProvider.ProviderName.TasksOrg) } private val providerOrNull: TaskProvider? by lazy { diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt index 09c587f..d754f9d 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt index a4bba3b..00ac8ce 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android import android.Manifest diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt index 8d6afa3..f7e9398 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt index 7b6f945..c907439 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt index 974910b..d51c1ad 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import org.junit.Assert diff --git a/src/androidTest/java/at/bitfire/ical4android/AttendeeMappingsTest.kt b/src/androidTest/java/at/bitfire/ical4android/AttendeeMappingsTest.kt index df507e8..69e0842 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AttendeeMappingsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AttendeeMappingsTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import android.content.ContentValues diff --git a/src/androidTest/java/at/bitfire/ical4android/Ical4jSettingsTest.kt b/src/androidTest/java/at/bitfire/ical4android/Ical4jSettingsTest.kt index 7f43b8a..7386ef4 100644 --- a/src/androidTest/java/at/bitfire/ical4android/Ical4jSettingsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/Ical4jSettingsTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import net.fortuna.ical4j.util.TimeZones diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt b/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt new file mode 100644 index 0000000..e1478fe --- /dev/null +++ b/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt @@ -0,0 +1,96 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.impl.TestJtxCollection +import at.techbee.jtx.JtxContract +import at.techbee.jtx.JtxContract.asSyncAdapter +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import org.junit.After +import org.junit.Before +import org.junit.Test + +class JtxCollectionTest { + + private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE) + private lateinit var contentResolver: ContentResolver + private lateinit var client: ContentProviderClient + var collection: JtxCollection<JtxICalObject>? = null + lateinit var context: Context + + private val url = "https://jtx.techbee.at" + private val displayname = "jtx" + private val syncversion = JtxContract.VERSION + + private val cv = ContentValues().apply { + put(JtxContract.JtxCollection.ACCOUNT_TYPE, testAccount.type) + put(JtxContract.JtxCollection.ACCOUNT_NAME, testAccount.name) + put(JtxContract.JtxCollection.URL, url) + put(JtxContract.JtxCollection.DISPLAYNAME, displayname) + put(JtxContract.JtxCollection.SYNC_VERSION, syncversion) + } + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + contentResolver = context.contentResolver + TestUtils.requestPermissions(TaskProvider.ProviderName.JtxBoard.permissions) + client = contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY)!! + } + + @After + fun tearDown() { + var collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) + collections.forEach { collection -> + collection.delete() + } + collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) + assertEquals(0, collections.size) + client.closeCompat() + } + + + @Test + fun create_populate_find() { + val collectionUri = JtxCollection.create(testAccount, client, cv) + assertNotNull(collectionUri) + val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) + + assertEquals(1, collections.size) + assertEquals(testAccount.type, collections[0].account.type) + assertEquals(testAccount.name, collections[0].account.name) + assertEquals(url, collections[0].url) + assertEquals(displayname, collections[0].displayname) + assertEquals(syncversion.toString(), collections[0].syncstate) + } + + @Test + fun queryICalObjects() { + val collectionUri = JtxCollection.create(testAccount, client, cv) + assertNotNull(collectionUri) + + val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) + val items = collections[0].queryICalObjects(null, null) + assertEquals(0, items.size) + + val cv = ContentValues().apply { + put(JtxContract.JtxICalObject.SUMMARY, "summary") + put(JtxContract.JtxICalObject.COMPONENT, JtxContract.JtxICalObject.Component.VJOURNAL.name) + put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collections[0].id) + } + client.insert(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv) + val icalobjects = collections[0].queryICalObjects(null, null) + + assertEquals(1, icalobjects.size) + } +} diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt new file mode 100644 index 0000000..9527dba --- /dev/null +++ b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -0,0 +1,866 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.database.DatabaseUtils +import android.os.ParcelFileDescriptor +import androidx.test.platform.app.InstrumentationRegistry +import at.bitfire.ical4android.impl.TestJtxCollection +import at.techbee.jtx.JtxContract +import at.techbee.jtx.JtxContract.JtxICalObject +import at.techbee.jtx.JtxContract.JtxICalObject.Component +import at.techbee.jtx.JtxContract.asSyncAdapter +import junit.framework.TestCase.* +import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.Property +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.InputStreamReader + +class JtxICalObjectTest { + + private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE) + private lateinit var contentResolver: ContentResolver + private lateinit var client: ContentProviderClient + var collection: JtxCollection<at.bitfire.ical4android.JtxICalObject>? = null + var sample: at.bitfire.ical4android.JtxICalObject? = null + lateinit var context: Context + + private val url = "https://jtx.techbee.at" + private val displayname = "jtxTest" + private val syncversion = JtxContract.VERSION + + private val cvCollection = ContentValues().apply { + put(JtxContract.JtxCollection.ACCOUNT_TYPE, testAccount.type) + put(JtxContract.JtxCollection.ACCOUNT_NAME, testAccount.name) + put(JtxContract.JtxCollection.URL, url) + put(JtxContract.JtxCollection.DISPLAYNAME, displayname) + put(JtxContract.JtxCollection.SYNC_VERSION, syncversion) + } + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + contentResolver = context.contentResolver + TestUtils.requestPermissions(TaskProvider.ProviderName.JtxBoard.permissions) + client = contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY)!! + + val collectionUri = JtxCollection.create(testAccount, client, cvCollection) + assertNotNull(collectionUri) + collection = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null)[0] + assertNotNull(collection) + + sample = JtxICalObject(collection!!).apply { + this.summary = "summ" + this.description = "desc" + this.dtstart = System.currentTimeMillis() + this.dtstartTimezone = "Europe/Vienna" + this.dtend = System.currentTimeMillis() + this.dtendTimezone = "Europe/Paris" + this.status = JtxICalObject.StatusJournal.FINAL.name + this.classification = JtxICalObject.Classification.PUBLIC.name + this.url = "https://jtx.techbee.at" + this.contact = "jtx@techbee.at" + this.geoLat = 48.2082 + this.geoLong = 16.3738 + this.location = "Vienna" + this.locationAltrep = "Wien" + this.percent = 99 + this.priority = 1 + this.due = System.currentTimeMillis() + this.dueTimezone = "Europe/Berlin" + this.completed = System.currentTimeMillis() + this.completedTimezone = "Europe/Budapest" + this.duration = "P15DT5H0M20S" + this.rrule = "FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30" + this.exdate = System.currentTimeMillis().toString() + this.rdate = System.currentTimeMillis().toString() + this.recurid = "1635796608864-b228364a-e633-449a-aeb2-d1a96941377c@at.techbee.jtx" + this.uid = "1635796608864-b228364a-e633-449a-aeb2-d1a96941377c@at.techbee.jtx" + this.created = System.currentTimeMillis() + this.lastModified = System.currentTimeMillis() + this.dtstamp = System.currentTimeMillis() + this.sequence = 1 + this.color = -2298423 + this.dirty = false + this.deleted = false + this.fileName = "test.ics" + this.eTag = "0" + this.scheduleTag = "0" + this.flags = 0 + } + + } + + @After + fun tearDown() { + + collection?.delete() + val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) + assertEquals(0, collections.size) + } + + + @Test fun check_SUMMARY() = insertRetrieveAssertString(JtxICalObject.SUMMARY, sample?.summary, Component.VJOURNAL.name) + @Test fun check_DESCRIPTION() = insertRetrieveAssertString(JtxICalObject.DESCRIPTION, sample?.description, Component.VJOURNAL.name) + @Test fun check_DTSTART() = insertRetrieveAssertLong(JtxICalObject.DTSTART, sample?.dtstart, Component.VJOURNAL.name) + @Test fun check_DTSTART_TIMEZONE() = insertRetrieveAssertString(JtxICalObject.DTSTART_TIMEZONE, sample?.dtstartTimezone, Component.VJOURNAL.name) + @Test fun check_DTEND() = insertRetrieveAssertLong(JtxICalObject.DTEND, sample?.dtend, Component.VJOURNAL.name) + @Test fun check_DTEND_TIMEZONE() = insertRetrieveAssertString(JtxICalObject.DTEND_TIMEZONE, sample?.dtendTimezone, Component.VJOURNAL.name) + @Test fun check_STATUS() = insertRetrieveAssertString(JtxICalObject.STATUS, sample?.status, Component.VJOURNAL.name) + @Test fun check_CLASSIFICATION() = insertRetrieveAssertString(JtxICalObject.CLASSIFICATION, sample?.classification, Component.VJOURNAL.name) + @Test fun check_URL() = insertRetrieveAssertString(JtxICalObject.URL, sample?.url, Component.VJOURNAL.name) + @Test fun check_CONTACT() = insertRetrieveAssertString(JtxICalObject.CONTACT, sample?.contact, Component.VJOURNAL.name) + @Test fun check_GEO_LAT() = insertRetrieveAssertDouble(JtxICalObject.GEO_LAT, sample?.geoLat, Component.VJOURNAL.name) + @Test fun check_GEO_LONG() = insertRetrieveAssertDouble(JtxICalObject.GEO_LONG, sample?.geoLong, Component.VJOURNAL.name) + @Test fun check_LOCATION() = insertRetrieveAssertString(JtxICalObject.LOCATION, sample?.location, Component.VJOURNAL.name) + @Test fun check_LOCATION_ALTREP() = insertRetrieveAssertString(JtxICalObject.LOCATION_ALTREP, sample?.locationAltrep, Component.VJOURNAL.name) + @Test fun check_PERCENT() = insertRetrieveAssertInt(JtxICalObject.PERCENT, sample?.percent, Component.VJOURNAL.name) + @Test fun check_PRIORITY() = insertRetrieveAssertInt(JtxICalObject.PRIORITY, sample?.priority, Component.VJOURNAL.name) + @Test fun check_DUE() = insertRetrieveAssertLong(JtxICalObject.DUE, sample?.due, Component.VJOURNAL.name) + @Test fun check_DUE_TIMEZONE() = insertRetrieveAssertString(JtxICalObject.DUE_TIMEZONE, sample?.dueTimezone, Component.VJOURNAL.name) + @Test fun check_COMPLETED() = insertRetrieveAssertLong(JtxICalObject.COMPLETED, sample?.completed, Component.VJOURNAL.name) + @Test fun check_COMPLETED_TIMEZONE() = insertRetrieveAssertString(JtxICalObject.COMPLETED_TIMEZONE, sample?.completedTimezone, Component.VJOURNAL.name) + @Test fun check_DURATION() = insertRetrieveAssertString(JtxICalObject.DURATION, sample?.duration, Component.VJOURNAL.name) + @Test fun check_RRULE() = insertRetrieveAssertString(JtxICalObject.RRULE, sample?.rrule, Component.VJOURNAL.name) + @Test fun check_RDATE() = insertRetrieveAssertString(JtxICalObject.RDATE, sample?.rdate, Component.VJOURNAL.name) + @Test fun check_EXDATE() = insertRetrieveAssertString(JtxICalObject.EXDATE, sample?.exdate, Component.VJOURNAL.name) + @Test fun check_RECURID() = insertRetrieveAssertString(JtxICalObject.RECURID, sample?.recurid, Component.VJOURNAL.name) + @Test fun check_UID() = insertRetrieveAssertString(JtxICalObject.UID, sample?.uid, Component.VJOURNAL.name) + @Test fun check_CREATED() = insertRetrieveAssertLong(JtxICalObject.CREATED, sample?.created, Component.VJOURNAL.name) + @Test fun check_DTSTAMP() = insertRetrieveAssertLong(JtxICalObject.DTSTAMP, sample?.dtstamp, Component.VJOURNAL.name) + @Test fun check_LAST_MODIFIED() = insertRetrieveAssertLong(JtxICalObject.LAST_MODIFIED, sample?.lastModified, Component.VJOURNAL.name) + @Test fun check_SEQUENCE() = insertRetrieveAssertLong(JtxICalObject.SEQUENCE, sample?.sequence, Component.VJOURNAL.name) + @Test fun check_COLOR() = insertRetrieveAssertInt(JtxICalObject.COLOR, sample?.color, Component.VJOURNAL.name) + @Test fun check_DIRTY() = insertRetrieveAssertBoolean(JtxICalObject.DIRTY, sample?.dirty, Component.VJOURNAL.name) + @Test fun check_DELETED() = insertRetrieveAssertBoolean(JtxICalObject.DELETED,sample?.deleted, Component.VJOURNAL.name) + @Test fun check_FILENAME() = insertRetrieveAssertString(JtxICalObject.FILENAME, sample?.fileName, Component.VJOURNAL.name) + @Test fun check_ETAG() = insertRetrieveAssertString(JtxICalObject.ETAG, sample?.eTag, Component.VJOURNAL.name) + @Test fun check_SCHEDULETAG() = insertRetrieveAssertString(JtxICalObject.SCHEDULETAG, sample?.scheduleTag, Component.VJOURNAL.name) + @Test fun check_FLAGS() = insertRetrieveAssertInt(JtxICalObject.FLAGS, sample?.flags, Component.VJOURNAL.name) + + + private fun insertRetrieveAssertString(field: String, fieldContent: String?, component: String) { + + assertNotNull(fieldContent) // fieldContent should not be null, check if the testcase was built correctly + + val cv = ContentValues().apply { + put(field, fieldContent) + put(JtxICalObject.COMPONENT, component) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + client.query(uri, null, null, null, null)?.use { + val itemCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, itemCV) + assertEquals(fieldContent, itemCV.getAsString(field)) + } + } + + private fun insertRetrieveAssertBoolean(field: String, fieldContent: Boolean?, component: String) { + + assertNotNull(fieldContent) // fieldContent should not be null, check if the testcase was built correctly + + val cv = ContentValues().apply { + put(field, fieldContent) + put(JtxICalObject.COMPONENT, component) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + client.query(uri, null, null, null, null)?.use { + val itemCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, itemCV) + assertEquals(fieldContent, itemCV.getAsBoolean(field)) + } + } + + private fun insertRetrieveAssertLong(field: String, fieldContent: Long?, component: String) { + + assertNotNull(fieldContent) // fieldContent should not be null, check if the testcase was built correctly + + val cv = ContentValues().apply { + put(field, fieldContent) + put(JtxICalObject.COMPONENT, component) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + client.query(uri, null, null, null, null)?.use { + val itemCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, itemCV) + assertEquals(fieldContent, itemCV.getAsLong(field)) + } + } + + private fun insertRetrieveAssertInt(field: String, fieldContent: Int?, component: String) { + + assertNotNull(fieldContent) // fieldContent should not be null, check if the testcase was built correctly + + val cv = ContentValues().apply { + put(field, fieldContent) + put(JtxICalObject.COMPONENT, component) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + client.query(uri, null, null, null, null)?.use { + val itemCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, itemCV) + assertEquals(fieldContent, itemCV.getAsInteger(field)) + } + } + + private fun insertRetrieveAssertDouble(field: String, fieldContent: Double?, component: String) { + + assertNotNull(fieldContent) // fieldContent should not be null, check if the testcase was built correctly + + val cv = ContentValues().apply { + put(field, fieldContent) + put(JtxICalObject.COMPONENT, component) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + client.query(uri, null, null, null, null)?.use { + val itemCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, itemCV) + assertEquals(fieldContent, itemCV.getAsDouble(field)) + } + } + + @Test + fun assertComment() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val comment = at.bitfire.ical4android.JtxICalObject.Comment( + text = "comment", + altrep = "Kommentar", + language = "de", + other = "X-OTHER:Test" + ) + + val commentCV = ContentValues().apply { + put(JtxContract.JtxComment.TEXT, comment.text) + put(JtxContract.JtxComment.ALTREP, comment.altrep) + put(JtxContract.JtxComment.LANGUAGE, comment.language) + put(JtxContract.JtxComment.OTHER, comment.other) + put(JtxContract.JtxComment.ICALOBJECT_ID, id) + } + + val commentUri = client.insert(JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(testAccount), commentCV)!! + client.query(commentUri, null, null, null, null)?.use { + val retrievedCommentCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedCommentCV) + assertEquals(comment.text, retrievedCommentCV.getAsString(JtxContract.JtxComment.TEXT)) + assertEquals(comment.altrep, retrievedCommentCV.getAsString(JtxContract.JtxComment.ALTREP)) + assertEquals(comment.language, retrievedCommentCV.getAsString(JtxContract.JtxComment.LANGUAGE)) + assertEquals(comment.other, retrievedCommentCV.getAsString(JtxContract.JtxComment.OTHER)) + } + } + + @Test + fun assertResource() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val resource = at.bitfire.ical4android.JtxICalObject.Resource( + text = "projector", + altrep = "Projektor", + language = "de", + other = "X-OTHER:Test" + ) + + val resourceCV = ContentValues().apply { + put(JtxContract.JtxResource.TEXT, resource.text) + put(JtxContract.JtxResource.LANGUAGE, resource.language) + put(JtxContract.JtxResource.OTHER, resource.other) + put(JtxContract.JtxResource.ICALOBJECT_ID, id) + } + + val resourceUri = client.insert(JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(testAccount), resourceCV)!! + client.query(resourceUri, null, null, null, null)?.use { + val retrievedResourceCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedResourceCV) + assertEquals(resource.text, retrievedResourceCV.getAsString(JtxContract.JtxResource.TEXT)) + assertEquals(resource.language, retrievedResourceCV.getAsString(JtxContract.JtxResource.LANGUAGE)) + assertEquals(resource.other, retrievedResourceCV.getAsString(JtxContract.JtxResource.OTHER)) + } + } + + @Test + fun assertAttendee() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val attendee = at.bitfire.ical4android.JtxICalObject.Attendee( + caladdress = "jtx@techbee.at", + cutype = JtxContract.JtxAttendee.Cutype.INDIVIDUAL.name, + member = "group", + partstat = "0", + role = JtxContract.JtxAttendee.Role.`REQ-PARTICIPANT`.name, + rsvp = false, + delegatedfrom = "jtx@techbee.at", + delegatedto = "jtx@techbee.at", + sentby = "jtx@techbee.at", + cn = "jtx Board", + dir = "dir", + language = "de", + other = "X-OTHER:Test" + ) + + val attendeeCV = ContentValues().apply { + put(JtxContract.JtxAttendee.CALADDRESS, attendee.caladdress) + put(JtxContract.JtxAttendee.CUTYPE, attendee.cutype) + put(JtxContract.JtxAttendee.MEMBER, attendee.member) + put(JtxContract.JtxAttendee.PARTSTAT, attendee.partstat) + put(JtxContract.JtxAttendee.ROLE, attendee.role) + put(JtxContract.JtxAttendee.RSVP, attendee.rsvp) + put(JtxContract.JtxAttendee.DELEGATEDFROM, attendee.delegatedfrom) + put(JtxContract.JtxAttendee.DELEGATEDTO, attendee.delegatedto) + put(JtxContract.JtxAttendee.SENTBY, attendee.sentby) + put(JtxContract.JtxAttendee.CN, attendee.cn) + put(JtxContract.JtxAttendee.DIR, attendee.dir) + put(JtxContract.JtxAttendee.LANGUAGE, attendee.language) + put(JtxContract.JtxAttendee.OTHER, attendee.other) + put(JtxContract.JtxAttendee.ICALOBJECT_ID, id) + } + + val attendeeUri = client.insert(JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter(testAccount), attendeeCV)!! + client.query(attendeeUri, null, null, null, null)?.use { + val retrievedAttendeeCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAttendeeCV) + assertEquals(attendee.caladdress, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.CALADDRESS)) + assertEquals(attendee.cutype, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.CUTYPE)) + assertEquals(attendee.member, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.MEMBER)) + assertEquals(attendee.partstat, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.PARTSTAT)) + assertEquals(attendee.role, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.ROLE)) + assertEquals(attendee.rsvp, retrievedAttendeeCV.getAsBoolean(JtxContract.JtxAttendee.RSVP)) + assertEquals(attendee.delegatedfrom, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.DELEGATEDFROM)) + assertEquals(attendee.delegatedto, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.DELEGATEDTO)) + assertEquals(attendee.sentby, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.SENTBY)) + assertEquals(attendee.cn, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.CN)) + assertEquals(attendee.dir, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.DIR)) + assertEquals(attendee.language, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.LANGUAGE)) + assertEquals(attendee.other, retrievedAttendeeCV.getAsString(JtxContract.JtxAttendee.OTHER)) + } + } + + @Test + fun assertCategory() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val category = at.bitfire.ical4android.JtxICalObject.Category( + text = "projector", + ) + + val categoryCV = ContentValues().apply { + put(JtxContract.JtxCategory.TEXT, category.text) + put(JtxContract.JtxCategory.ICALOBJECT_ID, id) + } + + val categoryUri = client.insert(JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(testAccount), categoryCV)!! + client.query(categoryUri, null, null, null, null)?.use { + val retrievedCategoryCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedCategoryCV) + assertEquals(category.text, retrievedCategoryCV.getAsString(JtxContract.JtxCategory.TEXT)) + } + } + + @Test + fun assertAttachment_without_binary() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val attachment = at.bitfire.ical4android.JtxICalObject.Attachment( + uri = "https://jtx.techbee.at/sample.pdf", + fmttype = "application/pdf", + other = "X-OTHER:other", + ) + + val attachmentCV = ContentValues().apply { + put(JtxContract.JtxAttachment.URI, attachment.uri) + put(JtxContract.JtxAttachment.FMTTYPE, attachment.fmttype) + put(JtxContract.JtxAttachment.OTHER, attachment.other) + put(JtxContract.JtxAttachment.ICALOBJECT_ID, id) + } + + val attachmentUri = client.insert(JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(testAccount), attachmentCV)!! + client.query(attachmentUri, null, null, null, null)?.use { + val retrievedAttachmentCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAttachmentCV) + assertEquals(attachment.uri, retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.URI)) + assertEquals(attachment.fmttype, retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.FMTTYPE)) + assertEquals(attachment.other, retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.OTHER)) + } + } + + + @Test + fun assertAttachment_without_binary_and_uri() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val attachment = at.bitfire.ical4android.JtxICalObject.Attachment( + fmttype = "application/pdf" + ) + + val attachmentCV = ContentValues().apply { + put(JtxContract.JtxAttachment.FMTTYPE, attachment.fmttype) + put(JtxContract.JtxAttachment.ICALOBJECT_ID, id) + } + + val attachmentUri = client.insert(JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(testAccount), attachmentCV)!! + client.query(attachmentUri, null, null, null, null)?.use { + val retrievedAttachmentCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAttachmentCV) + assertEquals(attachment.fmttype, retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.FMTTYPE)) + assertNotNull(retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.URI)) + } + + val textIn = "jtx Board rulz" + val pfd = client.openFile(attachmentUri, "w", null) + ParcelFileDescriptor.AutoCloseOutputStream(pfd).write(textIn.toByteArray()) + + val pfd2 = client.openFile(attachmentUri, "r", null) + val textCompare = String(ParcelFileDescriptor.AutoCloseInputStream(pfd2).readBytes()) + + Assert.assertEquals(textIn, textCompare) + + } + + @Test + fun assertAttachment_with_binary() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val attachment = at.bitfire.ical4android.JtxICalObject.Attachment( + //uri = "https://jtx.techbee.at/sample.pdf", + binary = "anR4IEJvYXJk", + fmttype = "application/pdf", + other = "X-OTHER:other", + ) + + val attachmentCV = ContentValues().apply { + //put(JtxContract.JtxAttachment.URI, attachment.uri) + put(JtxContract.JtxAttachment.BINARY, attachment.binary) + put(JtxContract.JtxAttachment.FMTTYPE, attachment.fmttype) + put(JtxContract.JtxAttachment.OTHER, attachment.other) + put(JtxContract.JtxAttachment.ICALOBJECT_ID, id) + } + + val attachmentUri = client.insert(JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(testAccount), attachmentCV)!! + client.query(attachmentUri, null, null, null, null)?.use { + val retrievedAttachmentCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAttachmentCV) + assertTrue(retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.URI).startsWith("content://")) // binary was replaced by content uri + assertNull(retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.BINARY)) + assertEquals(attachment.fmttype, retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.FMTTYPE)) + assertEquals(attachment.other, retrievedAttachmentCV.getAsString(JtxContract.JtxAttachment.OTHER)) + } + } + + @Test + fun assertRelatedto() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val relatedto = at.bitfire.ical4android.JtxICalObject.RelatedTo( + text = "1635164243187-3fd0f89e-d017-471e-a046-71ff1844d58e@at.techbee.jtx", + reltype = JtxContract.JtxRelatedto.Reltype.CHILD.name, + other = "X-OTHER: other" + ) + + val relatedtoCV = ContentValues().apply { + put(JtxContract.JtxRelatedto.TEXT, relatedto.text) + put(JtxContract.JtxRelatedto.RELTYPE, relatedto.reltype) + put(JtxContract.JtxRelatedto.OTHER, relatedto.other) + put(JtxContract.JtxRelatedto.ICALOBJECT_ID, id) + } + + val relatedtoUri = client.insert(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(testAccount), relatedtoCV)!! + client.query(relatedtoUri, null, null, null, null)?.use { + val retrievedRelatedtoCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedRelatedtoCV) + assertEquals(relatedto.text, retrievedRelatedtoCV.getAsString(JtxContract.JtxRelatedto.TEXT)) + assertEquals(relatedto.reltype, retrievedRelatedtoCV.getAsString(JtxContract.JtxRelatedto.RELTYPE)) + assertEquals(relatedto.other, retrievedRelatedtoCV.getAsString(JtxContract.JtxRelatedto.OTHER)) + } + } + + @Test + fun assertAlarm_basic() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val alarm = at.bitfire.ical4android.JtxICalObject.Alarm( + action = JtxContract.JtxAlarm.AlarmAction.AUDIO.name, + description = "desc", + summary = "summary", + duration = "PT15M", + triggerTime = 1641557428506L, + repeat = "4", + attach = "ftp://example.com/pub/sounds/bell-01.aud", + other = "X-OTHER: other", + ) + + val alarmCV = ContentValues().apply { + put(JtxContract.JtxAlarm.ACTION, alarm.action) + put(JtxContract.JtxAlarm.DESCRIPTION, alarm.description) + put(JtxContract.JtxAlarm.SUMMARY, alarm.summary) + put(JtxContract.JtxAlarm.DURATION, alarm.duration) + put(JtxContract.JtxAlarm.TRIGGER_TIME, alarm.triggerTime) + put(JtxContract.JtxAlarm.REPEAT, alarm.repeat) + put(JtxContract.JtxAlarm.ATTACH, alarm.attach) + put(JtxContract.JtxAlarm.OTHER, alarm.other) + put(JtxContract.JtxAlarm.ICALOBJECT_ID, id) + } + + val alarmUri = client.insert(JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(testAccount), alarmCV)!! + client.query(alarmUri, null, null, null, null)?.use { + val retrievedAlarmCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAlarmCV) + assertEquals(alarm.action, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.ACTION)) + assertEquals(alarm.description, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.DESCRIPTION)) + assertEquals(alarm.summary, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.SUMMARY)) + assertEquals(alarm.duration, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.DURATION)) + assertEquals(alarm.repeat, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.REPEAT)) + assertEquals(alarm.attach, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.ATTACH)) + assertEquals(alarm.other, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.OTHER)) + } + } + + + @Test + fun assertAlarm_trigger_duration() { + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VTODO.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val alarm = at.bitfire.ical4android.JtxICalObject.Alarm( + action = JtxContract.JtxAlarm.AlarmAction.DISPLAY.name, + description = "desc", + triggerRelativeDuration = "-PT5M", + triggerRelativeTo = JtxContract.JtxAlarm.AlarmRelativeTo.START.name + ) + + val alarmCV = ContentValues().apply { + put(JtxContract.JtxAlarm.ACTION, alarm.action) + put(JtxContract.JtxAlarm.DESCRIPTION, alarm.description) + put(JtxContract.JtxAlarm.TRIGGER_RELATIVE_DURATION, alarm.triggerRelativeDuration) + put(JtxContract.JtxAlarm.TRIGGER_RELATIVE_TO, alarm.triggerRelativeTo) + put(JtxContract.JtxAlarm.ICALOBJECT_ID, id) + } + + val alarmUri = client.insert(JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(testAccount), alarmCV)!! + client.query(alarmUri, null, null, null, null)?.use { + val retrievedAlarmCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAlarmCV) + assertEquals(alarm.action, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.ACTION)) + assertEquals(alarm.description, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.DESCRIPTION)) + assertEquals(alarm.triggerRelativeTo, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.TRIGGER_RELATIVE_TO)) + assertEquals(alarm.triggerRelativeDuration, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.TRIGGER_RELATIVE_DURATION)) + } + } + + @Test + fun assertAlarm_trigger_time_withoutTZ() { + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VTODO.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val alarm = at.bitfire.ical4android.JtxICalObject.Alarm( + action = JtxContract.JtxAlarm.AlarmAction.DISPLAY.name, + description = "desc", + triggerTime = 1641557428506L + ) + + val alarmCV = ContentValues().apply { + put(JtxContract.JtxAlarm.ACTION, alarm.action) + put(JtxContract.JtxAlarm.DESCRIPTION, alarm.description) + put(JtxContract.JtxAlarm.TRIGGER_TIME, alarm.triggerTime) + put(JtxContract.JtxAlarm.ICALOBJECT_ID, id) + } + + val alarmUri = client.insert(JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(testAccount), alarmCV)!! + client.query(alarmUri, null, null, null, null)?.use { + val retrievedAlarmCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAlarmCV) + assertEquals(alarm.action, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.ACTION)) + assertEquals(alarm.description, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.DESCRIPTION)) + assertEquals(alarm.triggerTime, retrievedAlarmCV.getAsLong(JtxContract.JtxAlarm.TRIGGER_TIME)) + } + } + + @Test + fun assertAlarm_trigger_time_UTC() { + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VTODO.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val alarm = at.bitfire.ical4android.JtxICalObject.Alarm( + action = JtxContract.JtxAlarm.AlarmAction.DISPLAY.name, + description = "desc", + triggerTime = 1641557428506L, + triggerTimezone = "UTC" + ) + + val alarmCV = ContentValues().apply { + put(JtxContract.JtxAlarm.ACTION, alarm.action) + put(JtxContract.JtxAlarm.DESCRIPTION, alarm.description) + put(JtxContract.JtxAlarm.TRIGGER_TIME, alarm.triggerTime) + put(JtxContract.JtxAlarm.TRIGGER_TIMEZONE, alarm.triggerTimezone) + put(JtxContract.JtxAlarm.ICALOBJECT_ID, id) + } + + val alarmUri = client.insert(JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(testAccount), alarmCV)!! + client.query(alarmUri, null, null, null, null)?.use { + val retrievedAlarmCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAlarmCV) + assertEquals(alarm.action, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.ACTION)) + assertEquals(alarm.description, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.DESCRIPTION)) + assertEquals(alarm.triggerTime, retrievedAlarmCV.getAsLong(JtxContract.JtxAlarm.TRIGGER_TIME)) + assertEquals(alarm.triggerTimezone, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.TRIGGER_TIMEZONE)) + } + } + + + @Test + fun assertAlarm_trigger_time_Vienna() { + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VTODO.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val alarm = at.bitfire.ical4android.JtxICalObject.Alarm( + action = "DISPLAY", + description = "desc", + triggerTime = 1641557428506L, + triggerTimezone = "Europe/Vienna" + ) + + val alarmCV = ContentValues().apply { + put(JtxContract.JtxAlarm.ACTION, alarm.action) + put(JtxContract.JtxAlarm.DESCRIPTION, alarm.description) + put(JtxContract.JtxAlarm.TRIGGER_TIME, alarm.triggerTime) + put(JtxContract.JtxAlarm.TRIGGER_TIMEZONE, alarm.triggerTimezone) + put(JtxContract.JtxAlarm.ICALOBJECT_ID, id) + } + + val alarmUri = client.insert(JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(testAccount), alarmCV)!! + client.query(alarmUri, null, null, null, null)?.use { + val retrievedAlarmCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedAlarmCV) + assertEquals(alarm.action, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.ACTION)) + assertEquals(alarm.description, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.DESCRIPTION)) + assertEquals(alarm.triggerTime, retrievedAlarmCV.getAsLong(JtxContract.JtxAlarm.TRIGGER_TIME)) + assertEquals(alarm.triggerTimezone, retrievedAlarmCV.getAsString(JtxContract.JtxAlarm.TRIGGER_TIMEZONE)) + } + } + + + @Test + fun assertUnknown() { + + val cv = ContentValues().apply { + put(JtxICalObject.COMPONENT, Component.VJOURNAL.name) + put(JtxICalObject.ICALOBJECT_COLLECTIONID, collection?.id) + } + val uri = client.insert(JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv)!! + val id = uri.lastPathSegment + + val unknown = at.bitfire.ical4android.JtxICalObject.Unknown( + value = "X-PROP:my value" + ) + + val unknownCV = ContentValues().apply { + put(JtxContract.JtxUnknown.UNKNOWN_VALUE, unknown.value) + put(JtxContract.JtxUnknown.ICALOBJECT_ID, id) + } + + val unknownUri = client.insert(JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(testAccount), unknownCV)!! + client.query(unknownUri, null, null, null, null)?.use { + val retrievedUnknownCV = ContentValues() + it.moveToFirst() + DatabaseUtils.cursorRowToContentValues(it, retrievedUnknownCV) + assertEquals(unknown.value, retrievedUnknownCV.getAsString(JtxContract.JtxUnknown.UNKNOWN_VALUE)) + } + } + + + + + /** TESTS TO READ A FILE; INSERT IT IN THE CONTENT PROVIDER; READ THE CONTENT PROVIDER AND THEN COMPARE IF THE CONTENT OF THE GENERATED ICAL IS STILL THE SAME AS FROM THE SERVER */ + + // VTODO + @Test fun check_input_equals_output_vtodo_most_fields1() = compare_properties("jtx/vtodo/most-fields1.ics", listOf("EXDATE", "RDATE")) + @Test fun check_input_equals_output_vtodo_most_fields2() = compare_properties("jtx/vtodo/most-fields2.ics", null) + @Test fun check_input_equals_output_vtodo_utf8() = compare_properties("jtx/vtodo/utf8.ics", null) + @Test fun check_input_equals_output_vtodo_rfc5545_sample() = compare_properties("jtx/vtodo/rfc5545-sample1.ics", null) + @Test fun check_input_equals_output_vtodo_empty_priority() = compare_properties("jtx/vtodo/empty-priority.ics", null) + @Test fun check_input_equals_output_vtodo_latin1() = compare_properties("jtx/vtodo/latin1.ics", null) + + // VJOURNAL + @Test fun check_input_equals_output_vjournal_default_example() = compare_properties("jtx/vjournal/default-example.ics", null) + @Test fun check_input_equals_output_vjournal_default_example_note() = compare_properties("jtx/vjournal/default-example-note.ics", null) + @Test fun check_input_equals_output_vjournal_utf8() = compare_properties("jtx/vjournal/utf8.ics", null) + @Test fun check_input_equals_output_vjournal_two_line() = compare_properties("jtx/vjournal/two-line-description-without-crlf.ics", listOf("CREATED", "LAST-MODIFIED", "DTSTART")) // expected:<CREATED;VALUE=DATE-TIME:20131008T205713Z > but was:<CREATED:20131008T205713Z is ignored here; expected:<LAST-MODIFIED;VALUE=DATE-TIME:20131008T205740> but was:<LAST-MODIFIED:20131008T205740Z is ignored here as the actual is the correct result + //@Test fun check_input_equals_output_vjournal_two_journals() = compare_properties("jtx/vjournal/two-events-without-exceptions.ics", null) // this file contains two events, the direct comparison through the given method would not work + @Test fun check_input_equals_output_vjournal_recurring() = compare_properties("jtx/vjournal/recurring.ics", null) + //@Test fun check_input_equals_output_vjournal_outlook_theoretical() = compare_properties("jtx/vjournal/outlook-theoretical.ics", null) // includes custom timezones, ignored for now + @Test fun check_input_equals_output_vjournal_outlook_theoretical2() = compare_properties("jtx/vjournal/outlook-theoretical2.ics", null) + @Test fun check_input_equals_output_vjournal_latin1() = compare_properties("jtx/vjournal/latin1.ics", null) + @Test fun check_input_equals_output_vjournal_journalonthatday() = compare_properties("jtx/vjournal/journal-on-that-day.ics", null) + //@Test fun check_input_equals_output_vjournal_dst_only_vtimezone() = compare_properties("jtx/vjournal/dst-only-vtimezone.ics", null) // includes custom timezones, ignored for now + @Test fun check_input_equals_output_vjournal_all_day() = compare_properties("jtx/vjournal/all-day.ics", null) + + /** + * This function takes a file asserts if the ICalendar is the same before and after processing with getIncomingIcal and getOutgoingIcal + * @param filename the filename to be processed + * @param exceptions a list of property names that should not cause the assertion to fail (DTSTAMP is taken in any case) + */ + private fun compare_properties(filename: String, exceptions: List<String>?) { + + val iCalIn = getIncomingIcal(filename) + val iCalOut = getOutgoingIcal(filename) + + //assertEquals(iCalIn.components[0].getProperty(Component.VTODO), iCalOut.components[0].getProperty(Component.VTODO)) + + // there should only be one component for VJOURNAL and VTODO! + for(i in 0 until iCalIn.components.size) { + + iCalIn.components[i].properties.forEach { inProp -> + + if(inProp.name == "DTSTAMP" || exceptions?.contains(inProp.name) == true) + return@forEach + val outProp = iCalOut.components[i].properties.getProperty<Property>(inProp.name) + assertEquals(inProp, outProp) + } + } + } + + + /** + * This function takes a file and returns the parsed ical4j Calendar object + * @param filename: The filename of the ics-file + * @return the ICalendar with the parsed information from the file + */ + private fun getIncomingIcal(filename: String): Calendar { + + val stream = javaClass.classLoader!!.getResourceAsStream(filename) + val reader = InputStreamReader(stream, Charsets.UTF_8) + + val iCalIn = ICalendar.fromReader(reader) + + stream.close() + reader.close() + + return iCalIn + } + + /** + * This function takes a filename and creates a JtxICalObject. + * Then it uses the object to create an ical4j Calendar again. + * @param filename: The filename of the ics-file + * @return The ICalendar after applying all functionalities of JtxICalObject.fromReader(...) + */ + private fun getOutgoingIcal(filename: String): Calendar { + + val stream = javaClass.classLoader!!.getResourceAsStream(filename) + val reader = InputStreamReader(stream, Charsets.UTF_8) + val iCalObject = at.bitfire.ical4android.JtxICalObject.fromReader(reader, collection!!) + + val os = ByteArrayOutputStream() + + iCalObject[0].write(os) + + val iCalOut = ICalendar.fromReader(os.toByteArray().inputStream().reader()) + + stream.close() + reader.close() + + return iCalOut + } +} diff --git a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt index a8c33b4..81f0783 100644 --- a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import net.fortuna.ical4j.model.property.TzOffsetFrom diff --git a/src/androidTest/java/at/bitfire/ical4android/MiscUtilsAndroidTest.kt b/src/androidTest/java/at/bitfire/ical4android/MiscUtilsAndroidTest.kt index 5c51c94..67016e1 100644 --- a/src/androidTest/java/at/bitfire/ical4android/MiscUtilsAndroidTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/MiscUtilsAndroidTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/androidTest/java/at/bitfire/ical4android/TestUtils.kt b/src/androidTest/java/at/bitfire/ical4android/TestUtils.kt index 57bbd94..3387920 100644 --- a/src/androidTest/java/at/bitfire/ical4android/TestUtils.kt +++ b/src/androidTest/java/at/bitfire/ical4android/TestUtils.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt b/src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt index 6ba223c..b4a8be5 100644 --- a/src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import androidx.test.filters.SmallTest diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestCalendar.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestCalendar.kt index e127ec6..19d37cb 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestCalendar.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestCalendar.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android.impl diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt index 3fd19c9..f9c9571 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android.impl diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt new file mode 100644 index 0000000..5a69060 --- /dev/null +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt @@ -0,0 +1,51 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android.impl + +import android.accounts.Account +import android.content.ContentProviderClient +import at.bitfire.ical4android.JtxCollection +import at.bitfire.ical4android.JtxCollectionFactory +import at.bitfire.ical4android.JtxICalObject +import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues +import at.techbee.jtx.JtxContract +import java.util.* + +class TestJtxCollection( + account: Account, + provider: ContentProviderClient, + id: Long +): JtxCollection<JtxICalObject>(account, provider, TestJtxIcalObject.Factory, id) { + + /** + * Queries [JtxContract.JtxICalObject] from this collection. Adds a WHERE clause that restricts the + * query to [JtxContract.JtxCollection.ID] = [id]. + * @param _where selection + * @param _whereArgs arguments for selection + * @return events from this calendar which match the selection + */ + fun queryICalObjects(_where: String? = null, _whereArgs: Array<String>? = null): List<JtxICalObject> { + val where = "(${_where ?: "1"}) AND ${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ?" + val whereArgs = (_whereArgs ?: arrayOf()) + id.toString() + + val iCalObjects = LinkedList<JtxICalObject>() + client.query(jtxSyncURI(), null, where, whereArgs, null)?.use { cursor -> + while (cursor.moveToNext()) + iCalObjects += TestJtxIcalObject.Factory.fromProvider(this, cursor.toValues()) + } + return iCalObjects + } + + + object Factory: JtxCollectionFactory<TestJtxCollection> { + + override fun newInstance( + account: Account, + client: ContentProviderClient, + id: Long + ): TestJtxCollection = TestJtxCollection(account, client, id) + } + +} diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt new file mode 100644 index 0000000..57615fb --- /dev/null +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt @@ -0,0 +1,19 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android.impl + +import android.content.ContentValues +import at.bitfire.ical4android.* + +class TestJtxIcalObject(testCollection: JtxCollection<JtxICalObject>): JtxICalObject(testCollection) { + + object Factory: JtxICalObjectFactory<JtxICalObject> { + + override fun fromProvider( + collection: JtxCollection<JtxICalObject>, + values: ContentValues + ): JtxICalObject = TestJtxIcalObject(collection) + } +} diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestTask.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestTask.kt index ddb2d66..6c03dab 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestTask.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestTask.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android.impl diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestTaskList.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestTaskList.kt index 6ae8479..0f4ef1e 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestTaskList.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestTaskList.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android.impl diff --git a/src/androidTest/resources/jtx/vjournal/all-day.ics b/src/androidTest/resources/jtx/vjournal/all-day.ics new file mode 100644 index 0000000..1300862 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/all-day.ics @@ -0,0 +1,10 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//hacksw/handcal//NONSGML v1.0//EN +BEGIN:VJOURNAL +UID:all-day-1day@example.com +DTSTAMP:20140101T000000Z +DTSTART;VALUE=DATE:19970714 +SUMMARY:All-Day 1 Day +END:VJOURNAL +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vjournal/default-example-note.ics b/src/androidTest/resources/jtx/vjournal/default-example-note.ics new file mode 100644 index 0000000..054cf68 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/default-example-note.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//hacksw/handcal//NONSGML v1.0//EN +BEGIN:VJOURNAL +UID:19970901T130000Z-123405@example.com +DTSTAMP:19970901T130000Z +SUMMARY:Staff meeting minutes +DESCRIPTION:1. Staff meeting: Participants include Joe\, + Lisa\, and Bob. Aurora project plans were reviewed. + There is currently no budget reserves for this project. + Lisa will escalate to management. Next meeting on Tuesday.\n + 2. Telephone Conference: ABC Corp. sales representative + called to discuss new printer. Promised to get us a demo by + Friday.\n3. Henry Miller (Handsoff Insurance): Car was + totaled by tree. Is looking into a loaner car. 555-2323 + (tel). +END:VJOURNAL +END:VCALENDAR
\ No newline at end of file diff --git a/src/androidTest/resources/jtx/vjournal/default-example.ics b/src/androidTest/resources/jtx/vjournal/default-example.ics new file mode 100644 index 0000000..0a88565 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/default-example.ics @@ -0,0 +1,19 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//hacksw/handcal//NONSGML v1.0//EN +BEGIN:VJOURNAL +UID:19970901T130000Z-123405@example.com +DTSTAMP:19970901T130000Z +DTSTART;VALUE=DATE:19970317 +SUMMARY:Staff meeting minutes +DESCRIPTION:1. Staff meeting: Participants include Joe\, + Lisa\, and Bob. Aurora project plans were reviewed. + There is currently no budget reserves for this project. + Lisa will escalate to management. Next meeting on Tuesday.\n + 2. Telephone Conference: ABC Corp. sales representative + called to discuss new printer. Promised to get us a demo by + Friday.\n3. Henry Miller (Handsoff Insurance): Car was + totaled by tree. Is looking into a loaner car. 555-2323 + (tel). +END:VJOURNAL +END:VCALENDAR
\ No newline at end of file diff --git a/src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics b/src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics new file mode 100644 index 0000000..e798232 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics @@ -0,0 +1,21 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:20180325T030000 +TZNAME:CEST +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VJOURNAL +UID:only-dst@example.com +DTSTAMP:20180329T084939Z +DTSTART;TZID=Europe/Berlin:20180403T090000 +DTEND;TZID=Europe/Berlin:20180403T101500 +SUMMARY:Sample Event +CREATED:20180329T084939Z +LAST-MODIFIED:20180329T084939Z +END:VJOURNAL +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics b/src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics new file mode 100644 index 0000000..20d8889 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//hacksw/handcal//NONSGML v1.0//EN +BEGIN:VJOURNAL +UID:event-on-that-day@example.com +DTSTAMP:19970714T170000Z +ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com +DTSTART;VALUE=DATE:19970714 +SUMMARY:Bastille Day Party +END:VJOURNAL +END:VCALENDAR
\ No newline at end of file diff --git a/src/androidTest/resources/jtx/vjournal/latin1.ics b/src/androidTest/resources/jtx/vjournal/latin1.ics new file mode 100644 index 0000000..969b2bf --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/latin1.ics @@ -0,0 +1,9 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VJOURNAL +UID:latin1@ical4android.EventTest +DTSTAMP:20150826T132300Z +SUMMARY: +DTSTART:20131009T170000T +END:VJOURNAL +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics b/src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics new file mode 100644 index 0000000..1f372bb --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics @@ -0,0 +1,71 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Calendar +BEGIN:VTIMEZONE +TZID:China Standard Time +BEGIN:STANDARD +DTSTART:16010101T000000 +TZOFFSETFROM:+0800 +TZOFFSETTO:+0800 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T000000 +TZOFFSETFROM:+0800 +TZOFFSETTO:+0800 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:W. Europe Standard Time +BEGIN:STANDARD +DTSTART:16010101T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:India Standard Time +BEGIN:STANDARD +DTSTART:16010101T000000 +TZOFFSETFROM:+0530 +TZOFFSETTO:+0530 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T000000 +TZOFFSETFROM:+0530 +TZOFFSETTO:+0530 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VJOURNAL +DESCRIPTION:\n............................................................. + .................................\n\n\n +RRULE:FREQ=WEEKLY;UNTIL=20191218T080000Z;INTERVAL=1;BYDAY=WE;WKST=MO +UID:040000008200E00074C5B7101A82E00800000000907682BE2B88D501000000000000000 + 01000000077F6CDA3B634104B9BC1ED539119F558 +SUMMARY:Weekly Meeting - Test +DTSTART;TZID=W. Europe Standard Time:20191023T090000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20191119T090349Z +TRANSP:OPAQUE +STATUS:DRAFT +SEQUENCE:1 +LOCATION:Skype-Besprechung +X-MICROSOFT-CDO-APPT-SEQUENCE:1 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +END:VJOURNAL +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics b/src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics new file mode 100644 index 0000000..eea376d --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics @@ -0,0 +1,29 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Calendar +BEGIN:VJOURNAL +DESCRIPTION:\n............................................................. + .................................\n\n\n +RRULE:FREQ=WEEKLY;UNTIL=20191218T080000Z;INTERVAL=1;BYDAY=WE;WKST=MO +UID:040000008200E00074C5B7101A82E00800000000907682BE2B88D501000000000000000 + 01000000077F6CDA3B634104B9BC1ED539119F558 +SUMMARY:Weekly Meeting - Test +DTSTART:20191023T090000 +CLASS:PUBLIC +DTSTAMP:20191119T090349Z +TRANSP:OPAQUE +STATUS:DRAFT +SEQUENCE:1 +LOCATION:Skype-Besprechung +X-MICROSOFT-CDO-APPT-SEQUENCE:1 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +END:VJOURNAL +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vjournal/recurring.ics b/src/androidTest/resources/jtx/vjournal/recurring.ics new file mode 100644 index 0000000..c071186 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/recurring.ics @@ -0,0 +1,9 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VJOURNAL +UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc +SUMMARY:Recurring event with one exception +RRULE:FREQ=DAILY;COUNT=5 +DTSTART;VALUE=DATE:20150501 +END:VJOURNAL +END:VCALENDAR
\ No newline at end of file diff --git a/src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics b/src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics new file mode 100644 index 0000000..ff7d460 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VJOURNAL +UID:event1 +DTSTAMP:20150826T132300Z +SUMMARY:Event 1 +DTSTART:20131009T170000T +END:VJOURNAL +BEGIN:VJOURNAL +UID:event2 +DTSTAMP:20150826T132300Z +SUMMARY:Event 2 +DTSTART:20131009T180000T +END:VJOURNAL +END:VCALENDAR
\ No newline at end of file diff --git a/src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics b/src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics new file mode 100644 index 0000000..5b6a49a --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Blabla +BEGIN:VJOURNAL +CLASS:PUBLIC +CREATED;VALUE=DATE-TIME:20131008T205713 +LAST-MODIFIED;VALUE=DATE-TIME:20131008T205740 +SUMMARY:online Anmeldung +DESCRIPTION:http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportce + ntergroup=&day=6 +UID:b99c41704b +DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:20131019T060000 +END:VJOURNAL +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vjournal/utf8.ics b/src/androidTest/resources/jtx/vjournal/utf8.ics new file mode 100644 index 0000000..8f39860 --- /dev/null +++ b/src/androidTest/resources/jtx/vjournal/utf8.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VJOURNAL +UID:utf8@ical4android.EventTest +DTSTAMP:20150826T132300Z +SUMMARY:© äö — üß +DESCRIPTION:Test Description +LOCATION:中华人民共和国 +COLOR:aliceblue +ATTENDEE;CN=Cyrus Daboo;EMAIL=cyrus@example.com:mailto:opaque-token-1234@example.com +X-UNKNOWN-PROP;param1=xxx:Unknown Value +END:VJOURNAL +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vtodo/empty-priority.ics b/src/androidTest/resources/jtx/vtodo/empty-priority.ics new file mode 100644 index 0000000..69ad1ae --- /dev/null +++ b/src/androidTest/resources/jtx/vtodo/empty-priority.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +X-WR-CALNAME:Wieseclan +PRODID:-//The Horde Project//Horde iCalendar Library//EN +BEGIN:VTODO +UID:1d58e8e0-4fac-4632-8d43-ec79757490d2.1519164997000 +SUMMARY:Alex Geschenkeliste +DESCRIPTION:[ ] Tip Toi Rund um die Uhr\n[ ] Ravensburger Wort für Wort\n[ + ] Hula Hoop Reifen\n[ ] Pfeil und Bogen mit Zielscheibe\n[ ] Tip Toi + Spielwelt Autorennen +PRIORITY: +STATUS:NEEDS-ACTION +END:VTODO +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vtodo/latin1.ics b/src/androidTest/resources/jtx/vtodo/latin1.ics new file mode 100644 index 0000000..b3502de --- /dev/null +++ b/src/androidTest/resources/jtx/vtodo/latin1.ics @@ -0,0 +1,9 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTODO +UID:latin1@ical4android.EventTest +DTSTAMP:20150826T132300Z +SUMMARY: +DTSTART:20131009T170000T +END:VTODO +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vtodo/most-fields1.ics b/src/androidTest/resources/jtx/vtodo/most-fields1.ics new file mode 100644 index 0000000..ff4dbb0 --- /dev/null +++ b/src/androidTest/resources/jtx/vtodo/most-fields1.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//ABC Corporation//NONSGML My Product//EN +BEGIN:VTODO +SEQUENCE:1 +UID:most-fields1@example.com +LOCATION;ALTREP="http://xyzcorp.com/conf-rooms/f123.vcf": + Conference Room - F123\, Bldg. 002 +GEO:37.386013;-122.082932 +DESCRIPTION:Meeting to provide technical review for "Phoenix" + design.\nHappy Face Conference Room. Phoenix design team + MUST attend this meeting.\nRSVP to team leader. +URL:http://example.com/pub/calendars/jsmith/mytime.ics +ORGANIZER:http://example.com/principals/jsmith +PRIORITY:1 +CLASS:CONFIDENTIAL +STATUS:IN-PROCESS +PERCENT-COMPLETE:25 +DTSTART;VALUE=DATE:20100101 +DUE;VALUE=DATE:20101001 +CATEGORIES:Test,Sample +RRULE:FREQ=YEARLY;INTERVAL=2 +EXDATE;VALUE=DATE:20120101 +EXDATE;VALUE=DATE:20140101,20180101 +RDATE;VALUE=DATE:20100310,20100315 +RDATE;VALUE=DATE:20100810 +RELATED-TO;RELTYPE=SIBLING:most-fields2@example.com +X-UNKNOWN-PROP;param1=xxx:Unknown Value +CREATED:19960329T133000Z +LAST-MODIFIED:19960817T133000Z +END:VTODO +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vtodo/most-fields2.ics b/src/androidTest/resources/jtx/vtodo/most-fields2.ics new file mode 100644 index 0000000..f6c7215 --- /dev/null +++ b/src/androidTest/resources/jtx/vtodo/most-fields2.ics @@ -0,0 +1,9 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//ABC Corporation//NONSGML My Product//EN +BEGIN:VTODO +UID:most-fields2@example.com +DTSTART:20100101T101010Z +DURATION:P4DT3H2M1S +END:VTODO +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics b/src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics new file mode 100644 index 0000000..18d53df --- /dev/null +++ b/src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics @@ -0,0 +1,22 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//ABC Corporation//NONSGML My Product//EN +BEGIN:VTODO +DTSTAMP:19980130T134500Z +SEQUENCE:2 +UID:uid4@example.com +ORGANIZER:mailto:unclesam@example.com +ATTENDEE;PARTSTAT=ACCEPTED:mailto:jqpublic@example.com +DUE:19980415T000000 +STATUS:NEEDS-ACTION +SUMMARY:Submit Income Taxes +BEGIN:VALARM +ACTION:AUDIO +TRIGGER;VALUE=DATE-TIME:19980403T120000Z +ATTACH;FMTTYPE=audio/basic:http://example.com/pub/audio- + files/ssbanner.aud +REPEAT:4 +DURATION:PT1H +END:VALARM +END:VTODO +END:VCALENDAR diff --git a/src/androidTest/resources/jtx/vtodo/utf8.ics b/src/androidTest/resources/jtx/vtodo/utf8.ics new file mode 100644 index 0000000..67aaa1e --- /dev/null +++ b/src/androidTest/resources/jtx/vtodo/utf8.ics @@ -0,0 +1,10 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTODO +UID:utf8@ical4android.TaskTest +DTSTAMP:20150826T132300Z +SUMMARY:© äö — üß +LOCATION:中华人民共和国 +DTSTART:20131009T170000T +END:VTODO +END:VCALENDAR diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 8f4f2c5..442d0ed 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -25,10 +25,15 @@ <uses-permission android:name="org.tasks.permission.READ_TASKS" /> <uses-permission android:name="org.tasks.permission.WRITE_TASKS" /> + <!-- read/write tasks & journals (jtx Board) --> + <uses-permission android:name="at.techbee.jtx.permission.READ" /> + <uses-permission android:name="at.techbee.jtx.permission.WRITE" /> + <application /> <queries> <!-- task providers --> + <package android:name="at.techbee.jtx" /> <package android:name="org.dmfs.tasks" /> <package android:name="org.tasks" /> </queries> diff --git a/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt b/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt index e0ac1bb..5736bde 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AndroidCalendarFactory.kt b/src/main/java/at/bitfire/ical4android/AndroidCalendarFactory.kt index 071ba9f..a0ba2b2 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidCalendarFactory.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidCalendarFactory.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt index 3fed6b6..ec5981b 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AndroidEventFactory.kt b/src/main/java/at/bitfire/ical4android/AndroidEventFactory.kt index cd5f7e4..3136956 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidEventFactory.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidEventFactory.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AndroidTask.kt b/src/main/java/at/bitfire/ical4android/AndroidTask.kt index c776321..84af609 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTask.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTask.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskFactory.kt b/src/main/java/at/bitfire/ical4android/AndroidTaskFactory.kt index 0e35d7b..41d3f70 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTaskFactory.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTaskFactory.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt b/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt index dd37586..4cd784b 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskListFactory.kt b/src/main/java/at/bitfire/ical4android/AndroidTaskListFactory.kt index bc62a60..f7f33bd 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTaskListFactory.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTaskListFactory.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/AttendeeMappings.kt b/src/main/java/at/bitfire/ical4android/AttendeeMappings.kt index d6dea03..69d4baf 100644 --- a/src/main/java/at/bitfire/ical4android/AttendeeMappings.kt +++ b/src/main/java/at/bitfire/ical4android/AttendeeMappings.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import android.content.ContentValues diff --git a/src/main/java/at/bitfire/ical4android/BatchOperation.kt b/src/main/java/at/bitfire/ical4android/BatchOperation.kt index 392b303..a7b881e 100644 --- a/src/main/java/at/bitfire/ical4android/BatchOperation.kt +++ b/src/main/java/at/bitfire/ical4android/BatchOperation.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/CalendarStorageException.kt b/src/main/java/at/bitfire/ical4android/CalendarStorageException.kt index 6ab3a67..85f463b 100644 --- a/src/main/java/at/bitfire/ical4android/CalendarStorageException.kt +++ b/src/main/java/at/bitfire/ical4android/CalendarStorageException.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/Css3Color.kt b/src/main/java/at/bitfire/ical4android/Css3Color.kt index b1b363f..57df5a2 100644 --- a/src/main/java/at/bitfire/ical4android/Css3Color.kt +++ b/src/main/java/at/bitfire/ical4android/Css3Color.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/DateUtils.kt b/src/main/java/at/bitfire/ical4android/DateUtils.kt index 52f8a98..c9ceffa 100644 --- a/src/main/java/at/bitfire/ical4android/DateUtils.kt +++ b/src/main/java/at/bitfire/ical4android/DateUtils.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/Event.kt b/src/main/java/at/bitfire/ical4android/Event.kt index c432511..d96ac73 100644 --- a/src/main/java/at/bitfire/ical4android/Event.kt +++ b/src/main/java/at/bitfire/ical4android/Event.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/ICalPreprocessor.kt b/src/main/java/at/bitfire/ical4android/ICalPreprocessor.kt index d43baae..ae7293c 100644 --- a/src/main/java/at/bitfire/ical4android/ICalPreprocessor.kt +++ b/src/main/java/at/bitfire/ical4android/ICalPreprocessor.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import net.fortuna.ical4j.model.Calendar diff --git a/src/main/java/at/bitfire/ical4android/ICalendar.kt b/src/main/java/at/bitfire/ical4android/ICalendar.kt index aaffa1c..619f193 100644 --- a/src/main/java/at/bitfire/ical4android/ICalendar.kt +++ b/src/main/java/at/bitfire/ical4android/ICalendar.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/Ical4Android.kt b/src/main/java/at/bitfire/ical4android/Ical4Android.kt index f97752f..806e981 100644 --- a/src/main/java/at/bitfire/ical4android/Ical4Android.kt +++ b/src/main/java/at/bitfire/ical4android/Ical4Android.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/InvalidCalendarException.kt b/src/main/java/at/bitfire/ical4android/InvalidCalendarException.kt index 00f1467..6c0da58 100644 --- a/src/main/java/at/bitfire/ical4android/InvalidCalendarException.kt +++ b/src/main/java/at/bitfire/ical4android/InvalidCalendarException.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/JtxCollection.kt b/src/main/java/at/bitfire/ical4android/JtxCollection.kt new file mode 100644 index 0000000..a10ee42 --- /dev/null +++ b/src/main/java/at/bitfire/ical4android/JtxCollection.kt @@ -0,0 +1,265 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues +import at.techbee.jtx.JtxContract +import at.techbee.jtx.JtxContract.asSyncAdapter +import java.util.* + +open class JtxCollection<out T: JtxICalObject>(val account: Account, + val client: ContentProviderClient, + private val iCalObjectFactory: JtxICalObjectFactory<JtxICalObject>, + val id: Long) { + + companion object { + + fun create(account: Account, client: ContentProviderClient, values: ContentValues): Uri = + client.insert(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), values)?: throw CalendarStorageException("Couldn't create JTX Collection") + + fun<T: JtxCollection<JtxICalObject>> find(account: Account, client: ContentProviderClient, context: Context, factory: JtxCollectionFactory<T>, where: String?, whereArgs: Array<String>?): List<T> { + val collections = LinkedList<T>() + client.query(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), null, where, whereArgs, null)?.use { cursor -> + while (cursor.moveToNext()) { + val values = cursor.toValues() + val collection = factory.newInstance(account, client, values.getAsLong(JtxContract.JtxCollection.ID)) + collection.populate(values, context) + collections += collection + } + } + return collections + } + } + + + var url: String? = null + var displayname: String? = null + var syncstate: String? = null + + var context: Context? = null + + + fun delete() { + client.delete(ContentUris.withAppendedId(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), id), null, null) + } + + fun update(values: ContentValues) { + + client.update(ContentUris.withAppendedId(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), id), values, null, null) + } + + protected fun populate(values: ContentValues, context: Context) { + url = values.getAsString(JtxContract.JtxCollection.URL) + displayname = values.getAsString(JtxContract.JtxCollection.DISPLAYNAME) + syncstate = values.getAsString(JtxContract.JtxCollection.SYNC_VERSION) + + this.context = context + } + + + /** + * Builds the JtxICalObject content uri with appended parameters for account and syncadapter + * @return the Uri for the JtxICalObject Sync in the content provider of jtx Board + */ + fun jtxSyncURI(): Uri = + JtxContract.JtxICalObject.CONTENT_URI.buildUpon() + .appendQueryParameter(JtxContract.ACCOUNT_NAME, account.name) + .appendQueryParameter(JtxContract.ACCOUNT_TYPE, account.type) + .appendQueryParameter(JtxContract.CALLER_IS_SYNCADAPTER, "true") + .build() + + + /** + * @return a list of content values of the deleted jtxICalObjects + */ + fun queryDeletedICalObjects(): List<ContentValues> { + val values = mutableListOf<ContentValues>() + client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DELETED} = ?", arrayOf(id.toString(), "1"), null).use { cursor -> + Ical4Android.log.fine("findDeleted: found ${cursor?.count} deleted records in ${account.name}") + while (cursor?.moveToNext() == true) { + values.add(cursor.toValues()) + } + } + return values + } + + + /** + * @return a list of content values of the dirty jtxICalObjects + */ + fun queryDirtyICalObjects(): List<ContentValues> { + val values = mutableListOf<ContentValues>() + client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DIRTY} = ?", arrayOf(id.toString(), "1"), null).use { cursor -> + Ical4Android.log.fine("findDirty: found ${cursor?.count} dirty records in ${account.name}") + while (cursor?.moveToNext() == true) { + values.add(cursor.toValues()) + } + } + return values + } + + /** + * @param [filename] of the entry that should be retrieved as content values + * @return Content Values of the found item with the given filename or null if the result was empty or more than 1 + */ + fun queryByFilename(filename: String): ContentValues? { + client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.FILENAME} = ?", arrayOf(id.toString(), filename), null).use { cursor -> + Ical4Android.log.fine("queryByFilename: found ${cursor?.count} records in ${account.name}") + if (cursor?.count != 1) + return null + cursor.moveToFirst() + return cursor.toValues() + } + } + + /** + * updates the flags of all entries in the collection with the given flag + * @param [flags] to be set + * @return the number of records that were updated + */ + fun updateSetFlags(flags: Int): Int { + val values = ContentValues(1) + values.put(JtxContract.JtxICalObject.FLAGS, flags) + return client.update(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), values, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DIRTY} = ?", arrayOf(id.toString(), "0")) + } + + /** + * deletes all entries with the given flags + * @param [flags] of the entries that should be deleted + * @return the number of deleted records + */ + fun deleteByFlags(flags: Int) = + client.delete(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), "${JtxContract.JtxICalObject.DIRTY} = ? AND ${JtxContract.JtxICalObject.FLAGS} = ? ", arrayOf("0", flags.toString())) + + /** + * Updates the eTag value of all entries within a collection to the given eTag + * @param [eTag] to be set (or null) + */ + fun updateSetETag(eTag: String?) { + val values = ContentValues(1) + if(eTag == null) + values.putNull(JtxContract.JtxICalObject.ETAG) + else + values.put(JtxContract.JtxICalObject.ETAG, eTag) + client.update(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), values, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ?", arrayOf(id.toString())) + } + + + /** + * This function updates the Related-To relations in jtx Board. + * STEP 1: find entries to update (all entries with 0 in related-to). When inserting the relation, we only know the parent iCalObjectId and the related UID (but not the related iCalObjectId). + * In this step we search for all Related-To relations where the LINKEDICALOBJEC_ID is not set, resolve it through the UID and set it. + * STEP 2/3: jtx Board saves the relations in both directions, the Parent has an entry for his Child, the Child has an entry for his Parent. Step 2 and Step 3 make sure, that the Child-Parent pair is + * present in both directions. + */ + fun updateRelatedTo() { + // STEP 1: first find entries to update (all entries with 0 in related-to) + client.query(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxRelatedto.TEXT), "${JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID} = ?", arrayOf("0"), null).use { + while(it?.moveToNext() == true) { + val uid2upddate = it.getString(0) + + client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxICalObject.ID), "${JtxContract.JtxICalObject.UID} = ?", arrayOf(uid2upddate), null).use { idOfthisUidCursor -> + if (idOfthisUidCursor?.moveToFirst() == true) { + val idOfthisUid = idOfthisUidCursor.getLong(0) + + val updateContentValues = ContentValues() + updateContentValues.put( + JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, + idOfthisUid + ) + + client.update( + JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter( + account + ), + updateContentValues, + "${JtxContract.JtxRelatedto.TEXT} = ? AND ${JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID} = ?", + arrayOf(uid2upddate, "0") + ) + } + } + } + } + + + // STEP 2: query all related to that are linking their PARENTS and check if they also have the opposite relationship entered, if not, then add it + client.query(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxRelatedto.ICALOBJECT_ID, JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, JtxContract.JtxRelatedto.RELTYPE), "${JtxContract.JtxRelatedto.RELTYPE} = ?", arrayOf(JtxContract.JtxRelatedto.Reltype.PARENT.name), null).use { + cursorAllLinkedParents -> + while (cursorAllLinkedParents?.moveToNext() == true) { + val icalObjectId = cursorAllLinkedParents.getString(0) + val linkedIcalObjectId = cursorAllLinkedParents.getString(1) + + client.query(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxRelatedto.ICALOBJECT_ID, JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, JtxContract.JtxRelatedto.RELTYPE), "${JtxContract.JtxRelatedto.ICALOBJECT_ID} = ? AND ${JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID} = ? AND ${JtxContract.JtxRelatedto.RELTYPE} = ?", arrayOf(linkedIcalObjectId.toString(), icalObjectId.toString(), JtxContract.JtxRelatedto.Reltype.CHILD.name), null).use { cursor -> + // if the query does not bring any result, then we insert the opposite relationship + if (cursor?.moveToFirst() == false) { + + //get the UID of the linked entry + client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxICalObject.UID), "${JtxContract.JtxICalObject.ID} = ?", arrayOf(linkedIcalObjectId.toString()), null).use { + foundIcalObjectCursor -> + + if (foundIcalObjectCursor?.moveToFirst() == true) { + val uid = foundIcalObjectCursor.getString(0) + + val cv = ContentValues().apply { + put(JtxContract.JtxRelatedto.ICALOBJECT_ID, linkedIcalObjectId) + put(JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, icalObjectId) + put(JtxContract.JtxRelatedto.RELTYPE, JtxContract.JtxRelatedto.Reltype.CHILD.name) + put(JtxContract.JtxRelatedto.TEXT, uid) + } + + client.insert(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), cv) + } + } + } + } + } + } + + + // STEP 3: query all related to that are linking their CHILD and check if they also have the opposite relationship entered, if not, then add it + client.query(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxRelatedto.ICALOBJECT_ID, JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, JtxContract.JtxRelatedto.RELTYPE), "${JtxContract.JtxRelatedto.RELTYPE} = ?", arrayOf(JtxContract.JtxRelatedto.Reltype.CHILD.name), null).use { + cursorAllLinkedParents -> + while (cursorAllLinkedParents?.moveToNext() == true) { + + val icalObjectId = cursorAllLinkedParents.getLong(0) + val linkedIcalObjectId = cursorAllLinkedParents.getLong(1) + + client.query(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxRelatedto.ICALOBJECT_ID, JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, JtxContract.JtxRelatedto.RELTYPE), "${JtxContract.JtxRelatedto.ICALOBJECT_ID} = ? AND ${JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID} = ? AND ${JtxContract.JtxRelatedto.RELTYPE} = ?", arrayOf(linkedIcalObjectId.toString(), icalObjectId.toString(), JtxContract.JtxRelatedto.Reltype.PARENT.name), null).use { + cursor -> + + // if the query does not bring any result, then we insert the opposite relationship + if (cursor?.moveToFirst() == false) { + + //get the UID of the linked entry + client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), arrayOf(JtxContract.JtxICalObject.UID), "${JtxContract.JtxICalObject.ID} = ?", arrayOf(linkedIcalObjectId.toString()), null).use { + foundIcalObjectCursor -> + + if(foundIcalObjectCursor?.moveToFirst() == true) { + + val uid = foundIcalObjectCursor.getString(0) + + val cv = ContentValues().apply { + put(JtxContract.JtxRelatedto.ICALOBJECT_ID, linkedIcalObjectId) + put(JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, icalObjectId) + put(JtxContract.JtxRelatedto.RELTYPE, JtxContract.JtxRelatedto.Reltype.PARENT.name) + put(JtxContract.JtxRelatedto.TEXT, uid) + } + + client.insert(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), cv) + } + } + } + } + } + } + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/ical4android/JtxCollectionFactory.kt b/src/main/java/at/bitfire/ical4android/JtxCollectionFactory.kt new file mode 100644 index 0000000..177b2d1 --- /dev/null +++ b/src/main/java/at/bitfire/ical4android/JtxCollectionFactory.kt @@ -0,0 +1,14 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android + +import android.accounts.Account +import android.content.ContentProviderClient + +interface JtxCollectionFactory<out T: JtxCollection<JtxICalObject>> { + + fun newInstance(account: Account, client: ContentProviderClient, id: Long): T + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt new file mode 100644 index 0000000..1b8a5f1 --- /dev/null +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -0,0 +1,1796 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android + +import android.content.ContentUris +import android.content.ContentValues +import android.net.ParseException +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.util.Base64 +import android.util.Log +import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues +import at.techbee.jtx.JtxContract +import at.techbee.jtx.JtxContract.JtxICalObject.TZ_ALLDAY +import at.techbee.jtx.JtxContract.asSyncAdapter +import net.fortuna.ical4j.data.CalendarOutputter +import net.fortuna.ical4j.data.ParserException +import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.component.VJournal +import net.fortuna.ical4j.model.component.VToDo +import net.fortuna.ical4j.model.parameter.* +import net.fortuna.ical4j.model.property.* +import java.io.* +import java.net.URI +import java.net.URISyntaxException +import java.time.format.DateTimeParseException +import java.util.* +import java.util.TimeZone +import java.util.logging.Level + +open class JtxICalObject( + val collection: JtxCollection<JtxICalObject> +) { + + var id: Long = 0L + lateinit var component: String + var summary: String? = null + var description: String? = null + var dtstart: Long? = null + var dtstartTimezone: String? = null + var dtend: Long? = null + var dtendTimezone: String? = null + + var classification: String? = null + var status: String? = null + + var priority: Int? = null + + var due: Long? = null // VTODO only! + var dueTimezone: String? = null //VTODO only! + var completed: Long? = null // VTODO only! + var completedTimezone: String? = null //VTODO only! + var duration: String? = null //VTODO only! + + var percent: Int? = null + var url: String? = null + var contact: String? = null + var geoLat: Double? = null + var geoLong: Double? = null + var location: String? = null + var locationAltrep: String? = null + + var uid: String = "${System.currentTimeMillis()}-${UUID.randomUUID()}@at.techbee.jtx" + + var created: Long = System.currentTimeMillis() + var dtstamp: Long = System.currentTimeMillis() + var lastModified: Long = System.currentTimeMillis() + var sequence: Long = 0 + + var color: Int? = null + + var rrule: String? = null //only for recurring events, see https://tools.ietf.org/html/rfc5545#section-3.8.5.3 + var exdate: String? = null //only for recurring events, see https://tools.ietf.org/html/rfc5545#section-3.8.5.1 + var rdate: String? = null //only for recurring events, see https://tools.ietf.org/html/rfc5545#section-3.8.5.2 + var recurid: String? = null //only for recurring events, see https://tools.ietf.org/html/rfc5545#section-3.8.5 + + var rstatus: String? = null + + var collectionId: Long = collection.id + + var dirty: Boolean = true + var deleted: Boolean = false + + var fileName: String? = null + var eTag: String? = null + var scheduleTag: String? = null + var flags: Int = 0 + + var categories: MutableList<Category> = mutableListOf() + var attachments: MutableList<Attachment> = mutableListOf() + var attendees: MutableList<Attendee> = mutableListOf() + var comments: MutableList<Comment> = mutableListOf() + var organizer: Organizer? = null + var resources: MutableList<Resource> = mutableListOf() + var relatedTo: MutableList<RelatedTo> = mutableListOf() + var alarms: MutableList<Alarm> = mutableListOf() + var unknown: MutableList<Unknown> = mutableListOf() + + + + + data class Category( + var categoryId: Long = 0L, + var text: String? = null, + var language: String? = null, + var other: String? = null + ) + + data class Attachment( + var attachmentId: Long = 0L, + var uri: String? = null, + var binary: String? = null, + var fmttype: String? = null, + var other: String? = null, + var filename: String? = null, + var extension: String? = null, + var filesize: Long? = null + ) + + data class Comment( + var commentId: Long = 0L, + var text: String? = null, + var altrep: String? = null, + var language: String? = null, + var other: String? = null + ) + + data class RelatedTo( + var relatedtoId: Long = 0L, + var text: String? = null, + var reltype: String? = null, + var other: String? = null + ) + + data class Attendee( + var attendeeId: Long = 0L, + var caladdress: String? = null, + var cutype: String? = JtxContract.JtxAttendee.Cutype.INDIVIDUAL.name, + var member: String? = null, + var role: String? = JtxContract.JtxAttendee.Role.`REQ-PARTICIPANT`.name, + var partstat: String? = null, + var rsvp: Boolean? = null, + var delegatedto: String? = null, + var delegatedfrom: String? = null, + var sentby: String? = null, + var cn: String? = null, + var dir: String? = null, + var language: String? = null, + var other: String? = null + ) + + data class Resource( + var resourceId: Long = 0L, + var text: String? = null, + var altrep: String? = null, + var language: String? = null, + var other: String? = null + ) + + data class Organizer( + var organizerId: Long = 0L, + var caladdress: String? = null, + var cn: String? = null, + var dir: String? = null, + var sentby: String? = null, + var language: String? = null, + var other: String? = null + ) + + data class Alarm( + var alarmId: Long = 0L, + var action: String? = null, + var description: String? = null, + var summary: String? = null, + var attendee: String? = null, + var duration: String? = null, + var repeat: String? = null, + var attach: String? = null, + var other: String? = null, + var triggerTime: Long? = null, + var triggerTimezone: String? = null, + var triggerRelativeTo: String? = null, + var triggerRelativeDuration: String? = null + ) + + data class Unknown( + var unknownId: Long = 0L, + var value: String? = null + ) + + + companion object { + + const val X_PROP_COMPLETEDTIMEZONE = "X-COMPLETEDTIMEZONE" + + const val MAX_ATTACHMENT_SYNC_SIZE = 102400 // = 100KB + + /** + * Parses an iCalendar resource, applies [ICalPreprocessor] to increase compatibility + * and extracts the VTODOs and/or VJOURNALS. + * + * @param reader where the iCalendar is taken from + * + * @return array of filled [JtxICalObject] data objects (may have size 0) + * + * @throws ParserException when the iCalendar can't be parsed + * @throws IllegalArgumentException when the iCalendar resource contains an invalid value + * @throws IOException on I/O errors + */ + @UsesThreadContextClassLoader + fun fromReader( + reader: Reader, + collection: JtxCollection<JtxICalObject> + ): List<JtxICalObject> { + val ical = ICalendar.fromReader(reader) + + val iCalObjectList = mutableListOf<JtxICalObject>() + + ical.components.forEach { component -> + + val iCalObject = JtxICalObject(collection) + + when(component) { + is VToDo -> { + iCalObject.component = JtxContract.JtxICalObject.Component.VTODO.name + if (component.uid != null) + iCalObject.uid = component.uid.value // generated UID is overwritten here (if present) + extractProperties(iCalObject, component.properties) + extractVAlarms(iCalObject, component.components) // accessing the components needs an explicit type + } + is VJournal -> { + iCalObject.component = JtxContract.JtxICalObject.Component.VJOURNAL.name + if (component.uid != null) + iCalObject.uid = component.uid.value + extractProperties(iCalObject, component.properties) + extractVAlarms(iCalObject, component.components) // accessing the components needs an explicit type + } + } + iCalObjectList.add(iCalObject) + } + return iCalObjectList + } + + /** + * Extracts VAlarms from the given Component (VJOURNAL or VTODO). The VAlarm is supposed to be a component within the VJOURNAL or VTODO component. + * Other components than VAlarms should not occur. + * @param [iCalObject] where the VAlarms should be inserted + * @param [calComponents] from which the VAlarms should be extracted + */ + private fun extractVAlarms(iCalObject: JtxICalObject, calComponents: ComponentList<*>) { + + calComponents.forEach { component -> + if(component is VAlarm) { + val jtxAlarm = Alarm().apply { + component.action?.value?.let { vAlarmAction -> this.action = vAlarmAction } + component.summary?.value?.let { vAlarmSummary -> this.summary = vAlarmSummary } + component.description?.value?.let { vAlarmDesc -> this.description = vAlarmDesc } + component.duration?.value?.let { vAlarmDur -> this.duration = vAlarmDur } + component.attachment?.uri?.let { uri -> this.attach = uri.toString() } + component.repeat?.value?.let { vAlarmRep -> this.repeat = vAlarmRep } + + // alarms can have a duration or an absolute dateTime, but not both! + if(component.trigger.duration != null) { + component.trigger?.duration?.let { duration -> this.triggerRelativeDuration = duration.toString() } + component.trigger?.getParameter<Related>(Parameter.RELATED)?.let { related -> this.triggerRelativeTo = related.value } + } else if(component.trigger.dateTime != null) { + component.trigger?.dateTime?.let { dateTime -> this.triggerTime = dateTime.time } + component.trigger?.dateTime?.timeZone?.let { timezone -> this.triggerTimezone = timezone.id } + } + + // remove properties to add the rest to other + component.properties.remove(component.action) + component.properties.remove(component.summary) + component.properties.remove(component.description) + component.properties.remove(component.duration) + component.properties.remove(component.attachment) + component.properties.remove(component.repeat) + component.properties.remove(component.trigger) + component.properties?.let { vAlarmProps -> this.other = JtxContract.getJsonStringFromXProperties(vAlarmProps) } + } + iCalObject.alarms.add(jtxAlarm) + } + } + } + + /** + * Extracts properties from a given Property list and maps it to a JtxICalObject + * @param [iCalObject] where the properties should be mapped to + * @param [properties] from which the properties can be extracted + */ + private fun extractProperties(iCalObject: JtxICalObject, properties: PropertyList<*>) { + + // sequence must only be null for locally created, not-yet-synchronized events + iCalObject.sequence = 0 + + for (prop in properties) { + when (prop) { + is Sequence -> iCalObject.sequence = prop.sequenceNo.toLong() + is Created -> iCalObject.created = prop.dateTime.time + is LastModified -> iCalObject.lastModified = prop.dateTime.time + is Summary -> iCalObject.summary = prop.value + is Location -> { + iCalObject.location = prop.value + if(!prop.parameters.isEmpty && prop.parameters.getParameter<AltRep>(Parameter.ALTREP) != null) + iCalObject.locationAltrep = prop.parameters.getParameter<AltRep>(Parameter.ALTREP).value + } + is Geo -> { + iCalObject.geoLat = prop.latitude.toDouble() + iCalObject.geoLong = prop.longitude.toDouble() + } + is Description -> iCalObject.description = prop.value + is Color -> iCalObject.color = Css3Color.fromString(prop.value)?.argb + is Url -> iCalObject.url = prop.value + is Priority -> iCalObject.priority = prop.level + is Clazz -> iCalObject.classification = prop.value + is Status -> iCalObject.status = prop.value + is DtEnd -> Ical4Android.log.warning("The property DtEnd must not be used for VTODO and VJOURNAL, this value is rejected.") + is Completed -> { + if (iCalObject.component == JtxContract.JtxICalObject.Component.VTODO.name) { + iCalObject.completed = prop.date.time + } else + Ical4Android.log.warning("The property Completed is only supported for VTODO, this value is rejected.") + } + + is Due -> { + if (iCalObject.component == JtxContract.JtxICalObject.Component.VTODO.name) { + iCalObject.due = prop.date.time + when { + prop.date is DateTime && prop.timeZone != null -> iCalObject.dueTimezone = prop.timeZone.id + prop.date is DateTime && prop.isUtc -> iCalObject.dueTimezone = TimeZone.getTimeZone("UTC").id + prop.date is DateTime && !prop.isUtc && prop.timeZone == null -> iCalObject.dueTimezone = null // this comparison is kept on purpose as "prop.date is Date" did not work as expected. + else -> iCalObject.dueTimezone = TZ_ALLDAY // prop.date is Date (and not DateTime), therefore it must be Allday + } + } else + Ical4Android.log.warning("The property Due is only supported for VTODO, this value is rejected.") + } + + is Duration -> iCalObject.duration = prop.value + + is DtStart -> { + iCalObject.dtstart = prop.date.time + when { + prop.date is DateTime && prop.timeZone != null -> iCalObject.dtstartTimezone = prop.timeZone.id + prop.date is DateTime && prop.isUtc -> iCalObject.dtstartTimezone = TimeZone.getTimeZone("UTC").id + prop.date is DateTime && !prop.isUtc && prop.timeZone == null -> iCalObject.dtstartTimezone = null // this comparison is kept on purpose as "prop.date is Date" did not work as expected. + else -> iCalObject.dtstartTimezone = TZ_ALLDAY // prop.date is Date (and not DateTime), therefore it must be Allday + } + } + + is PercentComplete -> { + if (iCalObject.component == JtxContract.JtxICalObject.Component.VTODO.name) + iCalObject.percent = prop.percentage + else + Ical4Android.log.warning("The property PercentComplete is only supported for VTODO, this value is rejected.") + } + + is RRule -> iCalObject.rrule = prop.value + is RDate -> { + val rdateList = if(iCalObject.rdate.isNullOrEmpty()) + mutableListOf() + else + JtxContract.getLongListFromString(iCalObject.rdate!!) + prop.dates.forEach { + rdateList.add(it.time) + } + iCalObject.rdate = rdateList.toTypedArray().joinToString(separator = ",") + } + is ExDate -> { + val exdateList = if(iCalObject.exdate.isNullOrEmpty()) + mutableListOf() + else + JtxContract.getLongListFromString(iCalObject.exdate!!) + prop.dates.forEach { + exdateList.add(it.time) + } + iCalObject.exdate = exdateList.toTypedArray().joinToString(separator = ",") + } + is RecurrenceId -> iCalObject.recurid = prop.value + + //is RequestStatus -> iCalObject.rstatus = prop.value + + is Categories -> + for (category in prop.categories) + iCalObject.categories.add(Category(text = category)) + + is net.fortuna.ical4j.model.property.Comment -> { + iCalObject.comments.add( + Comment().apply { + this.text = prop.value + this.language = prop.parameters?.getParameter<Language>(Parameter.LANGUAGE)?.value + this.altrep = prop.parameters?.getParameter<AltRep>(Parameter.ALTREP)?.value + + // remove the known parameter + prop.parameters?.removeAll(Parameter.LANGUAGE) + prop.parameters?.removeAll(Parameter.ALTREP) + + // save unknown parameters in the other field + this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + }) + + } + + is Resources -> + for (resource in prop.resources) + iCalObject.resources.add(Resource(text = resource)) + + is Attach -> { + val attachment = Attachment() + prop.uri?.let { attachment.uri = it.toString() } + prop.binary?.let { + attachment.binary = Base64.encodeToString(it, Base64.DEFAULT) + } + prop.parameters?.getParameter<FmtType>(Parameter.FMTTYPE)?.let { + attachment.fmttype = it.value + prop.parameters?.remove(it) + } + + attachment.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + + if (attachment.uri?.isNotEmpty() == true || attachment.binary?.isNotEmpty() == true) // either uri or value must be present! + iCalObject.attachments.add(attachment) + } + + is net.fortuna.ical4j.model.property.RelatedTo -> { + + iCalObject.relatedTo.add( + RelatedTo().apply { + this.text = prop.value + this.reltype = prop.getParameter<RelType>(RelType.RELTYPE)?.value ?: JtxContract.JtxRelatedto.Reltype.PARENT.name + + // remove the known parameter + prop.parameters?.removeAll(RelType.RELTYPE) + + // save unknown parameters in the other field + this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + }) + } + + is net.fortuna.ical4j.model.property.Attendee -> { + iCalObject.attendees.add( + Attendee().apply { + this.caladdress = prop.calAddress.toString() + this.cn = prop.parameters?.getParameter<Cn>(Parameter.CN)?.value + this.delegatedto = prop.parameters?.getParameter<DelegatedTo>(Parameter.DELEGATED_TO)?.value + this.delegatedfrom = prop.parameters?.getParameter<DelegatedFrom>(Parameter.DELEGATED_FROM)?.value + this.cutype = prop.parameters?.getParameter<CuType>(Parameter.CUTYPE)?.value + this.dir = prop.parameters?.getParameter<Dir>(Parameter.DIR)?.value + this.language = prop.parameters?.getParameter<Language>(Parameter.LANGUAGE)?.value + this.member = prop.parameters?.getParameter<Member>(Parameter.MEMBER)?.value + this.partstat = prop.parameters?.getParameter<PartStat>(Parameter.PARTSTAT)?.value + this.role = prop.parameters?.getParameter<Role>(Parameter.ROLE)?.value + this.rsvp = prop.parameters?.getParameter<Rsvp>(Parameter.RSVP)?.value?.toBoolean() + this.sentby = prop.parameters?.getParameter<SentBy>(Parameter.SENT_BY)?.value + + // remove all known parameters so that only unknown parameters remain + prop.parameters?.removeAll(Parameter.CN) + prop.parameters?.removeAll(Parameter.DELEGATED_TO) + prop.parameters?.removeAll(Parameter.DELEGATED_FROM) + prop.parameters?.removeAll(Parameter.CUTYPE) + prop.parameters?.removeAll(Parameter.DIR) + prop.parameters?.removeAll(Parameter.LANGUAGE) + prop.parameters?.removeAll(Parameter.MEMBER) + prop.parameters?.removeAll(Parameter.PARTSTAT) + prop.parameters?.removeAll(Parameter.ROLE) + prop.parameters?.removeAll(Parameter.RSVP) + prop.parameters?.removeAll(Parameter.SENT_BY) + + // save unknown parameters in the other field + this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + } + ) + } + is net.fortuna.ical4j.model.property.Organizer -> { + iCalObject.organizer = Organizer().apply { + this.caladdress = prop.calAddress.toString() + this.cn = prop.parameters?.getParameter<Cn>(Parameter.CN)?.value + this.dir = prop.parameters?.getParameter<Dir>(Parameter.DIR)?.value + this.language = prop.parameters?.getParameter<Language>(Parameter.LANGUAGE)?.value + this.sentby = prop.parameters?.getParameter<SentBy>(Parameter.SENT_BY)?.value + + // remove all known parameters so that only unknown parameters remain + prop.parameters?.removeAll(Parameter.CN) + prop.parameters?.removeAll(Parameter.DIR) + prop.parameters?.removeAll(Parameter.LANGUAGE) + prop.parameters?.removeAll(Parameter.SENT_BY) + + // save unknown parameters in the other field + this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + } + } + + is Uid -> iCalObject.uid = prop.value + //is Uid, + is ProdId, is DtStamp -> { + } /* don't save these as unknown properties */ + else -> { + if(prop.name == X_PROP_COMPLETEDTIMEZONE) + iCalObject.completedTimezone = prop.value + else + iCalObject.unknown.add(Unknown(value = UnknownProperty.toJsonString(prop))) // save the whole property for unknown properties + } + } + } + + + // There seem to be many invalid tasks out there because of some defect clients, do some validation. + val dtStartTZ = iCalObject.dtstartTimezone + val dueTZ = iCalObject.dueTimezone + + if (dtStartTZ != null && dueTZ != null) { + if (dtStartTZ == TZ_ALLDAY && dueTZ != TZ_ALLDAY) { + Ical4Android.log.warning("DTSTART is DATE but DUE is DATE-TIME, rewriting DTSTART to DATE-TIME") + iCalObject.dtstartTimezone = dueTZ + } else if (dtStartTZ != TZ_ALLDAY && dueTZ == TZ_ALLDAY) { + Ical4Android.log.warning("DTSTART is DATE-TIME but DUE is DATE, rewriting DUE to DATE-TIME") + iCalObject.dueTimezone = dtStartTZ + } + + if ( iCalObject.dtstart != null && iCalObject.due != null && iCalObject.due!! <= iCalObject.dtstart!!) { + Ical4Android.log.warning("Found invalid DUE <= DTSTART; dropping DUE") // Dtstart must not be dropped as it might be the basis for recurring tasks + iCalObject.due = null + iCalObject.dueTimezone = null + } + } + + if (iCalObject.duration != null && iCalObject.dtstart == null) { + Ical4Android.log.warning("Found DURATION without DTSTART; ignoring") + iCalObject.duration = null + } + } + } + + /** + * Takes the current JtxICalObject, transforms it to an iCalendar and writes it in an OutputStream + * @param [os] OutputStream where iCalendar should be written to + */ + @UsesThreadContextClassLoader + fun write(os: OutputStream) { + Ical4Android.checkThreadContextClassLoader() + + val ical = Calendar() + ical.properties += Version.VERSION_2_0 + ical.properties += ICalendar.prodId + + val calComponent = when (component) { + JtxContract.JtxICalObject.Component.VTODO.name -> VToDo(true /* generates DTSTAMP */) + JtxContract.JtxICalObject.Component.VJOURNAL.name -> VJournal(true /* generates DTSTAMP */) + else -> return + } + ical.components += calComponent + addProperties(calComponent.properties) + + alarms.forEach { alarm -> + + val vAlarm = VAlarm() + vAlarm.properties.apply { + alarm.action?.let { + when (it) { + JtxContract.JtxAlarm.AlarmAction.DISPLAY.name -> add(Action.DISPLAY) + JtxContract.JtxAlarm.AlarmAction.AUDIO.name -> add(Action.AUDIO) + JtxContract.JtxAlarm.AlarmAction.EMAIL.name -> add(Action.EMAIL) + else -> return@let + } + } + if(alarm.triggerRelativeDuration != null) { + add(Trigger().apply { + try { + val dur = java.time.Duration.parse(alarm.triggerRelativeDuration) + this.duration = dur + + // Add the RELATED parameter if present + alarm.triggerRelativeTo?.let { + if(it == JtxContract.JtxAlarm.AlarmRelativeTo.START.name) + this.parameters.add(Related.START) + if(it == JtxContract.JtxAlarm.AlarmRelativeTo.END.name) + this.parameters.add(Related.END) + } + } catch (e: DateTimeParseException) { + Ical4Android.log.log(Level.WARNING, "Could not parse Trigger duration as Duration.", e) + } + }) + + } else if (alarm.triggerTime != null) { + add(Trigger().apply { + try { + when { + alarm.triggerTimezone == TimeZone.getTimeZone("UTC").id -> this.dateTime = DateTime(alarm.triggerTime!!).apply { + this.isUtc = true + } + alarm.triggerTimezone.isNullOrEmpty() -> this.dateTime = DateTime(alarm.triggerTime!!).apply { + this.isUtc = true + } + else -> { + val timezone = TimeZoneRegistryFactory.getInstance().createRegistry() + .getTimeZone(alarm.triggerTimezone) + this.dateTime = DateTime(alarm.triggerTime!!).apply{ + this.timeZone = timezone + } + } + } + } catch (e: ParseException) { + Ical4Android.log.log(Level.WARNING, "TriggerTime could not be parsed.", e) + }}) + } + alarm.summary?.let { add(Summary(it)) } + alarm.repeat?.let { add(Repeat().apply { value = it }) } + alarm.duration?.let { add(Duration().apply { + try { + val dur = java.time.Duration.parse(it) + this.duration = dur + } catch (e: DateTimeParseException) { + Ical4Android.log.log(Level.WARNING, "Could not parse duration as Duration.", e) + } + }) } + alarm.description?.let { add(Description(it)) } + alarm.attach?.let { add(Attach().apply { value = it }) } + alarm.other?.let { addAll(JtxContract.getXPropertyListFromJson(it)) } + + } + calComponent.components.add(vAlarm) + } + + ICalendar.softValidate(ical) + CalendarOutputter(false).output(ical, os) + } + + /** + * This function maps the current JtxICalObject to a iCalendar property list + * @param [props] The PropertyList where the properties should be added + */ + private fun addProperties(props: PropertyList<Property>) { + + uid.let { props += Uid(it) } + sequence.let { props += Sequence(it.toInt()) } + + created.let { props += Created(DateTime(it).apply { + this.isUtc = true + }) } + lastModified.let { props += LastModified(DateTime(it).apply { + this.isUtc = true + }) } + + summary.let { props += Summary(it) } + description?.let { props += Description(it) } + + location?.let { location -> + val loc = Location(location) + locationAltrep?.let { locationAltrep -> + loc.parameters.add(AltRep(locationAltrep)) + } + props += loc + } + if (geoLat != null && geoLong != null) { + props += Geo(geoLat!!.toBigDecimal(), geoLong!!.toBigDecimal()) + } + color?.let { props += Color(null, Css3Color.nearestMatch(it).name) } + url?.let { + try { + props += Url(URI(it)) + } catch (e: URISyntaxException) { + Ical4Android.log.log(Level.WARNING, "Ignoring invalid task URL: $url", e) + } + } + //organizer?.let { props += it } + + + classification?.let { props += Clazz(it) } + status?.let { props += Status(it) } + + + val categoryTextList = TextList() + categories.forEach { + categoryTextList.add(it.text) + } + if (!categoryTextList.isEmpty) + props += Categories(categoryTextList) + + + val resourceTextList = TextList() + resources.forEach { + resourceTextList.add(it.text) + } + if (!resourceTextList.isEmpty) + props += Resources(resourceTextList) + + + comments.forEach { comment -> + val c = Comment(comment.text).apply { + comment.altrep?.let { this.parameters.add(AltRep(it)) } + comment.language?.let { this.parameters.add(Language(it)) } + comment.other?.let { + val xparams = JtxContract.getXParametersFromJson(it) + xparams.forEach { xparam -> + this.parameters.add(xparam) + } + } + } + props += c + } + + + attendees.forEach { attendee -> + val attendeeProp = net.fortuna.ical4j.model.property.Attendee().apply { + this.calAddress = URI(attendee.caladdress) + + attendee.cn?.let { + this.parameters.add(Cn(it)) + } + attendee.cutype?.let { + when { + it.equals(CuType.INDIVIDUAL.value, ignoreCase = true) -> this.parameters.add(CuType.INDIVIDUAL) + it.equals(CuType.GROUP.value, ignoreCase = true) -> this.parameters.add(CuType.GROUP) + it.equals(CuType.ROOM.value, ignoreCase = true) -> this.parameters.add(CuType.ROOM) + it.equals(CuType.RESOURCE.value, ignoreCase = true) -> this.parameters.add(CuType.RESOURCE) + it.equals(CuType.UNKNOWN.value, ignoreCase = true) -> this.parameters.add(CuType.UNKNOWN) + else -> this.parameters.add(CuType.UNKNOWN) + } + } + attendee.delegatedfrom?.let { + this.parameters.add(DelegatedFrom(it)) + } + attendee.delegatedto?.let { + this.parameters.add(DelegatedTo(it)) + } + attendee.dir?.let { + this.parameters.add(Dir(it)) + } + attendee.language?.let { + this.parameters.add(Language(it)) + } + attendee.member?.let { + this.parameters.add(Member(it)) + } + attendee.partstat?.let { + this.parameters.add(PartStat(it)) + } + attendee.role?.let { + this.parameters.add(Role(it)) + } + attendee.rsvp?.let { + this.parameters.add(Rsvp(it)) + } + attendee.sentby?.let { + this.parameters.add(SentBy(it)) + } + attendee.other?.let { + val params = JtxContract.getXParametersFromJson(it) + params.forEach { xparam -> + this.parameters.add(xparam) + } + } + } + props += attendeeProp + } + + organizer?.let { organizer -> + val organizerProp = net.fortuna.ical4j.model.property.Organizer().apply { + if(organizer.caladdress?.isNotEmpty() == true) + this.calAddress = URI(organizer.caladdress) + + organizer.cn?.let { + this.parameters.add(Cn(it)) + } + organizer.dir?.let { + this.parameters.add(Dir(it)) + } + organizer.language?.let { + this.parameters.add(Language(it)) + } + organizer.sentby?.let { + this.parameters.add(SentBy(it)) + } + organizer.other?.let { + val params = JtxContract.getXParametersFromJson(it) + params.forEach { xparam -> + this.parameters.add(xparam) + } + } + } + props += organizerProp + } + + attachments.forEach { attachment -> + + try { + if (attachment.uri?.startsWith("content://") == true) { + + val attachmentUri = ContentUris.withAppendedId(JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(collection.account), attachment.attachmentId) + val attachmentFile = collection.client.openFile(attachmentUri, "r") + val attachmentBytes = ParcelFileDescriptor.AutoCloseInputStream(attachmentFile).readBytes() + if(attachmentBytes.size <= MAX_ATTACHMENT_SYNC_SIZE) { // Sync only small attachments that are smaller than 100 KB, larger ones are ignored + val att = Attach(attachmentBytes).apply { + attachment.fmttype?.let { this.parameters.add(FmtType(it)) } + } + props += att + } + + } else { + attachment.uri?.let { uri -> + val att = Attach(URI(uri)).apply { + attachment.fmttype?.let { this.parameters.add(FmtType(it)) } + } + props += att + } + } + } catch (e: FileNotFoundException) { + Ical4Android.log.log(Level.WARNING, "File not found at the given Uri: ${attachment.uri}", e) + } catch (e: NullPointerException) { + Ical4Android.log.log(Level.WARNING, "Provided Uri was empty: ${attachment.uri}", e) + } catch (e: IllegalArgumentException) { + Ical4Android.log.log(Level.WARNING, "Uri could not be parsed: ${attachment.uri}", e) + } + } + + unknown.forEach { + it.value?.let { jsonString -> + props.add(UnknownProperty.fromJsonString(jsonString)) + } + } + + relatedTo.forEach { + val param: Parameter = + when (it.reltype) { + RelType.CHILD.value -> RelType.CHILD + RelType.SIBLING.value -> RelType.SIBLING + RelType.PARENT.value -> RelType.PARENT + else -> return@forEach + } + val parameterList = ParameterList() + parameterList.add(param) + props += RelatedTo(parameterList, it.text) + } + + dtstart?.let { + when { + dtstartTimezone == TZ_ALLDAY -> props += DtStart(Date(it)) + dtstartTimezone == TimeZone.getTimeZone("UTC").id -> props += DtStart(DateTime(it).apply { + this.isUtc = true + }) + dtstartTimezone.isNullOrEmpty() -> props += DtStart(DateTime(it).apply { + this.isUtc = false + }) + else -> { + val timezone = TimeZoneRegistryFactory.getInstance().createRegistry() + .getTimeZone(dtstartTimezone) + val withTimezone = DtStart(DateTime(it)) + withTimezone.timeZone = timezone + props += withTimezone + } + } + } + + rrule?.let { rrule -> + props += RRule(rrule) + } + recurid?.let { recurid -> + props += RecurrenceId(recurid) + } + + rdate?.let { rdateString -> + + when { + dtstartTimezone == TZ_ALLDAY -> { + val dateListDate = DateList(Value.DATE) + JtxContract.getLongListFromString(rdateString).forEach { + dateListDate.add(Date(it)) + } + props += RDate(dateListDate) + + } + dtstartTimezone == TimeZone.getTimeZone("UTC").id -> { + val dateListDateTime = DateList(Value.DATE_TIME) + JtxContract.getLongListFromString(rdateString).forEach { + dateListDateTime.add(DateTime(it).apply { + this.isUtc = true + }) + } + props += RDate(dateListDateTime) + } + dtstartTimezone.isNullOrEmpty() -> { + val dateListDateTime = DateList(Value.DATE_TIME) + JtxContract.getLongListFromString(rdateString).forEach { + dateListDateTime.add(DateTime(it).apply { + this.isUtc = false + }) + } + props += RDate(dateListDateTime) + } + else -> { + val dateListDateTime = DateList(Value.DATE_TIME) + val timezone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(dtstartTimezone) + JtxContract.getLongListFromString(rdateString).forEach { + val withTimezone = DateTime(it) + withTimezone.timeZone = timezone + dateListDateTime.add(DateTime(withTimezone)) + } + props += RDate(dateListDateTime) + } + } + } + + exdate?.let { exdateString -> + + when { + dtstartTimezone == TZ_ALLDAY -> { + val dateListDate = DateList(Value.DATE) + JtxContract.getLongListFromString(exdateString).forEach { + dateListDate.add(Date(it)) + } + props += ExDate(dateListDate) + + } + dtstartTimezone == TimeZone.getTimeZone("UTC").id -> { + val dateListDateTime = DateList(Value.DATE_TIME) + JtxContract.getLongListFromString(exdateString).forEach { + dateListDateTime.add(DateTime(it).apply { + this.isUtc = true + }) + } + props += ExDate(dateListDateTime) + } + dtstartTimezone.isNullOrEmpty() -> { + val dateListDateTime = DateList(Value.DATE_TIME) + JtxContract.getLongListFromString(exdateString).forEach { + dateListDateTime.add(DateTime(it).apply { + this.isUtc = false + }) + } + props += ExDate(dateListDateTime) + } + else -> { + val dateListDateTime = DateList(Value.DATE_TIME) + val timezone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(dtstartTimezone) + JtxContract.getLongListFromString(exdateString).forEach { + val withTimezone = DateTime(it) + withTimezone.timeZone = timezone + dateListDateTime.add(DateTime(withTimezone)) + } + props += ExDate(dateListDateTime) + } + } + } + + duration?.let { + val dur = Duration() + dur.value = it + props += dur + } + + + /* +// remember used time zones +val usedTimeZones = HashSet<TimeZone>() +duration?.let(props::add) +*/ + + + if(component == JtxContract.JtxICalObject.Component.VTODO.name) { + completed?.let { + //Completed is defines as always DateTime! And is always UTC!? + + props += Completed(DateTime(it)) + } + completedTimezone?.let { + props += XProperty(X_PROP_COMPLETEDTIMEZONE, it) + } + percent?.let { + props += PercentComplete(it) + } + + + if (priority != null && priority != Priority.UNDEFINED.level) + priority?.let { + props += Priority(it) + } + else { + props += Priority(Priority.UNDEFINED.level) + } + + due?.let { + when { + dueTimezone == TZ_ALLDAY -> props += Due(Date(it)) + dueTimezone == TimeZone.getTimeZone("UTC").id -> props += Due(DateTime(it).apply { + this.isUtc = true + }) + dueTimezone.isNullOrEmpty() -> props += Due(DateTime(it).apply { + this.isUtc = false + }) + else -> { + val timezone = TimeZoneRegistryFactory.getInstance().createRegistry() + .getTimeZone(dueTimezone) + val withTimezone = Due(DateTime(it)) + withTimezone.timeZone = timezone + props += withTimezone + } + } + } + } + + /* + + // determine earliest referenced date + val earliest = arrayOf( + dtStart?.date, + due?.date, + completedAt?.date + ).filterNotNull().min() + // add VTIMEZONE components + for (tz in usedTimeZones) + ical.components += ICalendar.minifyVTimeZone(tz.vTimeZone, earliest) +*/ + } + + + fun prepareForUpload(): String { + return "${this.uid}.ics" + } + + /** + * Updates the fileName, eTag and scheduleTag of the current JtxICalObject + */ + fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) { + + var updateUri = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) + updateUri = Uri.withAppendedPath(updateUri, this.id.toString()) + + val values = ContentValues() + fileName?.let { values.put(JtxContract.JtxICalObject.FILENAME, fileName) } + eTag?.let { values.put(JtxContract.JtxICalObject.ETAG, eTag) } + scheduleTag?.let { values.put(JtxContract.JtxICalObject.SCHEDULETAG, scheduleTag) } + values.put(JtxContract.JtxICalObject.DIRTY, false) + + collection.client.update(updateUri, values, null, null) + } + + /** + * Updates the flags of the current JtxICalObject + * @param [flags] to be set as [Int] + */ + fun updateFlags(flags: Int) { + + var updateUri = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) + updateUri = Uri.withAppendedPath(updateUri, this.id.toString()) + + val values = ContentValues() + values.put(JtxContract.JtxICalObject.FLAGS, flags) + + collection.client.update(updateUri, values, null, null) + } + + /** + * adds the current JtxICalObject in the jtx DB through the provider + * @return the Content [Uri] of the inserted object + */ + fun add(): Uri { + val values = this.toContentValues() + + val newUri = collection.client.insert( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account), + values + ) ?: return Uri.EMPTY + this.id = newUri.lastPathSegment?.toLong() ?: return Uri.EMPTY + + insertOrUpdateListProperties(false) + + return newUri + } + + /** + * Updates the current JtxICalObject with the given data + * @param [data] The JtxICalObject with the information that should be applied to this object and updated in the provider + * @return [Uri] of the updated entry + */ + fun update(data: JtxICalObject): Uri { + + this.applyNewData(data) + val values = this.toContentValues() + + var updateUri = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) + updateUri = Uri.withAppendedPath(updateUri, this.id.toString()) + collection.client.update( + updateUri, + values, + "${JtxContract.JtxICalObject.ID} = ?", + arrayOf(this.id.toString()) + ) + + insertOrUpdateListProperties(true) + + return updateUri + } + + + /** + * This function takes care of all list properties and inserts them in the DB through the provider + * @param isUpdate if true then the list properties are deleted through the provider before they are inserted + */ + private fun insertOrUpdateListProperties(isUpdate: Boolean) { + + // delete the categories, attendees, ... and insert them again after. Only relevant for Update, for an insert there will be no entries + if (isUpdate) { + collection.client.delete( + JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxCategory.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxComment.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxResource.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxRelatedto.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxAttendee.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxOrganizer.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxOrganizer.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxAttachment.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxAlarm.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + + collection.client.delete( + JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(collection.account), + "${JtxContract.JtxUnknown.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()) + ) + } + + this.categories.forEach { category -> + val categoryContentValues = ContentValues().apply { + put(JtxContract.JtxCategory.ICALOBJECT_ID, id) + put(JtxContract.JtxCategory.TEXT, category.text) + put(JtxContract.JtxCategory.ID, category.categoryId) + put(JtxContract.JtxCategory.LANGUAGE, category.language) + put(JtxContract.JtxCategory.OTHER, category.other) + } + collection.client.insert( + JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(collection.account), + categoryContentValues + ) + } + + this.comments.forEach { comment -> + val commentContentValues = ContentValues().apply { + put(JtxContract.JtxComment.ICALOBJECT_ID, id) + put(JtxContract.JtxComment.ID, comment.commentId) + put(JtxContract.JtxComment.TEXT, comment.text) + put(JtxContract.JtxComment.LANGUAGE, comment.language) + put(JtxContract.JtxComment.OTHER, comment.other) + } + collection.client.insert( + JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(collection.account), + commentContentValues + ) + } + + + this.resources.forEach { resource -> + val resourceContentValues = ContentValues().apply { + put(JtxContract.JtxResource.ICALOBJECT_ID, id) + put(JtxContract.JtxResource.ID, resource.resourceId) + put(JtxContract.JtxResource.TEXT, resource.text) + put(JtxContract.JtxResource.LANGUAGE, resource.language) + put(JtxContract.JtxResource.OTHER, resource.other) + } + collection.client.insert( + JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(collection.account), + resourceContentValues + ) + } + + + this.relatedTo.forEach { related -> + val relatedToContentValues = ContentValues().apply { + put(JtxContract.JtxRelatedto.ICALOBJECT_ID, id) + put(JtxContract.JtxRelatedto.TEXT, related.text) + put(JtxContract.JtxRelatedto.RELTYPE, related.reltype) + put(JtxContract.JtxRelatedto.OTHER, related.other) + } + collection.client.insert( + JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(collection.account), + relatedToContentValues + ) + } + + this.attendees.forEach { attendee -> + val attendeeContentValues = ContentValues().apply { + put(JtxContract.JtxAttendee.ICALOBJECT_ID, id) + put(JtxContract.JtxAttendee.CALADDRESS, attendee.caladdress) + put(JtxContract.JtxAttendee.CN, attendee.cn) + put(JtxContract.JtxAttendee.CUTYPE, attendee.cutype) + put(JtxContract.JtxAttendee.DELEGATEDFROM, attendee.delegatedfrom) + put(JtxContract.JtxAttendee.DELEGATEDTO, attendee.delegatedto) + put(JtxContract.JtxAttendee.DIR, attendee.dir) + put(JtxContract.JtxAttendee.LANGUAGE, attendee.language) + put(JtxContract.JtxAttendee.MEMBER, attendee.member) + put(JtxContract.JtxAttendee.PARTSTAT, attendee.partstat) + put(JtxContract.JtxAttendee.ROLE, attendee.role) + put(JtxContract.JtxAttendee.RSVP, attendee.rsvp) + put(JtxContract.JtxAttendee.SENTBY, attendee.sentby) + put(JtxContract.JtxAttendee.OTHER, attendee.other) + } + collection.client.insert( + JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter( + collection.account + ), attendeeContentValues + ) + } + + this.organizer.let { organizer -> + val organizerContentValues = ContentValues().apply { + put(JtxContract.JtxOrganizer.ICALOBJECT_ID, id) + put(JtxContract.JtxOrganizer.CALADDRESS, organizer?.caladdress) + + put(JtxContract.JtxOrganizer.CN, organizer?.cn) + put(JtxContract.JtxOrganizer.DIR, organizer?.dir) + put(JtxContract.JtxOrganizer.LANGUAGE, organizer?.language) + put(JtxContract.JtxOrganizer.SENTBY, organizer?.sentby) + put(JtxContract.JtxOrganizer.OTHER, organizer?.other) + } + collection.client.insert( + JtxContract.JtxOrganizer.CONTENT_URI.asSyncAdapter( + collection.account + ), organizerContentValues + ) + } + + this.attachments.forEach { attachment -> + val attachmentContentValues = ContentValues().apply { + put(JtxContract.JtxAttachment.ICALOBJECT_ID, id) + put(JtxContract.JtxAttachment.URI, attachment.uri) + put(JtxContract.JtxAttachment.FMTTYPE, attachment.fmttype) + put(JtxContract.JtxAttachment.OTHER, attachment.other) + } + val newAttachment = collection.client.insert( + JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter( + collection.account + ), attachmentContentValues + ) + if(attachment.uri.isNullOrEmpty() && newAttachment != null) { + val attachmentPFD = collection.client.openFile(newAttachment, "w") + ParcelFileDescriptor.AutoCloseOutputStream(attachmentPFD).write(Base64.decode(attachment.binary, Base64.DEFAULT)) + } + } + + this.alarms.forEach { alarm -> + val alarmContentValues = ContentValues().apply { + put(JtxContract.JtxAlarm.ICALOBJECT_ID, id) + put(JtxContract.JtxAlarm.ACTION, alarm.action) + put(JtxContract.JtxAlarm.ATTACH, alarm.attach) + //put(JtxContract.JtxAlarm.ATTENDEE, alarm.attendee) + put(JtxContract.JtxAlarm.DESCRIPTION, alarm.description) + put(JtxContract.JtxAlarm.DURATION, alarm.duration) + put(JtxContract.JtxAlarm.REPEAT, alarm.repeat) + put(JtxContract.JtxAlarm.SUMMARY, alarm.summary) + put(JtxContract.JtxAlarm.TRIGGER_RELATIVE_TO, alarm.triggerRelativeTo) + put(JtxContract.JtxAlarm.TRIGGER_RELATIVE_DURATION, alarm.triggerRelativeDuration) + put(JtxContract.JtxAlarm.TRIGGER_TIME, alarm.triggerTime) + put(JtxContract.JtxAlarm.TRIGGER_TIMEZONE, alarm.triggerTimezone) + put(JtxContract.JtxAlarm.OTHER, alarm.other) + } + collection.client.insert( + JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(collection.account), + alarmContentValues + ) + } + + this.unknown.forEach { unknown -> + val unknownContentValues = ContentValues().apply { + put(JtxContract.JtxUnknown.ICALOBJECT_ID, id) + put(JtxContract.JtxUnknown.UNKNOWN_VALUE, unknown.value) + } + collection.client.insert( + JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(collection.account), + unknownContentValues + ) + } + } + + /** + * Deletes the current JtxICalObject + * @return The number of deleted records (should always be 1) + */ + fun delete(): Int { + val uri = Uri.withAppendedPath( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account), + id.toString() + ) + return collection.client.delete(uri, null, null) + } + + + /** + * This function is used for empty JtxICalObjects that need new data applied, usually a LocalJtxICalObject. + * @param [newData], the (local) JtxICalObject that should be mapped onto the given JtxICalObject + */ + fun applyNewData(newData: JtxICalObject) { + + this.component = newData.component + this.sequence = newData.sequence + this.created = newData.created + this.lastModified = newData.lastModified + this.summary = newData.summary + this.description = newData.description + this.uid = newData.uid + + this.location = newData.location + this.locationAltrep = newData.locationAltrep + this.geoLat = newData.geoLat + this.geoLong = newData.geoLong + this.percent = newData.percent + this.classification = newData.classification + this.status = newData.status + this.priority = newData.priority + + this.dtstart = newData.dtstart + this.dtstartTimezone = newData.dtstartTimezone + this.dtend = newData.dtend + this.dtendTimezone = newData.dtendTimezone + this.completed = newData.completed + this.completedTimezone = newData.completedTimezone + this.due = newData.due + this.dueTimezone = newData.dueTimezone + this.duration = newData.duration + + this.rrule = newData.rrule + this.rdate = newData.rdate + this.exdate = newData.exdate + this.recurid = newData.recurid + + + this.categories = newData.categories + this.comments = newData.comments + this.resources = newData.resources + this.relatedTo = newData.relatedTo + this.attendees = newData.attendees + this.organizer = newData.organizer + this.attachments = newData.attachments + this.alarms = newData.alarms + this.unknown = newData.unknown + } + + /** + * Takes Content Values, applies them on the current JtxICalObject and retrieves all further list properties from the content provier and adds them. + * @param [values] The Content Values with the information about the JtxICalObject + */ + fun populateFromContentValues(values: ContentValues) { + + values.getAsLong(JtxContract.JtxICalObject.ID)?.let { id -> this.id = id } + + values.getAsString(JtxContract.JtxICalObject.COMPONENT)?.let { component -> this.component = component } + values.getAsString(JtxContract.JtxICalObject.SUMMARY)?.let { summary -> this.summary = summary } + values.getAsString(JtxContract.JtxICalObject.DESCRIPTION)?.let { description -> this.description = description } + values.getAsLong(JtxContract.JtxICalObject.DTSTART)?.let { dtstart -> this.dtstart = dtstart } + values.getAsString(JtxContract.JtxICalObject.DTSTART_TIMEZONE)?.let { dtstartTimezone -> this.dtstartTimezone = dtstartTimezone } + values.getAsLong(JtxContract.JtxICalObject.DTEND)?.let { dtend -> this.dtend = dtend } + values.getAsString(JtxContract.JtxICalObject.DTEND_TIMEZONE)?.let { dtendTimezone -> this.dtendTimezone = dtendTimezone } + values.getAsString(JtxContract.JtxICalObject.STATUS)?.let { status -> this.status = status } + values.getAsString(JtxContract.JtxICalObject.CLASSIFICATION)?.let { classification -> this.classification = classification } + values.getAsString(JtxContract.JtxICalObject.URL)?.let { url -> this.url = url } + values.getAsDouble(JtxContract.JtxICalObject.GEO_LAT)?.let { geoLat -> this.geoLat = geoLat } + values.getAsDouble(JtxContract.JtxICalObject.GEO_LONG)?.let { geoLong -> this.geoLong = geoLong } + values.getAsString(JtxContract.JtxICalObject.LOCATION)?.let { location -> this.location = location } + values.getAsString(JtxContract.JtxICalObject.LOCATION_ALTREP)?.let { locationAltrep -> this.locationAltrep = locationAltrep } + values.getAsInteger(JtxContract.JtxICalObject.PERCENT)?.let { percent -> this.percent = percent } + values.getAsInteger(JtxContract.JtxICalObject.PRIORITY)?.let { priority -> this.priority = priority } + values.getAsLong(JtxContract.JtxICalObject.DUE)?.let { due -> this.due = due } + values.getAsString(JtxContract.JtxICalObject.DUE_TIMEZONE)?.let { dueTimezone -> this.dueTimezone = dueTimezone } + values.getAsLong(JtxContract.JtxICalObject.COMPLETED)?.let { completed -> this.completed = completed } + values.getAsString(JtxContract.JtxICalObject.COMPLETED_TIMEZONE)?.let { completedTimezone -> this.completedTimezone = completedTimezone } + values.getAsString(JtxContract.JtxICalObject.DURATION)?.let { duration -> this.duration = duration } + values.getAsString(JtxContract.JtxICalObject.UID)?.let { uid -> this.uid = uid } + values.getAsLong(JtxContract.JtxICalObject.CREATED)?.let { created -> this.created = created } + values.getAsLong(JtxContract.JtxICalObject.DTSTAMP)?.let { dtstamp -> this.dtstamp = dtstamp } + values.getAsLong(JtxContract.JtxICalObject.LAST_MODIFIED)?.let { lastModified -> this.lastModified = lastModified } + values.getAsLong(JtxContract.JtxICalObject.SEQUENCE)?.let { sequence -> this.sequence = sequence } + values.getAsInteger(JtxContract.JtxICalObject.COLOR)?.let { color -> this.color = color } + + values.getAsString(JtxContract.JtxICalObject.RRULE)?.let { rrule -> this.rrule = rrule } + values.getAsString(JtxContract.JtxICalObject.EXDATE)?.let { exdate -> this.exdate = exdate } + values.getAsString(JtxContract.JtxICalObject.RDATE)?.let { rdate -> this.rdate = rdate } + values.getAsString(JtxContract.JtxICalObject.RECURID)?.let { recurid -> this.recurid = recurid } + + this.collectionId = collection.id + values.getAsBoolean(JtxContract.JtxICalObject.DIRTY)?.let { dirty -> this.dirty = dirty } + values.getAsBoolean(JtxContract.JtxICalObject.DELETED)?.let { deleted -> this.deleted = deleted } + + values.getAsString(JtxContract.JtxICalObject.FILENAME)?.let { fileName -> this.fileName = fileName } + values.getAsString(JtxContract.JtxICalObject.ETAG)?.let { eTag -> this.eTag = eTag } + values.getAsString(JtxContract.JtxICalObject.SCHEDULETAG)?.let { scheduleTag -> this.scheduleTag = scheduleTag } + values.getAsInteger(JtxContract.JtxICalObject.FLAGS)?.let { flags -> this.flags = flags } + + + // Take care of categories + val categoriesContentValues = getCategoryContentValues() + categoriesContentValues.forEach { catValues -> + val category = Category().apply { + catValues.getAsLong(JtxContract.JtxCategory.ID)?.let { id -> this.categoryId = id } + catValues.getAsString(JtxContract.JtxCategory.TEXT)?.let { text -> this.text = text } + catValues.getAsString(JtxContract.JtxCategory.LANGUAGE)?.let { language -> this.language = language } + catValues.getAsString(JtxContract.JtxCategory.OTHER)?.let { other -> this.other = other } + } + categories.add(category) + } + + // Take care of comments + val commentsContentValues = getCommentContentValues() + commentsContentValues.forEach { commentValues -> + val comment = Comment().apply { + commentValues.getAsLong(JtxContract.JtxComment.ID)?.let { id -> this.commentId = id } + commentValues.getAsString(JtxContract.JtxComment.TEXT)?.let { text -> this.text = text } + commentValues.getAsString(JtxContract.JtxComment.LANGUAGE)?.let { language -> this.language = language } + commentValues.getAsString(JtxContract.JtxComment.OTHER)?.let { other -> this.other = other } + } + comments.add(comment) + } + + // Take care of resources + val resourceContentValues = getResourceContentValues() + resourceContentValues.forEach { resourceValues -> + val resource = Resource().apply { + resourceValues.getAsLong(JtxContract.JtxResource.ID)?.let { id -> this.resourceId = id } + resourceValues.getAsString(JtxContract.JtxResource.TEXT)?.let { text -> this.text = text } + resourceValues.getAsString(JtxContract.JtxResource.LANGUAGE)?.let { language -> this.language = language } + resourceValues.getAsString(JtxContract.JtxResource.OTHER)?.let { other -> this.other = other } + } + resources.add(resource) + } + + + // Take care of related-to + val relatedToContentValues = getRelatedToContentValues() + relatedToContentValues.forEach { relatedToValues -> + val relTo = RelatedTo().apply { + relatedToValues.getAsLong(JtxContract.JtxRelatedto.ID)?.let { id -> this.relatedtoId = id } + relatedToValues.getAsString(JtxContract.JtxRelatedto.TEXT)?.let { text -> this.text = text } + relatedToValues.getAsString(JtxContract.JtxRelatedto.RELTYPE)?.let { reltype -> this.reltype = reltype } + relatedToValues.getAsString(JtxContract.JtxRelatedto.OTHER)?.let { other -> this.other = other } + + } + relatedTo.add(relTo) + } + + + // Take care of attendees + val attendeeContentValues = getAttendeesContentValues() + attendeeContentValues.forEach { attendeeValues -> + val attendee = Attendee().apply { + attendeeValues.getAsLong(JtxContract.JtxAttendee.ID)?.let { id -> this.attendeeId = id } + attendeeValues.getAsString(JtxContract.JtxAttendee.CALADDRESS)?.let { caladdress -> this.caladdress = caladdress } + attendeeValues.getAsString(JtxContract.JtxAttendee.CUTYPE)?.let { cutype -> this.cutype = cutype } + attendeeValues.getAsString(JtxContract.JtxAttendee.MEMBER)?.let { member -> this.member = member } + attendeeValues.getAsString(JtxContract.JtxAttendee.ROLE)?.let { role -> this.role = role } + attendeeValues.getAsString(JtxContract.JtxAttendee.PARTSTAT)?.let { partstat -> this.partstat = partstat } + attendeeValues.getAsBoolean(JtxContract.JtxAttendee.RSVP)?.let { rsvp -> this.rsvp = rsvp } + attendeeValues.getAsString(JtxContract.JtxAttendee.DELEGATEDTO)?.let { delto -> this.delegatedto = delto } + attendeeValues.getAsString(JtxContract.JtxAttendee.DELEGATEDFROM)?.let { delfrom -> this.delegatedfrom = delfrom } + attendeeValues.getAsString(JtxContract.JtxAttendee.SENTBY)?.let { sentby -> this.sentby = sentby } + attendeeValues.getAsString(JtxContract.JtxAttendee.CN)?.let { cn -> this.cn = cn } + attendeeValues.getAsString(JtxContract.JtxAttendee.DIR)?.let { dir -> this.dir = dir } + attendeeValues.getAsString(JtxContract.JtxAttendee.LANGUAGE)?.let { lang -> this.language = lang } + attendeeValues.getAsString(JtxContract.JtxAttendee.OTHER)?.let { other -> this.other = other } + + } + attendees.add(attendee) + } + + // Take care of organizer + val organizerContentValues = getOrganizerContentValues() + val orgnzr = Organizer().apply { + organizerId = organizerContentValues?.getAsLong(JtxContract.JtxOrganizer.ID) ?: 0L + caladdress = organizerContentValues?.getAsString(JtxContract.JtxOrganizer.CALADDRESS) + sentby = organizerContentValues?.getAsString(JtxContract.JtxOrganizer.SENTBY) + cn = organizerContentValues?.getAsString(JtxContract.JtxOrganizer.CN) + dir = organizerContentValues?.getAsString(JtxContract.JtxOrganizer.DIR) + language = organizerContentValues?.getAsString(JtxContract.JtxOrganizer.LANGUAGE) + other = organizerContentValues?.getAsString(JtxContract.JtxOrganizer.OTHER) + } + organizer = orgnzr + + // Take care of attachments + val attachmentContentValues = getAttachmentsContentValues() + attachmentContentValues.forEach { attachmentValues -> + val attachment = Attachment().apply { + attachmentValues.getAsLong(JtxContract.JtxAttachment.ID)?.let { id -> this.attachmentId = id } + attachmentValues.getAsString(JtxContract.JtxAttachment.URI)?.let { uri -> this.uri = uri } + attachmentValues.getAsString(JtxContract.JtxAttachment.BINARY)?.let { value -> this.binary = value } + attachmentValues.getAsString(JtxContract.JtxAttachment.FMTTYPE)?.let { fmttype -> this.fmttype = fmttype } + attachmentValues.getAsString(JtxContract.JtxAttachment.OTHER)?.let { other -> this.other = other } + + } + attachments.add(attachment) + } + + // Take care of alarms + val alarmContentValues = getAlarmsContentValues() + alarmContentValues.forEach { alarmValues -> + val alarm = Alarm().apply { + alarmValues.getAsLong(JtxContract.JtxAlarm.ID)?.let { id -> this.alarmId = id } + alarmValues.getAsString(JtxContract.JtxAlarm.ACTION)?.let { action -> this.action = action } + alarmValues.getAsString(JtxContract.JtxAlarm.DESCRIPTION)?.let { desc -> this.description = desc } + alarmValues.getAsLong(JtxContract.JtxAlarm.TRIGGER_TIME)?.let { time -> this.triggerTime = time } + alarmValues.getAsString(JtxContract.JtxAlarm.TRIGGER_TIMEZONE)?.let { tz -> this.triggerTimezone = tz } + alarmValues.getAsString(JtxContract.JtxAlarm.TRIGGER_RELATIVE_TO)?.let { relative -> this.triggerRelativeTo = relative } + alarmValues.getAsString(JtxContract.JtxAlarm.TRIGGER_RELATIVE_DURATION)?.let { duration -> this.triggerRelativeDuration = duration } + alarmValues.getAsString(JtxContract.JtxAlarm.SUMMARY)?.let { summary -> this.summary = summary } + alarmValues.getAsString(JtxContract.JtxAlarm.DURATION)?.let { dur -> this.duration = dur } + alarmValues.getAsString(JtxContract.JtxAlarm.REPEAT)?.let { repeat -> this.repeat = repeat } + alarmValues.getAsString(JtxContract.JtxAlarm.ATTACH)?.let { attach -> this.attach = attach } + alarmValues.getAsString(JtxContract.JtxAlarm.OTHER)?.let { other -> this.other = other } + } + alarms.add(alarm) + } + + // Take care of uknown properties + val unknownContentValues = getUnknownContentValues() + unknownContentValues.forEach { unknownValues -> + val unknwn = Unknown().apply { + unknownValues.getAsLong(JtxContract.JtxUnknown.ID)?.let { id -> this.unknownId = id } + unknownValues.getAsString(JtxContract.JtxUnknown.UNKNOWN_VALUE)?.let { value -> this.value = value } + } + unknown.add(unknwn) + } + } + + /** + * Puts the current JtxICalObjects attributes into Content Values + * @return The JtxICalObject attributes as [ContentValues] (exluding list properties) + */ + private fun toContentValues(): ContentValues { + + val values = ContentValues() + values.put(JtxContract.JtxICalObject.ID, id) + summary.let { values.put(JtxContract.JtxICalObject.SUMMARY, it) } + description.let { values.put(JtxContract.JtxICalObject.DESCRIPTION, it) } + values.put(JtxContract.JtxICalObject.COMPONENT, component) + status.let { values.put(JtxContract.JtxICalObject.STATUS, it) } + classification.let { values.put(JtxContract.JtxICalObject.CLASSIFICATION, it) } + priority.let { values.put(JtxContract.JtxICalObject.PRIORITY, it) } + values.put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collectionId) + values.put(JtxContract.JtxICalObject.UID, uid) + geoLat.let { values.put(JtxContract.JtxICalObject.GEO_LAT, it) } + geoLong.let { values.put(JtxContract.JtxICalObject.GEO_LONG, it) } + location.let { values.put(JtxContract.JtxICalObject.LOCATION, it) } + locationAltrep.let { values.put(JtxContract.JtxICalObject.LOCATION_ALTREP, it) } + percent.let { values.put(JtxContract.JtxICalObject.PERCENT, it) } + values.put(JtxContract.JtxICalObject.DTSTAMP, dtstamp) + dtstart.let { values.put(JtxContract.JtxICalObject.DTSTART, it) } + dtstartTimezone.let { values.put(JtxContract.JtxICalObject.DTSTART_TIMEZONE, it) } + dtend.let { values.put(JtxContract.JtxICalObject.DTEND, it) } + dtendTimezone.let { values.put(JtxContract.JtxICalObject.DTEND_TIMEZONE, it) } + completed.let { values.put(JtxContract.JtxICalObject.COMPLETED, it) } + completedTimezone.let { values.put(JtxContract.JtxICalObject.COMPLETED_TIMEZONE, it) } + due.let { values.put(JtxContract.JtxICalObject.DUE, it) } + dueTimezone.let { values.put(JtxContract.JtxICalObject.DUE_TIMEZONE, it) } + duration.let { values.put(JtxContract.JtxICalObject.DURATION, it) } + + rrule.let { values.put(JtxContract.JtxICalObject.RRULE, it) } + rdate.let { values.put(JtxContract.JtxICalObject.RDATE, it) } + exdate.let { values.put(JtxContract.JtxICalObject.EXDATE, it) } + recurid.let { values.put(JtxContract.JtxICalObject.RECURID, it) } + + fileName.let { values.put(JtxContract.JtxICalObject.FILENAME, it) } + eTag.let { values.put(JtxContract.JtxICalObject.ETAG, it) } + scheduleTag.let { values.put(JtxContract.JtxICalObject.SCHEDULETAG, it) } + values.put(JtxContract.JtxICalObject.FLAGS, flags) + + return values + } + + + /** + * @return The categories of the given JtxICalObject as a list of ContentValues + */ + private fun getCategoryContentValues(): List<ContentValues> { + + val categoryUrl = JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(collection.account) + val categoryValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + categoryUrl, + null, + "${JtxContract.JtxCategory.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + categoryValues.add(cursor.toValues()) + } + } + return categoryValues + } + + + /** + * @return The comments of the given JtxICalObject as a list of ContentValues + */ + private fun getCommentContentValues(): List<ContentValues> { + + val commentUrl = JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(collection.account) + val commentValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + commentUrl, + null, + "${JtxContract.JtxComment.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + commentValues.add(cursor.toValues()) + } + } + return commentValues + } + + /** + * @return The resources of the given JtxICalObject as a list of ContentValues + */ + private fun getResourceContentValues(): List<ContentValues> { + + val resourceUrl = JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(collection.account) + val resourceValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + resourceUrl, + null, + "${JtxContract.JtxResource.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + resourceValues.add(cursor.toValues()) + } + } + return resourceValues + } + + /** + * @return The RelatedTo of the given JtxICalObject as a list of ContentValues + */ + private fun getRelatedToContentValues(): List<ContentValues> { + + val relatedToUrl = JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(collection.account) + val relatedToValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + relatedToUrl, + null, + "${JtxContract.JtxRelatedto.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + relatedToValues.add(cursor.toValues()) + } + } + return relatedToValues + } + + /** + * @return The attendees of the given JtxICalObject as a list of ContentValues + */ + private fun getAttendeesContentValues(): List<ContentValues> { + + val attendeesUrl = JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter(collection.account) + val attendeesValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + attendeesUrl, + null, + "${JtxContract.JtxAttendee.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + attendeesValues.add(cursor.toValues()) + } + } + return attendeesValues + } + + /** + * @return The organizer of the given JtxICalObject as ContentValues + */ + private fun getOrganizerContentValues(): ContentValues? { + + val organizerUrl = JtxContract.JtxOrganizer.CONTENT_URI.asSyncAdapter(collection.account) + collection.client.query( + organizerUrl, + null, + "${JtxContract.JtxOrganizer.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + if(cursor.moveToFirst()) { + return cursor.toValues() + } + } + return null + } + + /** + * @return The attachments of the given JtxICalObject as a list of ContentValues + */ + private fun getAttachmentsContentValues(): List<ContentValues> { + + val attachmentsUrl = + JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(collection.account) + val attachmentsValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + attachmentsUrl, + null, + "${JtxContract.JtxAttachment.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + attachmentsValues.add(cursor.toValues()) + } + } + return attachmentsValues + } + + /** + * @return The alarms of the given JtxICalObject as a list of ContentValues + */ + private fun getAlarmsContentValues(): List<ContentValues> { + + val alarmsUrl = + JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(collection.account) + val alarmValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + alarmsUrl, + null, + "${JtxContract.JtxAlarm.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + alarmValues.add(cursor.toValues()) + } + } + return alarmValues + } + + /** + * @return The unknown properties of the given JtxICalObject as a list of ContentValues + */ + private fun getUnknownContentValues(): List<ContentValues> { + + val unknownUrl = + JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(collection.account) + val unknownValues: MutableList<ContentValues> = mutableListOf() + collection.client.query( + unknownUrl, + null, + "${JtxContract.JtxUnknown.ICALOBJECT_ID} = ?", + arrayOf(this.id.toString()), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + unknownValues.add(cursor.toValues()) + } + } + return unknownValues + } +} diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObjectFactory.kt b/src/main/java/at/bitfire/ical4android/JtxICalObjectFactory.kt new file mode 100644 index 0000000..8f6648c --- /dev/null +++ b/src/main/java/at/bitfire/ical4android/JtxICalObjectFactory.kt @@ -0,0 +1,13 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android + +import android.content.ContentValues + +interface JtxICalObjectFactory<out T: JtxICalObject> { + + fun fromProvider(collection: JtxCollection<JtxICalObject>, values: ContentValues): T + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/ical4android/MiscUtils.kt b/src/main/java/at/bitfire/ical4android/MiscUtils.kt index a9a91ec..2b0e57d 100644 --- a/src/main/java/at/bitfire/ical4android/MiscUtils.kt +++ b/src/main/java/at/bitfire/ical4android/MiscUtils.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/Task.kt b/src/main/java/at/bitfire/ical4android/Task.kt index 6e84a0f..0e70ca0 100644 --- a/src/main/java/at/bitfire/ical4android/Task.kt +++ b/src/main/java/at/bitfire/ical4android/Task.kt @@ -1,12 +1,6 @@ -/* - * Copyright © Ricki Hirner and contributors (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - * - * Contributors: Alex Baker - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/main/java/at/bitfire/ical4android/TaskProvider.kt b/src/main/java/at/bitfire/ical4android/TaskProvider.kt index da8dd38..108875f 100644 --- a/src/main/java/at/bitfire/ical4android/TaskProvider.kt +++ b/src/main/java/at/bitfire/ical4android/TaskProvider.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android @@ -35,7 +31,8 @@ class TaskProvider private constructor( private val writePermission: String ) { OpenTasks("org.dmfs.tasks", "org.dmfs.tasks", 103, "1.1.8.2", PERMISSION_OPENTASKS_READ, PERMISSION_OPENTASKS_WRITE), - TasksOrg("org.tasks.opentasks", "org.tasks", 100000, "10.0", PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE); + TasksOrg("org.tasks.opentasks", "org.tasks", 100000, "10.0", PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE), + JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 1, "0.1", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE); companion object { fun fromAuthority(authority: String): ProviderName { @@ -54,7 +51,8 @@ class TaskProvider private constructor( val TASK_PROVIDERS = listOf( ProviderName.OpenTasks, - ProviderName.TasksOrg + ProviderName.TasksOrg, + ProviderName.JtxBoard ) const val PERMISSION_OPENTASKS_READ = "org.dmfs.permission.READ_TASKS" @@ -65,6 +63,10 @@ class TaskProvider private constructor( const val PERMISSION_TASKS_ORG_WRITE = "org.tasks.permission.WRITE_TASKS" val PERMISSIONS_TASKS_ORG = arrayOf(PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE) + const val PERMISSION_JTX_READ = "at.techbee.jtx.permission.READ" + const val PERMISSION_JTX_WRITE = "at.techbee.jtx.permission.WRITE" + val PERMISSIONS_JTX = arrayOf(PERMISSION_JTX_READ, PERMISSION_JTX_WRITE) + /** * Acquires a content provider for a given task provider. The content provider will * be released when the TaskProvider is closed with [close]. diff --git a/src/main/java/at/bitfire/ical4android/UnknownProperty.kt b/src/main/java/at/bitfire/ical4android/UnknownProperty.kt index 2ffad4a..f1fba04 100644 --- a/src/main/java/at/bitfire/ical4android/UnknownProperty.kt +++ b/src/main/java/at/bitfire/ical4android/UnknownProperty.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import android.content.ContentResolver diff --git a/src/main/java/at/bitfire/ical4android/UsesThreadContextClassLoader.kt b/src/main/java/at/bitfire/ical4android/UsesThreadContextClassLoader.kt index 68f1093..8bc96ba 100644 --- a/src/main/java/at/bitfire/ical4android/UsesThreadContextClassLoader.kt +++ b/src/main/java/at/bitfire/ical4android/UsesThreadContextClassLoader.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android @Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) diff --git a/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt index 1a9c1b2..e0c8c11 100644 --- a/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + @file:Suppress("DEPRECATION") package at.bitfire.ical4android.util diff --git a/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt b/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt index af9e407..f9b1ac1 100644 --- a/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt +++ b/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android.util import at.bitfire.ical4android.DateUtils diff --git a/src/main/java/at/techbee/jtx/JtxContract.kt b/src/main/java/at/techbee/jtx/JtxContract.kt new file mode 100644 index 0000000..ce1f42e --- /dev/null +++ b/src/main/java/at/techbee/jtx/JtxContract.kt @@ -0,0 +1,1409 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.techbee.jtx + +import android.accounts.Account +import android.net.Uri +import android.provider.BaseColumns +import android.util.Log +import at.bitfire.ical4android.Ical4Android +import net.fortuna.ical4j.model.ParameterList +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.parameter.XParameter +import net.fortuna.ical4j.model.property.XProperty +import org.json.JSONObject +import java.util.logging.Level + +@Suppress("unused") +object JtxContract { + + /** + * URI parameter to signal that the caller is a sync adapter. + */ + const val CALLER_IS_SYNCADAPTER = "caller_is_syncadapter" + + /** + * URI parameter to submit the account name of the account we operate on. + */ + const val ACCOUNT_NAME = "account_name" + + /** + * URI parameter to submit the account type of the account we operate on. + */ + const val ACCOUNT_TYPE = "account_type" + + /** The authority under which the content provider can be accessed */ + const val AUTHORITY = "at.techbee.jtx.provider" + + /** The version of this SyncContentProviderContract */ + const val VERSION = 1 + + /** Constructs an Uri for the Jtx Sync Adapter with the given Account + * @param [account] The account that should be appended to the Base Uri + * @return [Uri] with the appended Account + */ + fun Uri.asSyncAdapter(account: Account): Uri = + buildUpon() + .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ACCOUNT_NAME, account.name) + .appendQueryParameter(ACCOUNT_TYPE, account.type) + .build() + + /** + * This function takes a string and tries to parse it to a list of XParameters. + * This is the counterpart of getJsonStringFromXParameters(...) + * @param [string] that should be parsed + * @return The list of XParameter parsed from the string + */ + fun getXParametersFromJson(string: String): List<XParameter> { + val jsonObject = JSONObject(string) + val xparamList = mutableListOf<XParameter>() + for (i in 0 until jsonObject.length()) { + val names = jsonObject.names() ?: break + val xparamName = names[i]?.toString() ?: break + val xparamValue = jsonObject.getString(xparamName).toString() + if (xparamName.isNotBlank() && xparamValue.isNotBlank()) { + val xparam = XParameter(xparamName, xparamValue) + xparamList.add(xparam) + } + } + return xparamList + } + + /** + * This function takes a string and tries to parse it to a list of XProperty. + * This is the counterpart of getJsonStringFromXProperties(...) + * @param [string] that should be parsed + * @return The list of XProperty parsed from the string + */ + fun getXPropertyListFromJson(string: String): PropertyList<Property> { + val propertyList = PropertyList<Property>() + + if (string.isBlank()) + return propertyList + + try { + val jsonObject = JSONObject(string) + for (i in 0 until jsonObject.length()) { + val names = jsonObject.names() ?: break + val propertyName = names[i]?.toString() ?: break + val propertyValue = jsonObject.getString(propertyName).toString() + if (propertyName.isNotBlank() && propertyValue.isNotBlank()) { + val prop = XProperty(propertyName, propertyValue) + propertyList.add(prop) + } + } + } catch (e: NullPointerException) { + Ical4Android.log.log(Level.WARNING, "Error parsing x-property-list $string", e) + } + return propertyList + } + + + /** + * Takes a Parameter List and returns a Json String to be saved in a DB field. + * This is the counterpart to getXParameterFromJson(...) + * @param [parameters] The ParameterList that should be transformed into a Json String + * @return The generated Json object as a [String] + */ + fun getJsonStringFromXParameters(parameters: ParameterList?): String? { + if (parameters == null) + return null + + val jsonObject = JSONObject() + parameters.forEach { parameter -> + jsonObject.put(parameter.name, parameter.value) + } + return if (jsonObject.length() == 0) + null + else + jsonObject.toString() + } + + /** + * Takes a Property List and returns a Json String to be saved in a DB field. + * This is the counterpart to getXPropertyListFromJson(...) + * @param [propertyList] The PropertyList that should be transformed into a Json String + * @return The generated Json object as a [String] + */ + fun getJsonStringFromXProperties(propertyList: PropertyList<*>?): String? { + if (propertyList == null) + return null + + val jsonObject = JSONObject() + propertyList.forEach { property -> + jsonObject.put(property.name, property.value) + } + return if (jsonObject.length() == 0) + null + else + jsonObject.toString() + } + + + /** + * Some date fields in jtx Board are saved as a list of Long values separated by commas. + * This applies for example to the exdate for recurring events. + * This function takes a string and tries to parse it to a list of long values (timestamps) + * @param [string] that should be parsed + * @return a [MutableList<Long>] with the timestamps parsed from the string + * + */ + fun getLongListFromString(string: String): MutableList<Long> { + val stringList = string.split(",") + val longList = mutableListOf<Long>() + + stringList.forEach { + try { + longList.add(it.toLong()) + } catch (e: NumberFormatException) { + Ical4Android.log.log(Level.WARNING, "String could not be cast to Long ($it)") + return@forEach + } + } + return longList + } + + + @Suppress("unused") + object JtxICalObject { + + /** The name of the the content URI for IcalObjects. + * This is a general purpose table containing general columns + * for Journals, Notes and Todos */ + private const val CONTENT_URI_PATH = "icalobject" + + /** The content uri of the ICalObject table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + /** Constant to define all day values (for dtstart, due, completed timezone fields */ + const val TZ_ALLDAY = "ALLDAY" + + /** The name of the ID column. + * This is the unique identifier of an ICalObject. + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /** The column for the module. + * This is an internal differentiation for JOURNAL, NOTE and TODO as provided in the enum [Module]. + * The Module does not need to be set. On import it will be derived from the component from the [Component] (Todo or Journal/Note) and if a + * dtstart is present or not (Journal or Note). If the module was set, it might be overwritten on import. In this sense also + * unknown values are overwritten. + * Use e.g. Module.JOURNAL.name for a correct String value in this field. + * Type: [String] + */ + const val MODULE = "module" + + /***** The names of all the other columns *****/ + /** The column for the component based on the values provided in the enum [Component]. + * Use e.g. Component.VTODO.name for a correct String value in this field. + * If no Component is provided on Insert, an IllegalArgumentException is thrown + * Type: [String] + */ + const val COMPONENT = "component" + + /** + * Purpose: This column/property defines a short summary or subject for the calendar component. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.12] + * Type: [String] + */ + const val SUMMARY = "summary" + + /** + * Purpose: This column/property provides a more complete description of the calendar component than that provided by the "SUMMARY" property. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.5] + * Type: [String] + */ + const val DESCRIPTION = "description" + + /** + * Purpose: This column/property specifies when the calendar component begins. + * This value is stored as UNIX timestamp (milliseconds). + * The corresponding timezone is stored in [DTSTART_TIMEZONE]. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.4] + * Type: [Long] + */ + const val DTSTART = "dtstart" + + /** + * Purpose: This column/property specifies the timezone of when the calendar component begins. + * The corresponding datetime is stored in [DTSTART]. + * The value of a timezone can be: + * 1. the id of a Java timezone to represent the given timezone. + * 2. null to represent floating time. + * 3. the value of [TZ_ALLDAY] to represent an all-day entry. + * If an invalid value is passed, the Timezone is ignored and interpreted as UTC. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.4] + * Type: [String] + */ + const val DTSTART_TIMEZONE = "dtstarttimezone" + + /** + * Purpose: This column/property specifies when the calendar component ends. + * This value is stored as UNIX timestamp (milliseconds). + * The corresponding timezone is stored in [DTEND_TIMEZONE]. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.4] + * Type: [Long] + */ + const val DTEND = "dtend" + + /** + * Purpose: This column/property specifies the timezone of when the calendar component ends. + * The corresponding datetime is stored in [DTEND]. + * See [DTSTART_TIMEZONE] for information about the timezone handling + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.2] + * Type: [String] + */ + const val DTEND_TIMEZONE = "dtendtimezone" + + /** + * Purpose: This property defines the overall status or confirmation for the calendar component. + * The possible values of a status are defined in [StatusTodo] for To-Dos and in [StatusJournal] for Notes and Journals + * If no valid Status is used, the value is taken and will be shown as-is. + * Also null-value is allowed. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.11] + * Type: [String] + */ + const val STATUS = "status" + + /** + * Purpose: This property defines the access classification for a calendar component. + * The possible values of a status are defined in the enum [Classification]. + * Use e.g. Classification.PUBLIC.name to put a correct String value in this field. + * If no valid Classification is used, the value is taken and will be shown as-is. + * Also null-value is allowed. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.11] + * Type: [String] + */ + const val CLASSIFICATION = "classification" + + /** + * Purpose: This property defines a Uniform Resource Locator (URL) associated with the iCalendar object. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.4.6] + * Type: [String] + */ + const val URL = "url" + + /** + * Purpose: This property is used to represent contact information or alternately a reference + * to contact information associated with the calendar component. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.4.2] + * Type: [String] + */ + const val CONTACT = "contact" + + /** + * Purpose: This property specifies information related to the global position for the activity specified by a calendar component. + * This property is split in the fields [GEO_LAT] for the latitude + * and [GEO_LONG] for the longitude coordinates using the WGS84 ellipsoid. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.6] + * Type: [Float] + */ + const val GEO_LAT = "geolat" + + /** + * Purpose: This property specifies information related to the global position for the activity specified by a calendar component. + * This property is split in the fields [GEO_LAT] for the latitude + * and [GEO_LONG] for the longitude coordinates using the WGS84 ellipsoid. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.6] + * Type: [Double] + */ + const val GEO_LONG = "geolong" + + /** + * Purpose: This property defines the intended venue for the activity defined by a calendar component. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.7] + * Type: [Double] + */ + const val LOCATION = "location" + + /** + * Purpose: This property defines the alternative representation of the intended venue for the activity defined by a calendar component. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.7] + * Type: [String] + */ + const val LOCATION_ALTREP = "locationaltrep" + + /** + * Purpose: This property is used by an assignee or delegatee of a to-do to convey the percent completion of a to-do to the "Organizer". + * The value must either be null or between 0 and 100. The value 0 is interpreted as null. + * If a value outside of the boundaries (0-100) is passed, the value is reset to null. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.8] + * Type: [Int] + */ + const val PERCENT = "percent" + + /** + * Purpose: This property defines the relative priority for a calendar component. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.1.9]. The priority can be null or between 0 and 9. + * Values outside of these boundaries are accepted but internally not mapped to a string resource. + * Type: [Int] + */ + const val PRIORITY = "priority" + + /** + * Purpose: This property defines the date and time that a to-do is expected to be completed. + * This value is stored as UNIX timestamp (milliseconds). + * The corresponding timezone is stored in [DUE_TIMEZONE]. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.3] + * Type: [Long] + */ + const val DUE = "due" + + /** + * Purpose: This column/property specifies the timezone of when a to-do is expected to be completed. + * The corresponding datetime is stored in [DUE]. + * See [DTSTART_TIMEZONE] for information about the timezone handling + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.2] + * Type: [String] + */ + const val DUE_TIMEZONE = "duetimezone" + + /** + * Purpose: This property defines the date and time that a to-do was actually completed. + * This value is stored as UNIX timestamp (milliseconds). + * The corresponding timezone is stored in [COMPLETED_TIMEZONE]. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.1] + * Type: [Long] + */ + const val COMPLETED = "completed" + + /** + * Purpose: This column/property specifies the timezone of when a to-do was actually completed. + * The corresponding datetime is stored in [COMPLETED]. + * See [DTSTART_TIMEZONE] for information about the timezone handling + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.1] + * Type: [String] + */ + const val COMPLETED_TIMEZONE = "completedtimezone" + + /** + * Purpose: This property specifies a positive duration of time. + * See [DTSTART_TIMEZONE] for information about the timezone handling + * The string representation follows the notation as given in RFC-5545 + * for the value type duration: [https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.6] + * Exampe: "P15DT5H0M20S". This field is currently not in use. If present, the user would + * see a notification that a duration cannot be processed and will be overwritten if the entry is changed. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.5] + * Type: [String] + */ + const val DURATION = "duration" + + + /** + * Purpose: This property defines a rule or repeating pattern for recurring events, + * to-dos, journal entries, or time zone definitions. + * The representation of the RRULE follows the value type RECUR of RFC-5545 as given in + * [https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10] + * For example: "FREQ=DAILY;COUNT=10" + * See also [https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3]. + * Type: [String] + */ + const val RRULE = "rrule" + + /** + * Purpose: This property defines the list of DATE-TIME values for + * recurring events, to-dos, journal entries, or time zone definitions. + * Type: [String], contains a list of comma-separated date values as + * UNIX timestamps (milliseconds) e.g. "1636751475000,1636837875000". + * Invalid values that cannot be transformed to [Long] are ignored. + */ + const val RDATE = "rdate" + + /** + * Purpose: This property defines the list of DATE-TIME exceptions for + * recurring events, to-dos, journal entries, or time zone definitions. + * Type: [String], contains a list of comma-separated date values as + * UNIX timestamps (milliseconds) e.g. "1636751475000,1636837875000". + * Invalid values that cannot be transformed to [Long] are ignored. + */ + const val EXDATE = "exdate" + + /** + * Purpose: This property is used in conjunction with the "UID" and + * "SEQUENCE" properties to identify a specific instance of a + * recurring "VEVENT", "VTODO", or "VJOURNAL" calendar component. + * The property value is the original value of the "DTSTART" property + * of the recurrence instance, ie. a DATE or DATETIME value e.g. "20211101T160000". + * Must be null for non-recurring and original events from which recurring events are derived. + * Type: [String] + */ + const val RECURID = "recurid" + + /** + * Stores the reference to the original event from which the recurring event was derived. + * This value is NULL for the orignal event or if the event is not recurring + * Type: [Long], References [JtxICalObject.ID] + */ + const val RECUR_ORIGINALICALOBJECTID = "original_id" + + /** + * Purpose: This property defines the persistent, globally unique identifier for the calendar component. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.4.7] + * Type: [String] + */ + const val UID = "uid" + + /** + * Purpose: This property specifies the date and time that the calendar information + * was created by the calendar user agent in the calendar store and is stored as UNIX timestamp (milliseconds). + * See [https://tools.ietf.org/html/rfc5545#section-3.8.7.1] + * Type: [Long] + */ + const val CREATED = "created" + + /** + * Purpose: In the case of an iCalendar object that specifies a + * "METHOD" property, this property specifies the date and time that + * the instance of the iCalendar object was created. In the case of + * an iCalendar object that doesn't specify a "METHOD" property, this + * property specifies the date and time that the information + * associated with the calendar component was last revised in the + * calendar store. It is saved as UNIX timestamp (milliseconds). + * See [https://tools.ietf.org/html/rfc5545#section-3.8.7.2] + * Type: [Long] + */ + const val DTSTAMP = "dtstamp" + + /** + * Purpose: This property specifies the date and time that the information associated + * with the calendar component was last revised in the calendar store. + * It is saved as UNIX timestamp (milliseconds). + * See [https://tools.ietf.org/html/rfc5545#section-3.8.7.3] + * Type: [Long] + */ + const val LAST_MODIFIED = "lastmodified" + + /** + * Purpose: This property defines the revision sequence number of the calendar component within a sequence of revisions. + * If no sequence is present, it is automatically initialized with 0 + * See [https://tools.ietf.org/html/rfc5545#section-3.8.7.4] + * Type: [Int] + */ + const val SEQUENCE = "sequence" + + /** + * Purpose: This property specifies a color used for displaying the calendar, event, todo, or journal data. + * See [https://tools.ietf.org/html/rfc7986#section-5.9]. + * The color is represented as Int-value as described in [https://developer.android.com/reference/android/graphics/Color#color-ints] + * Type: [Int] + */ + const val COLOR = "color" + + /** + * Purpose: This column is the foreign key to the [JtxCollection]. + * Type: [Long], references [JtxCollection.ID] + */ + const val ICALOBJECT_COLLECTIONID = "collectionId" + + /** + * Purpose: This column defines if the local ICalObject was changed and that it is supposed to be synchronised. + * Type: [Boolean] + */ + const val DIRTY = "dirty" + + /** + * Purpose: This column defines if a collection that is supposed to be synchonized was locally marked as deleted. + * Type: [Boolean] + */ + const val DELETED = "deleted" + + /** + * Purpose: The remote file name of the synchronized entry (*.ics), only relevant for synchronized + * entries through the sync-adapter + * Type: [String] + */ + const val FILENAME = "filename" + + /** + * Purpose: eTag for SyncAdapter, only relevant for synched entries through sync-adapter + * Type: [String] + */ + const val ETAG = "etag" + + /** + * Purpose: scheduleTag for SyncAdapter, only relevant for synched entries through sync-adapter + * Type: [String] + */ + const val SCHEDULETAG = "scheduletag" + + /** + * Purpose: flags for SyncAdapter, only relevant for synched entries through sync-adapter + * Type: [Int] + */ + const val FLAGS = "flags" + + /** + * Purpose: To specify other properties for the ICalObject. + * This is especially used for additional properties that cannot be put into another field, especially relevant for the synchronization + * The Properties are stored as JSON. There are two helper functions provided: + * getJsonStringFromXProperties(PropertyList<*> that returns a Json String from the property list + * to be stored in this other field. The counterpart to this function is + * getXPropertyListFromJson(String) that returns a PropertyList from a Json that was created with getJsonStringFromXProperties() + * Type: [String] + */ + const val OTHER = "other" + + + /** + * This enum class defines the possible values for the attribute status of an [JtxICalObject] for Journals/Notes + * Use its name when the string representation is required, e.g. StatusJournal.DRAFT.name. + */ + enum class StatusJournal { + DRAFT, FINAL, CANCELLED + } + + /** + * This enum class defines the possible values for the attribute status of an [JtxICalObject] for Todos + * Use its name when the string representation is required, e.g. StatusTodo.`NEEDS-ACTION`.name. + */ + enum class StatusTodo { + `NEEDS-ACTION`, COMPLETED, `IN-PROCESS`, CANCELLED + } + + /** + * This enum class defines the possible values for the attribute classification of an [JtxICalObject] + * Use its name when the string representation is required, e.g. Classification.PUBLIC.name. + */ + enum class Classification { + PUBLIC, PRIVATE, CONFIDENTIAL + } + + /** + * This enum class defines the possible values for the attribute component of an [JtxICalObject] + * Use its name when the string representation is required, e.g. Component.VJOURNAL.name. + */ + enum class Component { + VJOURNAL, VTODO + } + + /** + * This enum class defines the possible values for the attribute module of an [JtxICalObject] + * Use its name when the string representation is required, e.g. Module.JOURNAL.name. + */ + enum class Module { + JOURNAL, NOTE, TODO + } + } + + + @Suppress("unused") + object JtxAttendee { + + /** The name of the the table for Attendees that are linked to an ICalObject. + * [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] */ + private const val CONTENT_URI_PATH = "attendee" + + /** The content uri of the Attendee table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column. + * This is the unique identifier of an Attendee + * Type: [Long] */ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This value type is used to identify properties that contain a calendar user address (in this case of the attendee). + * This is usually an e-mail address as defined in [https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.3]. + * The value should be passed as String containing the URI with "mailto:", for example: "mailto:jane_doe@example.com" + * See also [https://tools.ietf.org/html/rfc5545#section-3.8.4.1]. + * Type: [String] + */ + const val CALADDRESS = "caladdress" + + /** + * Purpose: To identify the type of calendar user specified by the property in this case for the attendee. + * The possible values are defined in the enum [Cutype]. + * Use e.g. Cutype.INDIVIDUAL.name to put a correct String value in this field. + * If no value is passed for the Cutype, the Cutype will be interpreted as INDIVIDUAL as according to the RFC. + * Other values are accepted and treated as UNKNOWN. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.3] + * Type: [String] + */ + const val CUTYPE = "cutype" + + /** + * Purpose: To specify the group or list membership of the calendar user specified by the property in this case for the attendee. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.11] + * Type: [String] + */ + const val MEMBER = "member" + + /** + * Purpose: To specify the participation role for the calendar user specified by the property in this case for the attendee. + * The possible values are defined in the enum [Role] + * Use e.g. Role.CHAIR.name to put a correct String value in this field. + * If no value (null) is passed for the Role, it will be interpreted as REQ-PARTICIPANT as according to the RFC. + * Other values are accepted and treated as REQ-PARTICIPANTs. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.16] + * Type: [String] + */ + const val ROLE = "role" + + /** + * Purpose: To specify the participation status for the calendar user specified by the property in this case for the attendee. + * The possible values are defined in the enum [PartstatJournal] and [PartstatTodo] + * Use e.g. PartstatJournal.ACCEPTED.name to put a correct String value in this field. + * If no value (null) is passed for the Partstat, it will be interpreted as NEEDS-ACTION as according to the RFC. + * Other values are accepted and treated as NEEDS-ACTION. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.12] + * Type: [String] + */ + const val PARTSTAT = "partstat" + + /** + * Purpose: To specify whether there is an expectation of a favor of a reply from the calendar user specified by the property value + * in this case for the attendee. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.17] + * Type: [Boolean] + */ + const val RSVP = "rsvp" + + /** + * Purpose: To specify the calendar users to whom the calendar user specified by the property + * has delegated participation in this case for the attendee. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.5] + * Type: [String] + */ + const val DELEGATEDTO = "delegatedto" + + /** + * Purpose: To specify the calendar users that have delegated their participation to the calendar user specified by the property + * in this case for the attendee. + * This is usually an e-mail address as defined in [https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.3]. + * The value should be passed as String containing the URI with "mailto:", for example: "mailto:jane_doe@example.com" + * See also [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.4] + * Type: [String] + */ + const val DELEGATEDFROM = "delegatedfrom" + + /** + * Purpose: To specify the calendar user that is acting on behalf of the calendar user specified by the property in this case for the attendee. + * This is usually an e-mail address as defined in [https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.3]. + * The value should be passed as String containing the URI with "mailto:", for example: "mailto:jane_doe@example.com" + * See also [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.18] + * Type: [String] + */ + const val SENTBY = "sentby" + + /** + * Purpose: To specify the common name to be associated with the calendar user specified by the property in this case for the attendee. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.2] + * Type: [String] + */ + const val CN = "cn" + + /** + * Purpose: To specify reference to a directory entry associated with the calendar user specified by the property in this case for the attendee. + * Expected is an URI as defined in [https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.6]. + * The value should be passed as String, e.g. "ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)" + * Type: [String] + */ + const val DIR = "dir" + + /** + * Purpose: To specify the language for text values in a property or property parameter, in this case of the attendee. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] and [https://tools.ietf.org/html/rfc5545#section-3.2.10] + * Language-Tag as defined in RFC5646, e.g. "en:Germany" + * Type: [String] + */ + const val LANGUAGE = "language" + + /** + * Purpose: To specify other parameters for the attendee. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.1] + * The Parameters are stored as JSON. There are two helper functions provided: + * getJsonStringFromXParameters(ParameterList?) that returns a Json String from the parameter list + * to be stored in this other field. The counterpart to this function is + * getXParameterFromJson(String) that returns a list of XParameters from a Json that was created with getJsonStringFromXParameters(...) + * Type: [String] + */ + const val OTHER = "other" + + + /** + * This enum class defines the possible values for the attribute Cutype of an [JtxAttendee] + * Use its name when the string representation is required, e.g. Cutype.INDIVIDUAL.name. + */ + enum class Cutype { + INDIVIDUAL, GROUP, RESOURCE, ROOM, UNKNOWN + } + + /** + * This enum class defines the possible values for the attribute Role of an [JtxAttendee] + * Use its name when the string representation is required, e.g. Role.`REQ-PARTICIPANT`.name. + */ + enum class Role { + CHAIR, `REQ-PARTICIPANT`, `OPT-PARTICIPANT`, `NON-PARTICIPANT` + } + + /** This enum class defines the possible values for the attribute [JtxAttendee] for the Component VJOURNAL */ + @Suppress("unused") + enum class PartstatJournal { + `NEEDS-ACTION`, ACCEPTED, DECLINED + } + + /** This enum class defines the possible values for the attribute [JtxAttendee] for the Component VTODO */ + @Suppress("unused") + enum class PartstatTodo { + `NEEDS-ACTION`, ACCEPTED, DECLINED, TENTATIVE, DELEGATED, COMPLETED, `IN-PROCESS` + } + } + + @Suppress("unused") + object JtxCategory { + + /** The name of the the table for Categories that are linked to an ICalObject. + * [https://tools.ietf.org/html/rfc5545#section-3.8.1.2]*/ + private const val CONTENT_URI_PATH = "category" + + /** The content uri of the Category table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for categories. + * This is the unique identifier of a Category + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This property defines the name of the category for a calendar component. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.2] + * Type: [String] + */ + const val TEXT = "text" + + /** + * Purpose: To specify the language for text values in a property or property parameter, in this case of the category. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.2] and [https://tools.ietf.org/html/rfc5545#section-3.2.10] + * Language-Tag as defined in RFC5646, e.g. "en:Germany" + * Type: [String] + */ + const val LANGUAGE = "language" + + /** + * Purpose: To specify other properties for the category. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.2] + * The Parameters are stored as JSON. There are two helper functions provided: + * getJsonStringFromXParameters(ParameterList?) that returns a Json String from the parameter list + * to be stored in this other field. The counterpart to this function is + * getXParameterFromJson(String) that returns a list of XParameters from a Json that was created with getJsonStringFromXParameters(...) + * Type: [String] + */ + const val OTHER = "other" + } + + @Suppress("unused") + object JtxComment { + + /** The name of the the table for Comments that are linked to an ICalObject. + * [https://tools.ietf.org/html/rfc5545#section-3.8.1.4]*/ + private const val CONTENT_URI_PATH = "comment" + + /** The content uri of the Comment table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for comments. + * This is the unique identifier of a Comment + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This property specifies non-processing information intended to provide a comment to the calendar user. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.4] + * Type: [String] + */ + const val TEXT = "text" + + /** + * Purpose: To specify the language for text values in a property or property parameter, in this case of the comment. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.4] + * Language-Tag as defined in RFC5646, e.g. "en:Germany" + * Type: [String] + */ + const val ALTREP = "altrep" + + /** + * Purpose: To specify an alternate text representation for the property value, in this case of the comment. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.4] + * Type: [String] + */ + const val LANGUAGE = "language" + + /** + * Purpose: To specify other properties for the comment. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.4] + * The Parameters are stored as JSON. There are two helper functions provided: + * getJsonStringFromXParameters(ParameterList?) that returns a Json String from the parameter list + * to be stored in this other field. The counterpart to this function is + * getXParameterFromJson(String) that returns a list of XParameters from a Json that was created with getJsonStringFromXParameters(...) + * Type: [String] + */ + const val OTHER = "other" + } + + + @Suppress("unused") + object JtxOrganizer { + /** The name of the the table for Organizer that are linked to an ICalObject. + * [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] + */ + private const val CONTENT_URI_PATH = "organizer" + + /** The content uri of the Organizer table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for the organizer. + * This is the unique identifier of a Organizer + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This value type is used to identify properties that contain a calendar user address (in this case of the organizer). + * This is usually an e-mail address as defined in [https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.3]. + * The value should be passed as String containing the URI with "mailto:", for example: "mailto:jane_doe@example.com" + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.3]. + * Type: [String] + */ + const val CALADDRESS = "caladdress" + + /** + * Purpose: To specify the common name to be associated with the calendar user specified by the property in this case for the organizer. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] and [https://tools.ietf.org/html/rfc5545#section-3.2.18] + * Type: [String] + */ + const val CN = "cnparam" + + /** + * Purpose: To specify reference to a directory entry associated with the calendar user specified by the property in this case for the organizer. + * Expected is an URI as defined in [https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.6]. + * The value should be passed as String, e.g. "ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)" + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] and [https://tools.ietf.org/html/rfc5545#section-3.2.2] + * Type: [String] + */ + const val DIR = "dirparam" + + /** + * Purpose: To specify the calendar user that is acting on behalf of the calendar user specified by the property in this case for the organizer. + * This is usually an e-mail address as defined in [https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.3]. + * The value should be passed as String containing the URI with "mailto:", for example: "mailto:jane_doe@example.com" + * See also [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] and [https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.18] + * Type: [String] + */ + const val SENTBY = "sentbyparam" + + /** + * Purpose: To specify the language for text values in a property or property parameter, in this case of the organizer. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] and [https://tools.ietf.org/html/rfc5545#section-3.2.10] + * Language-Tag as defined in RFC5646, e.g. "en:Germany" + * Type: [String] + */ + const val LANGUAGE = "language" + + /** + * Purpose: To specify other properties for the organizer. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] + * The Parameters are stored as JSON. There are two helper functions provided: + * getJsonStringFromXParameters(ParameterList?) that returns a Json String from the parameter list + * to be stored in this other field. The counterpart to this function is + * getXParameterFromJson(String) that returns a list of XParameters from a Json that was created with getJsonStringFromXParameters(...) + * Type: [String] + */ + const val OTHER = "other" + + + } + + @Suppress("unused") + object JtxRelatedto { + + /** The name of the the table for Relationships (related-to) that are linked to an ICalObject. + * [https://tools.ietf.org/html/rfc5545#section-3.8.4.5] + */ + private const val CONTENT_URI_PATH = "relatedto" + + /** The content uri of the relatedto table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for the related-to. + * This is the unique identifier of a Related-to + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + /** The name of the second Foreign Key Column of the related IcalObject + * Type: [Long] + */ + const val LINKEDICALOBJECT_ID = "linkedICalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This property is used to represent a relationship or reference between one calendar component and another. + * The text gives the UID of the related calendar entry. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.5] + * Type: [String] + */ + const val TEXT = "text" + + /** + * Purpose: To specify the type of hierarchical relationship associated + * with the calendar component specified by the property. + * The possible relationship types are defined in the enum [Reltype]. + * Use e.g. Reltype.PARENT.name to put a correct String value in this field. + * Other values and null-values are allowed, but will not be processed by the app. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.5] and [https://tools.ietf.org/html/rfc5545#section-3.2.15] + * Type: [String] + */ + const val RELTYPE = "reltype" + + /** + * Purpose: To specify other properties for the related-to. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.5] + * The Parameters are stored as JSON. There are two helper functions provided: + * getJsonStringFromXParameters(ParameterList?) that returns a Json String from the parameter list + * to be stored in this other field. The counterpart to this function is + * getXParameterFromJson(String) that returns a list of XParameters from a Json that was created with getJsonStringFromXParameters(...) + * Type: [String] + */ + const val OTHER = "other" + + + /** + * This enum class defines the possible values for the attribute Reltype of an [JtxRelatedto]. + * Use its name when the string representation is required, e.g. Reltype.PARENT.name. + */ + enum class Reltype { + PARENT, CHILD, SIBLING + } + + } + + @Suppress("unused") + object JtxResource { + /** The name of the the table for Resources that are linked to an ICalObject. + * [https://tools.ietf.org/html/rfc5545#section-3.8.1.10]*/ + private const val CONTENT_URI_PATH = "resource" + + /** The content uri of the resources table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for resources. + * This is the unique identifier of a Resource + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This property defines the name of the resource for a calendar component. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.10] + * Type: [String] + */ + const val TEXT = "text" + + /** + * Purpose: To specify an alternate text representation for the property value, in this case of the resource. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.4] + * Language-Tag as defined in RFC5646, e.g. "en:Germany" + * Type: [String] + */ + const val LANGUAGE = "language" + + /** + * Purpose: To specify other properties for the resource. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.10] + * The Parameters are stored as JSON. There are two helper functions provided: + * getJsonStringFromXParameters(ParameterList?) that returns a Json String from the parameter list + * to be stored in this other field. The counterpart to this function is + * getXParameterFromJson(String) that returns a list of XParameters from a Json that was created with getJsonStringFromXParameters(...) + * Type: [String] + */ + const val OTHER = "other" + + } + + @Suppress("unused") + object JtxCollection { + + /** The name of the the table for Collections + * ICalObjects MUST be linked to a collection! */ + private const val CONTENT_URI_PATH = "collection" + + /** The content uri of the collections table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + /** + * Account type used for testing. This account type must be used for integrated testing. + * Otherwise the application would check if an account exists in the Android accounts + * and delete the test account/collections immediately. Using this test account prevents + * this behaviour for Debug builds. + */ + const val TEST_ACCOUNT_TYPE = "TEST" + + + /** The name of the the table for Collections. + * ICalObjects MUST be linked to a collection! */ + const val TABLE_NAME_COLLECTION = "collection" + + /** The name of the ID column for collections. + * This is the unique identifier of a Collection + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /***** The names of all the other columns *****/ + /** + * Purpose: This column/property defines the url of the collection. + * Type: [String] + */ + const val URL = "url" + + /** + * Purpose: This column/property defines the display name of the collection. + * Type: [String] + */ + const val DISPLAYNAME = "displayname" + + /** + * Purpose: This column/property defines a description of the collection. + * Type: [String] + */ + const val DESCRIPTION = "description" + + /** + * Purpose: This column/property defines the owner of the collection. + * Type: [String] + */ + const val OWNER = "owner" + + /** + * Purpose: This column/property defines the color of the collection items. + * This color can also be overwritten by the color in an ICalObject. + * The color is represented as Int-value as described in [https://developer.android.com/reference/android/graphics/Color#color-ints] + * Type: [Int] + */ + const val COLOR = "color" + + /** + * Purpose: This column/property defines the if the collection supports VEVENTs. + * Type: [Boolean] + */ + const val SUPPORTSVEVENT = "supportsVEVENT" + + /** + * Purpose: This column/property defines the if the collection supports VTODOs. + * Type: [Boolean] + */ + const val SUPPORTSVTODO = "supportsVTODO" + + /** + * Purpose: This column/property defines the if the collection supports VJOURNALs. + * Type: [Boolean] + */ + const val SUPPORTSVJOURNAL = "supportsVJOURNAL" + + /** + * Purpose: This column/property defines the if the account name under which the collection resides. + * Type: [String] + */ + const val ACCOUNT_NAME = "accountname" + + /** + * Purpose: This column/property defines the if the account type under which the collection resides. + * Type: [String] + */ + const val ACCOUNT_TYPE = "accounttype" + + /** + * Purpose: This column/property defines a field for the Sync Version for the Sync Adapter + * Type: [String] + */ + const val SYNC_VERSION = "syncversion" + + /** + * Purpose: This column/property defines if a collection is marked as read-only by the Sync Adapter + * Type: [Boolean] + */ + const val READONLY = "readonly" + + } + + + @Suppress("unused") + object JtxAttachment { + + /** The name of the the table for Attachments that are linked to an ICalObject.*/ + private const val CONTENT_URI_PATH = "attachment" + + /** The content uri of the resources table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for attachments. + * This is the unique identifier of an Attachment + * Type: [Long]*/ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This property specifies the uri of an attachment. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.1] + * Type: [String] + */ + const val URI = "uri" + + /** + * Purpose: To specify the value of the attachment (binary). + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.1] + * Type: [String] + */ + const val BINARY = "binary" + + /** + * Purpose: To specify the fmttype of the attachment. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.1] + * Type: [String] + */ + const val FMTTYPE = "fmttype" + + /** + * Purpose: To specify other properties for the attachment. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.1.1] + * The Parameters are stored as JSON. There are two helper functions provided: + * getJsonStringFromXParameters(ParameterList?) that returns a Json String from the parameter list + * to be stored in this other field. The counterpart to this function is + * getXParameterFromJson(String) that returns a list of XParameters from a Json that was created with getJsonStringFromXParameters(...) + * Type: [String] + */ + const val OTHER = "other" + + } + + + @Suppress("unused") + object JtxAlarm { + + /** The name of the the table for Alarms that are linked to an ICalObject.*/ + private const val CONTENT_URI_PATH = "alarm" + + /** The content uri of the resources table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for attachments. + * This is the unique identifier of an Attachment + * Type: [Long], references [JtxICalObject.ID] + */ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long], references [JtxICalObject.ID] + */ + const val ICALOBJECT_ID = "icalObjectId" + + /** + * Each "VALARM" calendar component has a particular type + * of action with which it is associated. This property specifies + * the type of action. Applications MUST ignore alarms with x-name + * and iana-token values they don't recognize. + * Currently only "DISPLAY" is supported, all other values can be stored but are ignored + * Type: [String] + */ + const val ACTION = "action" + + /** + * This property provides a more complete description of the + * calendar component than that provided by the "SUMMARY" property. + * Type: [String] + */ + const val DESCRIPTION = "description" + + /** + * This property defines a short summary or subject for the + * calendar component. + * Type: [String] + */ + const val SUMMARY = "summary" + + /** + * This property contains a CSV-list of attendees as Uris + * e.g. "mailto:contact@techbee.at,mailto:jtx@techbee.at" + */ + const val ATTENDEE = "attendee" + + /** + * The alarm can be defined such that it triggers repeatedly. A + * definition of an alarm with a repeating trigger MUST include both + * the "DURATION" and "REPEAT" properties. The "DURATION" property + * specifies the delay period, after which the alarm will repeat. + * Type: [String] + */ + const val DURATION = "duration" + + /** + * The "REPEAT" property specifies the number of additional + * repetitions that the alarm will be triggered. This repetition + * count is in addition to the initial triggering of the alarm. Both + * of these properties MUST be present in order to specify a + * repeating alarm. If one of these two properties is absent, then + * the alarm will not repeat beyond the initial trigger. + * Type: [String] + */ + const val REPEAT = "repeat" + + /** + * Contains the uri of an attachment + * Type: [String] + */ + const val ATTACH = "attach" + + /** + * Purpose: To specify other properties for the alarm. + * see [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] + * Type: [String] + */ + const val OTHER = "other" + + /** + * Stores a timestamp with the absolute time when the alarm should be triggered. + * This value is stored as UNIX timestamp (milliseconds). + * Either a Alarm Trigger Time OR a Alarm Relative Duration must be provided, but not both! + * Type: [Long] + */ + const val TRIGGER_TIME = "triggerTime" + + /** + * Purpose: This column/property specifies the timezone of the absolute trigger time. + * The corresponding datetime is stored in [TRIGGER_TIME]. + * The value of a timezone can be: + * 1. the id of a Java timezone to represent the given timezone. + * 2. null to represent floating time. + * If an invalid value is passed, the Timezone is ignored and interpreted as UTC. + * See [https://tools.ietf.org/html/rfc5545#section-3.8.2.4] + * Type: [String] + */ + const val TRIGGER_TIMEZONE = "triggerTimezone" + + /** + * Purpose: This property defines the field to which the duration is relatiive to. + * The possible values of a status are defined in the enum [AlarmRelativeTo]. + * Use e.g. AlarmRelativeTo.START.name to put a correct String value in this field. + * AlarmRelativeTo.START would make the duration relative to DTSTART. + * AlarmRelativeTo.END would make the duration relative to DUE (only VTODO is supported!). + * If no valid AlarmRelativeTo is provided, the default value is AlarmRelativeTo.START. + * Type: [String] + */ + const val TRIGGER_RELATIVE_TO = "triggerRelativeTo" + + /** + * Purpose: Specifying a relative time for the + * trigger of the alarm. The default duration is relative to the + * start of an event or to-do with which the alarm is associated. + * The duration can be explicitly set to trigger from either the end + * or the start of the associated event or to-do with the [TRIGGER_RELATIVE_TO] + * parameter. A value of START will set the alarm to trigger off the + * start of the associated event or to-do. A value of END will set + * the alarm to trigger off the end of the associated event or to-do. + * Either a positive or negative duration may be specified for the + * "TRIGGER" property. An alarm with a positive duration is + * triggered after the associated start or end of the event or to-do. + * An alarm with a negative duration is triggered before the + * associated start or end of the event or to-do. + * Type: [String] + */ + const val TRIGGER_RELATIVE_DURATION = "triggerRelativeDuration" + + /** This enum class defines the possible values for the attribute [TRIGGER_RELATIVE_TO] for the Component VALARM */ + @Suppress("unused") + enum class AlarmRelativeTo { + START, END + } + + /** This enum class defines the possible values for the attribute [ACTION] for the Component VALARM */ + @Suppress("unused") + enum class AlarmAction { + AUDIO, DISPLAY, EMAIL + } + } + + @Suppress("unused") + object JtxUnknown { + + /** The name of the the table for Unknown properties that are linked to an ICalObject.*/ + private const val CONTENT_URI_PATH = "unknown" + + /** The content uri of the resources table */ + val CONTENT_URI: Uri by lazy { Uri.parse("content://$AUTHORITY/$CONTENT_URI_PATH") } + + + /** The name of the ID column for attachments. + * This is the unique identifier of an Attachment + * Type: [Long], references [JtxICalObject.ID] + */ + const val ID = BaseColumns._ID + + /** The name of the Foreign Key Column for IcalObjects. + * Type: [Long] */ + const val ICALOBJECT_ID = "icalObjectId" + + + /***** The names of all the other columns *****/ + /** + * Purpose: This property stores the unknown value as json + * Type: [String] + */ + const val UNKNOWN_VALUE = "value" + } + +}
\ No newline at end of file diff --git a/src/test/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt b/src/test/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt index 0d8e176..fd8bca0 100644 --- a/src/test/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt +++ b/src/test/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import at.bitfire.ical4android.util.AndroidTimeUtils diff --git a/src/test/java/at/bitfire/ical4android/Css3ColorTest.kt b/src/test/java/at/bitfire/ical4android/Css3ColorTest.kt index a4bd6c0..9828752 100644 --- a/src/test/java/at/bitfire/ical4android/Css3ColorTest.kt +++ b/src/test/java/at/bitfire/ical4android/Css3ColorTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/test/java/at/bitfire/ical4android/DateUtilsTest.kt b/src/test/java/at/bitfire/ical4android/DateUtilsTest.kt index 6f03906..395b8da 100644 --- a/src/test/java/at/bitfire/ical4android/DateUtilsTest.kt +++ b/src/test/java/at/bitfire/ical4android/DateUtilsTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/test/java/at/bitfire/ical4android/EventTest.kt b/src/test/java/at/bitfire/ical4android/EventTest.kt index a2ce504..e4d25c8 100644 --- a/src/test/java/at/bitfire/ical4android/EventTest.kt +++ b/src/test/java/at/bitfire/ical4android/EventTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android import net.fortuna.ical4j.model.Date diff --git a/src/test/java/at/bitfire/ical4android/ICalPreprocessorTest.kt b/src/test/java/at/bitfire/ical4android/ICalPreprocessorTest.kt index ca7433e..894452e 100644 --- a/src/test/java/at/bitfire/ical4android/ICalPreprocessorTest.kt +++ b/src/test/java/at/bitfire/ical4android/ICalPreprocessorTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import net.fortuna.ical4j.data.CalendarBuilder diff --git a/src/test/java/at/bitfire/ical4android/ICalendarTest.kt b/src/test/java/at/bitfire/ical4android/ICalendarTest.kt index 329fd1b..7e1d0e7 100644 --- a/src/test/java/at/bitfire/ical4android/ICalendarTest.kt +++ b/src/test/java/at/bitfire/ical4android/ICalendarTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/test/java/at/bitfire/ical4android/Ical4jTest.kt b/src/test/java/at/bitfire/ical4android/Ical4jTest.kt index cec2c29..ad8d11b 100644 --- a/src/test/java/at/bitfire/ical4android/Ical4jTest.kt +++ b/src/test/java/at/bitfire/ical4android/Ical4jTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import net.fortuna.ical4j.data.CalendarBuilder diff --git a/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt b/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt index 3ff78d9..e32ba0a 100644 --- a/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt +++ b/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import org.junit.Assert.assertTrue diff --git a/src/test/java/at/bitfire/ical4android/TaskTest.kt b/src/test/java/at/bitfire/ical4android/TaskTest.kt index 850b8e2..cbe0bcc 100644 --- a/src/test/java/at/bitfire/ical4android/TaskTest.kt +++ b/src/test/java/at/bitfire/ical4android/TaskTest.kt @@ -1,10 +1,6 @@ -/* - * Copyright © Ricki Hirner (bitfire web engineering). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ package at.bitfire.ical4android diff --git a/src/test/java/at/bitfire/ical4android/TimeApiExtensionsTest.kt b/src/test/java/at/bitfire/ical4android/TimeApiExtensionsTest.kt index 3f632ca..44a02a0 100644 --- a/src/test/java/at/bitfire/ical4android/TimeApiExtensionsTest.kt +++ b/src/test/java/at/bitfire/ical4android/TimeApiExtensionsTest.kt @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + package at.bitfire.ical4android import at.bitfire.ical4android.util.TimeApiExtensions.requireTimeZone |