From 1c6bcb3ffb0cef0b83ba59487c3ce9cfc10d262b Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 8 Feb 2023 11:01:17 +0100 Subject: [PATCH 01/92] Handling period durations of days with `T` suffix (#80) * Improved test Signed-off-by: Arnau Mora * Added more fix cases Signed-off-by: Arnau Mora * Use assert signature --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner --- .../FixInvalidDayOffsetPreprocessor.kt | 11 +++++-- .../FixInvalidDayOffsetPreprocessorTest.kt | 30 +++++++++---------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt b/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt index 843510cf..aea8efc9 100644 --- a/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt +++ b/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt @@ -6,12 +6,15 @@ package at.bitfire.ical4android.validation /** * Fixes durations with day offsets with the 'T' prefix. - * See also https://github.com/bitfireAT/icsx5/issues/100 + * See also https://github.com/bitfireAT/ical4android/issues/77 */ object FixInvalidDayOffsetPreprocessor : StreamPreprocessor() { override fun regexpForProblem() = Regex( - "^(DURATION|TRIGGER):-?PT-?\\d+D$", + // Examples: + // TRIGGER:-P2DT + // TRIGGER:-PT2D + "^(DURATION|TRIGGER):-?P((T-?\\d+D)|(-?\\d+DT))\$", setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE) ) @@ -21,7 +24,9 @@ object FixInvalidDayOffsetPreprocessor : StreamPreprocessor() { // Find all matches for the expression val found = regexpForProblem().find(s) ?: return s for (match in found.groupValues) { - val fixed = match.replace("PT", "P") + val fixed = match + .replace("PT", "P") + .replace("DT", "D") s = s.replace(match, fixed) } return s diff --git a/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt b/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt index 7471ca8e..2271b379 100644 --- a/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt +++ b/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt @@ -6,9 +6,16 @@ package at.bitfire.ical4android.validation import org.junit.Assert.* import org.junit.Test +import java.time.Duration class FixInvalidDayOffsetPreprocessorTest { + private fun fixStringAndAssert(expected: String, string: String) { + val fixed = FixInvalidDayOffsetPreprocessor.fixString(string) + Duration.parse(fixed.substring(fixed.indexOf(':') + 1)) + assertEquals(expected, fixed) + } + @Test fun test_FixString_NoOccurrence() { assertEquals( @@ -19,26 +26,17 @@ class FixInvalidDayOffsetPreprocessorTest { @Test fun test_FixString_DayOffsetFrom_Invalid() { - assertEquals( - "DURATION:-P1D", - FixInvalidDayOffsetPreprocessor.fixString("DURATION:-PT1D"), - ) - assertEquals( - "TRIGGER:-P2D", - FixInvalidDayOffsetPreprocessor.fixString("TRIGGER:-PT2D"), - ) + fixStringAndAssert("DURATION:-P1D", "DURATION:-PT1D") + fixStringAndAssert("TRIGGER:-P2D", "TRIGGER:-PT2D") + + fixStringAndAssert("DURATION:-P1D", "DURATION:-P1DT") + fixStringAndAssert("TRIGGER:-P2D", "TRIGGER:-P2DT") } @Test fun test_FixString_DayOffsetFrom_Valid() { - assertEquals( - "DURATION:-PT12H", - FixInvalidDayOffsetPreprocessor.fixString("DURATION:-PT12H"), - ) - assertEquals( - "TRIGGER:-PT12H", - FixInvalidDayOffsetPreprocessor.fixString("TRIGGER:-PT12H"), - ) + fixStringAndAssert("DURATION:-PT12H", "DURATION:-PT12H") + fixStringAndAssert("TRIGGER:-PT12H", "TRIGGER:-PT12H") } @Test From de24b0ac52614ab54d7d3d3217d5cb3abaf9b2c1 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 10 Feb 2023 13:03:24 +0100 Subject: [PATCH 02/92] jtx Board 2.3 with improved recur handling (#79) * Including recur instances in generated ics * Exclude recur instances from query dirty and query deleted code format updates removed deprecated method updateRelatedTo() * Fixed bug in recuid handling on ical generation * added queryRecur(...) and excluded recur instances when using query by filename * updated getICSForCollection() to exclude unchanged recur instances * updated minVersion for jtx Board * Fixed: export all from collection includes the recurring instances twice --- .../at/bitfire/ical4android/JtxCollection.kt | 152 ++++++------------ .../at/bitfire/ical4android/JtxICalObject.kt | 150 ++++++++++------- .../at/bitfire/ical4android/TaskProvider.kt | 2 +- 3 files changed, 150 insertions(+), 154 deletions(-) diff --git a/src/main/java/at/bitfire/ical4android/JtxCollection.kt b/src/main/java/at/bitfire/ical4android/JtxCollection.kt index 83d54033..a84a43dd 100644 --- a/src/main/java/at/bitfire/ical4android/JtxCollection.kt +++ b/src/main/java/at/bitfire/ical4android/JtxCollection.kt @@ -102,7 +102,12 @@ open class JtxCollection(val account: Account, */ fun queryDeletedICalObjects(): List { val values = mutableListOf() - client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DELETED} = ?", arrayOf(id.toString(), "1"), null).use { cursor -> + client.query( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), + null, + "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DELETED} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NULL", 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()) @@ -117,7 +122,12 @@ open class JtxCollection(val account: Account, */ fun queryDirtyICalObjects(): List { val values = mutableListOf() - client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DIRTY} = ?", arrayOf(id.toString(), "1"), null).use { cursor -> + client.query( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), + null, + "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DIRTY} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NULL", 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()) @@ -131,7 +141,12 @@ open class JtxCollection(val account: Account, * @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 -> + client.query( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), + null, + "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.FILENAME} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NULL", arrayOf(id.toString(), filename), + null + ).use { cursor -> Ical4Android.log.fine("queryByFilename: found ${cursor?.count} records in ${account.name}") if (cursor?.count != 1) return null @@ -155,6 +170,28 @@ open class JtxCollection(val account: Account, } } + + /** + * @param [uid] of the entry that should be retrieved as content values + * @return Content Values of the found item with the given UID or null if the result was empty or more than 1 + * The query checks for the [uid] within all collections of this account, not only the current collection. + */ + fun queryRecur(uid: String, recurid: String, dtstart: Long): ContentValues? { + client.query( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), + null, + "${JtxContract.JtxICalObject.UID} = ? AND ${JtxContract.JtxICalObject.RECURID} = ? AND ${JtxContract.JtxICalObject.DTSTART} = ?", + arrayOf(uid, recurid, dtstart.toString()), + null + ).use { cursor -> + Ical4Android.log.fine("queryByUID: 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 @@ -163,7 +200,12 @@ open class JtxCollection(val account: Account, 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")) + return client.update( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), + values, + "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DIRTY} = ?", + arrayOf(id.toString(), "0") + ) } /** @@ -188,105 +230,17 @@ open class JtxCollection(val account: Account, } - /** - * 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. - */ - @Deprecated("Moved to jtx Board content provider (function updateRelatedTo()). This function here will be deleted in one of the next versions.") - 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 childId = cursorAllLinkedParents.getString(0) - val parentId = 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(parentId.toString(), childId.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(childId.toString()), null).use { - foundIcalObjectCursor -> - - if (foundIcalObjectCursor?.moveToFirst() == true) { - val childUID = foundIcalObjectCursor.getString(0) - val cv = ContentValues().apply { - put(JtxContract.JtxRelatedto.ICALOBJECT_ID, parentId) - put(JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, childId) - put(JtxContract.JtxRelatedto.RELTYPE, JtxContract.JtxRelatedto.Reltype.CHILD.name) - put(JtxContract.JtxRelatedto.TEXT, childUID) - } - 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 parentId = cursorAllLinkedParents.getLong(0) - val childId = 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(childId.toString(), parentId.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(parentId.toString()), null).use { - foundIcalObjectCursor -> - - if(foundIcalObjectCursor?.moveToFirst() == true) { - val parentUID = foundIcalObjectCursor.getString(0) - val cv = ContentValues().apply { - put(JtxContract.JtxRelatedto.ICALOBJECT_ID, childId) - put(JtxContract.JtxRelatedto.LINKEDICALOBJECT_ID, parentId) - put(JtxContract.JtxRelatedto.RELTYPE, JtxContract.JtxRelatedto.Reltype.PARENT.name) - put(JtxContract.JtxRelatedto.TEXT, parentUID) - } - client.insert(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(account), cv) - } - } - } - } - } - } - } - /** * @return a string with all JtxICalObjects within the collection as iCalendar */ fun getICSForCollection(): String { - client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DELETED} = ?", arrayOf(id.toString(), "0"), null).use { cursor -> + client.query( + JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), + null, + "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DELETED} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NULL", + arrayOf(id.toString(), "0"), + null + ).use { cursor -> Ical4Android.log.fine("getICSForCollection: found ${cursor?.count} records in ${account.name}") val ical = Calendar() diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index 6b19aec2..7dd98164 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -24,7 +24,10 @@ 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.io.FileNotFoundException +import java.io.IOException +import java.io.OutputStream +import java.io.Reader import java.net.URI import java.net.URISyntaxException import java.time.format.DateTimeParseException @@ -100,6 +103,8 @@ open class JtxICalObject( var alarms: MutableList = mutableListOf() var unknown: MutableList = mutableListOf() + private var recurInstances: MutableList = mutableListOf() + @@ -628,6 +633,17 @@ open class JtxICalObject( calComponent.components.add(vAlarm) } + + recurInstances.forEach { recurInstance -> + val recurCalComponent = when (recurInstance.component) { + JtxContract.JtxICalObject.Component.VTODO.name -> VToDo(true /* generates DTSTAMP */) + JtxContract.JtxICalObject.Component.VJOURNAL.name -> VJournal(true /* generates DTSTAMP */) + else -> return null + } + ical.components += recurCalComponent + recurInstance.addProperties(recurCalComponent.properties) + } + ICalendar.softValidate(ical) return ical } @@ -870,7 +886,10 @@ open class JtxICalObject( props += RRule(rrule) } recurid?.let { recurid -> - props += RecurrenceId(recurid) + props += if(dtstartTimezone == TZ_ALLDAY) + RecurrenceId(Date(recurid)) + else + RecurrenceId(DateTime(recurid)) } rdate?.let { rdateString -> @@ -1562,11 +1581,18 @@ duration?.let(props::add) // 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 } + 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) } - unknown.add(unknwn) + + getRecurInstancesContentValues().forEach { recurInstanceValues -> + recurInstances.add( + JtxICalObject(collection).apply { populateFromContentValues(recurInstanceValues) } + ) + } } @@ -1574,54 +1600,47 @@ duration?.let(props::add) * 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) - values.put(JtxContract.JtxICalObject.COLOR, color) - values.put(JtxContract.JtxICalObject.URL, url) - 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) } - - created.let { values.put(JtxContract.JtxICalObject.CREATED, it) } - lastModified.let { values.put(JtxContract.JtxICalObject.LAST_MODIFIED, it) } - sequence.let { values.put(JtxContract.JtxICalObject.SEQUENCE, 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) - values.put(JtxContract.JtxICalObject.DIRTY, dirty) - - return values - } - + private fun toContentValues() = ContentValues().apply { + put(JtxContract.JtxICalObject.ID, id) + put(JtxContract.JtxICalObject.SUMMARY, summary) + put(JtxContract.JtxICalObject.DESCRIPTION, description) + put(JtxContract.JtxICalObject.COMPONENT, component) + put(JtxContract.JtxICalObject.STATUS, status) + put(JtxContract.JtxICalObject.CLASSIFICATION, classification) + put(JtxContract.JtxICalObject.PRIORITY, priority) + put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collectionId) + put(JtxContract.JtxICalObject.UID, uid) + put(JtxContract.JtxICalObject.COLOR, color) + put(JtxContract.JtxICalObject.URL, url) + put(JtxContract.JtxICalObject.GEO_LAT, geoLat) + put(JtxContract.JtxICalObject.GEO_LONG, geoLong) + put(JtxContract.JtxICalObject.LOCATION, location) + put(JtxContract.JtxICalObject.LOCATION_ALTREP, locationAltrep) + put(JtxContract.JtxICalObject.PERCENT, percent) + put(JtxContract.JtxICalObject.DTSTAMP, dtstamp) + put(JtxContract.JtxICalObject.DTSTART, dtstart) + put(JtxContract.JtxICalObject.DTSTART_TIMEZONE, dtstartTimezone) + put(JtxContract.JtxICalObject.DTEND, dtend) + put(JtxContract.JtxICalObject.DTEND_TIMEZONE, dtendTimezone) + put(JtxContract.JtxICalObject.COMPLETED, completed) + put(JtxContract.JtxICalObject.COMPLETED_TIMEZONE, completedTimezone) + put(JtxContract.JtxICalObject.DUE, due) + put(JtxContract.JtxICalObject.DUE_TIMEZONE, dueTimezone) + put(JtxContract.JtxICalObject.DURATION, duration) + put(JtxContract.JtxICalObject.CREATED, created) + put(JtxContract.JtxICalObject.LAST_MODIFIED, lastModified) + put(JtxContract.JtxICalObject.SEQUENCE, sequence) + put(JtxContract.JtxICalObject.RRULE, rrule) + put(JtxContract.JtxICalObject.RDATE, rdate) + put(JtxContract.JtxICalObject.EXDATE, exdate) + put(JtxContract.JtxICalObject.RECURID, recurid) + + put(JtxContract.JtxICalObject.FILENAME, fileName) + put(JtxContract.JtxICalObject.ETAG, eTag) + put(JtxContract.JtxICalObject.SCHEDULETAG, scheduleTag) + put(JtxContract.JtxICalObject.FLAGS, flags) + put(JtxContract.JtxICalObject.DIRTY, dirty) + } /** * @return The categories of the given JtxICalObject as a list of ContentValues @@ -1814,4 +1833,27 @@ duration?.let(props::add) } return unknownValues } + + /** + * @return The unknown properties of the given JtxICalObject as a list of ContentValues + */ + private fun getRecurInstancesContentValues(): List { + + val instancesUrl = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) + val instancesValues: MutableList = mutableListOf() + if(rrule?.isNotEmpty() == true) { + collection.client.query( + instancesUrl, + null, + "${JtxContract.JtxICalObject.UID} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NOT NULL AND ${JtxContract.JtxICalObject.SEQUENCE} > 0", + arrayOf(uid), + null + )?.use { cursor -> + while (cursor.moveToNext()) { + instancesValues.add(cursor.toValues()) + } + } + } + return instancesValues + } } diff --git a/src/main/java/at/bitfire/ical4android/TaskProvider.kt b/src/main/java/at/bitfire/ical4android/TaskProvider.kt index ccb88790..641d0f0c 100644 --- a/src/main/java/at/bitfire/ical4android/TaskProvider.kt +++ b/src/main/java/at/bitfire/ical4android/TaskProvider.kt @@ -28,7 +28,7 @@ class TaskProvider private constructor( private val readPermission: String, private val writePermission: String ) { - JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 101010006, "1.01.01", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), + JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 203000001, "2.03.00", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), TasksOrg("org.tasks.opentasks", "org.tasks", 100000, "10.0", PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE), OpenTasks("org.dmfs.tasks", "org.dmfs.tasks", 103, "1.1.8.2", PERMISSION_OPENTASKS_READ, PERMISSION_OPENTASKS_WRITE); From 43ef1460bf457dadfae9bc432b3041415cd88329 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 18 Feb 2023 11:29:52 +0100 Subject: [PATCH 03/92] Fixes [BUG] Attachment names get scrambled (#83) --- .../at/bitfire/ical4android/JtxICalObject.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index 7dd98164..fc9768fc 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -81,7 +81,7 @@ open class JtxICalObject( 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 rstatus: String? = null var collectionId: Long = collection.id @@ -201,7 +201,8 @@ open class JtxICalObject( companion object { const val X_PROP_COMPLETEDTIMEZONE = "X-COMPLETEDTIMEZONE" - const val X_PARAM_ATTACH_LABEL = "X-LABEL" // used for filename + const val X_PARAM_ATTACH_LABEL = "X-LABEL" // used for filename in KOrganizer + const val X_PARAM_FILENAME = "FILENAME" // used for filename in GNOME Evolution /** * Parses an iCalendar resource and extracts the VTODOs and/or VJOURNALS. @@ -429,6 +430,10 @@ open class JtxICalObject( attachment.filename = it.value prop.parameters.remove(it) } + prop.parameters?.getParameter(X_PARAM_FILENAME)?.let { + attachment.filename = it.value + prop.parameters.remove(it) + } attachment.other = JtxContract.getJsonStringFromXParameters(prop.parameters) @@ -822,7 +827,10 @@ open class JtxICalObject( val attachmentBytes = ParcelFileDescriptor.AutoCloseInputStream(attachmentFile).readBytes() val att = Attach(attachmentBytes).apply { attachment.fmttype?.let { this.parameters.add(FmtType(it)) } - attachment.filename?.let { this.parameters.add(XParameter(X_PARAM_ATTACH_LABEL, it)) } + attachment.filename?.let { + this.parameters.add(XParameter(X_PARAM_ATTACH_LABEL, it)) + this.parameters.add(XParameter(X_PARAM_FILENAME, it)) + } } props += att @@ -830,7 +838,10 @@ open class JtxICalObject( attachment.uri?.let { uri -> val att = Attach(URI(uri)).apply { attachment.fmttype?.let { this.parameters.add(FmtType(it)) } - attachment.filename?.let { this.parameters.add(XParameter(X_PARAM_ATTACH_LABEL, it)) } + attachment.filename?.let { + this.parameters.add(XParameter(X_PARAM_ATTACH_LABEL, it)) + this.parameters.add(XParameter(X_PARAM_FILENAME, it)) + } } props += att } From 1de8a888475156dfe4fdd7e8370b7a6dfcc5294d Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 24 Feb 2023 12:56:08 +0100 Subject: [PATCH 04/92] Update JtxContract.kt (#85) Added owner displayname --- src/main/java/at/techbee/jtx/JtxContract.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/at/techbee/jtx/JtxContract.kt b/src/main/java/at/techbee/jtx/JtxContract.kt index 66ede8b2..a2a65a2e 100644 --- a/src/main/java/at/techbee/jtx/JtxContract.kt +++ b/src/main/java/at/techbee/jtx/JtxContract.kt @@ -42,7 +42,7 @@ object JtxContract { const val AUTHORITY = "at.techbee.jtx.provider" /** The version of this SyncContentProviderContract */ - const val VERSION = 2 + const val VERSION = 3 /** Constructs an Uri for the Jtx Sync Adapter with the given Account * @param [account] The account that should be appended to the Base Uri @@ -1119,11 +1119,17 @@ object JtxContract { const val DESCRIPTION = "description" /** - * Purpose: This column/property defines the owner of the collection. + * Purpose: This column/property defines the URL of the owner of the collection. * Type: [String] */ const val OWNER = "owner" + /** + * Purpose: This column/property defines the display name of the owner of the collection. + * Type: [String] + */ + const val OWNER_DISPLAYNAME = "ownerdisplayname" + /** * Purpose: This column/property defines the color of the collection items. * This color can also be overwritten by the color in an ICalObject. From cf5c663175d16d75cddc614767458de5d92da9e4 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 15 Mar 2023 11:34:29 +0100 Subject: [PATCH 05/92] Updated ical4j to `3.2.10` (#87) * Updated ical4j to `3.2.10` Signed-off-by: Arnau Mora * Removed not needed test Signed-off-by: Arnau Mora * Recovered persian test case Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- build.gradle | 2 +- .../at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 3160e704..7332225a 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { ext.versions = [ kotlin: '1.7.21', dokka: '1.7.20', - ical4j: '3.2.7', + ical4j: '3.2.10', // latest Apache Commons versions that don't require Java 8 (Android 7) commonsIO: '2.6' ] diff --git a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt index 81f91c16..55b57a5a 100644 --- a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt @@ -4,14 +4,13 @@ package at.bitfire.ical4android +import java.time.ZoneOffset +import java.util.Locale import net.fortuna.ical4j.model.property.TzOffsetFrom import org.junit.AfterClass import org.junit.Assert.assertEquals import org.junit.BeforeClass -import org.junit.ComparisonFailure import org.junit.Test -import java.time.ZoneOffset -import java.util.* class LocaleNonWesternDigitsTest { @@ -45,7 +44,7 @@ class LocaleNonWesternDigitsTest { assertEquals("2020", String.format(Locale.ROOT, "%d", 2020)) } - @Test(expected = ComparisonFailure::class) // should not fail in future + @Test() fun testLocale_ical4j() { val offset = TzOffsetFrom(ZoneOffset.ofHours(1)) val iCal = offset.toString() From 8c52fc842d93e6dee62e49ac304ee1288e4e3bfd Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 23 Mar 2023 16:51:03 +0100 Subject: [PATCH 06/92] Ignore invalid properties like empty GEO: (#82) * Updated ical4j to `3.2.9` Signed-off-by: Arnau Mora * Enabled suppress invalid properties Signed-off-by: Arnau Mora * Added test with event with invalid property Signed-off-by: Arnau Mora * Changed invalid geo test ical Signed-off-by: Arnau Mora * Removed trailing comma Signed-off-by: Arnau Mora * Removed old comment Signed-off-by: Arnau Mora * Compressed `invalid-geo.ics` into `testFromReader_invalidProperty` Signed-off-by: Arnau Mora * Add comment for test --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner --- .../at/bitfire/ical4android/ICalendarTest.kt | 22 +++++++++++++++++-- .../java/at/bitfire/ical4android/ICalendar.kt | 12 +++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt b/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt index a62fb725..a2bdff56 100644 --- a/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt @@ -18,8 +18,7 @@ import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due import net.fortuna.ical4j.util.TimeZones -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull +import org.junit.Assert.* import org.junit.Test import java.io.StringReader import java.time.Duration @@ -70,6 +69,25 @@ class ICalendarTest { assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).value) } + @Test + fun testFromReader_invalidProperty() { + // The GEO property is invalid and should be ignored. + // The calendar is however parsed without exception. + assertNotNull(ICalendar.fromReader( + StringReader( + "BEGIN:VCALENDAR\n" + + "PRODID:something\n" + + "VERSION:2.0\n" + + "BEGIN:VEVENT\n" + + "UID:xxx@example.com\n" + + "SUMMARY:Example Event with invalid GEO property\n" + + "GEO:37.7957246371765\n" + + "END:VEVENT\n" + + "END:VCALENDAR" + ) + )) + } + @Test fun testMinifyVTimezone_UTC() { // Keep the only observance for UTC. diff --git a/src/main/java/at/bitfire/ical4android/ICalendar.kt b/src/main/java/at/bitfire/ical4android/ICalendar.kt index 7cbc3fe3..caff81fa 100644 --- a/src/main/java/at/bitfire/ical4android/ICalendar.kt +++ b/src/main/java/at/bitfire/ical4android/ICalendar.kt @@ -6,12 +6,10 @@ package at.bitfire.ical4android import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.validation.ICalPreprocessor -import net.fortuna.ical4j.data.CalendarBuilder -import net.fortuna.ical4j.data.ParserException +import net.fortuna.ical4j.data.* +import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.* import net.fortuna.ical4j.model.parameter.Related import net.fortuna.ical4j.model.property.* @@ -86,7 +84,11 @@ open class ICalendar { // parse stream val calendar: Calendar try { - calendar = CalendarBuilder().build(preprocessed) + calendar = CalendarBuilder( + CalendarParserFactory.getInstance().get(), + ContentHandlerContext().withSupressInvalidProperties(true), + TimeZoneRegistryFactory.getInstance().createRegistry() + ).build(preprocessed) } catch(e: ParserException) { throw InvalidCalendarException("Couldn't parse iCalendar", e) } catch(e: IllegalArgumentException) { From f95d63c4044a730f16379fdafcb112036a1cdc00 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 27 Mar 2023 16:09:21 +0200 Subject: [PATCH 07/92] Handle period durations of days with T prefix (#86) * Fixed invalid day offset parsing (closes #77) Signed-off-by: Arnau Mora * Strengthened tests for `FixInvalidDayOffsetPreprocessor` Signed-off-by: Arnau Mora * Optimized imports Signed-off-by: Arnau Mora * Use mockk to mock objects; ICalPreprocessorTest: test stream processor application * Added ICS test Signed-off-by: Arnau Mora * Removed ICS test Signed-off-by: Arnau Mora * Added multiple durations test Signed-off-by: Arnau Mora * Rename methods and add clarifying comments --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner Co-authored-by: Sunik Kupfer --- build.gradle | 7 +- .../ical4android/ICalPreprocessorTest.kt | 114 +++--------------- .../FixInvalidDayOffsetPreprocessor.kt | 13 +- .../FixInvalidDayOffsetPreprocessorTest.kt | 50 ++++++-- 4 files changed, 67 insertions(+), 117 deletions(-) diff --git a/build.gradle b/build.gradle index 7332225a..3f29a07b 100644 --- a/build.gradle +++ b/build.gradle @@ -95,8 +95,9 @@ dependencies { implementation 'org.slf4j:slf4j-jdk14:2.0.3' implementation 'androidx.core:core-ktx:1.9.0' - androidTestImplementation 'androidx.test:core:1.4.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test:core:1.5.0' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'io.mockk:mockk-android:1.13.4' testImplementation 'junit:junit:4.13.2' } \ No newline at end of file diff --git a/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt b/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt index 31238e2e..aafb3e7a 100644 --- a/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt @@ -4,119 +4,37 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.validation.FixInvalidDayOffsetPreprocessor +import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor import at.bitfire.ical4android.validation.ICalPreprocessor +import io.mockk.mockkObject +import io.mockk.verify +import java.io.InputStreamReader +import java.io.StringReader import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.component.VEvent -import org.apache.commons.io.IOUtils import org.junit.Assert.assertEquals import org.junit.Test -import java.io.InputStreamReader -import java.io.StringReader -import java.time.Duration class ICalPreprocessorTest { @Test - fun testFixInvalidUtcOffset() { - val invalid = "BEGIN:VEVENT" + - "SUMMARY:Test" + - "DTSTART;TZID=Test:19970714T133000" + - "END:VEVENT" + - "BEGIN:VTIMEZONE\n" + - "TZID:Test\n" + - "BEGIN:DAYLIGHT\n" + - "DTSTART:19670430T020000\n" + - "TZOFFSETFROM:-5730\n" + - "TZOFFSETTO:+1920\n" + - "TZNAME:EDT\n" + - "END:DAYLIGHT\n" + - "BEGIN:STANDARD\n" + - "DTSTART:19671029T020000\n" + - "TZOFFSETFROM:-0400\n" + - "TZOFFSETTO:-0500\n" + - "TZNAME:EST" + - "END:STANDARD\n" + - "END:VTIMEZONE" - val valid = "BEGIN:VEVENT" + - "SUMMARY:Test" + - "DTSTART;TZID=Test:19970714T133000" + - "END:VEVENT" + - "BEGIN:VTIMEZONE\n" + - "TZID:Test\n" + - "BEGIN:DAYLIGHT\n" + - "DTSTART:19670430T020000\n" + - "TZOFFSETFROM:-005730\n" + - "TZOFFSETTO:+001920\n" + - "TZNAME:EDT\n" + - "END:DAYLIGHT\n" + - "BEGIN:STANDARD\n" + - "DTSTART:19671029T020000\n" + - "TZOFFSETFROM:-0400\n" + - "TZOFFSETTO:-0500\n" + - "TZNAME:EST" + - "END:STANDARD\n" + - "END:VTIMEZONE" - ICalPreprocessor.preprocessStream(StringReader(invalid)).let { result -> - assertEquals(valid, IOUtils.toString(result)) - } - ICalPreprocessor.preprocessStream(StringReader(valid)).let { result -> - assertEquals(valid, IOUtils.toString(result)) + fun testPreprocessStream_appliesStreamProcessors() { + mockkObject(FixInvalidDayOffsetPreprocessor, FixInvalidUtcOffsetPreprocessor) { + ICalPreprocessor.preprocessStream(StringReader("")) + + // verify that the required stream processors have been called + verify { + FixInvalidDayOffsetPreprocessor.preprocess(any()) + FixInvalidUtcOffsetPreprocessor.preprocess(any()) + } } } - @Test - fun testFixInvalidDuration() { - val invalid = "BEGIN:VEVENT\n" + - "LAST-MODIFIED:20230108T011226Z\n" + - "DTSTAMP:20230108T011226Z\n" + - "X-ECAL-SCHEDULE:63b0e38979739f000d5c1724\n" + - "DTSTART:20230101T015100Z\n" + - "DTEND:20230101T020600Z\n" + - "SUMMARY:This is a test event\n" + - "TRANSP:TRANSPARENT\n" + - "SEQUENCE:0\n" + - "UID:63b0e389453c5d000e1161ae\n" + - "PRIORITY:5\n" + - "X-MICROSOFT-CDO-IMPORTANCE:1\n" + - "CLASS:PUBLIC\n" + - "DESCRIPTION:Example description\n" + - "BEGIN:VALARM\n" + - "TRIGGER:-PT2D\n" + - "ACTION:DISPLAY\n" + - "DESCRIPTION:Reminder\n" + - "END:VALARM\n" + - "END:VEVENT" - val valid = "BEGIN:VEVENT\n" + - "LAST-MODIFIED:20230108T011226Z\n" + - "DTSTAMP:20230108T011226Z\n" + - "X-ECAL-SCHEDULE:63b0e38979739f000d5c1724\n" + - "DTSTART:20230101T015100Z\n" + - "DTEND:20230101T020600Z\n" + - "SUMMARY:This is a test event\n" + - "TRANSP:TRANSPARENT\n" + - "SEQUENCE:0\n" + - "UID:63b0e389453c5d000e1161ae\n" + - "PRIORITY:5\n" + - "X-MICROSOFT-CDO-IMPORTANCE:1\n" + - "CLASS:PUBLIC\n" + - "DESCRIPTION:Example description\n" + - "BEGIN:VALARM\n" + - "TRIGGER:-P2D\n" + - "ACTION:DISPLAY\n" + - "DESCRIPTION:Reminder\n" + - "END:VALARM\n" + - "END:VEVENT" - ICalPreprocessor.preprocessStream(StringReader(invalid)).let { result -> - assertEquals(valid, IOUtils.toString(result)) - } - ICalPreprocessor.preprocessStream(StringReader(valid)).let { result -> - assertEquals(valid, IOUtils.toString(result)) - } - } @Test - fun testMsTimeZones() { + fun testPreprocessCalendar_MsTimeZones() { javaClass.classLoader!!.getResourceAsStream("events/outlook1.ics").use { stream -> val reader = InputStreamReader(stream, Charsets.UTF_8) val calendar = CalendarBuilder().build(reader) diff --git a/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt b/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt index aea8efc9..54559628 100644 --- a/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt +++ b/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt @@ -21,13 +21,16 @@ object FixInvalidDayOffsetPreprocessor : StreamPreprocessor() { override fun fixString(original: String): String { var s: String = original - // Find all matches for the expression - val found = regexpForProblem().find(s) ?: return s - for (match in found.groupValues) { - val fixed = match + // Find all instances matching the defined expression + val found = regexpForProblem().findAll(s) + + // ..and repair them + for (match in found) { + val matchStr = match.value + val fixed = matchStr .replace("PT", "P") .replace("DT", "D") - s = s.replace(match, fixed) + s = s.replace(matchStr, fixed) } return s } diff --git a/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt b/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt index 2271b379..39f7bf23 100644 --- a/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt +++ b/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt @@ -4,15 +4,26 @@ package at.bitfire.ical4android.validation -import org.junit.Assert.* -import org.junit.Test import java.time.Duration +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test class FixInvalidDayOffsetPreprocessorTest { - private fun fixStringAndAssert(expected: String, string: String) { - val fixed = FixInvalidDayOffsetPreprocessor.fixString(string) - Duration.parse(fixed.substring(fixed.indexOf(':') + 1)) + private fun fixAndAssert(expected: String, testValue: String) { + + // Fix the duration string + val fixed = FixInvalidDayOffsetPreprocessor.fixString(testValue) + + // Test the duration can now be parsed + for (line in fixed.split('\n')) { + val duration = line.substring(line.indexOf(':') + 1) + Duration.parse(duration) + } + + // Assert assertEquals(expected, fixed) } @@ -26,17 +37,34 @@ class FixInvalidDayOffsetPreprocessorTest { @Test fun test_FixString_DayOffsetFrom_Invalid() { - fixStringAndAssert("DURATION:-P1D", "DURATION:-PT1D") - fixStringAndAssert("TRIGGER:-P2D", "TRIGGER:-PT2D") + fixAndAssert("DURATION:-P1D", "DURATION:-PT1D") + fixAndAssert("TRIGGER:-P2D", "TRIGGER:-PT2D") - fixStringAndAssert("DURATION:-P1D", "DURATION:-P1DT") - fixStringAndAssert("TRIGGER:-P2D", "TRIGGER:-P2DT") + fixAndAssert("DURATION:-P1D", "DURATION:-P1DT") + fixAndAssert("TRIGGER:-P2D", "TRIGGER:-P2DT") } @Test fun test_FixString_DayOffsetFrom_Valid() { - fixStringAndAssert("DURATION:-PT12H", "DURATION:-PT12H") - fixStringAndAssert("TRIGGER:-PT12H", "TRIGGER:-PT12H") + fixAndAssert("DURATION:-PT12H", "DURATION:-PT12H") + fixAndAssert("TRIGGER:-PT12H", "TRIGGER:-PT12H") + } + + @Test + fun test_FixString_DayOffsetFromMultiple_Invalid() { + fixAndAssert("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-PT1D\nTRIGGER:-PT2D") + fixAndAssert("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-P1DT\nTRIGGER:-P2DT") + } + + @Test + fun test_FixString_DayOffsetFromMultiple_Valid() { + fixAndAssert("DURATION:-PT12H\nTRIGGER:-PT12H", "DURATION:-PT12H\nTRIGGER:-PT12H") + } + + @Test + fun test_FixString_DayOffsetFromMultiple_Mixed() { + fixAndAssert("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-PT1D\nDURATION:-PT12H\nTRIGGER:-PT2D") + fixAndAssert("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-P1DT\nDURATION:-PT12H\nTRIGGER:-P2DT") } @Test From 4b2cea2f36e6e7121021025a156738c027ad0675 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 31 Mar 2023 16:51:11 +0200 Subject: [PATCH 08/92] Use large runners --- .github/workflows/build-kdoc.yml | 5 +- .github/workflows/test-dev.yml | 80 +++++++++++++++++++++----------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index ceecf6f3..4aadd0aa 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -14,14 +14,13 @@ jobs: with: distribution: 'temurin' java-version: 11 - cache: 'gradle' - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/gradle-build-action@v2 - name: Build KDoc run: ./gradlew dokkaHtml - name: Publish KDoc if: success() - uses: crazy-max/ghaction-github-pages@v2.5.0 + uses: crazy-max/ghaction-github-pages@v3 with: target_branch: gh-pages build_dir: build/dokka/html diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index 9f93759a..e0aae02e 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -12,11 +12,10 @@ jobs: with: distribution: 'temurin' java-version: 11 - cache: 'gradle' - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/gradle-build-action@v2 - name: Check - run: ./gradlew check + run: ./gradlew --no-daemon check - name: Archive results uses: actions/upload-artifact@v2 with: @@ -27,42 +26,67 @@ jobs: test_on_emulator: name: Tests with emulator - runs-on: privileged - container: - image: ghcr.io/bitfireat/docker-android-ci:main - options: --privileged - env: - ANDROID_HOME: /sdk - ANDROID_AVD_HOME: /root/.android/avd + runs-on: ubuntu-latest-4-cores + strategy: + matrix: + api-level: [ 31 ] steps: - uses: actions/checkout@v2 with: submodules: true - - uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: 11 + - uses: gradle/gradle-build-action@v2 + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm - - name: Cache APKs and gradle dependencies - uses: actions/cache@v2 + - name: Cache AVD and APKs + uses: actions/cache@v3 + id: avd-cache with: - key: ${{ runner.os }}-1 path: | + ~/.android/avd/* + ~/.android/adb* ~/.apk - ~/.gradle/caches - ~/.gradle/wrapper + key: avd-${{ matrix.api-level }} + + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Install task apps and run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: | + mkdir .apk && cd .apk + wget -cq -O org.dmfs.tasks.apk https://f-droid.org/archive/org.dmfs.tasks_80800.apk && adb install org.dmfs.tasks.apk + wget -cq -O org.tasks.apk https://f-droid.org/archive/org.tasks_120400.apk && adb install org.tasks.apk + wget -cq -O at.techbee.jtx.apk https://f-droid.org/archive/at.techbee.jtx_100140002.apk && adb install at.techbee.jtx.apk + cd .. + ./gradlew --no-daemon connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest - - name: Start emulator - run: start-emulator.sh - - name: Install task apps - run: | - mkdir .apk && cd .apk - wget -cq -O org.dmfs.tasks.apk https://f-droid.org/archive/org.dmfs.tasks_80800.apk && adb install org.dmfs.tasks.apk - wget -cq -O org.tasks.apk https://f-droid.org/archive/org.tasks_120400.apk && adb install org.tasks.apk - wget -cq -O at.techbee.jtx.apk https://f-droid.org/archive/at.techbee.jtx_100140002.apk && adb install at.techbee.jtx.apk - cd .. - - name: Run connected tests - run: ./gradlew connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest - name: Archive results + if: always() uses: actions/upload-artifact@v2 with: name: test-results path: | - build/reports + app/build/reports From f9705ffe97fff28f18f9a6c5fc930277158398c5 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 3 Apr 2023 15:55:03 +0200 Subject: [PATCH 09/92] Added handler for properties with null value when inserting (#89) Fixes #88 * Added handler for properties with null value when inserting Signed-off-by: Arnau Mora * Added test for checking ignored attach with invalid encoding Signed-off-by: Arnau Mora * Removed test Signed-off-by: Arnau Mora * Added test for unknown properties with null value Signed-off-by: Arnau Mora * Optimize imports --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner --- .../java/at/bitfire/ical4android/AndroidEventTest.kt | 11 ++++++++++- .../at/bitfire/ical4android/ICalPreprocessorTest.kt | 4 ++-- .../ical4android/LocaleNonWesternDigitsTest.kt | 4 ++-- src/main/java/at/bitfire/ical4android/AndroidEvent.kt | 4 ++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt index cb094cac..c6dbf3e1 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt @@ -29,7 +29,6 @@ import org.junit.Assert.* import java.net.URI import java.time.Duration import java.time.Period -import java.util.TimeZone class AndroidEventTest { @@ -1269,6 +1268,16 @@ class AndroidEventTest { } } + @Test + fun testBuildUnknownProperty_NoValue() { + buildEvent(true) { + unknownProperties += XProperty("ATTACH", ParameterList(), null) + }.let { result -> + // The property should not have been added, so the first unknown property should be null + assertNull(firstUnknownProperty(result)) + } + } + private fun firstException(values: ContentValues): ContentValues? { val id = values.getAsInteger(Events._ID) provider.query(Events.CONTENT_URI.asSyncAdapter(testAccount), null, diff --git a/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt b/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt index aafb3e7a..96b8e246 100644 --- a/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt @@ -9,13 +9,13 @@ import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor import at.bitfire.ical4android.validation.ICalPreprocessor import io.mockk.mockkObject import io.mockk.verify -import java.io.InputStreamReader -import java.io.StringReader import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Test +import java.io.InputStreamReader +import java.io.StringReader class ICalPreprocessorTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt index 55b57a5a..2d2946f6 100644 --- a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt @@ -4,13 +4,13 @@ package at.bitfire.ical4android -import java.time.ZoneOffset -import java.util.Locale import net.fortuna.ical4j.model.property.TzOffsetFrom import org.junit.AfterClass import org.junit.Assert.assertEquals import org.junit.BeforeClass import org.junit.Test +import java.time.ZoneOffset +import java.util.* class LocaleNonWesternDigitsTest { diff --git a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt index b4823c13..94a57a30 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt @@ -990,6 +990,10 @@ abstract class AndroidEvent( } protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int?, property: Property) { + if (property.value == null) { + Ical4Android.log.warning("Ignoring unknown property with null value") + return + } if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) { Ical4Android.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)") return From 92e3abfc7a278ed1ac45d97a2fe72cb4dd71325b Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 13 Apr 2023 00:21:31 +0200 Subject: [PATCH 10/92] Updated timezone checks (#90) * Updated timezone checks Signed-off-by: Arnau Mora * Renamed variable Signed-off-by: Arnau Mora * Changed regex to equals Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- .../ical4android/util/AndroidTimeUtilsTest.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt index 18a37f97..43471f2e 100644 --- a/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt @@ -4,6 +4,9 @@ package at.bitfire.ical4android.util +import java.io.StringReader +import java.time.Duration +import java.time.Period import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VTimeZone @@ -14,11 +17,11 @@ import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ExDate import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.util.TimeZones -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test -import java.io.StringReader -import java.time.Duration -import java.time.Period class AndroidTimeUtilsTest { @@ -321,8 +324,10 @@ class AndroidTimeUtilsTest { fun testRecurrenceSetsToAndroidString_Date() { // DATEs (without time) have to be converted to T000000Z for Android val list = ArrayList(1) - list.add(RDate(DateList("20150101,20150702", Value.DATE))) - assertEquals("20150101T000000Z,20150702T000000Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, true)) + list.add(RDate(DateList("20150101,20150702", Value.DATE, tzDefault))) + val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, true) + // We ignore the timezone + assertEquals("20150101T000000Z,20150702T000000Z", androidTimeString.substringAfter(';')) } @Test @@ -338,8 +343,10 @@ class AndroidTimeUtilsTest { fun testRecurrenceSetsToAndroidString_TimeAlthoughAllDay() { // DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to T000000Z for Android val list = ArrayList(1) - list.add(RDate(DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME))) - assertEquals("20150101T000000Z,20150702T000000Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, true)) + list.add(RDate(DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME, tzDefault))) + val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, true) + // We ignore the timezone + assertEquals("20150101T000000Z,20150702T000000Z", androidTimeString.substringAfter(';')) } @Test From 5758beea58f1f522ac7a7f4a90acf609d18a6df9 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Thu, 13 Apr 2023 00:21:55 +0200 Subject: [PATCH 11/92] Fixed missing contact field in jtx sync (#91) --- .../at/bitfire/ical4android/JtxICalObject.kt | 399 +++++++++--------- 1 file changed, 202 insertions(+), 197 deletions(-) diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index fc9768fc..0bb1182e 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -323,6 +323,7 @@ open class JtxICalObject( is Description -> iCalObject.description = prop.value is Color -> iCalObject.color = Css3Color.fromString(prop.value)?.argb is Url -> iCalObject.url = prop.value + is Contact -> iCalObject.contact = prop.value is Priority -> iCalObject.priority = prop.level is Clazz -> iCalObject.classification = prop.value is Status -> iCalObject.status = prop.value @@ -700,6 +701,7 @@ open class JtxICalObject( Ical4Android.log.log(Level.WARNING, "Ignoring invalid task URL: $url", e) } } + contact?.let { props += Contact(it) } classification?.let { props += Clazz(it) } status?.let { props += Status(it) } @@ -1390,6 +1392,7 @@ duration?.let(props::add) this.priority = newData.priority this.color = newData.color this.url = newData.url + this.contact = newData.contact this.dtstart = newData.dtstart this.dtstartTimezone = newData.dtstartTimezone @@ -1423,181 +1426,182 @@ duration?.let(props::add) * @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.getAsString(JtxContract.JtxICalObject.DIRTY)?.let { dirty -> this.dirty = dirty == "1" || dirty == "true" } - values.getAsString(JtxContract.JtxICalObject.DELETED)?.let { deleted -> this.deleted = deleted == "1" || deleted == "true" } - - 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) + 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.getAsString(JtxContract.JtxICalObject.CONTACT)?.let { contact -> this.contact = contact } + 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.getAsString(JtxContract.JtxICalObject.DIRTY)?.let { dirty -> this.dirty = dirty == "1" || dirty == "true" } + values.getAsString(JtxContract.JtxICalObject.DELETED)?.let { deleted -> this.deleted = deleted == "1" || deleted == "true" } + + 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 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 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 } + // 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) } + 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.getAsString(JtxContract.JtxAttendee.RSVP)?.let { rsvp -> this.rsvp = rsvp == "1" } - 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 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.getAsString(JtxContract.JtxAttendee.RSVP)?.let { rsvp -> this.rsvp = rsvp == "1" } + 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) - } - if(orgnzr.caladdress?.isNotEmpty() == true) // we only take the organizer if there was a caladdress (otherwise an empty ORGANIZER is created) - 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 } - attachmentValues.getAsString(JtxContract.JtxAttachment.FILENAME)?.let { filename -> this.filename = filename } - } - attachments.add(attachment) + // 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) + } + if(orgnzr.caladdress?.isNotEmpty() == true) // we only take the organizer if there was a caladdress (otherwise an empty ORGANIZER is created) + 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 } + attachmentValues.getAsString(JtxContract.JtxAttachment.FILENAME)?.let { filename -> this.filename = filename } } + 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 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) + // 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) + } getRecurInstancesContentValues().forEach { recurInstanceValues -> recurInstances.add( @@ -1612,46 +1616,47 @@ duration?.let(props::add) * @return The JtxICalObject attributes as [ContentValues] (exluding list properties) */ private fun toContentValues() = ContentValues().apply { - put(JtxContract.JtxICalObject.ID, id) - put(JtxContract.JtxICalObject.SUMMARY, summary) - put(JtxContract.JtxICalObject.DESCRIPTION, description) - put(JtxContract.JtxICalObject.COMPONENT, component) - put(JtxContract.JtxICalObject.STATUS, status) - put(JtxContract.JtxICalObject.CLASSIFICATION, classification) - put(JtxContract.JtxICalObject.PRIORITY, priority) - put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collectionId) - put(JtxContract.JtxICalObject.UID, uid) - put(JtxContract.JtxICalObject.COLOR, color) - put(JtxContract.JtxICalObject.URL, url) - put(JtxContract.JtxICalObject.GEO_LAT, geoLat) - put(JtxContract.JtxICalObject.GEO_LONG, geoLong) - put(JtxContract.JtxICalObject.LOCATION, location) - put(JtxContract.JtxICalObject.LOCATION_ALTREP, locationAltrep) - put(JtxContract.JtxICalObject.PERCENT, percent) - put(JtxContract.JtxICalObject.DTSTAMP, dtstamp) - put(JtxContract.JtxICalObject.DTSTART, dtstart) - put(JtxContract.JtxICalObject.DTSTART_TIMEZONE, dtstartTimezone) - put(JtxContract.JtxICalObject.DTEND, dtend) - put(JtxContract.JtxICalObject.DTEND_TIMEZONE, dtendTimezone) - put(JtxContract.JtxICalObject.COMPLETED, completed) - put(JtxContract.JtxICalObject.COMPLETED_TIMEZONE, completedTimezone) - put(JtxContract.JtxICalObject.DUE, due) - put(JtxContract.JtxICalObject.DUE_TIMEZONE, dueTimezone) - put(JtxContract.JtxICalObject.DURATION, duration) - put(JtxContract.JtxICalObject.CREATED, created) - put(JtxContract.JtxICalObject.LAST_MODIFIED, lastModified) - put(JtxContract.JtxICalObject.SEQUENCE, sequence) - put(JtxContract.JtxICalObject.RRULE, rrule) - put(JtxContract.JtxICalObject.RDATE, rdate) - put(JtxContract.JtxICalObject.EXDATE, exdate) - put(JtxContract.JtxICalObject.RECURID, recurid) - - put(JtxContract.JtxICalObject.FILENAME, fileName) - put(JtxContract.JtxICalObject.ETAG, eTag) - put(JtxContract.JtxICalObject.SCHEDULETAG, scheduleTag) - put(JtxContract.JtxICalObject.FLAGS, flags) - put(JtxContract.JtxICalObject.DIRTY, dirty) - } + put(JtxContract.JtxICalObject.ID, id) + put(JtxContract.JtxICalObject.SUMMARY, summary) + put(JtxContract.JtxICalObject.DESCRIPTION, description) + put(JtxContract.JtxICalObject.COMPONENT, component) + put(JtxContract.JtxICalObject.STATUS, status) + put(JtxContract.JtxICalObject.CLASSIFICATION, classification) + put(JtxContract.JtxICalObject.PRIORITY, priority) + put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collectionId) + put(JtxContract.JtxICalObject.UID, uid) + put(JtxContract.JtxICalObject.COLOR, color) + put(JtxContract.JtxICalObject.URL, url) + put(JtxContract.JtxICalObject.CONTACT, contact) + put(JtxContract.JtxICalObject.GEO_LAT, geoLat) + put(JtxContract.JtxICalObject.GEO_LONG, geoLong) + put(JtxContract.JtxICalObject.LOCATION, location) + put(JtxContract.JtxICalObject.LOCATION_ALTREP, locationAltrep) + put(JtxContract.JtxICalObject.PERCENT, percent) + put(JtxContract.JtxICalObject.DTSTAMP, dtstamp) + put(JtxContract.JtxICalObject.DTSTART, dtstart) + put(JtxContract.JtxICalObject.DTSTART_TIMEZONE, dtstartTimezone) + put(JtxContract.JtxICalObject.DTEND, dtend) + put(JtxContract.JtxICalObject.DTEND_TIMEZONE, dtendTimezone) + put(JtxContract.JtxICalObject.COMPLETED, completed) + put(JtxContract.JtxICalObject.COMPLETED_TIMEZONE, completedTimezone) + put(JtxContract.JtxICalObject.DUE, due) + put(JtxContract.JtxICalObject.DUE_TIMEZONE, dueTimezone) + put(JtxContract.JtxICalObject.DURATION, duration) + put(JtxContract.JtxICalObject.CREATED, created) + put(JtxContract.JtxICalObject.LAST_MODIFIED, lastModified) + put(JtxContract.JtxICalObject.SEQUENCE, sequence) + put(JtxContract.JtxICalObject.RRULE, rrule) + put(JtxContract.JtxICalObject.RDATE, rdate) + put(JtxContract.JtxICalObject.EXDATE, exdate) + put(JtxContract.JtxICalObject.RECURID, recurid) + + put(JtxContract.JtxICalObject.FILENAME, fileName) + put(JtxContract.JtxICalObject.ETAG, eTag) + put(JtxContract.JtxICalObject.SCHEDULETAG, scheduleTag) + put(JtxContract.JtxICalObject.FLAGS, flags) + put(JtxContract.JtxICalObject.DIRTY, dirty) + } /** * @return The categories of the given JtxICalObject as a list of ContentValues From 0133bea8ea1c985d7cde52a9df5026ab486f0f2a Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 28 Apr 2023 17:39:54 +0200 Subject: [PATCH 12/92] Upgrade dependencies; use Java 17 --- .github/workflows/build-kdoc.yml | 4 +- .github/workflows/codeql.yml | 28 +++++-------- .github/workflows/test-dev.yml | 6 +-- build.gradle | 51 ++++++++++++++---------- gradle/wrapper/gradle-wrapper.properties | 2 +- 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index 4aadd0aa..5a783a1f 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -8,12 +8,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - with: - submodules: true - uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: 11 + java-version: 17 - uses: gradle/gradle-build-action@v2 - name: Build KDoc diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9be6f6dc..36683498 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,33 +40,25 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: 17 + - uses: gradle/gradle-build-action@v2 + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + #- name: Autobuild + # uses: github/codeql-action/autobuild@v2 - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + - name: Build + run: ./gradlew --no-daemon assemble - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index e0aae02e..bdb04ee0 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -6,12 +6,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - with: - submodules: true - uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: 11 + java-version: 17 - uses: gradle/gradle-build-action@v2 - name: Check @@ -37,7 +35,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: 11 + java-version: 17 - uses: gradle/gradle-build-action@v2 - name: Enable KVM group perms diff --git a/build.gradle b/build.gradle index 3f29a07b..bdb3c9c3 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ buildscript { ext.versions = [ - kotlin: '1.7.21', - dokka: '1.7.20', - ical4j: '3.2.10', + kotlin: '1.8.20', + dokka: '1.8.10', + ical4j: '3.2.11', // latest Apache Commons versions that don't require Java 8 (Android 7) commonsIO: '2.6' ] @@ -17,7 +17,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:8.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } @@ -36,44 +36,47 @@ android { compileSdkVersion 33 buildToolsVersion '33.0.0' + namespace 'at.bitfire.ical4android' + defaultConfig { minSdkVersion 21 // Android 5.0 - targetSdkVersion 32 // Android 12 + targetSdkVersion 33 // Android 13 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "version_ical4j", "\"${versions.ical4j}\"" } - namespace 'at.bitfire.ical4android' - compileOptions { // ical4j >= 3.x uses the Java 8 Time API coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + buildFeatures { + buildConfig = true } - kotlinOptions { - jvmTarget = "1.8" + + sourceSets { + main.java.srcDirs = [ "src/main/java", "opentasks-contract/src/main/java" ] } + packagingOptions { resources { excludes += ['META-INF/DEPENDENCIES', 'META-INF/LICENSE', 'META-INF/*.md'] } } + lint { disable 'AllowBackup', 'InvalidPackage' } - - sourceSets { - main.java.srcDirs = [ "src/main/java", "opentasks-contract/src/main/java" ] - } } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6' // 2.0.0 produces "Unsupported desugared library configuration version, please upgrade the D8/R8 compiler." + implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' api("org.mnode.ical4j:ical4j:${versions.ical4j}") { // exclude modules which are in conflict with system libraries @@ -85,15 +88,23 @@ dependencies { } // ical4j requires newer Apache Commons libraries, which require Java8. Force latest Java7 versions. // noinspection GradleDependency - api("org.apache.commons:commons-collections4:4.2") { force = true } + api("org.apache.commons:commons-collections4") { + version { + strictly '4.2' + } + } // noinspection GradleDependency - api("org.apache.commons:commons-lang3:3.8.1") { force = true } + api("org.apache.commons:commons-lang3:3.8.1") { + version { + strictly '3.8.1' + } + } // noinspection GradleDependency implementation "commons-io:commons-io:${versions.commonsIO}" implementation 'org.slf4j:slf4j-jdk14:2.0.3' - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.core:core-ktx:1.10.0' androidTestImplementation 'androidx.test:core:1.5.0' androidTestImplementation 'androidx.test:runner:1.5.2' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 92f06b50..3a029079 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 2cdc7261709a090194b05cf9a766d4d94cdb21b7 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 2 May 2023 13:25:59 +0200 Subject: [PATCH 13/92] Build/deploy KDoc over artifact instead of gh-pages branch --- .github/workflows/build-kdoc.yml | 33 ++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index 5a783a1f..dddb7866 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -1,10 +1,16 @@ -name: Build KDoc +name: Build and publish KDoc on: push: branches: [main] + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + jobs: build: - name: Build and publish KDoc runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -15,12 +21,19 @@ jobs: - uses: gradle/gradle-build-action@v2 - name: Build KDoc - run: ./gradlew dokkaHtml - - name: Publish KDoc - if: success() - uses: crazy-max/ghaction-github-pages@v3 + run: ./gradlew --no-daemon dokkaHtml + + - uses: actions/upload-pages-artifact@v1 with: - target_branch: gh-pages - build_dir: build/dokka/html - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + path: build/dokka/html + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 From fa53d4cd9b5c8987e464ec1f4317733f35dc2f45 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 7 May 2023 07:31:54 +0200 Subject: [PATCH 14/92] added x-status to sync, refactored list-properties to use batch operations (#95) --- .../at/bitfire/ical4android/JtxICalObject.kt | 608 +++++++----------- src/main/java/at/techbee/jtx/JtxContract.kt | 9 +- 2 files changed, 227 insertions(+), 390 deletions(-) diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index 0bb1182e..0356f70d 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -50,6 +50,7 @@ open class JtxICalObject( var classification: String? = null var status: String? = null + var xstatus: String? = null var priority: Int? = null @@ -203,6 +204,7 @@ open class JtxICalObject( const val X_PROP_COMPLETEDTIMEZONE = "X-COMPLETEDTIMEZONE" const val X_PARAM_ATTACH_LABEL = "X-LABEL" // used for filename in KOrganizer const val X_PARAM_FILENAME = "FILENAME" // used for filename in GNOME Evolution + const val X_PROP_XSTATUS = "X-STATUS" // used to define an extended status (additionally to standard status) /** * Parses an iCalendar resource and extracts the VTODOs and/or VJOURNALS. @@ -507,18 +509,17 @@ open class JtxICalObject( // 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 + else -> when(prop.name) { + X_PROP_COMPLETEDTIMEZONE -> iCalObject.completedTimezone = prop.value + X_PROP_XSTATUS -> iCalObject.xstatus = prop.value + else -> iCalObject.unknown.add(Unknown(value = UnknownProperty.toJsonString(prop))) // save the whole property for unknown properties } } } @@ -705,7 +706,9 @@ open class JtxICalObject( classification?.let { props += Clazz(it) } status?.let { props += Status(it) } - + xstatus?.let { xstatus -> + props += XProperty(X_PROP_XSTATUS, xstatus) + } val categoryTextList = TextList() categories.forEach { @@ -1149,156 +1152,147 @@ duration?.let(props::add) // 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()) + val deleteBatch = BatchOperation(collection.client) + + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxOrganizer.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${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()) + deleteBatch.enqueue( + BatchOperation.CpoBuilder + .newDelete(JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(collection.account)) + .withSelection("${JtxContract.JtxUnknown.ICALOBJECT_ID} = ?", arrayOf(this.id.toString())) ) + + deleteBatch.commit() } + val insertBatch = BatchOperation(collection.client) + 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 + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxCategory.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxCategory.TEXT, category.text) + .withValue(JtxContract.JtxCategory.ID, category.categoryId) + .withValue(JtxContract.JtxCategory.LANGUAGE, category.language) + .withValue(JtxContract.JtxCategory.OTHER, category.other) ) } 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 + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxComment.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxComment.ID, comment.commentId) + .withValue(JtxContract.JtxComment.TEXT, comment.text) + .withValue(JtxContract.JtxComment.LANGUAGE, comment.language) + .withValue(JtxContract.JtxComment.OTHER, comment.other) ) } 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 + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxResource.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxResource.ID, resource.resourceId) + .withValue(JtxContract.JtxResource.TEXT, resource.text) + .withValue(JtxContract.JtxResource.LANGUAGE, resource.language) + .withValue(JtxContract.JtxResource.OTHER, resource.other) ) } 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 + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxRelatedto.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxRelatedto.TEXT, related.text) + .withValue(JtxContract.JtxRelatedto.RELTYPE, related.reltype) + .withValue(JtxContract.JtxRelatedto.OTHER, related.other) ) } 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 + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxAttendee.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxAttendee.CALADDRESS, attendee.caladdress) + .withValue(JtxContract.JtxAttendee.CN, attendee.cn) + .withValue(JtxContract.JtxAttendee.CUTYPE, attendee.cutype) + .withValue(JtxContract.JtxAttendee.DELEGATEDFROM, attendee.delegatedfrom) + .withValue(JtxContract.JtxAttendee.DELEGATEDTO, attendee.delegatedto) + .withValue(JtxContract.JtxAttendee.DIR, attendee.dir) + .withValue(JtxContract.JtxAttendee.LANGUAGE, attendee.language) + .withValue(JtxContract.JtxAttendee.MEMBER, attendee.member) + .withValue(JtxContract.JtxAttendee.PARTSTAT, attendee.partstat) + .withValue(JtxContract.JtxAttendee.ROLE, attendee.role) + .withValue(JtxContract.JtxAttendee.RSVP, attendee.rsvp) + .withValue(JtxContract.JtxAttendee.SENTBY, attendee.sentby) + .withValue(JtxContract.JtxAttendee.OTHER, attendee.other) ) } - 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.organizer?.let { organizer -> + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxOrganizer.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxOrganizer.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxOrganizer.CALADDRESS, organizer.caladdress) + .withValue(JtxContract.JtxOrganizer.CN, organizer.cn) + .withValue(JtxContract.JtxOrganizer.DIR, organizer.dir) + .withValue(JtxContract.JtxOrganizer.LANGUAGE, organizer.language) + .withValue(JtxContract.JtxOrganizer.SENTBY, organizer.sentby) + .withValue(JtxContract.JtxOrganizer.OTHER, organizer.other) ) } @@ -1310,11 +1304,7 @@ duration?.let(props::add) put(JtxContract.JtxAttachment.OTHER, attachment.other) put(JtxContract.JtxAttachment.FILENAME, attachment.filename) } - val newAttachment = collection.client.insert( - JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter( - collection.account - ), attachmentContentValues - ) + 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)) @@ -1322,37 +1312,35 @@ duration?.let(props::add) } 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 + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxAlarm.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxAlarm.ACTION, alarm.action) + .withValue(JtxContract.JtxAlarm.ATTACH, alarm.attach) + //.withValue(JtxContract.JtxAlarm.ATTENDEE, alarm.attendee) + .withValue(JtxContract.JtxAlarm.DESCRIPTION, alarm.description) + .withValue(JtxContract.JtxAlarm.DURATION, alarm.duration) + .withValue(JtxContract.JtxAlarm.REPEAT, alarm.repeat) + .withValue(JtxContract.JtxAlarm.SUMMARY, alarm.summary) + .withValue(JtxContract.JtxAlarm.TRIGGER_RELATIVE_TO, alarm.triggerRelativeTo) + .withValue(JtxContract.JtxAlarm.TRIGGER_RELATIVE_DURATION, alarm.triggerRelativeDuration) + .withValue(JtxContract.JtxAlarm.TRIGGER_TIME, alarm.triggerTime) + .withValue(JtxContract.JtxAlarm.TRIGGER_TIMEZONE, alarm.triggerTimezone) + .withValue(JtxContract.JtxAlarm.OTHER, alarm.other) ) } 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 + insertBatch.enqueue( + BatchOperation.CpoBuilder + .newInsert(JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(collection.account)) + .withValue(JtxContract.JtxUnknown.ICALOBJECT_ID, id) + .withValue(JtxContract.JtxUnknown.UNKNOWN_VALUE, unknown.value) ) } + + insertBatch.commit() } /** @@ -1389,6 +1377,7 @@ duration?.let(props::add) this.percent = newData.percent this.classification = newData.classification this.status = newData.status + this.xstatus = newData.xstatus this.priority = newData.priority this.color = newData.color this.url = newData.url @@ -1436,6 +1425,7 @@ duration?.let(props::add) 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.EXTENDED_STATUS)?.let { xstatus -> this.xstatus = xstatus } values.getAsString(JtxContract.JtxICalObject.CLASSIFICATION)?.let { classification -> this.classification = classification } values.getAsString(JtxContract.JtxICalObject.URL)?.let { url -> this.url = url } values.getAsString(JtxContract.JtxICalObject.CONTACT)?.let { contact -> this.contact = contact } @@ -1473,8 +1463,11 @@ duration?.let(props::add) // Take care of categories - val categoriesContentValues = getCategoryContentValues() - categoriesContentValues.forEach { catValues -> + getAsContentValues( + uri = JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxCategory.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()) + ).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 } @@ -1485,8 +1478,11 @@ duration?.let(props::add) } // Take care of comments - val commentsContentValues = getCommentContentValues() - commentsContentValues.forEach { commentValues -> + getAsContentValues( + uri = JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxComment.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()) + ).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 } @@ -1497,8 +1493,11 @@ duration?.let(props::add) } // Take care of resources - val resourceContentValues = getResourceContentValues() - resourceContentValues.forEach { resourceValues -> + getAsContentValues( + uri = JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxResource.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()) + ).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 } @@ -1510,8 +1509,11 @@ duration?.let(props::add) // Take care of related-to - val relatedToContentValues = getRelatedToContentValues() - relatedToContentValues.forEach { relatedToValues -> + getAsContentValues( + uri = JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxRelatedto.ICALOBJECT_ID} = ? AND ${JtxContract.JtxRelatedto.RELTYPE} = ?", + selectionArgs = arrayOf(this.id.toString(), JtxContract.JtxRelatedto.Reltype.PARENT.name) + ).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 } @@ -1522,10 +1524,12 @@ duration?.let(props::add) relatedTo.add(relTo) } - // Take care of attendees - val attendeeContentValues = getAttendeesContentValues() - attendeeContentValues.forEach { attendeeValues -> + getAsContentValues( + uri = JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxAttendee.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()) + ).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 } @@ -1546,22 +1550,30 @@ duration?.let(props::add) } // 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) + getAsContentValues( + uri = JtxContract.JtxOrganizer.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxOrganizer.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()) + ).firstOrNull()?.let { organizerContentValues -> + 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) + } + if(orgnzr.caladdress?.isNotEmpty() == true) // we only take the organizer if there was a caladdress (otherwise an empty ORGANIZER is created) + organizer = orgnzr } - if(orgnzr.caladdress?.isNotEmpty() == true) // we only take the organizer if there was a caladdress (otherwise an empty ORGANIZER is created) - organizer = orgnzr // Take care of attachments - val attachmentContentValues = getAttachmentsContentValues() - attachmentContentValues.forEach { attachmentValues -> + getAsContentValues( + uri = JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxAttachment.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()) + ).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 } @@ -1574,8 +1586,11 @@ duration?.let(props::add) } // Take care of alarms - val alarmContentValues = getAlarmsContentValues() - alarmContentValues.forEach { alarmValues -> + getAsContentValues( + uri = JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxAlarm.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()) + ).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 } @@ -1593,9 +1608,13 @@ duration?.let(props::add) alarms.add(alarm) } - // Take care of uknown properties - val unknownContentValues = getUnknownContentValues() - unknownContentValues.forEach { unknownValues -> + + // Take care of unknown properties + getAsContentValues( + uri = JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxUnknown.ICALOBJECT_ID} = ?", + selectionArgs = arrayOf(this.id.toString()), + ).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 } @@ -1603,11 +1622,17 @@ duration?.let(props::add) unknown.add(unknwn) } - getRecurInstancesContentValues().forEach { recurInstanceValues -> - recurInstances.add( - JtxICalObject(collection).apply { populateFromContentValues(recurInstanceValues) } - ) + if(rrule?.isNotEmpty() == true) { + getAsContentValues( + uri = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account), + selection = "${JtxContract.JtxICalObject.UID} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NOT NULL AND ${JtxContract.JtxICalObject.SEQUENCE} > 0", + selectionArgs = arrayOf(uid) + ).forEach { recurInstanceValues -> + recurInstances.add( + JtxICalObject(collection).apply { populateFromContentValues(recurInstanceValues) } + ) + } } } @@ -1621,6 +1646,7 @@ duration?.let(props::add) put(JtxContract.JtxICalObject.DESCRIPTION, description) put(JtxContract.JtxICalObject.COMPONENT, component) put(JtxContract.JtxICalObject.STATUS, status) + put(JtxContract.JtxICalObject.EXTENDED_STATUS, xstatus) put(JtxContract.JtxICalObject.CLASSIFICATION, classification) put(JtxContract.JtxICalObject.PRIORITY, priority) put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collectionId) @@ -1659,217 +1685,21 @@ duration?.let(props::add) } /** - * @return The categories of the given JtxICalObject as a list of ContentValues - */ - private fun getCategoryContentValues(): List { - - val categoryUrl = JtxContract.JtxCategory.CONTENT_URI.asSyncAdapter(collection.account) - val categoryValues: MutableList = 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 + * @return The result of the given query as content values of the given JtxICalObject as a list of ContentValues */ - private fun getCommentContentValues(): List { - - val commentUrl = JtxContract.JtxComment.CONTENT_URI.asSyncAdapter(collection.account) - val commentValues: MutableList = mutableListOf() - collection.client.query( - commentUrl, - null, - "${JtxContract.JtxComment.ICALOBJECT_ID} = ?", - arrayOf(this.id.toString()), - null + private fun getAsContentValues( + uri: Uri, + projection: Array? = null, + selection: String, + selectionArgs: Array, + sortOrder: String? = null + ): List { + + val values: MutableList = mutableListOf() + collection.client.query(uri, projection, selection, selectionArgs, sortOrder )?.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 { - - val resourceUrl = JtxContract.JtxResource.CONTENT_URI.asSyncAdapter(collection.account) - val resourceValues: MutableList = 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 { - - val relatedToUrl = JtxContract.JtxRelatedto.CONTENT_URI.asSyncAdapter(collection.account) - val relatedToValues: MutableList = mutableListOf() - collection.client.query( - relatedToUrl, - null, - "${JtxContract.JtxRelatedto.ICALOBJECT_ID} = ? AND ${JtxContract.JtxRelatedto.RELTYPE} = ?", - arrayOf(this.id.toString(), JtxContract.JtxRelatedto.Reltype.PARENT.name), - 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 { - - val attendeesUrl = JtxContract.JtxAttendee.CONTENT_URI.asSyncAdapter(collection.account) - val attendeesValues: MutableList = 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 { - - val attachmentsUrl = - JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(collection.account) - val attachmentsValues: MutableList = 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 { - - val alarmsUrl = - JtxContract.JtxAlarm.CONTENT_URI.asSyncAdapter(collection.account) - val alarmValues: MutableList = 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 { - - val unknownUrl = - JtxContract.JtxUnknown.CONTENT_URI.asSyncAdapter(collection.account) - val unknownValues: MutableList = 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 - } - - /** - * @return The unknown properties of the given JtxICalObject as a list of ContentValues - */ - private fun getRecurInstancesContentValues(): List { - - val instancesUrl = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) - val instancesValues: MutableList = mutableListOf() - if(rrule?.isNotEmpty() == true) { - collection.client.query( - instancesUrl, - null, - "${JtxContract.JtxICalObject.UID} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NOT NULL AND ${JtxContract.JtxICalObject.SEQUENCE} > 0", - arrayOf(uid), - null - )?.use { cursor -> - while (cursor.moveToNext()) { - instancesValues.add(cursor.toValues()) - } - } + while (cursor.moveToNext()) { values.add(cursor.toValues()) } } - return instancesValues + return values } } diff --git a/src/main/java/at/techbee/jtx/JtxContract.kt b/src/main/java/at/techbee/jtx/JtxContract.kt index a2a65a2e..9328a96a 100644 --- a/src/main/java/at/techbee/jtx/JtxContract.kt +++ b/src/main/java/at/techbee/jtx/JtxContract.kt @@ -42,7 +42,7 @@ object JtxContract { const val AUTHORITY = "at.techbee.jtx.provider" /** The version of this SyncContentProviderContract */ - const val VERSION = 3 + const val VERSION = 4 /** Constructs an Uri for the Jtx Sync Adapter with the given Account * @param [account] The account that should be appended to the Base Uri @@ -272,6 +272,13 @@ object JtxContract { */ const val STATUS = "status" + /** + * Purpose: To specify the filename of the attachment. + * This is an X-PROPERTY that should be addressed as "X-LABEL" + * Type: [String] + */ + const val EXTENDED_STATUS = "xstatus" + /** * Purpose: This property defines the access classification for a calendar component. * The possible values of a status are defined in the enum [Classification]. From b6fc7727765440fd8594045419f8df9bd9077f39 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 10 May 2023 11:59:44 +0200 Subject: [PATCH 15/92] Improve build speed by enabling gradle configuration cache --- gradle.properties | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d9cf55df..66d4a93e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,10 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true +# [https://developer.android.com/build/optimize-your-build#optimize] +org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m +org.gradle.parallel=true + +# configuration cache [https://developer.android.com/build/optimize-your-build#use-the-configuration-cache-experimental] +org.gradle.unsafe.configuration-cache=true +org.gradle.unsafe.configuration-cache-problems=warn + +# Android +android.useAndroidX=true \ No newline at end of file From a51f7e1a8c09f7620829020d62b1f5e5ad52a36f Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 10 May 2023 12:23:19 +0200 Subject: [PATCH 16/92] Ignore custom VTIMEZONEs that can't be properly handled by ical4j (#96) * Improve build speed by enabling gradle configuration cache * Added rules for fixing Dublin timezone Signed-off-by: Arnau Mora * Added rules for fixing Dublin timezone Signed-off-by: Arnau Mora * Added test for Dublin preprocessing Signed-off-by: Arnau Mora * Applying rules for properties and components Signed-off-by: Arnau Mora * Added test for `testTzDublin_external` Signed-off-by: Arnau Mora * Changed rule replacement method Signed-off-by: Arnau Mora * Use ValidatingTimeZoneRegistry to ignore time zone updates that can't be handled properly by ical4j * Rollback Signed-off-by: Arnau Mora Gras * Enable ical4j flag to support timezone definitions with negative DST * Optimize imports * Remove duplicate test --------- Signed-off-by: Arnau Mora Signed-off-by: Arnau Mora Gras Co-authored-by: Arnau Mora --- gradle.properties | 2 +- .../ical4android/AndroidCalendarTest.kt | 6 ++- .../AndroidCompatTimeZoneRegistryTest.kt | 10 +++-- .../bitfire/ical4android/AndroidEventTest.kt | 39 ++++++++++++++++--- .../ical4android/AndroidTaskListTest.kt | 4 +- .../bitfire/ical4android/AndroidTaskTest.kt | 29 ++++++++++++-- .../ical4android/AndroidTimeZonesTest.kt | 2 +- .../ical4android/BatchOperationTest.kt | 9 ++++- .../java/at/bitfire/ical4android/EventTest.kt | 12 +++++- .../at/bitfire/ical4android/ICalendarTest.kt | 15 ++++--- .../at/bitfire/ical4android/Ical4jTest.kt | 12 ++++-- .../bitfire/ical4android/JtxCollectionTest.kt | 11 +++++- .../bitfire/ical4android/JtxICalObjectTest.kt | 14 ++++++- .../LocaleNonWesternDigitsTest.kt | 2 +- .../java/at/bitfire/ical4android/TaskTest.kt | 23 +++++++++-- .../at/bitfire/ical4android/impl/TestEvent.kt | 8 +++- .../ical4android/impl/TestJtxCollection.kt | 2 +- .../ical4android/util/AndroidTimeUtilsTest.kt | 6 +-- .../ical4android/util/DateUtilsTest.kt | 8 +++- .../util/TimeApiExtensionsTest.kt | 10 ++++- .../validation/EventValidatorTest.kt | 12 +++++- .../bitfire/ical4android/AndroidCalendar.kt | 9 ++++- .../AndroidCompatTimeZoneRegistry.kt | 21 ++++++---- .../at/bitfire/ical4android/AndroidTask.kt | 27 +++++++++++-- .../bitfire/ical4android/AndroidTaskList.kt | 2 +- .../at/bitfire/ical4android/BatchOperation.kt | 8 +++- .../java/at/bitfire/ical4android/ICalendar.kt | 24 +++++++++--- .../at/bitfire/ical4android/JtxCollection.kt | 2 +- .../at/bitfire/ical4android/JtxICalObject.kt | 30 ++++++++++++-- .../bitfire/ical4android/UnknownProperty.kt | 7 +++- .../ical4android/util/TimeApiExtensions.kt | 12 +++++- .../ical4android/validation/EventValidator.kt | 3 +- .../validation/ICalPreprocessor.kt | 4 +- .../validation/StreamPreprocessor.kt | 2 +- src/main/resources/ical4j.properties | 1 + .../FixInvalidDayOffsetPreprocessorTest.kt | 2 +- .../FixInvalidUtcOffsetPreprocessorTest.kt | 4 +- 37 files changed, 311 insertions(+), 83 deletions(-) diff --git a/gradle.properties b/gradle.properties index 66d4a93e..38127cb3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.unsafe.configuration-cache=true org.gradle.unsafe.configuration-cache-problems=warn # Android -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt index dbd9c4c4..e443323a 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt @@ -20,9 +20,13 @@ import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeC import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart -import org.junit.* +import org.junit.AfterClass import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test class AndroidCalendarTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt index d4e2209d..cd8764db 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt @@ -7,7 +7,9 @@ package at.bitfire.ical4android import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistry -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assume import org.junit.Before import org.junit.Test @@ -17,9 +19,9 @@ import java.time.zone.ZoneRulesException class AndroidCompatTimeZoneRegistryTest { lateinit var ical4jRegistry: TimeZoneRegistry - lateinit var registry: TimeZoneRegistry + lateinit var registry: AndroidCompatTimeZoneRegistry - val systemKnowsKyiv = + private val systemKnowsKyiv = try { ZoneId.of("Europe/Kyiv") true @@ -29,7 +31,7 @@ class AndroidCompatTimeZoneRegistryTest { @Before fun createRegistry() { - ical4jRegistry = DefaultTimeZoneRegistryFactory.getInstance().createRegistry() + ical4jRegistry = DefaultTimeZoneRegistryFactory().createRegistry() registry = AndroidCompatTimeZoneRegistry.Factory().createRegistry() } diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt index c6dbf3e1..78d9cbd3 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt @@ -10,7 +10,13 @@ import android.content.ContentUris import android.content.ContentValues import android.database.DatabaseUtils import android.net.Uri -import android.provider.CalendarContract.* +import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL +import android.provider.CalendarContract.AUTHORITY +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.ExtendedProperties +import android.provider.CalendarContract.Reminders import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestCalendar @@ -19,13 +25,36 @@ import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterList +import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VAlarm -import net.fortuna.ical4j.model.parameter.* +import net.fortuna.ical4j.model.parameter.Cn +import net.fortuna.ical4j.model.parameter.CuType +import net.fortuna.ical4j.model.parameter.Email +import net.fortuna.ical4j.model.parameter.Language +import net.fortuna.ical4j.model.parameter.PartStat +import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.parameter.Role +import net.fortuna.ical4j.model.parameter.Rsvp +import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.* import net.fortuna.ical4j.util.TimeZones -import org.junit.* -import org.junit.Assert.* +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test import java.net.URI import java.time.Duration import java.time.Period diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt index f7e9398d..b89f6df5 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt @@ -15,7 +15,9 @@ import org.dmfs.tasks.contract.TaskContract import org.dmfs.tasks.contract.TaskContract.Properties import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.Tasks -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue import org.junit.Test class AndroidTaskListTest(providerName: TaskProvider.ProviderName): diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt index 61565cb0..7d2dbc73 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt @@ -16,15 +16,36 @@ import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.parameter.* +import net.fortuna.ical4j.model.parameter.Email +import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.TzId -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.parameter.Value +import net.fortuna.ical4j.model.parameter.XParameter +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Completed +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.Geo +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RelatedTo +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.XProperty import org.dmfs.tasks.contract.TaskContract -import org.dmfs.tasks.contract.TaskContract.* +import org.dmfs.tasks.contract.TaskContract.Properties import org.dmfs.tasks.contract.TaskContract.Property.Category import org.dmfs.tasks.contract.TaskContract.Property.Relation +import org.dmfs.tasks.contract.TaskContract.PropertyColumns +import org.dmfs.tasks.contract.TaskContract.Tasks import org.junit.After -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.time.ZoneId diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt index d6199adc..e4715f25 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt @@ -10,7 +10,7 @@ import org.junit.Assert.assertNotNull import org.junit.Test import java.time.ZoneId import java.time.format.TextStyle -import java.util.* +import java.util.Locale class AndroidTimeZonesTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt b/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt index 4ef31480..8fbc7cf7 100644 --- a/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt @@ -19,11 +19,16 @@ import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeC import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart -import org.junit.* +import org.junit.After +import org.junit.AfterClass import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test import java.net.URI -import java.util.* +import java.util.Arrays class BatchOperationTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/EventTest.kt b/src/androidTest/java/at/bitfire/ical4android/EventTest.kt index e016b07b..83e77474 100644 --- a/src/androidTest/java/at/bitfire/ical4android/EventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/EventTest.kt @@ -11,8 +11,16 @@ import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.Email -import net.fortuna.ical4j.model.property.* -import org.junit.Assert.* +import net.fortuna.ical4j.model.property.Attendee +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test import java.io.ByteArrayOutputStream import java.io.FileNotFoundException diff --git a/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt b/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt index a2bdff56..9136d25f 100644 --- a/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt @@ -18,7 +18,9 @@ import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due import net.fortuna.ical4j.util.TimeZones -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Test import java.io.StringReader import java.time.Duration @@ -27,19 +29,19 @@ import java.time.Period class ICalendarTest { // UTC timezone - val tzUTC = DateUtils.ical4jTimeZone(TimeZones.UTC_ID)!!.vTimeZone + private val tzUTC = DateUtils.ical4jTimeZone(TimeZones.UTC_ID)!!.vTimeZone // Austria (Europa/Vienna) uses DST regularly - val vtzVienna = readTimeZone("Vienna.ics") + private val vtzVienna = readTimeZone("Vienna.ics") // Pakistan (Asia/Karachi) used DST only in 2002, 2008 and 2009; no known future occurrences - val vtzKarachi = readTimeZone("Karachi.ics") + private val vtzKarachi = readTimeZone("Karachi.ics") // Somalia (Africa/Mogadishu) has never used DST - val vtzMogadishu = readTimeZone("Mogadishu.ics") + private val vtzMogadishu = readTimeZone("Mogadishu.ics") // current time stamp - val currentTime = java.util.Date().time + private val currentTime = java.util.Date().time private fun readTimeZone(fileName: String): VTimeZone { @@ -88,6 +90,7 @@ class ICalendarTest { )) } + @Test fun testMinifyVTimezone_UTC() { // Keep the only observance for UTC. diff --git a/src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt b/src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt index 52d988dd..6ddb6ed5 100644 --- a/src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt @@ -5,7 +5,12 @@ package at.bitfire.ical4android import net.fortuna.ical4j.data.CalendarBuilder -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.TemporalAmountAdapter +import net.fortuna.ical4j.model.TimeZone +import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Email import org.junit.Assert.assertEquals @@ -68,9 +73,10 @@ class Ical4jTest { assertEquals(1616720400000, dt2.time) } - @Test(expected = AssertionError::class) - fun testTzDublin_external() { + @Test + fun testTzDublin_negativeDst() { // https://github.com/ical4j/ical4j/issues/493 + // fixed by enabling net.fortuna.ical4j.timezone.offset.negative_dst_supported in ical4j.properties val vtzFromGoogle = "BEGIN:VCALENDAR\n" + "CALSCALE:GREGORIAN\n" + "VERSION:2.0\n" + diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt b/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt index 3e9f8868..57e7f75a 100644 --- a/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt @@ -13,8 +13,15 @@ import at.bitfire.ical4android.impl.TestJtxCollection import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter -import junit.framework.TestCase.* -import org.junit.* +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import org.junit.After +import org.junit.AfterClass +import org.junit.Assume +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test class JtxCollectionTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt index 8cf65ba2..34e2adb2 100644 --- a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -17,10 +17,20 @@ 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 junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property -import org.junit.* +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert +import org.junit.Assume +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test import java.io.ByteArrayOutputStream import java.io.InputStreamReader diff --git a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt index 2d2946f6..d6b3ed9d 100644 --- a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt @@ -10,7 +10,7 @@ import org.junit.Assert.assertEquals import org.junit.BeforeClass import org.junit.Test import java.time.ZoneOffset -import java.util.* +import java.util.Locale class LocaleNonWesternDigitsTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt b/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt index 35b266d5..1056b246 100644 --- a/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt @@ -5,12 +5,29 @@ package at.bitfire.ical4android import at.bitfire.ical4android.util.DateUtils -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.TimeZone +import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.Value -import net.fortuna.ical4j.model.property.* -import org.junit.Assert.* +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.Status +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt index 11270a11..e8a4a76d 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt @@ -6,8 +6,12 @@ package at.bitfire.ical4android.impl import android.content.ContentValues import android.provider.CalendarContract.Events -import at.bitfire.ical4android.* -import java.util.* +import at.bitfire.ical4android.AndroidCalendar +import at.bitfire.ical4android.AndroidEvent +import at.bitfire.ical4android.AndroidEventFactory +import at.bitfire.ical4android.BatchOperation +import at.bitfire.ical4android.Event +import java.util.UUID class TestEvent: AndroidEvent { diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt index 2bef3064..5b0195bf 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt @@ -11,7 +11,7 @@ import at.bitfire.ical4android.JtxCollectionFactory import at.bitfire.ical4android.JtxICalObject import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues import at.techbee.jtx.JtxContract -import java.util.* +import java.util.LinkedList class TestJtxCollection( account: Account, diff --git a/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt index 43471f2e..ef186ea1 100644 --- a/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt @@ -4,9 +4,6 @@ package at.bitfire.ical4android.util -import java.io.StringReader -import java.time.Duration -import java.time.Period import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VTimeZone @@ -22,6 +19,9 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test +import java.io.StringReader +import java.time.Duration +import java.time.Period class AndroidTimeUtilsTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt index fe1f468c..7e7195c2 100644 --- a/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt @@ -8,10 +8,14 @@ import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test import java.time.ZoneId -import java.util.* +import java.util.TimeZone class DateUtilsTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt index e5c6fc08..676cc3ea 100644 --- a/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt @@ -20,7 +20,15 @@ import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -import java.time.* +import java.time.DayOfWeek +import java.time.Duration +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.Period +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime class TimeApiExtensionsTest { diff --git a/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt b/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt index fa3d9594..47375c5f 100644 --- a/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -6,11 +6,19 @@ package at.bitfire.ical4android.validation import at.bitfire.ical4android.Event import at.bitfire.ical4android.InvalidCalendarException -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Recur +import net.fortuna.ical4j.model.TimeZone +import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Assume import org.junit.Test import java.io.StringReader diff --git a/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt b/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt index 127adf03..b393a012 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt @@ -9,11 +9,16 @@ import android.content.ContentProviderClient import android.content.ContentUris import android.content.ContentValues import android.net.Uri -import android.provider.CalendarContract.* +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.CalendarEntity +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Colors +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.Reminders import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import java.io.FileNotFoundException -import java.util.* +import java.util.LinkedList import java.util.logging.Level /** diff --git a/src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt index 14fc31d0..f22ad7c2 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt @@ -1,13 +1,21 @@ package at.bitfire.ical4android -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory +import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.TimeZone +import net.fortuna.ical4j.model.TimeZoneRegistry +import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.TimeZoneRegistryImpl import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.TzId import java.time.ZoneId /** - * The purpose of this class is that if a time zone has a different name in ical4j and Android, - * it should use the Android name. + * Wrapper around default [TimeZoneRegistry] that uses the Android name if a time zone has a + * different name in ical4j and Android. + * + * **This time zone registry is set as default registry for ical4android projects in + * resources/ical4j.properties.** * * For instance, if a time zone is known as "Europe/Kyiv" (with alias "Europe/Kiev") in ical4j * and only "Europe/Kiev" in Android, this registry behaves like the default [TimeZoneRegistryImpl], @@ -32,9 +40,8 @@ class AndroidCompatTimeZoneRegistry( * @return time zone */ override fun getTimeZone(id: String): TimeZone? { - val tz: TimeZone? = base.getTimeZone(id) - if (tz == null) // ical4j doesn't know time zone, return null - return null + val tz: TimeZone = base.getTimeZone(id) + ?: return null // ical4j doesn't know time zone, return null // check whether time zone is available on Android, too val androidTzId = @@ -76,7 +83,7 @@ class AndroidCompatTimeZoneRegistry( class Factory : TimeZoneRegistryFactory() { - override fun createRegistry(): TimeZoneRegistry { + override fun createRegistry(): AndroidCompatTimeZoneRegistry { val ical4jRegistry = DefaultTimeZoneRegistryFactory().createRegistry() return AndroidCompatTimeZoneRegistry(ical4jRegistry) } diff --git a/src/main/java/at/bitfire/ical4android/AndroidTask.kt b/src/main/java/at/bitfire/ical4android/AndroidTask.kt index 1ddcafd5..f442ea4e 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTask.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTask.kt @@ -14,22 +14,41 @@ import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues -import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Completed +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.Geo +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RelatedTo +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Trigger import net.fortuna.ical4j.util.TimeZones import org.dmfs.tasks.contract.TaskContract.Properties -import org.dmfs.tasks.contract.TaskContract.Property.* +import org.dmfs.tasks.contract.TaskContract.Property.Alarm +import org.dmfs.tasks.contract.TaskContract.Property.Category +import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.Tasks import java.io.FileNotFoundException import java.net.URISyntaxException import java.time.ZoneId -import java.util.* +import java.util.Locale import java.util.logging.Level /** diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt b/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt index 8e2a4913..1c30867a 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt @@ -15,7 +15,7 @@ import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.TaskLists import org.dmfs.tasks.contract.TaskContract.Tasks import java.io.FileNotFoundException -import java.util.* +import java.util.LinkedList import java.util.logging.Level diff --git a/src/main/java/at/bitfire/ical4android/BatchOperation.kt b/src/main/java/at/bitfire/ical4android/BatchOperation.kt index a7b881eb..b57c1819 100644 --- a/src/main/java/at/bitfire/ical4android/BatchOperation.kt +++ b/src/main/java/at/bitfire/ical4android/BatchOperation.kt @@ -4,11 +4,15 @@ package at.bitfire.ical4android -import android.content.* +import android.content.ContentProviderClient +import android.content.ContentProviderOperation +import android.content.ContentProviderResult +import android.content.ContentUris +import android.content.OperationApplicationException import android.net.Uri import android.os.RemoteException import android.os.TransactionTooLargeException -import java.util.* +import java.util.LinkedList import java.util.logging.Level class BatchOperation( diff --git a/src/main/java/at/bitfire/ical4android/ICalendar.kt b/src/main/java/at/bitfire/ical4android/ICalendar.kt index caff81fa..71c3b25a 100644 --- a/src/main/java/at/bitfire/ical4android/ICalendar.kt +++ b/src/main/java/at/bitfire/ical4android/ICalendar.kt @@ -7,18 +7,32 @@ package at.bitfire.ical4android import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.validation.ICalPreprocessor import net.fortuna.ical4j.data.* -import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.component.* +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.component.Daylight +import net.fortuna.ical4j.model.component.Observance +import net.fortuna.ical4j.model.component.Standard +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.component.VToDo import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.property.Color +import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.TzUrl +import net.fortuna.ical4j.model.property.XProperty import net.fortuna.ical4j.validate.ValidationException import java.io.Reader import java.io.StringReader import java.time.Duration import java.time.Period -import java.util.* +import java.util.LinkedList +import java.util.UUID import java.util.logging.Level import java.util.logging.Logger @@ -87,7 +101,7 @@ open class ICalendar { calendar = CalendarBuilder( CalendarParserFactory.getInstance().get(), ContentHandlerContext().withSupressInvalidProperties(true), - TimeZoneRegistryFactory.getInstance().createRegistry() + TimeZoneRegistryFactory.getInstance().createRegistry() // AndroidCompatTimeZoneRegistry ).build(preprocessed) } catch(e: ParserException) { throw InvalidCalendarException("Couldn't parse iCalendar", e) diff --git a/src/main/java/at/bitfire/ical4android/JtxCollection.kt b/src/main/java/at/bitfire/ical4android/JtxCollection.kt index a84a43dd..453040cf 100644 --- a/src/main/java/at/bitfire/ical4android/JtxCollection.kt +++ b/src/main/java/at/bitfire/ical4android/JtxCollection.kt @@ -17,7 +17,7 @@ import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.component.VJournal import net.fortuna.ical4j.model.component.VToDo import net.fortuna.ical4j.model.property.Version -import java.util.* +import java.util.LinkedList import java.util.logging.Level open class JtxCollection(val account: Account, diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index 0356f70d..c5fc44ac 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -16,13 +16,37 @@ 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.ComponentList import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterList +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.TextList +import net.fortuna.ical4j.model.TimeZoneRegistryFactory 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.parameter.AltRep +import net.fortuna.ical4j.model.parameter.Cn +import net.fortuna.ical4j.model.parameter.CuType +import net.fortuna.ical4j.model.parameter.DelegatedFrom +import net.fortuna.ical4j.model.parameter.DelegatedTo +import net.fortuna.ical4j.model.parameter.Dir +import net.fortuna.ical4j.model.parameter.FmtType +import net.fortuna.ical4j.model.parameter.Language +import net.fortuna.ical4j.model.parameter.Member +import net.fortuna.ical4j.model.parameter.PartStat +import net.fortuna.ical4j.model.parameter.RelType +import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.parameter.Role +import net.fortuna.ical4j.model.parameter.Rsvp +import net.fortuna.ical4j.model.parameter.SentBy +import net.fortuna.ical4j.model.parameter.Value +import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.* import java.io.FileNotFoundException import java.io.IOException @@ -31,8 +55,8 @@ import java.io.Reader import java.net.URI import java.net.URISyntaxException import java.time.format.DateTimeParseException -import java.util.* import java.util.TimeZone +import java.util.UUID import java.util.logging.Level open class JtxICalObject( diff --git a/src/main/java/at/bitfire/ical4android/UnknownProperty.kt b/src/main/java/at/bitfire/ical4android/UnknownProperty.kt index f1fba041..65373d60 100644 --- a/src/main/java/at/bitfire/ical4android/UnknownProperty.kt +++ b/src/main/java/at/bitfire/ical4android/UnknownProperty.kt @@ -7,7 +7,12 @@ package at.bitfire.ical4android import android.content.ContentResolver import net.fortuna.ical4j.data.DefaultParameterFactorySupplier import net.fortuna.ical4j.data.DefaultPropertyFactorySupplier -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterBuilder +import net.fortuna.ical4j.model.ParameterFactory +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyBuilder +import net.fortuna.ical4j.model.PropertyFactory import org.json.JSONArray import org.json.JSONObject diff --git a/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt b/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt index 9ff5529b..a4aaf443 100644 --- a/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt +++ b/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt @@ -7,9 +7,17 @@ package at.bitfire.ical4android.util import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.util.TimeZones -import java.time.* +import java.time.Duration +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.Period +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime import java.time.temporal.TemporalAmount -import java.util.* +import java.util.Calendar +import java.util.TimeZone object TimeApiExtensions { diff --git a/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt b/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt index 71e61477..5696eef0 100644 --- a/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt +++ b/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt @@ -18,7 +18,8 @@ import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.util.TimeZones import java.time.LocalTime import java.time.ZonedDateTime -import java.util.* +import java.util.Calendar +import java.util.TimeZone /** * Sometimes CalendarStorage or servers respond with invalid event definitions. Here we try to diff --git a/src/main/java/at/bitfire/ical4android/validation/ICalPreprocessor.kt b/src/main/java/at/bitfire/ical4android/validation/ICalPreprocessor.kt index f2cb6a1b..e1731d24 100644 --- a/src/main/java/at/bitfire/ical4android/validation/ICalPreprocessor.kt +++ b/src/main/java/at/bitfire/ical4android/validation/ICalPreprocessor.kt @@ -29,10 +29,10 @@ object ICalPreprocessor { CreatedPropertyRule(), // make sure CREATED is UTC DatePropertyRule(), // These two rules also replace VTIMEZONEs of the iCalendar ... - DateListPropertyRule(), // ... by the ical4j VTIMEZONE with the same TZID! + DateListPropertyRule() // ... by the ical4j VTIMEZONE with the same TZID! ) - val streamPreprocessors = arrayOf( + private val streamPreprocessors = arrayOf( FixInvalidUtcOffsetPreprocessor, // fix things like TZOFFSET(FROM,TO):+5730 FixInvalidDayOffsetPreprocessor // fix things like DURATION:PT2D ) diff --git a/src/main/java/at/bitfire/ical4android/validation/StreamPreprocessor.kt b/src/main/java/at/bitfire/ical4android/validation/StreamPreprocessor.kt index 68f0aaa3..02579c20 100644 --- a/src/main/java/at/bitfire/ical4android/validation/StreamPreprocessor.kt +++ b/src/main/java/at/bitfire/ical4android/validation/StreamPreprocessor.kt @@ -8,7 +8,7 @@ import org.apache.commons.io.IOUtils import java.io.IOException import java.io.Reader import java.io.StringReader -import java.util.* +import java.util.Scanner abstract class StreamPreprocessor { diff --git a/src/main/resources/ical4j.properties b/src/main/resources/ical4j.properties index 32a2d3d8..edc3d429 100644 --- a/src/main/resources/ical4j.properties +++ b/src/main/resources/ical4j.properties @@ -1,4 +1,5 @@ net.fortuna.ical4j.timezone.cache.impl=net.fortuna.ical4j.util.MapTimeZoneCache +net.fortuna.ical4j.timezone.offset.negative_dst_supported=true net.fortuna.ical4j.timezone.registry=at.bitfire.ical4android.AndroidCompatTimeZoneRegistry$Factory net.fortuna.ical4j.timezone.update.enabled=false ical4j.unfolding.relaxed=true diff --git a/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt b/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt index 39f7bf23..9a167ecd 100644 --- a/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt +++ b/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt @@ -4,11 +4,11 @@ package at.bitfire.ical4android.validation -import java.time.Duration import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test +import java.time.Duration class FixInvalidDayOffsetPreprocessorTest { diff --git a/src/test/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt b/src/test/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt index 52cf3aa6..5bb8dec1 100644 --- a/src/test/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt +++ b/src/test/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt @@ -4,7 +4,9 @@ package at.bitfire.ical4android.validation -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class FixInvalidUtcOffsetPreprocessorTest { From 260cee113f062fc4531c316bd8e4b549c8e2c909 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 10 May 2023 12:31:22 +0200 Subject: [PATCH 17/92] Don't use configuration cache for Dokka https://github.com/Kotlin/dokka/issues/1217 --- .github/workflows/build-kdoc.yml | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index dddb7866..51795ae7 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -21,7 +21,7 @@ jobs: - uses: gradle/gradle-build-action@v2 - name: Build KDoc - run: ./gradlew --no-daemon dokkaHtml + run: ./gradlew --no-daemon --no-configuration-cache dokkaHtml - uses: actions/upload-pages-artifact@v1 with: diff --git a/build.gradle b/build.gradle index bdb3c9c3..13e8a9c3 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.0.0' + classpath 'com.android.tools.build:gradle:8.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } From eeca0119ba3febb17fa3c8fffa7a9f289e4cef35 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Thu, 11 May 2023 16:03:35 +0200 Subject: [PATCH 18/92] Add geofence radius x property (#97) * Added geofence radius x-property * updated tests * Update JtxICalObjectTest.kt * Update JtxICalObjectTest.kt --- .../bitfire/ical4android/JtxICalObjectTest.kt | 26 +++++++++++++++---- .../at/bitfire/ical4android/JtxICalObject.kt | 9 +++++++ src/main/java/at/techbee/jtx/JtxContract.kt | 9 ++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt index 34e2adb2..b29f04c7 100644 --- a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -6,7 +6,9 @@ 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 @@ -38,14 +40,14 @@ class JtxICalObjectTest { companion object { - val context = InstrumentationRegistry.getInstrumentation().targetContext - val contentResolver = context.contentResolver + private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + private val contentResolver: ContentResolver = context.contentResolver private lateinit var client: ContentProviderClient @JvmField @ClassRule - val permissionRule = GrantPermissionRule.grant(*TaskProvider.PERMISSIONS_JTX) + val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(*TaskProvider.PERMISSIONS_JTX) @BeforeClass @JvmStatic @@ -65,8 +67,8 @@ class JtxICalObjectTest { } private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE) - var collection: JtxCollection? = null - var sample: at.bitfire.ical4android.JtxICalObject? = null + private var collection: JtxCollection? = null + private var sample: at.bitfire.ical4android.JtxICalObject? = null private val url = "https://jtx.techbee.at" private val displayname = "jtxTest" @@ -95,6 +97,7 @@ class JtxICalObjectTest { this.dtend = System.currentTimeMillis() this.dtendTimezone = "Europe/Paris" this.status = JtxICalObject.StatusJournal.FINAL.name + this.xstatus = "my status" this.classification = JtxICalObject.Classification.PUBLIC.name this.url = "https://jtx.techbee.at" this.contact = "jtx@techbee.at" @@ -102,6 +105,7 @@ class JtxICalObjectTest { this.geoLong = 16.3738 this.location = "Vienna" this.locationAltrep = "Wien" + this.geofenceRadius = 10 this.percent = 99 this.priority = 1 this.due = System.currentTimeMillis() @@ -144,6 +148,12 @@ class JtxICalObjectTest { @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_XSTATUS() { + val jtxVersionCode = context.packageManager.getPackageInfo("at.techbee.jtx", 0).longVersionCode + Assume.assumeTrue(jtxVersionCode > 204020003) + insertRetrieveAssertString(JtxICalObject.EXTENDED_STATUS, sample?.xstatus, 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) @@ -151,6 +161,12 @@ class JtxICalObjectTest { @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_GEOFENCE_RADIUS() { + val jtxVersionCode = context.packageManager.getPackageInfo("at.techbee.jtx", 0).longVersionCode + Assume.assumeTrue(jtxVersionCode > 204020003) + insertRetrieveAssertInt(JtxICalObject.GEOFENCE_RADIUS, sample?.geofenceRadius, 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) diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index c5fc44ac..79d2ec1a 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -91,6 +91,7 @@ open class JtxICalObject( var geoLong: Double? = null var location: String? = null var locationAltrep: String? = null + var geofenceRadius: Int? = null var uid: String = UUID.randomUUID().toString() @@ -229,6 +230,7 @@ open class JtxICalObject( const val X_PARAM_ATTACH_LABEL = "X-LABEL" // used for filename in KOrganizer const val X_PARAM_FILENAME = "FILENAME" // used for filename in GNOME Evolution const val X_PROP_XSTATUS = "X-STATUS" // used to define an extended status (additionally to standard status) + const val X_PROP_GEOFENCE_RADIUS = "X-GEOFENCE-RADIUS" // used to define a Geofence-Radius to notifiy the user when close /** * Parses an iCalendar resource and extracts the VTODOs and/or VJOURNALS. @@ -543,6 +545,7 @@ open class JtxICalObject( else -> when(prop.name) { X_PROP_COMPLETEDTIMEZONE -> iCalObject.completedTimezone = prop.value X_PROP_XSTATUS -> iCalObject.xstatus = prop.value + X_PROP_GEOFENCE_RADIUS -> iCalObject.geofenceRadius = try { prop.value.toInt() } catch (e: NumberFormatException) { Ical4Android.log.warning("Wrong format for geofenceRadius: ${prop.value}"); null } else -> iCalObject.unknown.add(Unknown(value = UnknownProperty.toJsonString(prop))) // save the whole property for unknown properties } } @@ -718,6 +721,9 @@ open class JtxICalObject( if (geoLat != null && geoLong != null) { props += Geo(geoLat!!.toBigDecimal(), geoLong!!.toBigDecimal()) } + geofenceRadius?.let { geofenceRadius -> + props += XProperty(X_PROP_GEOFENCE_RADIUS, geofenceRadius.toString()) + } color?.let { props += Color(null, Css3Color.nearestMatch(it).name) } url?.let { try { @@ -1398,6 +1404,7 @@ duration?.let(props::add) this.locationAltrep = newData.locationAltrep this.geoLat = newData.geoLat this.geoLong = newData.geoLong + this.geofenceRadius = newData.geofenceRadius this.percent = newData.percent this.classification = newData.classification this.status = newData.status @@ -1457,6 +1464,7 @@ duration?.let(props::add) 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.GEOFENCE_RADIUS)?.let { geofenceRadius -> this.geofenceRadius = geofenceRadius } 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 } @@ -1682,6 +1690,7 @@ duration?.let(props::add) put(JtxContract.JtxICalObject.GEO_LONG, geoLong) put(JtxContract.JtxICalObject.LOCATION, location) put(JtxContract.JtxICalObject.LOCATION_ALTREP, locationAltrep) + put(JtxContract.JtxICalObject.GEOFENCE_RADIUS, geofenceRadius) put(JtxContract.JtxICalObject.PERCENT, percent) put(JtxContract.JtxICalObject.DTSTAMP, dtstamp) put(JtxContract.JtxICalObject.DTSTART, dtstart) diff --git a/src/main/java/at/techbee/jtx/JtxContract.kt b/src/main/java/at/techbee/jtx/JtxContract.kt index 9328a96a..7d5eace0 100644 --- a/src/main/java/at/techbee/jtx/JtxContract.kt +++ b/src/main/java/at/techbee/jtx/JtxContract.kt @@ -42,7 +42,7 @@ object JtxContract { const val AUTHORITY = "at.techbee.jtx.provider" /** The version of this SyncContentProviderContract */ - const val VERSION = 4 + const val VERSION = 5 /** Constructs an Uri for the Jtx Sync Adapter with the given Account * @param [account] The account that should be appended to the Base Uri @@ -279,6 +279,13 @@ object JtxContract { */ const val EXTENDED_STATUS = "xstatus" + /** + * Purpose: Defines the radius for a geofence in meters + * This is put into an extended property in the iCalendar-file + * Type: [String] + */ + const val GEOFENCE_RADIUS = "geofenceRadius" + /** * Purpose: This property defines the access classification for a calendar component. * The possible values of a status are defined in the enum [Classification]. From 8c34e814a44f75f4b1200e2f031d1c469d181a63 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 12 May 2023 15:59:50 +0200 Subject: [PATCH 19/92] Update TaskProvider.kt (#98) Updated minVersion for jtx --- src/main/java/at/bitfire/ical4android/TaskProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/at/bitfire/ical4android/TaskProvider.kt b/src/main/java/at/bitfire/ical4android/TaskProvider.kt index 641d0f0c..ea874d78 100644 --- a/src/main/java/at/bitfire/ical4android/TaskProvider.kt +++ b/src/main/java/at/bitfire/ical4android/TaskProvider.kt @@ -28,7 +28,7 @@ class TaskProvider private constructor( private val readPermission: String, private val writePermission: String ) { - JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 203000001, "2.03.00", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), + JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 204030000, "2.04.03", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), TasksOrg("org.tasks.opentasks", "org.tasks", 100000, "10.0", PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE), OpenTasks("org.dmfs.tasks", "org.dmfs.tasks", 103, "1.1.8.2", PERMISSION_OPENTASKS_READ, PERMISSION_OPENTASKS_WRITE); From 75e5be4f06cbf41a71f1e9d72113ff1ec5d51e4c Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 26 May 2023 16:05:30 +0200 Subject: [PATCH 20/92] Added recuridTimezone and adapted logic for RecurrenceId (#100) * Added recuridTimezone and adapted logic for RecurrenceId * minor code improvement --- .../at/bitfire/ical4android/JtxICalObject.kt | 25 ++++++++++++++----- src/main/java/at/techbee/jtx/JtxContract.kt | 13 +++++++++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index 79d2ec1a..dd813ad5 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -106,7 +106,7 @@ open class JtxICalObject( 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 recuridTimezone: String? = null //var rstatus: String? = null var collectionId: Long = collection.id @@ -416,7 +416,15 @@ open class JtxICalObject( } iCalObject.exdate = exdateList.toTypedArray().joinToString(separator = ",") } - is RecurrenceId -> iCalObject.recurid = prop.value + is RecurrenceId -> { + iCalObject.recurid = prop.date.toString() + iCalObject.recuridTimezone = when { + prop.date is DateTime && prop.timeZone != null -> prop.timeZone.id + prop.date is DateTime && prop.isUtc -> TimeZone.getTimeZone("UTC").id + prop.date is DateTime && !prop.isUtc && prop.timeZone == null -> null + else -> TZ_ALLDAY // prop.date is Date (and not DateTime), therefore it must be Allday + } + } //is RequestStatus -> iCalObject.rstatus = prop.value @@ -932,10 +940,12 @@ open class JtxICalObject( props += RRule(rrule) } recurid?.let { recurid -> - props += if(dtstartTimezone == TZ_ALLDAY) - RecurrenceId(Date(recurid)) - else - RecurrenceId(DateTime(recurid)) + props += when { + recuridTimezone == TZ_ALLDAY -> RecurrenceId(Date(recurid)) + recuridTimezone == TimeZone.getTimeZone("UTC").id -> RecurrenceId(DateTime(recurid).apply { this.isUtc = true }) + recuridTimezone.isNullOrEmpty() -> RecurrenceId(DateTime(recurid).apply { this.isUtc = false }) + else -> RecurrenceId(DateTime(recurid, TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(recuridTimezone))) + } } rdate?.let { rdateString -> @@ -1428,6 +1438,7 @@ duration?.let(props::add) this.rdate = newData.rdate this.exdate = newData.exdate this.recurid = newData.recurid + this.recuridTimezone = newData.recuridTimezone this.categories = newData.categories @@ -1483,6 +1494,7 @@ duration?.let(props::add) 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 } + values.getAsString(JtxContract.JtxICalObject.RECURID_TIMEZONE)?.let { recuridTimezone -> this.recuridTimezone = recuridTimezone } this.collectionId = collection.id values.getAsString(JtxContract.JtxICalObject.DIRTY)?.let { dirty -> this.dirty = dirty == "1" || dirty == "true" } @@ -1709,6 +1721,7 @@ duration?.let(props::add) put(JtxContract.JtxICalObject.RDATE, rdate) put(JtxContract.JtxICalObject.EXDATE, exdate) put(JtxContract.JtxICalObject.RECURID, recurid) + put(JtxContract.JtxICalObject.RECURID_TIMEZONE, recuridTimezone) put(JtxContract.JtxICalObject.FILENAME, fileName) put(JtxContract.JtxICalObject.ETAG, eTag) diff --git a/src/main/java/at/techbee/jtx/JtxContract.kt b/src/main/java/at/techbee/jtx/JtxContract.kt index 7d5eace0..8d6daf94 100644 --- a/src/main/java/at/techbee/jtx/JtxContract.kt +++ b/src/main/java/at/techbee/jtx/JtxContract.kt @@ -42,7 +42,7 @@ object JtxContract { const val AUTHORITY = "at.techbee.jtx.provider" /** The version of this SyncContentProviderContract */ - const val VERSION = 5 + const val VERSION = 6 /** Constructs an Uri for the Jtx Sync Adapter with the given Account * @param [account] The account that should be appended to the Base Uri @@ -450,6 +450,17 @@ object JtxContract { */ const val RECURID = "recurid" + /** + * 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_TIMEZONE = "recuridtimezone" + /** * 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 From a78e72f580bbe02d2cd65e183dbbb39bef37a203 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 28 Jun 2023 17:37:17 +0200 Subject: [PATCH 21/92] Make it a real library (#102) * Moved files to module Signed-off-by: Arnau Mora Gras * Configured modules Signed-off-by: Arnau Mora Gras * Fixed paths Signed-off-by: Arnau Mora Gras * Configured Jitpack Signed-off-by: Arnau Mora Gras * Updated usage instructions Signed-off-by: Arnau Mora Gras * Config for `maven-publish` Signed-off-by: Arnau Mora Gras * typo Signed-off-by: Arnau Mora * Deprecations Signed-off-by: Arnau Mora * Using environment variable for version Signed-off-by: Arnau Mora * Excluded `META-INF/INDEX.LIST` Signed-off-by: Arnau Mora * Included `META-INF/INDEX.LIST` again Signed-off-by: Arnau Mora * Excluded dateutils Signed-off-by: Arnau Mora * Excluded all groovy trans. dependencies Signed-off-by: Arnau Mora * Excluded groovy with configurations Signed-off-by: Arnau Mora * Removed transitive dependencies Signed-off-by: Arnau Mora * Minor changes, rename source directories from java to kotlin --------- Signed-off-by: Arnau Mora Gras Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner --- .github/workflows/build-kdoc.yml | 4 +- README.md | 25 +++- build.gradle | 114 ---------------- build.gradle.kts | 12 ++ jitpack.yml | 5 + lib/build.gradle.kts | 126 ++++++++++++++++++ .../bitfire/ical4android/AbstractTasksTest.kt | 0 .../ical4android/AndroidCalendarTest.kt | 0 .../AndroidCompatTimeZoneRegistryTest.kt | 0 .../bitfire/ical4android/AndroidEventTest.kt | 0 .../ical4android/AndroidTaskListTest.kt | 0 .../bitfire/ical4android/AndroidTaskTest.kt | 0 .../ical4android/AndroidTimeZonesTest.kt | 0 .../at/bitfire/ical4android/AospTest.kt | 0 .../ical4android/AttendeeMappingsTest.kt | 0 .../ical4android/BatchOperationTest.kt | 0 .../at/bitfire/ical4android/Css3ColorTest.kt | 0 .../at/bitfire/ical4android/EventTest.kt | 0 .../ical4android/ICalPreprocessorTest.kt | 0 .../at/bitfire/ical4android/ICalendarTest.kt | 0 .../ical4android/Ical4jSettingsTest.kt | 0 .../at/bitfire/ical4android/Ical4jTest.kt | 0 .../bitfire/ical4android/JtxCollectionTest.kt | 0 .../bitfire/ical4android/JtxICalObjectTest.kt | 0 .../LocaleNonWesternDigitsTest.kt | 0 .../at/bitfire/ical4android/TaskTest.kt | 0 .../ical4android/UnknownPropertyTest.kt | 0 .../bitfire/ical4android/impl/TestCalendar.kt | 0 .../at/bitfire/ical4android/impl/TestEvent.kt | 0 .../ical4android/impl/TestJtxCollection.kt | 0 .../ical4android/impl/TestJtxIcalObject.kt | 0 .../at/bitfire/ical4android/impl/TestTask.kt | 0 .../bitfire/ical4android/impl/TestTaskList.kt | 0 .../ical4android/util/AndroidTimeUtilsTest.kt | 0 .../ical4android/util/DateUtilsTest.kt | 0 .../ical4android/util/MiscUtilsTest.kt | 0 .../util/TimeApiExtensionsTest.kt | 0 .../validation/EventValidatorTest.kt | 0 .../resources/events/all-day-0sec.ics | 0 .../resources/events/all-day-10days.ics | 0 .../resources/events/all-day-1day.ics | 0 .../resources/events/dst-only-vtimezone.ics | 0 .../resources/events/event-on-that-day.ics | 0 .../androidTest/resources/events/latin1.ics | 0 .../androidTest/resources/events/multiple.ics | 0 .../one-event-with-exception-one-without.ics | 0 ...t-with-multiple-exceptions-one-without.ics | 0 .../androidTest/resources/events/outlook1.ics | 0 .../events/recurring-only-exception.ics | 0 .../events/recurring-with-exception1.ics | 0 .../events/two-events-without-exceptions.ics | 0 .../two-line-description-without-crlf.ics | 0 .../androidTest/resources/events/utf8.ics | 0 .../resources/events/vienna-evolution.ics | 0 .../resources/jtx/vjournal/all-day.ics | 0 .../jtx/vjournal/default-example-note.ics | 0 .../jtx/vjournal/default-example.ics | 0 .../jtx/vjournal/dst-only-vtimezone.ics | 0 .../jtx/vjournal/journal-on-that-day.ics | 0 .../resources/jtx/vjournal/latin1.ics | 0 .../jtx/vjournal/outlook-theoretical.ics | 0 .../jtx/vjournal/outlook-theoretical2.ics | 0 .../resources/jtx/vjournal/recurring.ics | 0 .../two-events-without-exceptions.ics | 0 .../two-line-description-without-crlf.ics | 0 .../resources/jtx/vjournal/utf8.ics | 0 .../resources/jtx/vtodo/empty-priority.ics | 0 .../resources/jtx/vtodo/latin1.ics | 0 .../resources/jtx/vtodo/most-fields1.ics | 0 .../resources/jtx/vtodo/most-fields2.ics | 0 .../resources/jtx/vtodo/rfc5545-sample1.ics | 0 .../androidTest/resources/jtx/vtodo/utf8.ics | 0 .../resources/tasks/empty-priority.ics | 0 .../androidTest/resources/tasks/latin1.ics | 0 .../resources/tasks/most-fields1.ics | 0 .../resources/tasks/most-fields2.ics | 0 .../resources/tasks/rfc5545-sample1.ics | 0 .../src}/androidTest/resources/tasks/utf8.ics | 0 .../src}/androidTest/resources/tz/Karachi.ics | 0 .../androidTest/resources/tz/Mogadishu.ics | 0 .../src}/androidTest/resources/tz/Vienna.ics | 0 {src => lib/src}/main/AndroidManifest.xml | 0 .../bitfire/ical4android/AndroidCalendar.kt | 0 .../ical4android/AndroidCalendarFactory.kt | 0 .../AndroidCompatTimeZoneRegistry.kt | 0 .../at/bitfire/ical4android/AndroidEvent.kt | 0 .../ical4android/AndroidEventFactory.kt | 0 .../at/bitfire/ical4android/AndroidTask.kt | 0 .../ical4android/AndroidTaskFactory.kt | 0 .../bitfire/ical4android/AndroidTaskList.kt | 0 .../ical4android/AndroidTaskListFactory.kt | 0 .../bitfire/ical4android/AttendeeMappings.kt | 0 .../at/bitfire/ical4android/BatchOperation.kt | 0 .../ical4android/CalendarStorageException.kt | 0 .../at/bitfire/ical4android/Css3Color.kt | 0 .../kotlin}/at/bitfire/ical4android/Event.kt | 0 .../at/bitfire/ical4android/ICalendar.kt | 0 .../at/bitfire/ical4android/Ical4Android.kt | 0 .../ical4android/InvalidCalendarException.kt | 0 .../at/bitfire/ical4android/JtxCollection.kt | 0 .../ical4android/JtxCollectionFactory.kt | 0 .../at/bitfire/ical4android/JtxICalObject.kt | 0 .../ical4android/JtxICalObjectFactory.kt | 0 .../kotlin}/at/bitfire/ical4android/Task.kt | 0 .../at/bitfire/ical4android/TaskProvider.kt | 0 .../bitfire/ical4android/UnknownProperty.kt | 0 .../UsesThreadContextClassLoader.kt | 0 .../ical4android/util/AndroidTimeUtils.kt | 0 .../at/bitfire/ical4android/util/DateUtils.kt | 0 .../at/bitfire/ical4android/util/MiscUtils.kt | 0 .../ical4android/util/TimeApiExtensions.kt | 0 .../ical4android/validation/EventValidator.kt | 0 .../FixInvalidDayOffsetPreprocessor.kt | 0 .../FixInvalidUtcOffsetPreprocessor.kt | 0 .../validation/ICalPreprocessor.kt | 0 .../validation/StreamPreprocessor.kt | 0 .../kotlin}/at/techbee/jtx/JtxContract.kt | 0 .../src}/main/resources/ical4j.properties | 0 {src => lib/src}/main/resources/tz.alias | 0 {src => lib/src}/test/README.txt | 0 .../at/bitfire/ical4android/MiscUtilsTest.kt | 0 .../FixInvalidDayOffsetPreprocessorTest.kt | 0 .../FixInvalidUtcOffsetPreprocessorTest.kt | 0 settings.gradle | 23 ++++ 124 files changed, 188 insertions(+), 121 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 jitpack.yml create mode 100644 lib/build.gradle.kts rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AbstractTasksTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AndroidCalendarTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AndroidEventTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AndroidTaskListTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AndroidTaskTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AndroidTimeZonesTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AospTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/AttendeeMappingsTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/BatchOperationTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/Css3ColorTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/EventTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/ICalPreprocessorTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/ICalendarTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/Ical4jSettingsTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/Ical4jTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/JtxCollectionTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/JtxICalObjectTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/TaskTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/UnknownPropertyTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/impl/TestCalendar.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/impl/TestEvent.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/impl/TestJtxCollection.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/impl/TestJtxIcalObject.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/impl/TestTask.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/impl/TestTaskList.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/util/DateUtilsTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/util/MiscUtilsTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt (100%) rename {src/androidTest/java => lib/src/androidTest/kotlin}/at/bitfire/ical4android/validation/EventValidatorTest.kt (100%) rename {src => lib/src}/androidTest/resources/events/all-day-0sec.ics (100%) rename {src => lib/src}/androidTest/resources/events/all-day-10days.ics (100%) rename {src => lib/src}/androidTest/resources/events/all-day-1day.ics (100%) rename {src => lib/src}/androidTest/resources/events/dst-only-vtimezone.ics (100%) rename {src => lib/src}/androidTest/resources/events/event-on-that-day.ics (100%) rename {src => lib/src}/androidTest/resources/events/latin1.ics (100%) rename {src => lib/src}/androidTest/resources/events/multiple.ics (100%) rename {src => lib/src}/androidTest/resources/events/one-event-with-exception-one-without.ics (100%) rename {src => lib/src}/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics (100%) rename {src => lib/src}/androidTest/resources/events/outlook1.ics (100%) rename {src => lib/src}/androidTest/resources/events/recurring-only-exception.ics (100%) rename {src => lib/src}/androidTest/resources/events/recurring-with-exception1.ics (100%) rename {src => lib/src}/androidTest/resources/events/two-events-without-exceptions.ics (100%) rename {src => lib/src}/androidTest/resources/events/two-line-description-without-crlf.ics (100%) rename {src => lib/src}/androidTest/resources/events/utf8.ics (100%) rename {src => lib/src}/androidTest/resources/events/vienna-evolution.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/all-day.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/default-example-note.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/default-example.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/journal-on-that-day.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/latin1.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/outlook-theoretical.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/recurring.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vjournal/utf8.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vtodo/empty-priority.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vtodo/latin1.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vtodo/most-fields1.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vtodo/most-fields2.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics (100%) rename {src => lib/src}/androidTest/resources/jtx/vtodo/utf8.ics (100%) rename {src => lib/src}/androidTest/resources/tasks/empty-priority.ics (100%) rename {src => lib/src}/androidTest/resources/tasks/latin1.ics (100%) rename {src => lib/src}/androidTest/resources/tasks/most-fields1.ics (100%) rename {src => lib/src}/androidTest/resources/tasks/most-fields2.ics (100%) rename {src => lib/src}/androidTest/resources/tasks/rfc5545-sample1.ics (100%) rename {src => lib/src}/androidTest/resources/tasks/utf8.ics (100%) rename {src => lib/src}/androidTest/resources/tz/Karachi.ics (100%) rename {src => lib/src}/androidTest/resources/tz/Mogadishu.ics (100%) rename {src => lib/src}/androidTest/resources/tz/Vienna.ics (100%) rename {src => lib/src}/main/AndroidManifest.xml (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidCalendar.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidCalendarFactory.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidEvent.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidEventFactory.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidTask.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidTaskFactory.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidTaskList.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AndroidTaskListFactory.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/AttendeeMappings.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/BatchOperation.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/CalendarStorageException.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/Css3Color.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/Event.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/ICalendar.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/Ical4Android.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/InvalidCalendarException.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/JtxCollection.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/JtxCollectionFactory.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/JtxICalObject.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/JtxICalObjectFactory.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/Task.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/TaskProvider.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/UnknownProperty.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/UsesThreadContextClassLoader.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/util/AndroidTimeUtils.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/util/DateUtils.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/util/MiscUtils.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/util/TimeApiExtensions.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/validation/EventValidator.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/validation/ICalPreprocessor.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/bitfire/ical4android/validation/StreamPreprocessor.kt (100%) rename {src/main/java => lib/src/main/kotlin}/at/techbee/jtx/JtxContract.kt (100%) rename {src => lib/src}/main/resources/ical4j.properties (100%) rename {src => lib/src}/main/resources/tz.alias (100%) rename {src => lib/src}/test/README.txt (100%) rename {src/test/java => lib/src/test/kotlin}/at/bitfire/ical4android/MiscUtilsTest.kt (100%) rename {src/test/java => lib/src/test/kotlin}/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt (100%) rename {src/test/java => lib/src/test/kotlin}/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt (100%) create mode 100644 settings.gradle diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index 51795ae7..c2b3d654 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -21,11 +21,11 @@ jobs: - uses: gradle/gradle-build-action@v2 - name: Build KDoc - run: ./gradlew --no-daemon --no-configuration-cache dokkaHtml + run: ./gradlew --no-daemon --no-configuration-cache ical4android:dokkaHtml - uses: actions/upload-pages-artifact@v1 with: - path: build/dokka/html + path: lib/build/dokka/html deploy: environment: diff --git a/README.md b/README.md index f0c28ddb..59aa56be 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![Development tests](https://github.com/bitfireAT/ical4android/actions/workflows/test-dev.yml/badge.svg)](https://github.com/bitfireAT/ical4android/actions/workflows/test-dev.yml) [![Documentation](https://img.shields.io/badge/documentation-kdoc-brightgreen)](https://bitfireat.github.io/ical4android/) +[![Latest Version](https://img.shields.io/jitpack/version/com.github.bitfireAT/ical4android)](https://jitpack.io/#bitfireAT/ical4android) # ical4android @@ -32,17 +33,31 @@ by Google LLC. Android is a trademark of Google LLC._ ## How to use -You can use ical4android as a git submodule or using [jitpack.io](https://jitpack.io/#bitfireAT/ical4android): - +1. Add the [jitpack.io](https://jitpack.io) repository to your project's level `build.gradle`: + ```groovy allprojects { repositories { - maven { url 'https://jitpack.io' } + // ... more repos + maven { url "https://jitpack.io" } + } + } + ``` + or if you are using `settings.gradle`: + ```groovy + dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + // ... more repos + maven { url "https://jitpack.io" } } } + ``` +2. Add the dependency to your module's `build.gradle` file: + ```groovy dependencies { - implementation 'com.github.bitfireAT:ical4android:' // see tags for latest version, like 1.0, or use the latest commit ID from main branch - //implementation 'com.github.bitfireAT:ical4android:main-SNAPSHOT' // use it only for testing because it doesn't generate reproducible builds + implementation 'com.github.bitfireAT:ical4android:' } + ``` ## Contact diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 13e8a9c3..00000000 --- a/build.gradle +++ /dev/null @@ -1,114 +0,0 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ - -buildscript { - ext.versions = [ - kotlin: '1.8.20', - dokka: '1.8.10', - ical4j: '3.2.11', - // latest Apache Commons versions that don't require Java 8 (Android 7) - commonsIO: '2.6' - ] - - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.0.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" - classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" - } -} - -repositories { - google() - mavenCentral() -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'org.jetbrains.dokka' - -android { - compileSdkVersion 33 - buildToolsVersion '33.0.0' - - namespace 'at.bitfire.ical4android' - - defaultConfig { - minSdkVersion 21 // Android 5.0 - targetSdkVersion 33 // Android 13 - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - buildConfigField "String", "version_ical4j", "\"${versions.ical4j}\"" - } - - compileOptions { - // ical4j >= 3.x uses the Java 8 Time API - coreLibraryDesugaringEnabled true - - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - buildFeatures { - buildConfig = true - } - - sourceSets { - main.java.srcDirs = [ "src/main/java", "opentasks-contract/src/main/java" ] - } - - packagingOptions { - resources { - excludes += ['META-INF/DEPENDENCIES', 'META-INF/LICENSE', 'META-INF/*.md'] - } - } - - lint { - disable 'AllowBackup', 'InvalidPackage' - } -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' - - api("org.mnode.ical4j:ical4j:${versions.ical4j}") { - // exclude modules which are in conflict with system libraries - exclude group: 'commons-logging' - exclude group: 'org.json', module: 'json' - // exclude groovy because we don't need it - exclude group: 'org.codehaus.groovy', module: 'groovy' - exclude group: 'org.codehaus.groovy', module: 'groovy-dateutil' - } - // ical4j requires newer Apache Commons libraries, which require Java8. Force latest Java7 versions. - // noinspection GradleDependency - api("org.apache.commons:commons-collections4") { - version { - strictly '4.2' - } - } - // noinspection GradleDependency - api("org.apache.commons:commons-lang3:3.8.1") { - version { - strictly '3.8.1' - } - } - - // noinspection GradleDependency - implementation "commons-io:commons-io:${versions.commonsIO}" - - implementation 'org.slf4j:slf4j-jdk14:2.0.3' - implementation 'androidx.core:core-ktx:1.10.0' - - androidTestImplementation 'androidx.test:core:1.5.0' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'io.mockk:mockk-android:1.13.4' - testImplementation 'junit:junit:4.13.2' -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..227ef250 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,12 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +plugins { + id("com.android.library") version "8.0.1" apply false + id("org.jetbrains.kotlin.android") version "1.8.21" apply false + id("org.jetbrains.dokka") version "1.8.10" apply false +} + +group = "at.bitfire" +version = System.getenv("GIT_COMMIT") diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000..f29a6676 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,5 @@ +jdk: + - openjdk17 +before_install: + - sdk install java 17.0.1-open + - sdk use java 17.0.1-open \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 00000000..bb6092ce --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,126 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +plugins { + id("com.android.library") + id("kotlin-android") + id("maven-publish") + id("org.jetbrains.dokka") +} + +val version_ical4j = "3.2.11" + +android { + compileSdk = 33 + + namespace = "at.bitfire.ical4android" + + defaultConfig { + minSdk = 21 // Android 5.0 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "version_ical4j", "\"${version_ical4j}\"") + + aarMetadata { + minCompileSdk = 29 + } + } + + compileOptions { + // ical4j >= 3.x uses the Java 8 Time API + isCoreLibraryDesugaringEnabled = true + + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } + + buildFeatures.buildConfig = true + + sourceSets["main"].apply { + kotlin { + srcDir("${projectDir}/src/main/kotlin") + } + java { + srcDir("${rootDir}/opentasks-contract/src/main/java") + } + } + + packaging { + resources { + excludes += listOf("META-INF/DEPENDENCIES", "META-INF/LICENSE", "META-INF/*.md") + } + } + + lint { + disable += listOf("AllowBackup", "InvalidPackage") + } + + publishing { + // Configure publish variant + singleVariant("release") { + withSourcesJar() + } + } +} + +publishing { + // Configure publishing data + publications { + register("release", MavenPublication::class.java) { + groupId = "com.github.bitfireAT" + artifactId = "ical4android" + version = System.getenv("GIT_COMMIT") + + afterEvaluate { + from(components["release"]) + } + } + } +} + +configurations.forEach { + // exclude modules which are in conflict with system libraries + it.exclude("commons-logging") + it.exclude("org.json", "json") + + // exclude groovy because we don"t need it, and it needs API 26+ + it.exclude("org.codehaus.groovy", "groovy") + it.exclude("org.codehaus.groovy", "groovy-dateutil") +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") + + implementation("androidx.core:core-ktx:1.10.1") + api("org.mnode.ical4j:ical4j:${version_ical4j}") + implementation("org.slf4j:slf4j-jdk14:2.0.3") // ical4j logging over java.util.Logger + + // ical4j requires newer Apache Commons libraries, which require Java8. Force latest Java7 versions. + // noinspection GradleDependency + api("org.apache.commons:commons-collections4") { + version { + strictly("4.2") + } + } + // noinspection GradleDependency + api("org.apache.commons:commons-lang3:3.8.1") { + version { + strictly("3.8.1") + } + } + // noinspection GradleDependency + @Suppress("GradleDependency") + implementation("commons-io:commons-io:2.6") + + androidTestImplementation("androidx.test:core:1.5.0") + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") + androidTestImplementation("io.mockk:mockk-android:1.13.4") + testImplementation("junit:junit:4.13.2") +} \ No newline at end of file diff --git a/src/androidTest/java/at/bitfire/ical4android/AbstractTasksTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AbstractTasksTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskListTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskListTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AospTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AospTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/AttendeeMappingsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AttendeeMappingsTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/AttendeeMappingsTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/AttendeeMappingsTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/Css3ColorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Css3ColorTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/Css3ColorTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/Css3ColorTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/EventTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/Ical4jSettingsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jSettingsTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/Ical4jSettingsTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jSettingsTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/TaskTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestCalendar.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/impl/TestCalendar.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestEvent.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestEvent.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxIcalObject.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxIcalObject.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestTask.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/impl/TestTask.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestTaskList.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/impl/TestTaskList.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/util/MiscUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/util/MiscUtilsTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt diff --git a/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt similarity index 100% rename from src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt diff --git a/src/androidTest/resources/events/all-day-0sec.ics b/lib/src/androidTest/resources/events/all-day-0sec.ics similarity index 100% rename from src/androidTest/resources/events/all-day-0sec.ics rename to lib/src/androidTest/resources/events/all-day-0sec.ics diff --git a/src/androidTest/resources/events/all-day-10days.ics b/lib/src/androidTest/resources/events/all-day-10days.ics similarity index 100% rename from src/androidTest/resources/events/all-day-10days.ics rename to lib/src/androidTest/resources/events/all-day-10days.ics diff --git a/src/androidTest/resources/events/all-day-1day.ics b/lib/src/androidTest/resources/events/all-day-1day.ics similarity index 100% rename from src/androidTest/resources/events/all-day-1day.ics rename to lib/src/androidTest/resources/events/all-day-1day.ics diff --git a/src/androidTest/resources/events/dst-only-vtimezone.ics b/lib/src/androidTest/resources/events/dst-only-vtimezone.ics similarity index 100% rename from src/androidTest/resources/events/dst-only-vtimezone.ics rename to lib/src/androidTest/resources/events/dst-only-vtimezone.ics diff --git a/src/androidTest/resources/events/event-on-that-day.ics b/lib/src/androidTest/resources/events/event-on-that-day.ics similarity index 100% rename from src/androidTest/resources/events/event-on-that-day.ics rename to lib/src/androidTest/resources/events/event-on-that-day.ics diff --git a/src/androidTest/resources/events/latin1.ics b/lib/src/androidTest/resources/events/latin1.ics similarity index 100% rename from src/androidTest/resources/events/latin1.ics rename to lib/src/androidTest/resources/events/latin1.ics diff --git a/src/androidTest/resources/events/multiple.ics b/lib/src/androidTest/resources/events/multiple.ics similarity index 100% rename from src/androidTest/resources/events/multiple.ics rename to lib/src/androidTest/resources/events/multiple.ics diff --git a/src/androidTest/resources/events/one-event-with-exception-one-without.ics b/lib/src/androidTest/resources/events/one-event-with-exception-one-without.ics similarity index 100% rename from src/androidTest/resources/events/one-event-with-exception-one-without.ics rename to lib/src/androidTest/resources/events/one-event-with-exception-one-without.ics diff --git a/src/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics b/lib/src/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics similarity index 100% rename from src/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics rename to lib/src/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics diff --git a/src/androidTest/resources/events/outlook1.ics b/lib/src/androidTest/resources/events/outlook1.ics similarity index 100% rename from src/androidTest/resources/events/outlook1.ics rename to lib/src/androidTest/resources/events/outlook1.ics diff --git a/src/androidTest/resources/events/recurring-only-exception.ics b/lib/src/androidTest/resources/events/recurring-only-exception.ics similarity index 100% rename from src/androidTest/resources/events/recurring-only-exception.ics rename to lib/src/androidTest/resources/events/recurring-only-exception.ics diff --git a/src/androidTest/resources/events/recurring-with-exception1.ics b/lib/src/androidTest/resources/events/recurring-with-exception1.ics similarity index 100% rename from src/androidTest/resources/events/recurring-with-exception1.ics rename to lib/src/androidTest/resources/events/recurring-with-exception1.ics diff --git a/src/androidTest/resources/events/two-events-without-exceptions.ics b/lib/src/androidTest/resources/events/two-events-without-exceptions.ics similarity index 100% rename from src/androidTest/resources/events/two-events-without-exceptions.ics rename to lib/src/androidTest/resources/events/two-events-without-exceptions.ics diff --git a/src/androidTest/resources/events/two-line-description-without-crlf.ics b/lib/src/androidTest/resources/events/two-line-description-without-crlf.ics similarity index 100% rename from src/androidTest/resources/events/two-line-description-without-crlf.ics rename to lib/src/androidTest/resources/events/two-line-description-without-crlf.ics diff --git a/src/androidTest/resources/events/utf8.ics b/lib/src/androidTest/resources/events/utf8.ics similarity index 100% rename from src/androidTest/resources/events/utf8.ics rename to lib/src/androidTest/resources/events/utf8.ics diff --git a/src/androidTest/resources/events/vienna-evolution.ics b/lib/src/androidTest/resources/events/vienna-evolution.ics similarity index 100% rename from src/androidTest/resources/events/vienna-evolution.ics rename to lib/src/androidTest/resources/events/vienna-evolution.ics diff --git a/src/androidTest/resources/jtx/vjournal/all-day.ics b/lib/src/androidTest/resources/jtx/vjournal/all-day.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/all-day.ics rename to lib/src/androidTest/resources/jtx/vjournal/all-day.ics diff --git a/src/androidTest/resources/jtx/vjournal/default-example-note.ics b/lib/src/androidTest/resources/jtx/vjournal/default-example-note.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/default-example-note.ics rename to lib/src/androidTest/resources/jtx/vjournal/default-example-note.ics diff --git a/src/androidTest/resources/jtx/vjournal/default-example.ics b/lib/src/androidTest/resources/jtx/vjournal/default-example.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/default-example.ics rename to lib/src/androidTest/resources/jtx/vjournal/default-example.ics diff --git a/src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics b/lib/src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics rename to lib/src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics diff --git a/src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics b/lib/src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics rename to lib/src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics diff --git a/src/androidTest/resources/jtx/vjournal/latin1.ics b/lib/src/androidTest/resources/jtx/vjournal/latin1.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/latin1.ics rename to lib/src/androidTest/resources/jtx/vjournal/latin1.ics diff --git a/src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics b/lib/src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics rename to lib/src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics diff --git a/src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics b/lib/src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics rename to lib/src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics diff --git a/src/androidTest/resources/jtx/vjournal/recurring.ics b/lib/src/androidTest/resources/jtx/vjournal/recurring.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/recurring.ics rename to lib/src/androidTest/resources/jtx/vjournal/recurring.ics diff --git a/src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics b/lib/src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics rename to lib/src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics diff --git a/src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics b/lib/src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics rename to lib/src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics diff --git a/src/androidTest/resources/jtx/vjournal/utf8.ics b/lib/src/androidTest/resources/jtx/vjournal/utf8.ics similarity index 100% rename from src/androidTest/resources/jtx/vjournal/utf8.ics rename to lib/src/androidTest/resources/jtx/vjournal/utf8.ics diff --git a/src/androidTest/resources/jtx/vtodo/empty-priority.ics b/lib/src/androidTest/resources/jtx/vtodo/empty-priority.ics similarity index 100% rename from src/androidTest/resources/jtx/vtodo/empty-priority.ics rename to lib/src/androidTest/resources/jtx/vtodo/empty-priority.ics diff --git a/src/androidTest/resources/jtx/vtodo/latin1.ics b/lib/src/androidTest/resources/jtx/vtodo/latin1.ics similarity index 100% rename from src/androidTest/resources/jtx/vtodo/latin1.ics rename to lib/src/androidTest/resources/jtx/vtodo/latin1.ics diff --git a/src/androidTest/resources/jtx/vtodo/most-fields1.ics b/lib/src/androidTest/resources/jtx/vtodo/most-fields1.ics similarity index 100% rename from src/androidTest/resources/jtx/vtodo/most-fields1.ics rename to lib/src/androidTest/resources/jtx/vtodo/most-fields1.ics diff --git a/src/androidTest/resources/jtx/vtodo/most-fields2.ics b/lib/src/androidTest/resources/jtx/vtodo/most-fields2.ics similarity index 100% rename from src/androidTest/resources/jtx/vtodo/most-fields2.ics rename to lib/src/androidTest/resources/jtx/vtodo/most-fields2.ics diff --git a/src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics b/lib/src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics similarity index 100% rename from src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics rename to lib/src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics diff --git a/src/androidTest/resources/jtx/vtodo/utf8.ics b/lib/src/androidTest/resources/jtx/vtodo/utf8.ics similarity index 100% rename from src/androidTest/resources/jtx/vtodo/utf8.ics rename to lib/src/androidTest/resources/jtx/vtodo/utf8.ics diff --git a/src/androidTest/resources/tasks/empty-priority.ics b/lib/src/androidTest/resources/tasks/empty-priority.ics similarity index 100% rename from src/androidTest/resources/tasks/empty-priority.ics rename to lib/src/androidTest/resources/tasks/empty-priority.ics diff --git a/src/androidTest/resources/tasks/latin1.ics b/lib/src/androidTest/resources/tasks/latin1.ics similarity index 100% rename from src/androidTest/resources/tasks/latin1.ics rename to lib/src/androidTest/resources/tasks/latin1.ics diff --git a/src/androidTest/resources/tasks/most-fields1.ics b/lib/src/androidTest/resources/tasks/most-fields1.ics similarity index 100% rename from src/androidTest/resources/tasks/most-fields1.ics rename to lib/src/androidTest/resources/tasks/most-fields1.ics diff --git a/src/androidTest/resources/tasks/most-fields2.ics b/lib/src/androidTest/resources/tasks/most-fields2.ics similarity index 100% rename from src/androidTest/resources/tasks/most-fields2.ics rename to lib/src/androidTest/resources/tasks/most-fields2.ics diff --git a/src/androidTest/resources/tasks/rfc5545-sample1.ics b/lib/src/androidTest/resources/tasks/rfc5545-sample1.ics similarity index 100% rename from src/androidTest/resources/tasks/rfc5545-sample1.ics rename to lib/src/androidTest/resources/tasks/rfc5545-sample1.ics diff --git a/src/androidTest/resources/tasks/utf8.ics b/lib/src/androidTest/resources/tasks/utf8.ics similarity index 100% rename from src/androidTest/resources/tasks/utf8.ics rename to lib/src/androidTest/resources/tasks/utf8.ics diff --git a/src/androidTest/resources/tz/Karachi.ics b/lib/src/androidTest/resources/tz/Karachi.ics similarity index 100% rename from src/androidTest/resources/tz/Karachi.ics rename to lib/src/androidTest/resources/tz/Karachi.ics diff --git a/src/androidTest/resources/tz/Mogadishu.ics b/lib/src/androidTest/resources/tz/Mogadishu.ics similarity index 100% rename from src/androidTest/resources/tz/Mogadishu.ics rename to lib/src/androidTest/resources/tz/Mogadishu.ics diff --git a/src/androidTest/resources/tz/Vienna.ics b/lib/src/androidTest/resources/tz/Vienna.ics similarity index 100% rename from src/androidTest/resources/tz/Vienna.ics rename to lib/src/androidTest/resources/tz/Vienna.ics diff --git a/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml similarity index 100% rename from src/main/AndroidManifest.xml rename to lib/src/main/AndroidManifest.xml diff --git a/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidCalendar.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidCalendarFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendarFactory.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidCalendarFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendarFactory.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidEvent.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidEventFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEventFactory.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidEventFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidEventFactory.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidTask.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskFactory.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidTaskFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskFactory.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidTaskList.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskListFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskListFactory.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AndroidTaskListFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskListFactory.kt diff --git a/src/main/java/at/bitfire/ical4android/AttendeeMappings.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AttendeeMappings.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/AttendeeMappings.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/AttendeeMappings.kt diff --git a/src/main/java/at/bitfire/ical4android/BatchOperation.kt b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/BatchOperation.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt diff --git a/src/main/java/at/bitfire/ical4android/CalendarStorageException.kt b/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/CalendarStorageException.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt diff --git a/src/main/java/at/bitfire/ical4android/Css3Color.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/Css3Color.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt diff --git a/src/main/java/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/Event.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/Event.kt diff --git a/src/main/java/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/ICalendar.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt diff --git a/src/main/java/at/bitfire/ical4android/Ical4Android.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/Ical4Android.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt diff --git a/src/main/java/at/bitfire/ical4android/InvalidCalendarException.kt b/lib/src/main/kotlin/at/bitfire/ical4android/InvalidCalendarException.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/InvalidCalendarException.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/InvalidCalendarException.kt diff --git a/src/main/java/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/JtxCollection.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt diff --git a/src/main/java/at/bitfire/ical4android/JtxCollectionFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollectionFactory.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/JtxCollectionFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/JtxCollectionFactory.kt diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/JtxICalObject.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObjectFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObjectFactory.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/JtxICalObjectFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObjectFactory.kt diff --git a/src/main/java/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/Task.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/Task.kt diff --git a/src/main/java/at/bitfire/ical4android/TaskProvider.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/TaskProvider.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt diff --git a/src/main/java/at/bitfire/ical4android/UnknownProperty.kt b/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/UnknownProperty.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt diff --git a/src/main/java/at/bitfire/ical4android/UsesThreadContextClassLoader.kt b/lib/src/main/kotlin/at/bitfire/ical4android/UsesThreadContextClassLoader.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/UsesThreadContextClassLoader.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/UsesThreadContextClassLoader.kt diff --git a/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt diff --git a/src/main/java/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/util/DateUtils.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt diff --git a/src/main/java/at/bitfire/ical4android/util/MiscUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/util/MiscUtils.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt diff --git a/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt diff --git a/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/validation/EventValidator.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt diff --git a/src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt diff --git a/src/main/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt diff --git a/src/main/java/at/bitfire/ical4android/validation/ICalPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/validation/ICalPreprocessor.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt diff --git a/src/main/java/at/bitfire/ical4android/validation/StreamPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt similarity index 100% rename from src/main/java/at/bitfire/ical4android/validation/StreamPreprocessor.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt diff --git a/src/main/java/at/techbee/jtx/JtxContract.kt b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt similarity index 100% rename from src/main/java/at/techbee/jtx/JtxContract.kt rename to lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt diff --git a/src/main/resources/ical4j.properties b/lib/src/main/resources/ical4j.properties similarity index 100% rename from src/main/resources/ical4j.properties rename to lib/src/main/resources/ical4j.properties diff --git a/src/main/resources/tz.alias b/lib/src/main/resources/tz.alias similarity index 100% rename from src/main/resources/tz.alias rename to lib/src/main/resources/tz.alias diff --git a/src/test/README.txt b/lib/src/test/README.txt similarity index 100% rename from src/test/README.txt rename to lib/src/test/README.txt diff --git a/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt similarity index 100% rename from src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt rename to lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt diff --git a/src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt similarity index 100% rename from src/test/java/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt rename to lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt diff --git a/src/test/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt similarity index 100% rename from src/test/java/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt rename to lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..1c20da9e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,23 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "ical4android" +// include ':sample-app' +include ':lib' +project(':lib').name = 'ical4android' \ No newline at end of file From aaf607c37dafeaf1ca8a573f2f25f14d4e3f5c1e Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 22 Jul 2023 12:16:21 +0200 Subject: [PATCH 22/92] Add dependent issues workflow --- .github/workflows/dependent-issues.yml | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/dependent-issues.yml diff --git a/.github/workflows/dependent-issues.yml b/.github/workflows/dependent-issues.yml new file mode 100644 index 00000000..62b122fd --- /dev/null +++ b/.github/workflows/dependent-issues.yml @@ -0,0 +1,55 @@ +name: Dependent Issues + +on: + issues: + types: + - opened + - edited + - closed + - reopened + pull_request_target: + types: + - opened + - edited + - closed + - reopened + # Makes sure we always add status check for PRs. Useful only if + # this action is required to pass before merging. Otherwise, it + # can be removed. + - synchronize + + # Schedule a daily check. Useful if you reference cross-repository + # issues or pull requests. Otherwise, it can be removed. + schedule: + - cron: '12 9 * * *' + +permissions: write-all + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: z0al/dependent-issues@v1 + env: + # (Required) The token to use to make API calls to GitHub. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # (Optional) The token to use to make API calls to GitHub for remote repos. + GITHUB_READ_TOKEN: ${{ secrets.DEPENDENT_ISSUES_READ_TOKEN }} + + with: + # (Optional) The label to use to mark dependent issues + # label: dependent + + # (Optional) Enable checking for dependencies in issues. + # Enable by setting the value to "on". Default "off" + check_issues: on + + # (Optional) A comma-separated list of keywords. Default + # "depends on, blocked by" + keywords: depends on, blocked by + + # (Optional) A custom comment body. It supports `{{ dependencies }}` token. + comment: > + This PR/issue depends on: + + {{ dependencies }} From 9b21834927f337bf75aedaca970f9467370ac25f Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 16 Aug 2023 17:41:58 +0200 Subject: [PATCH 23/92] Remove whitespace values from content providers (#109) * Extend removeEmptyStrings to remove blank strings as well * Apply removeEmptyAndBlankStrings to populateTask() * Move removeBlankStrings() to extension method; minor changes * Minor MiscUtils restructurizing --------- Co-authored-by: Ricki Hirner --- .../ical4android/AndroidCalendarTest.kt | 12 +- .../bitfire/ical4android/AndroidEventTest.kt | 43 +------ .../bitfire/ical4android/AndroidTaskTest.kt | 121 ++++++++---------- .../at/bitfire/ical4android/AospTest.kt | 2 +- .../ical4android/BatchOperationTest.kt | 9 +- .../bitfire/ical4android/JtxCollectionTest.kt | 13 +- .../bitfire/ical4android/JtxICalObjectTest.kt | 16 +-- .../ical4android/impl/TestJtxCollection.kt | 2 +- .../ical4android/util/MiscUtilsTest.kt | 30 +++-- .../bitfire/ical4android/AndroidCalendar.kt | 11 +- .../at/bitfire/ical4android/AndroidEvent.kt | 9 +- .../at/bitfire/ical4android/AndroidTask.kt | 29 +---- .../bitfire/ical4android/AndroidTaskList.kt | 4 +- .../at/bitfire/ical4android/JtxCollection.kt | 2 +- .../at/bitfire/ical4android/JtxICalObject.kt | 32 +---- .../at/bitfire/ical4android/TaskProvider.kt | 2 +- .../at/bitfire/ical4android/util/MiscUtils.kt | 89 ++++++------- .../at/bitfire/ical4android/MiscUtilsTest.kt | 4 +- 18 files changed, 148 insertions(+), 282 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt index e443323a..d409f151 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt @@ -16,17 +16,13 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestCalendar import at.bitfire.ical4android.impl.TestEvent -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.closeCompat import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart -import org.junit.AfterClass +import org.junit.* import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Before -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Test class AndroidCalendarTest { @@ -121,7 +117,7 @@ class AndroidCalendarTest { } private fun countColors(account: Account): Int { - val uri = Colors.CONTENT_URI.asSyncAdapter(testAccount) + val uri = Colors.CONTENT_URI.asSyncAdapter(account) provider.query(uri, null, null, null, null)!!.use { cursor -> cursor.moveToNext() return cursor.count diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index 78d9cbd3..70ff88bf 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -10,51 +10,22 @@ import android.content.ContentUris import android.content.ContentValues import android.database.DatabaseUtils import android.net.Uri -import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL -import android.provider.CalendarContract.AUTHORITY -import android.provider.CalendarContract.Attendees -import android.provider.CalendarContract.Calendars -import android.provider.CalendarContract.Events -import android.provider.CalendarContract.ExtendedProperties -import android.provider.CalendarContract.Reminders +import android.provider.CalendarContract.* import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestCalendar import at.bitfire.ical4android.impl.TestEvent import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateList -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.ParameterList -import net.fortuna.ical4j.model.Property +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.closeCompat +import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VAlarm -import net.fortuna.ical4j.model.parameter.Cn -import net.fortuna.ical4j.model.parameter.CuType -import net.fortuna.ical4j.model.parameter.Email -import net.fortuna.ical4j.model.parameter.Language -import net.fortuna.ical4j.model.parameter.PartStat -import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.parameter.Role -import net.fortuna.ical4j.model.parameter.Rsvp -import net.fortuna.ical4j.model.parameter.Value +import net.fortuna.ical4j.model.parameter.* import net.fortuna.ical4j.model.property.* import net.fortuna.ical4j.util.TimeZones -import org.junit.After -import org.junit.AfterClass -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Test +import org.junit.* +import org.junit.Assert.* import java.net.URI import java.time.Duration import java.time.Period diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt index 7d2dbc73..ffc90943 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt @@ -16,36 +16,15 @@ import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.parameter.Email -import net.fortuna.ical4j.model.parameter.RelType +import net.fortuna.ical4j.model.parameter.* import net.fortuna.ical4j.model.parameter.TzId -import net.fortuna.ical4j.model.parameter.Value -import net.fortuna.ical4j.model.parameter.XParameter -import net.fortuna.ical4j.model.property.Clazz -import net.fortuna.ical4j.model.property.Completed -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.Due -import net.fortuna.ical4j.model.property.Duration -import net.fortuna.ical4j.model.property.ExDate -import net.fortuna.ical4j.model.property.Geo -import net.fortuna.ical4j.model.property.Organizer -import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.RelatedTo -import net.fortuna.ical4j.model.property.Status -import net.fortuna.ical4j.model.property.XProperty +import net.fortuna.ical4j.model.property.* import org.dmfs.tasks.contract.TaskContract -import org.dmfs.tasks.contract.TaskContract.Properties +import org.dmfs.tasks.contract.TaskContract.* import org.dmfs.tasks.contract.TaskContract.Property.Category import org.dmfs.tasks.contract.TaskContract.Property.Relation -import org.dmfs.tasks.contract.TaskContract.PropertyColumns -import org.dmfs.tasks.contract.TaskContract.Tasks import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.junit.Before import org.junit.Test import java.time.ZoneId @@ -97,7 +76,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Sequence() { - buildTask() { + buildTask { sequence = 12345 }.let { result -> assertEquals(12345, result.getAsInteger(Tasks.SYNC_VERSION)) @@ -106,7 +85,7 @@ class AndroidTaskTest( @Test fun testBuildTask_CreatedAt() { - buildTask() { + buildTask { createdAt = 1593771404 // Fri Jul 03 10:16:44 2020 UTC }.let { result -> assertEquals(1593771404, result.getAsLong(Tasks.CREATED)) @@ -115,7 +94,7 @@ class AndroidTaskTest( @Test fun testBuildTask_LastModified() { - buildTask() { + buildTask { lastModified = 1593771404 }.let { result -> assertEquals(1593771404, result.getAsLong(Tasks.LAST_MODIFIED)) @@ -124,7 +103,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Summary() { - buildTask() { + buildTask { summary = "Sample Summary" }.let { result -> assertEquals("Sample Summary", result.get(Tasks.TITLE)) @@ -133,7 +112,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Location() { - buildTask() { + buildTask { location = "Sample Location" }.let { result -> assertEquals("Sample Location", result.get(Tasks.LOCATION)) @@ -142,7 +121,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Geo() { - buildTask() { + buildTask { geoPosition = Geo(47.913563.toBigDecimal(), 16.159601.toBigDecimal()) }.let { result -> assertEquals("16.159601,47.913563", result.get(Tasks.GEO)) @@ -151,7 +130,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Description() { - buildTask() { + buildTask { description = "Sample Description" }.let { result -> assertEquals("Sample Description", result.get(Tasks.DESCRIPTION)) @@ -160,7 +139,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Color() { - buildTask() { + buildTask { color = 0x11223344 }.let { result -> assertEquals(0x11223344, result.getAsInteger(Tasks.TASK_COLOR)) @@ -169,7 +148,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Url() { - buildTask() { + buildTask { url = "https://www.example.com" }.let { result -> assertEquals("https://www.example.com", result.getAsString(Tasks.URL)) @@ -178,7 +157,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Organizer_MailTo() { - buildTask() { + buildTask { organizer = Organizer("mailto:organizer@example.com") }.let { result -> assertEquals("organizer@example.com", result.getAsString(Tasks.ORGANIZER)) @@ -187,7 +166,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Organizer_EmailParameter() { - buildTask() { + buildTask { organizer = Organizer("uri:unknown").apply { parameters.add(Email("organizer@example.com")) } @@ -198,7 +177,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Organizer_NotEmail() { - buildTask() { + buildTask { organizer = Organizer("uri:unknown") }.let { result -> assertNull(result.get(Tasks.ORGANIZER)) @@ -207,7 +186,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Priority() { - buildTask() { + buildTask { priority = 2 }.let { result -> assertEquals(2, result.getAsInteger(Tasks.PRIORITY)) @@ -216,7 +195,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Classification_Public() { - buildTask() { + buildTask { classification = Clazz.PUBLIC }.let { result -> assertEquals(Tasks.CLASSIFICATION_PUBLIC, result.getAsInteger(Tasks.CLASSIFICATION)) @@ -225,7 +204,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Classification_Private() { - buildTask() { + buildTask { classification = Clazz.PRIVATE }.let { result -> assertEquals(Tasks.CLASSIFICATION_PRIVATE, result.getAsInteger(Tasks.CLASSIFICATION)) @@ -234,7 +213,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Classification_Confidential() { - buildTask() { + buildTask { classification = Clazz.CONFIDENTIAL }.let { result -> assertEquals(Tasks.CLASSIFICATION_CONFIDENTIAL, result.getAsInteger(Tasks.CLASSIFICATION)) @@ -243,7 +222,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Classification_Custom() { - buildTask() { + buildTask { classification = Clazz("x-custom") }.let { result -> assertEquals(Tasks.CLASSIFICATION_PRIVATE, result.getAsInteger(Tasks.CLASSIFICATION)) @@ -252,7 +231,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Classification_None() { - buildTask() { + buildTask { }.let { result -> assertEquals(Tasks.CLASSIFICATION_DEFAULT /* null */, result.getAsInteger(Tasks.CLASSIFICATION)) } @@ -260,7 +239,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Status_NeedsAction() { - buildTask() { + buildTask { status = Status.VTODO_NEEDS_ACTION }.let { result -> assertEquals(Tasks.STATUS_NEEDS_ACTION, result.getAsInteger(Tasks.STATUS)) @@ -269,7 +248,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Status_Completed() { - buildTask() { + buildTask { status = Status.VTODO_COMPLETED }.let { result -> assertEquals(Tasks.STATUS_COMPLETED, result.getAsInteger(Tasks.STATUS)) @@ -278,7 +257,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Status_InProcess() { - buildTask() { + buildTask { status = Status.VTODO_IN_PROCESS }.let { result -> assertEquals(Tasks.STATUS_IN_PROCESS, result.getAsInteger(Tasks.STATUS)) @@ -287,7 +266,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Status_Cancelled() { - buildTask() { + buildTask { status = Status.VTODO_CANCELLED }.let { result -> assertEquals(Tasks.STATUS_CANCELLED, result.getAsInteger(Tasks.STATUS)) @@ -296,7 +275,7 @@ class AndroidTaskTest( @Test fun testBuildTask_DtStart() { - buildTask() { + buildTask { dtStart = DtStart("20200703T155722", tzVienna) }.let { result -> assertEquals(1593784642000L, result.getAsLong(Tasks.DTSTART)) @@ -307,7 +286,7 @@ class AndroidTaskTest( @Test fun testBuildTask_DtStart_AllDay() { - buildTask() { + buildTask { dtStart = DtStart(Date("20200703")) }.let { result -> assertEquals(1593734400000L, result.getAsLong(Tasks.DTSTART)) @@ -318,7 +297,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Due() { - buildTask() { + buildTask { due = Due(DateTime("20200703T155722", tzVienna)) }.let { result -> assertEquals(1593784642000L, result.getAsLong(Tasks.DUE)) @@ -329,7 +308,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Due_AllDay() { - buildTask() { + buildTask { due = Due(Date("20200703")) }.let { result -> assertEquals(1593734400000L, result.getAsLong(Tasks.DUE)) @@ -340,7 +319,7 @@ class AndroidTaskTest( @Test fun testBuildTask_DtStart_NonAllDay_Due_AllDay() { - buildTask() { + buildTask { dtStart = DtStart(DateTime("20200101T010203")) due = Due(Date("20200201")) }.let { result -> @@ -351,7 +330,7 @@ class AndroidTaskTest( @Test fun testBuildTask_DtStart_AllDay_Due_NonAllDay() { - buildTask() { + buildTask { dtStart = DtStart(Date("20200101")) due = Due(DateTime("20200201T010203")) }.let { result -> @@ -362,7 +341,7 @@ class AndroidTaskTest( @Test fun testBuildTask_DtStart_AllDay_Due_AllDay() { - buildTask() { + buildTask { dtStart = DtStart(Date("20200101")) due = Due(Date("20200201")) }.let { result -> @@ -372,7 +351,7 @@ class AndroidTaskTest( @Test fun testBuildTask_DtStart_FloatingTime() { - buildTask() { + buildTask { dtStart = DtStart("20200703T010203") }.let { result -> assertEquals(DateTime("20200703T010203").time, result.getAsLong(Tasks.DTSTART)) @@ -383,7 +362,7 @@ class AndroidTaskTest( @Test fun testBuildTask_DtStart_Utc() { - buildTask() { + buildTask { dtStart = DtStart(DateTime(1593730923000), true) }.let { result -> assertEquals(1593730923000L, result.getAsLong(Tasks.DTSTART)) @@ -394,7 +373,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Due_FloatingTime() { - buildTask() { + buildTask { due = Due("20200703T010203") }.let { result -> assertEquals(DateTime("20200703T010203").time, result.getAsLong(Tasks.DUE)) @@ -405,7 +384,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Due_Utc() { - buildTask() { + buildTask { due = Due(DateTime(1593730923000).apply { isUtc = true }) }.let { result -> assertEquals(1593730923000L, result.getAsLong(Tasks.DUE)) @@ -416,7 +395,7 @@ class AndroidTaskTest( @Test fun testBuildTask_Duration() { - buildTask() { + buildTask { dtStart = DtStart(DateTime()) duration = Duration(null, "P1D") }.let { result -> @@ -427,7 +406,7 @@ class AndroidTaskTest( @Test fun testBuildTask_CompletedAt() { val now = DateTime() - buildTask() { + buildTask { completedAt = Completed(now) }.let { result -> // Note: iCalendar does not allow COMPLETED to be all-day [RFC 5545 3.8.2.1] @@ -438,7 +417,7 @@ class AndroidTaskTest( @Test fun testBuildTask_PercentComplete() { - buildTask() { + buildTask { percentComplete = 50 }.let { result -> assertEquals(50, result.getAsInteger(Tasks.PERCENT_COMPLETE)) @@ -448,7 +427,7 @@ class AndroidTaskTest( @Test fun testBuildTask_RRule() { // Note: OpenTasks only supports one RRULE per VTODO (iCalendar: multiple RRULEs are allowed, but SHOULD not be used) - buildTask() { + buildTask { rRule = RRule("FREQ=DAILY;COUNT=10") }.let { result -> assertEquals("FREQ=DAILY;COUNT=10", result.getAsString(Tasks.RRULE)) @@ -457,7 +436,7 @@ class AndroidTaskTest( @Test fun testBuildTask_RDate() { - buildTask() { + buildTask { dtStart = DtStart(DateTime("20200101T010203", tzVienna)) rDates += RDate(DateList("20200102T020304", Value.DATE_TIME, tzVienna)) rDates += RDate(DateList("20200102T020304", Value.DATE_TIME, tzChicago)) @@ -471,7 +450,7 @@ class AndroidTaskTest( @Test fun testBuildTask_ExDate() { - buildTask() { + buildTask { dtStart = DtStart(DateTime("20200101T010203", tzVienna)) rRule = RRule("FREQ=DAILY;COUNT=10") exDates += ExDate(DateList("20200102T020304", Value.DATE_TIME, tzVienna)) @@ -488,7 +467,7 @@ class AndroidTaskTest( fun testBuildTask_Categories() { var hasCat1 = false var hasCat2 = false - buildTask() { + buildTask { categories.addAll(arrayOf("Cat_1", "Cat 2")) }.let { result -> val id = result.getAsLong(Tasks._ID) @@ -521,7 +500,7 @@ class AndroidTaskTest( @Test fun testBuildTask_RelatedTo_Parent() { - buildTask() { + buildTask { relatedTo.add(RelatedTo("Parent-Task").apply { parameters.add(RelType.PARENT) }) @@ -536,7 +515,7 @@ class AndroidTaskTest( @Test fun testBuildTask_RelatedTo_Child() { - buildTask() { + buildTask { relatedTo.add(RelatedTo("Child-Task").apply { parameters.add(RelType.CHILD) }) @@ -551,7 +530,7 @@ class AndroidTaskTest( @Test fun testBuildTask_RelatedTo_Sibling() { - buildTask() { + buildTask { relatedTo.add(RelatedTo("Sibling-Task").apply { parameters.add(RelType.SIBLING) }) @@ -566,7 +545,7 @@ class AndroidTaskTest( @Test fun testBuildTask_RelatedTo_Custom() { - buildTask() { + buildTask { relatedTo.add(RelatedTo("Sibling-Task").apply { parameters.add(RelType("custom-relationship")) }) @@ -581,7 +560,7 @@ class AndroidTaskTest( @Test fun testBuildTask_RelatedTo_Default() { - buildTask() { + buildTask { relatedTo.add(RelatedTo("Parent-Task")) }.let { result -> val taskId = result.getAsLong(Tasks._ID) @@ -599,7 +578,7 @@ class AndroidTaskTest( parameters.add(TzId(tzVienna.id)) parameters.add(XParameter("X-TEST-PARAMETER", "12345")) } - buildTask() { + buildTask { unknownProperties.add(xProperty) }.let { result -> val taskId = result.getAsLong(Tasks._ID) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt index c048f25d..2c7cb863 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt @@ -12,7 +12,7 @@ import android.net.Uri import android.provider.CalendarContract import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.closeCompat import org.junit.After import org.junit.Assert.assertNotNull import org.junit.Before diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt index 8fbc7cf7..3a2d426e 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt @@ -15,18 +15,13 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestCalendar import at.bitfire.ical4android.impl.TestEvent -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.closeCompat import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart -import org.junit.After -import org.junit.AfterClass +import org.junit.* import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Before -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Test import java.net.URI import java.util.Arrays diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt index 57e7f75a..14a8e0a9 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt @@ -10,18 +10,11 @@ import android.content.ContentValues import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestJtxCollection -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.closeCompat import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.assertTrue -import org.junit.After -import org.junit.AfterClass -import org.junit.Assume -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Test +import junit.framework.TestCase.* +import org.junit.* class JtxCollectionTest { diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt index b29f04c7..8d7cf820 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -14,25 +14,15 @@ import android.os.ParcelFileDescriptor import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestJtxCollection -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.closeCompat 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.assertEquals -import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.assertNull -import junit.framework.TestCase.assertTrue +import junit.framework.TestCase.* import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property -import org.junit.After -import org.junit.AfterClass -import org.junit.Assert -import org.junit.Assume -import org.junit.Before -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Test +import org.junit.* import java.io.ByteArrayOutputStream import java.io.InputStreamReader diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt index 5b0195bf..86cb0df7 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt @@ -9,7 +9,7 @@ import android.content.ContentProviderClient import at.bitfire.ical4android.JtxCollection import at.bitfire.ical4android.JtxCollectionFactory import at.bitfire.ical4android.JtxICalObject -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.toValues import at.techbee.jtx.JtxContract import java.util.LinkedList diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt index dee8dd91..985a04ca 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt @@ -9,17 +9,15 @@ import android.content.ContentValues import android.database.MatrixCursor import android.net.Uri import androidx.test.filters.SmallTest -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter -import org.junit.Assert +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.removeBlankStrings +import at.bitfire.ical4android.util.MiscUtils.toValues import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test class MiscUtilsTest { - private val tzVienna = DateUtils.ical4jTimeZone("Europe/Vienna") - - @Test @SmallTest fun testCursorToValues() { @@ -34,15 +32,21 @@ class MiscUtilsTest { @Test @SmallTest - fun testRemoveEmptyStrings() { - val values = ContentValues(2) + fun testRemoveEmptyAndBlankStrings() { + val values = ContentValues() values.put("key1", "value") values.put("key2", 1L) values.put("key3", "") - MiscUtils.removeEmptyStrings(values) - Assert.assertEquals("value", values.getAsString("key1")) - Assert.assertEquals(1L, values.getAsLong("key2").toLong()) - Assert.assertNull(values.get("key3")) + values.put("key4", "\n") + values.put("key5", " \n ") + values.put("key6", " ") + values.removeBlankStrings() + assertEquals("value", values.getAsString("key1")) + assertEquals(1L, values.getAsLong("key2")) + assertNull(values.get("key3")) + assertNull(values.get("key4")) + assertNull(values.get("key5")) + assertNull(values.get("key6")) } @@ -56,4 +60,4 @@ class MiscUtilsTest { ) } -} +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt index b393a012..cc135889 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt @@ -9,14 +9,9 @@ import android.content.ContentProviderClient import android.content.ContentUris import android.content.ContentValues import android.net.Uri -import android.provider.CalendarContract.Attendees -import android.provider.CalendarContract.CalendarEntity -import android.provider.CalendarContract.Calendars -import android.provider.CalendarContract.Colors -import android.provider.CalendarContract.Events -import android.provider.CalendarContract.Reminders -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import android.provider.CalendarContract.* +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.toValues import java.io.FileNotFoundException import java.util.LinkedList import java.util.logging.Level diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 94a57a30..18d22db7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -17,8 +17,9 @@ import at.bitfire.ical4android.BatchOperation.CpoBuilder import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.MiscUtils -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.removeBlankStrings +import at.bitfire.ical4android.util.MiscUtils.toValues import at.bitfire.ical4android.util.TimeApiExtensions import at.bitfire.ical4android.util.TimeApiExtensions.requireZoneId import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate @@ -132,10 +133,10 @@ abstract class AndroidEvent( val groupScheduled = e.subValues.any { it.uri == Attendees.CONTENT_URI } val isOrganizer = (e.entityValues.getAsInteger(Events.IS_ORGANIZER) ?: 0) != 0 - populateEvent(MiscUtils.removeEmptyStrings(e.entityValues), groupScheduled) + populateEvent(e.entityValues.removeBlankStrings(), groupScheduled) for (subValue in e.subValues) { - val subValues = MiscUtils.removeEmptyStrings(subValue.values) + val subValues = subValue.values.removeBlankStrings() when (subValue.uri) { Attendees.CONTENT_URI -> populateAttendee(subValues, isOrganizer) Reminders.CONTENT_URI -> populateReminder(subValues) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt index f442ea4e..67b4d2de 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt @@ -13,37 +13,16 @@ import at.bitfire.ical4android.BatchOperation.CpoBuilder import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.MiscUtils -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.PropertyList -import net.fortuna.ical4j.model.TimeZone +import at.bitfire.ical4android.util.MiscUtils.toValues +import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.property.Action -import net.fortuna.ical4j.model.property.Clazz -import net.fortuna.ical4j.model.property.Completed -import net.fortuna.ical4j.model.property.Description -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.Due -import net.fortuna.ical4j.model.property.Duration -import net.fortuna.ical4j.model.property.ExDate -import net.fortuna.ical4j.model.property.Geo -import net.fortuna.ical4j.model.property.Organizer -import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.RelatedTo -import net.fortuna.ical4j.model.property.Status -import net.fortuna.ical4j.model.property.Trigger +import net.fortuna.ical4j.model.property.* import net.fortuna.ical4j.util.TimeZones import org.dmfs.tasks.contract.TaskContract.Properties -import org.dmfs.tasks.contract.TaskContract.Property.Alarm -import org.dmfs.tasks.contract.TaskContract.Property.Category -import org.dmfs.tasks.contract.TaskContract.Property.Relation +import org.dmfs.tasks.contract.TaskContract.Property.* import org.dmfs.tasks.contract.TaskContract.Tasks import java.io.FileNotFoundException import java.net.URISyntaxException diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt index 1c30867a..69a8adc9 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt @@ -8,8 +8,8 @@ import android.accounts.Account import android.content.ContentUris import android.content.ContentValues import android.net.Uri -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.toValues import org.dmfs.tasks.contract.TaskContract import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.TaskLists diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index 453040cf..4ed91752 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -10,7 +10,7 @@ import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.net.Uri -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.toValues import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter import net.fortuna.ical4j.model.Calendar diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt index dd813ad5..dd072132 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt @@ -10,43 +10,17 @@ import android.net.ParseException import android.net.Uri import android.os.ParcelFileDescriptor import android.util.Base64 -import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.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.Calendar -import net.fortuna.ical4j.model.ComponentList -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateList -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.ParameterList -import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.PropertyList -import net.fortuna.ical4j.model.TextList -import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.* 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.AltRep -import net.fortuna.ical4j.model.parameter.Cn -import net.fortuna.ical4j.model.parameter.CuType -import net.fortuna.ical4j.model.parameter.DelegatedFrom -import net.fortuna.ical4j.model.parameter.DelegatedTo -import net.fortuna.ical4j.model.parameter.Dir -import net.fortuna.ical4j.model.parameter.FmtType -import net.fortuna.ical4j.model.parameter.Language -import net.fortuna.ical4j.model.parameter.Member -import net.fortuna.ical4j.model.parameter.PartStat -import net.fortuna.ical4j.model.parameter.RelType -import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.parameter.Role -import net.fortuna.ical4j.model.parameter.Rsvp -import net.fortuna.ical4j.model.parameter.SentBy -import net.fortuna.ical4j.model.parameter.Value -import net.fortuna.ical4j.model.parameter.XParameter +import net.fortuna.ical4j.model.parameter.* import net.fortuna.ical4j.model.property.* import java.io.FileNotFoundException import java.io.IOException diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt index ea874d78..b6ab20cd 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt @@ -9,7 +9,7 @@ import android.content.ContentProviderClient import android.content.Context import android.content.pm.PackageManager import androidx.core.content.pm.PackageInfoCompat -import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.closeCompat import org.dmfs.tasks.contract.TaskContract import java.io.Closeable import java.util.logging.Level diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt index 76b3e02e..950075a1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt @@ -12,13 +12,15 @@ import android.database.DatabaseUtils import android.net.Uri import android.os.Build import android.provider.CalendarContract +import at.bitfire.ical4android.Ical4Android import org.apache.commons.lang3.StringUtils import java.lang.reflect.Modifier import java.util.* +import kotlin.ConcurrentModificationException object MiscUtils { - const val TOSTRING_MAXCHARS = 10000 + private const val TOSTRING_MAXCHARS = 10000 /** * Generates useful toString info (fields and values) from [obj] by reflection. @@ -44,65 +46,52 @@ object MiscUtils { return "${obj.javaClass.simpleName}=[${s.joinToString(", ")}]" } + + // various extension methods + + fun ContentProviderClient.closeCompat() { + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + close() + else + release() + } + /** - * Removes empty [String] values from [values]. + * Removes blank (empty or only white-space) [String] values from [ContentValues]. * - * @param values set of values to be modified * @return the modified object (which is the same object as passed in; for chaining) */ - fun removeEmptyStrings(values: ContentValues): ContentValues { - val it = values.keySet().iterator() - while (it.hasNext()) { - val obj = values[it.next()] - if (obj is String && obj.isEmpty()) - it.remove() - } - return values - } - - - object ContentProviderClientHelper { - - fun ContentProviderClient.closeCompat() { - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - close() - else - release() + fun ContentValues.removeBlankStrings(): ContentValues { + val iter = keySet().iterator() + while (iter.hasNext()) { + val obj = this[iter.next()] + if (obj is CharSequence && obj.isBlank()) + iter.remove() } - + return this } + /** + * Returns the entire contents of the current row as a [ContentValues] object. + * + * @param removeBlankRows whether rows with blank values should be removed + * @return entire contents of the current row + */ + fun Cursor.toValues(removeBlankRows: Boolean = false): ContentValues { + val values = ContentValues(columnCount) + DatabaseUtils.cursorRowToContentValues(this, values) - object CursorHelper { - - /** - * Returns the entire contents of the current row as a [ContentValues] object. - * - * @param removeEmptyRows whether rows with empty values should be removed - * @return entire contents of the current row - */ - fun Cursor.toValues(removeEmptyRows: Boolean = false): ContentValues { - val values = ContentValues(columnCount) - DatabaseUtils.cursorRowToContentValues(this, values) - - if (removeEmptyRows) - removeEmptyStrings(values) - - return values - } + if (removeBlankRows) + values.removeBlankStrings() + return values } - - object UriHelper { - - fun Uri.asSyncAdapter(account: Account): Uri = buildUpon() - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, account.name) - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, account.type) - .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") - .build() - - } + fun Uri.asSyncAdapter(account: Account): Uri = buildUpon() + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .build() } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt index c9cf19d1..313e31c5 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt @@ -12,7 +12,7 @@ class MiscUtilsTest { @Test fun testReflectionToString() { - val s = MiscUtils.reflectionToString(MiscUtilsTest.TestClass()) + val s = MiscUtils.reflectionToString(TestClass()) assertTrue(s.startsWith("TestClass=[")) assertTrue(s.contains("i=2")) assertTrue(s.contains("large=null")) @@ -21,7 +21,7 @@ class MiscUtilsTest { @Test fun testReflectionToString_OOM() { - val t = MiscUtilsTest.TestClass() + val t = TestClass() t.large = object: Any() { override fun toString(): String { throw OutOfMemoryError("toString() causes OOM") From b6824766ed64a0a4b38912e7d0fc01d3568dfe46 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 16 Aug 2023 17:44:45 +0200 Subject: [PATCH 24/92] Update dependencies and tasks apps --- .github/workflows/test-dev.yml | 10 +++++++--- lib/build.gradle.kts | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index bdb04ee0..f99404b8 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -67,6 +67,10 @@ jobs: - name: Install task apps and run tests uses: reactivecircus/android-emulator-runner@v2 + env: + version_at_techbee_jtx: 205020009 + version_org_tasks: 130501 + version_org_dmfs_tasks: 82200 with: api-level: ${{ matrix.api-level }} arch: x86_64 @@ -75,9 +79,9 @@ jobs: disable-animations: true script: | mkdir .apk && cd .apk - wget -cq -O org.dmfs.tasks.apk https://f-droid.org/archive/org.dmfs.tasks_80800.apk && adb install org.dmfs.tasks.apk - wget -cq -O org.tasks.apk https://f-droid.org/archive/org.tasks_120400.apk && adb install org.tasks.apk - wget -cq -O at.techbee.jtx.apk https://f-droid.org/archive/at.techbee.jtx_100140002.apk && adb install at.techbee.jtx.apk + (wget -cq -O org.dmfs.tasks.apk https://f-droid.org/repo/org.dmfs.tasks_${{ env.version_org_dmfs_tasks }}.apk || wget -cq -O org.dmfs.tasks.apk https://f-droid.org/archive/org.dmfs.tasks_${{ env.version_org_dmfs_tasks }}.apk) && adb install org.dmfs.tasks.apk + (wget -cq -O org.tasks.apk https://f-droid.org/repo/org.tasks_${{ env.version_org_tasks }}.apk || wget -cq -O org.tasks.apk https://f-droid.org/archive/org.tasks_${{ env.version_org_tasks }}.apk) && adb install org.tasks.apk + (wget -cq -O at.techbee.jtx.apk https://f-droid.org/repo/at.techbee.jtx_${{ env.version_at_techbee_jtx }}.apk || wget -cq -O at.techbee.jtx.apk https://f-droid.org/archive/at.techbee.jtx_${{ env.version_at_techbee_jtx }}.apk) && adb install at.techbee.jtx.apk cd .. ./gradlew --no-daemon connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index bb6092ce..eb097002 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -9,7 +9,7 @@ plugins { id("org.jetbrains.dokka") } -val version_ical4j = "3.2.11" +val version_ical4j = "3.2.12" android { compileSdk = 33 @@ -99,7 +99,7 @@ dependencies { implementation("androidx.core:core-ktx:1.10.1") api("org.mnode.ical4j:ical4j:${version_ical4j}") - implementation("org.slf4j:slf4j-jdk14:2.0.3") // ical4j logging over java.util.Logger + implementation("org.slf4j:slf4j-jdk14:2.0.7") // ical4j logging over java.util.Logger // ical4j requires newer Apache Commons libraries, which require Java8. Force latest Java7 versions. // noinspection GradleDependency @@ -121,6 +121,6 @@ dependencies { androidTestImplementation("androidx.test:core:1.5.0") androidTestImplementation("androidx.test:runner:1.5.2") androidTestImplementation("androidx.test:rules:1.5.0") - androidTestImplementation("io.mockk:mockk-android:1.13.4") + androidTestImplementation("io.mockk:mockk-android:1.13.7") testImplementation("junit:junit:4.13.2") } \ No newline at end of file From dd0ac1fc0d0c0967c80e18e0bd7e73381faba060 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sun, 10 Sep 2023 15:12:57 +0200 Subject: [PATCH 25/92] Updated AGP to 8.1.0 and Kotlin to 1.9.0 (#111) * Updated AGP to 8.1.0 and Kotlin to 1.9.0 Signed-off-by: Arnau Mora * Set Dokka to latest version Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 227ef250..4b94c9f9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,9 @@ **************************************************************************************************/ plugins { - id("com.android.library") version "8.0.1" apply false - id("org.jetbrains.kotlin.android") version "1.8.21" apply false - id("org.jetbrains.dokka") version "1.8.10" apply false + id("com.android.library") version "8.1.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.0" apply false + id("org.jetbrains.dokka") version "1.8.20" apply false } group = "at.bitfire" From 76e30b7ebd9660d0cbd1849f50a5003ae498374c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 12 Sep 2023 11:37:57 +0200 Subject: [PATCH 26/92] Ignore RDATEs when there's an also an infinite RRULE to avoid calendar provider exception (#114) * Ignore RDATEs when there's an also an infinite RRULE to avoid calendar provider exception * Added test Signed-off-by: Arnau Mora * Tighten test intention * Remove unused import directive --------- Signed-off-by: Arnau Mora Co-authored-by: Arnau Mora Co-authored-by: Sunik Kupfer --- .../bitfire/ical4android/AndroidEventTest.kt | 15 ++++++++++++ .../at/bitfire/ical4android/AndroidEvent.kt | 24 +++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index 70ff88bf..b3ca36d2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -264,6 +264,21 @@ class AndroidEventTest { assertEquals("${tzShanghai.id};20200601T123000,20200701T183000,20200702T183000,20200801T123000,20200802T123000", values.getAsString(Events.RDATE)) } + @Test + fun testBuildEvent_NonAllDay_DtEnd_NoDuration_Recurring_InfiniteRruleAndRdate() { + val values = buildEvent(false) { + dtStart = DtStart("20200601T123000", tzShanghai) + dtEnd = DtEnd("20200601T123000", tzVienna) + rRules += RRule( + Recur("FREQ=DAILY;INTERVAL=2") + ) + rDates += RDate(DateList("20200701T123000,20200702T123000", Value.DATE_TIME, tzVienna)) + } + + assertNull(values.get(Events.RDATE)) + assertEquals("FREQ=DAILY;INTERVAL=2", values.get(Events.RRULE)) + } + @Test fun testBuildEvent_NonAllDay_DtEnd_Duration_NonRecurring() { val values = buildEvent(false) { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 18d22db7..9dbce1d7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -791,15 +791,25 @@ abstract class AndroidEvent( builder.withValue(Events.RRULE, null) if (event.rDates.isNotEmpty()) { - for (rDate in event.rDates) - AndroidTimeUtils.androidifyTimeZone(rDate) + // ignore RDATEs when there's also an infinite RRULE [https://issuetracker.google.com/issues/216374004] + val infiniteRrule = event.rRules.any { rRule -> + rRule.recur.count == -1 && // no COUNT AND + rRule.recur.until == null // no UNTIL + } + + if (infiniteRrule) + Ical4Android.log.warning("Android can't handle infinite RRULE + RDATE [https://issuetracker.google.com/issues/216374004]; ignoring RDATE(s)") + else { + for (rDate in event.rDates) + AndroidTimeUtils.androidifyTimeZone(rDate) - // Calendar provider drops DTSTART instance when using RDATE [https://code.google.com/p/android/issues/detail?id=171292] - val listWithDtStart = DateList() - listWithDtStart.add(dtStart.date) - event.rDates.addFirst(RDate(listWithDtStart)) + // Calendar provider drops DTSTART instance when using RDATE [https://code.google.com/p/android/issues/detail?id=171292] + val listWithDtStart = DateList() + listWithDtStart.add(dtStart.date) + event.rDates.addFirst(RDate(listWithDtStart)) - builder.withValue(Events.RDATE, AndroidTimeUtils.recurrenceSetsToAndroidString(event.rDates, allDay)) + builder.withValue(Events.RDATE, AndroidTimeUtils.recurrenceSetsToAndroidString(event.rDates, allDay)) + } } else builder.withValue(Events.RDATE, null) From 916f2228e8fb55d26dea171733feaf22d6e45594 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 25 Sep 2023 10:54:33 +0200 Subject: [PATCH 27/92] Updated ical4j to `3.2.13` (#116) Signed-off-by: Arnau Mora --- lib/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index eb097002..535482b0 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -9,7 +9,7 @@ plugins { id("org.jetbrains.dokka") } -val version_ical4j = "3.2.12" +val version_ical4j = "3.2.13" android { compileSdk = 33 From a134eb6864d40074145207e2074ad00eebf1c26e Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 3 Dec 2023 21:01:38 +0100 Subject: [PATCH 28/92] Update dependencies --- build.gradle.kts | 4 ++-- gradle.properties | 6 +++++- gradle/wrapper/gradle-wrapper.properties | 2 +- lib/build.gradle.kts | 14 +++++++------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4b94c9f9..bf97ead6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,8 +3,8 @@ **************************************************************************************************/ plugins { - id("com.android.library") version "8.1.0" apply false - id("org.jetbrains.kotlin.android") version "1.9.0" apply false + id("com.android.library") version "8.2.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.20" apply false id("org.jetbrains.dokka") version "1.8.20" apply false } diff --git a/gradle.properties b/gradle.properties index 38127cb3..20aaaa86 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,14 @@ # [https://developer.android.com/build/optimize-your-build#optimize] -org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g org.gradle.parallel=true # configuration cache [https://developer.android.com/build/optimize-your-build#use-the-configuration-cache-experimental] org.gradle.unsafe.configuration-cache=true org.gradle.unsafe.configuration-cache-problems=warn +# https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching=true + # Android android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3a029079..a1f2792d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 535482b0..04f4588c 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -9,10 +9,10 @@ plugins { id("org.jetbrains.dokka") } -val version_ical4j = "3.2.13" +val version_ical4j = "3.2.14" android { - compileSdk = 33 + compileSdk = 34 namespace = "at.bitfire.ical4android" @@ -95,9 +95,9 @@ configurations.forEach { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") - implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.core:core-ktx:1.12.0") api("org.mnode.ical4j:ical4j:${version_ical4j}") implementation("org.slf4j:slf4j-jdk14:2.0.7") // ical4j logging over java.util.Logger @@ -109,7 +109,7 @@ dependencies { } } // noinspection GradleDependency - api("org.apache.commons:commons-lang3:3.8.1") { + api("org.apache.commons:commons-lang3") { version { strictly("3.8.1") } @@ -121,6 +121,6 @@ dependencies { androidTestImplementation("androidx.test:core:1.5.0") androidTestImplementation("androidx.test:runner:1.5.2") androidTestImplementation("androidx.test:rules:1.5.0") - androidTestImplementation("io.mockk:mockk-android:1.13.7") + androidTestImplementation("io.mockk:mockk-android:1.13.8") testImplementation("junit:junit:4.13.2") -} \ No newline at end of file +} From 8972b85c4d9462de2f2cbb587f7210a17bdad36b Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 3 Dec 2023 21:11:49 +0100 Subject: [PATCH 29/92] Note that Reminders.METHOD_DEFAULT won't trigger an alarm on the Android device --- lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 9dbce1d7..7278afa3 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -53,7 +53,7 @@ import java.util.logging.Level * in populateEvent() / buildEvent. Setting _ID and ORIGINAL_ID is not sufficient. */ abstract class AndroidEvent( - val calendar: AndroidCalendar + val calendar: AndroidCalendar ) { companion object { @@ -930,12 +930,12 @@ abstract class AndroidEvent( val method = when (alarm.action?.value?.uppercase(Locale.ROOT)) { Action.DISPLAY.value, - Action.AUDIO.value -> Reminders.METHOD_ALERT + Action.AUDIO.value -> Reminders.METHOD_ALERT // will trigger an alarm on the Android device // Note: The calendar provider doesn't support saving specific attendees for email reminders. Action.EMAIL.value -> Reminders.METHOD_EMAIL - else -> Reminders.METHOD_DEFAULT + else -> Reminders.METHOD_DEFAULT // won't trigger an alarm on the Android device } val minutes = ICalendar.vAlarmToMin(alarm, event!!, false)?.second ?: Reminders.MINUTES_DEFAULT From db6c8a4e37c826e87a456a677f45acb4037dd59b Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Tue, 2 Jan 2024 10:16:29 +0100 Subject: [PATCH 30/92] updated-jtx-board-version-source (#122) --- .github/workflows/test-dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index f99404b8..385a64df 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -68,7 +68,7 @@ jobs: - name: Install task apps and run tests uses: reactivecircus/android-emulator-runner@v2 env: - version_at_techbee_jtx: 205020009 + version_at_techbee_jtx: v2.6.4 version_org_tasks: 130501 version_org_dmfs_tasks: 82200 with: @@ -81,7 +81,7 @@ jobs: mkdir .apk && cd .apk (wget -cq -O org.dmfs.tasks.apk https://f-droid.org/repo/org.dmfs.tasks_${{ env.version_org_dmfs_tasks }}.apk || wget -cq -O org.dmfs.tasks.apk https://f-droid.org/archive/org.dmfs.tasks_${{ env.version_org_dmfs_tasks }}.apk) && adb install org.dmfs.tasks.apk (wget -cq -O org.tasks.apk https://f-droid.org/repo/org.tasks_${{ env.version_org_tasks }}.apk || wget -cq -O org.tasks.apk https://f-droid.org/archive/org.tasks_${{ env.version_org_tasks }}.apk) && adb install org.tasks.apk - (wget -cq -O at.techbee.jtx.apk https://f-droid.org/repo/at.techbee.jtx_${{ env.version_at_techbee_jtx }}.apk || wget -cq -O at.techbee.jtx.apk https://f-droid.org/archive/at.techbee.jtx_${{ env.version_at_techbee_jtx }}.apk) && adb install at.techbee.jtx.apk + (wget -cq -O at.techbee.jtx.apk https://github.com/TechbeeAT/jtxBoard/releases/download/${{ env.version_at_techbee_jtx }}/jtxBoard-${{ env.version_at_techbee_jtx }}.apk) && adb install at.techbee.jtx.apk cd .. ./gradlew --no-daemon connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest From 93262f53c2f6e200f73ec9e0ca876e9b120841ae Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 2 Jan 2024 18:40:31 +0100 Subject: [PATCH 31/92] Fix CodeQL (#124) * Upgrade Gradle wrapper Signed-off-by: Arnau Mora Gras * Upgrade Kotlin Signed-off-by: Arnau Mora Gras * Upgrade Dokka Signed-off-by: Arnau Mora Gras * Got rid of `--no-daemon` Signed-off-by: Arnau Mora Gras * Try to remove assemble Signed-off-by: Arnau Mora Gras * Added autobuild Signed-off-by: Arnau Mora Gras --------- Signed-off-by: Arnau Mora Gras --- .github/workflows/codeql.yml | 8 ++++---- build.gradle.kts | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 36683498..6b9d70d8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,11 +54,11 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - #- name: Autobuild - # uses: github/codeql-action/autobuild@v2 + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - - name: Build - run: ./gradlew --no-daemon assemble + # - name: Build + # run: ./gradlew assemble - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/build.gradle.kts b/build.gradle.kts index bf97ead6..b3177b2c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,8 @@ plugins { id("com.android.library") version "8.2.0" apply false - id("org.jetbrains.kotlin.android") version "1.9.20" apply false - id("org.jetbrains.dokka") version "1.8.20" apply false + id("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("org.jetbrains.dokka") version "1.9.10" apply false } group = "at.bitfire" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a1f2792d..d0d403e2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d4f4544a0c8c028ecf846c45fa3d1deee506a157 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 23 Jan 2024 02:10:07 -0800 Subject: [PATCH 32/92] Added uid handling (#129) Signed-off-by: Arnau Mora Gras --- .../bitfire/ical4android/AndroidEventTest.kt | 57 +++++++++++++++++-- .../at/bitfire/ical4android/AndroidEvent.kt | 40 +++++++++++-- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index b3ca36d2..6f2168c5 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -10,7 +10,13 @@ import android.content.ContentUris import android.content.ContentValues import android.database.DatabaseUtils import android.net.Uri -import android.provider.CalendarContract.* +import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL +import android.provider.CalendarContract.AUTHORITY +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.ExtendedProperties +import android.provider.CalendarContract.Reminders import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestCalendar @@ -19,13 +25,37 @@ import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.closeCompat -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterList +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.component.VAlarm -import net.fortuna.ical4j.model.parameter.* +import net.fortuna.ical4j.model.parameter.Cn +import net.fortuna.ical4j.model.parameter.CuType +import net.fortuna.ical4j.model.parameter.Email +import net.fortuna.ical4j.model.parameter.Language +import net.fortuna.ical4j.model.parameter.PartStat +import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.parameter.Role +import net.fortuna.ical4j.model.parameter.Rsvp +import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.* import net.fortuna.ical4j.util.TimeZones -import org.junit.* -import org.junit.Assert.* +import org.junit.After +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test import java.net.URI import java.time.Duration import java.time.Period @@ -717,6 +747,14 @@ class AndroidEventTest { } } + fun testBuildEvent_UID2445() { + buildEvent(true) { + uid = "event1@example.com" + }.let { result -> + assertEquals("event1@example.com", result.getAsString(Events.UID_2445)) + } + } + private fun firstReminder(row: ContentValues): ContentValues? { val id = row.getAsInteger(Events._ID) @@ -1810,6 +1848,15 @@ class AndroidEventTest { } } + @Test + fun testPopulateEvent_Uid_UID_2445() { + populateEvent(true) { + put(Events.UID_2445, "event1@example.com") + }.let { result -> + assertEquals("event1@example.com", result.uid) + } + } + private fun populateReminder(destinationCalendar: TestCalendar = calendar, builder: ContentValues.() -> Unit): VAlarm? { populateEvent(true, destinationCalendar = destinationCalendar, valuesBuilder = {}, insertCallback = { id -> diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 7278afa3..c7e9bd9d 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -10,7 +10,12 @@ import android.content.ContentValues import android.content.EntityIterator import android.net.Uri import android.os.RemoteException -import android.provider.CalendarContract.* +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Colors +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.EventsEntity +import android.provider.CalendarContract.ExtendedProperties +import android.provider.CalendarContract.Reminders import android.util.Patterns import androidx.annotation.CallSuper import at.bitfire.ical4android.BatchOperation.CpoBuilder @@ -28,19 +33,41 @@ import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.ical4android.util.TimeApiExtensions.toLocalTime import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime -import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VAlarm -import net.fortuna.ical4j.model.parameter.* -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.parameter.Cn +import net.fortuna.ical4j.model.parameter.Email +import net.fortuna.ical4j.model.parameter.PartStat +import net.fortuna.ical4j.model.parameter.Rsvp +import net.fortuna.ical4j.model.parameter.Value +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Attendee +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.ExRule +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Summary import net.fortuna.ical4j.util.TimeZones import java.io.FileNotFoundException import java.net.URI import java.net.URISyntaxException -import java.time.* import java.time.Duration +import java.time.Instant import java.time.Period -import java.util.* +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.util.Locale import java.util.logging.Level /** @@ -288,6 +315,7 @@ abstract class AndroidEvent( Ical4Android.log.log(Level.WARNING, "Couldn't parse recurrence rules, ignoring", e) } + event.uid = row.getAsString(Events.UID_2445) event.summary = row.getAsString(Events.TITLE) event.location = row.getAsString(Events.EVENT_LOCATION) event.description = row.getAsString(Events.DESCRIPTION) From cc21286cd2baea0f92346efbb17871c173ae9b8e Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:55:27 +0100 Subject: [PATCH 33/92] Updated contract, added method in JtxCollection + Test (#121) * Updated contract, added method in JtxCollection + Test * fixes * updated min version code * updated min version code * Update test-dev.yml updated jtx version * Update test-dev.yml --- .github/workflows/test-dev.yml | 2 +- .../bitfire/ical4android/JtxCollectionTest.kt | 22 +++++++++++++++++++ .../at/bitfire/ical4android/JtxCollection.kt | 9 ++++++++ .../at/bitfire/ical4android/TaskProvider.kt | 2 +- .../main/kotlin/at/techbee/jtx/JtxContract.kt | 15 +++++++++---- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index 385a64df..2da6a807 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -68,7 +68,7 @@ jobs: - name: Install task apps and run tests uses: reactivecircus/android-emulator-runner@v2 env: - version_at_techbee_jtx: v2.6.4 + version_at_techbee_jtx: v2.7.0 version_org_tasks: 130501 version_org_dmfs_tasks: 82200 with: diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt index 14a8e0a9..ed38d0e5 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt @@ -135,4 +135,26 @@ class JtxCollectionTest { assertTrue(ics.contains(Regex("BEGIN:VJOURNAL(\\n*|\\r*|\\t*|.*)*END:VJOURNAL"))) assertTrue(ics.contains(Regex("BEGIN:VTODO(\\n*|\\r*|\\t*|.*)*END:VTODO"))) } + + + @Test + fun updateLastSync_test() { + val collectionUri = JtxCollection.create(testAccount, client, cv) + assertNotNull(collectionUri) + val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) + + collections.forEach { collection -> + client.query(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(testAccount), arrayOf(JtxContract.JtxCollection.LAST_SYNC), null, emptyArray(), null).use { + assertNotNull(it) + assertTrue(it!!.moveToFirst()) + assertTrue(it.isNull(0)) + } + collection.updateLastSync() + client.query(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(testAccount), arrayOf(JtxContract.JtxCollection.LAST_SYNC), null, emptyArray(), null).use { + assertNotNull(it) + assertTrue(it!!.moveToFirst()) + assertTrue(!it.isNull(0)) + } + } + } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index 4ed91752..3df4f26b 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -259,4 +259,13 @@ open class JtxCollection(val account: Account, return ical.toString() } } + + /** + * Updates the last sync datetime for all collections of an account + */ + fun updateLastSync() { + val values = ContentValues(1) + values.put(JtxContract.JtxCollection.LAST_SYNC, System.currentTimeMillis()) + client.update(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), values, "${JtxContract.JtxCollection.ID} = ?", arrayOf(id.toString())) + } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt index b6ab20cd..763ea7ff 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt @@ -28,7 +28,7 @@ class TaskProvider private constructor( private val readPermission: String, private val writePermission: String ) { - JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 204030000, "2.04.03", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), + JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 207000001, "2.07.00", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), TasksOrg("org.tasks.opentasks", "org.tasks", 100000, "10.0", PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE), OpenTasks("org.dmfs.tasks", "org.dmfs.tasks", 103, "1.1.8.2", PERMISSION_OPENTASKS_READ, PERMISSION_OPENTASKS_WRITE); diff --git a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt index 8d6daf94..d729ab8c 100644 --- a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt +++ b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt @@ -20,6 +20,7 @@ import net.fortuna.ical4j.model.property.XProperty import org.json.JSONObject import java.util.logging.Level + @Suppress("unused") object JtxContract { @@ -42,7 +43,7 @@ object JtxContract { const val AUTHORITY = "at.techbee.jtx.provider" /** The version of this SyncContentProviderContract */ - const val VERSION = 6 + const val VERSION = 7 /** Constructs an Uri for the Jtx Sync Adapter with the given Account * @param [account] The account that should be appended to the Base Uri @@ -191,8 +192,8 @@ object JtxContract { 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 + * This is an internal differentiation for JOURNAL, NOTE and T0DO 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] (T0do 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. @@ -514,7 +515,7 @@ object JtxContract { const val SEQUENCE = "sequence" /** - * Purpose: This property specifies a color used for displaying the calendar, event, todo, or journal data. + * Purpose: This property specifies a color used for displaying the calendar, event, t0do, 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] @@ -1205,6 +1206,12 @@ object JtxContract { */ const val READONLY = "readonly" + /** + * Purpose: This column/property defines the date/time of the last sync + * Type: [Long] + */ + const val LAST_SYNC = "lastsync" + } From 90804fc0471c7414c224cb5689f6699a2353410e Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 25 Jan 2024 16:18:56 +0100 Subject: [PATCH 34/92] Update AGP to 8.2.1, minimum SDK level to 23 --- build.gradle.kts | 2 +- lib/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b3177b2c..4c7ee0d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ **************************************************************************************************/ plugins { - id("com.android.library") version "8.2.0" apply false + id("com.android.library") version "8.2.1" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false id("org.jetbrains.dokka") version "1.9.10" apply false } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 04f4588c..16ff89e9 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -17,7 +17,7 @@ android { namespace = "at.bitfire.ical4android" defaultConfig { - minSdk = 21 // Android 5.0 + minSdk = 23 // Android 6 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 18d86b3ae13e74fbae9e2e8040a5b9c53057b770 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 26 Jan 2024 03:55:01 -0800 Subject: [PATCH 35/92] Fixed UID loading (#131) * Added missing annotation Signed-off-by: Arnau Mora * Fixed UID loading Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora --- .../kotlin/at/bitfire/ical4android/AndroidEventTest.kt | 1 + lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index 6f2168c5..9b8755b2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -747,6 +747,7 @@ class AndroidEventTest { } } + @Test fun testBuildEvent_UID2445() { buildEvent(true) { uid = "event1@example.com" diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index c7e9bd9d..6cc0d535 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -897,6 +897,7 @@ abstract class AndroidEvent( .withValue(Events.EXDATE, null) } + builder.withValue(Events.UID_2445, event.uid) builder.withValue(Events.TITLE, event.summary) builder.withValue(Events.EVENT_LOCATION, event.location) builder.withValue(Events.DESCRIPTION, event.description) From 66dc34c01a7c379c7687f6db73045c68f1f7dd65 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 1 Feb 2024 10:42:00 -0800 Subject: [PATCH 36/92] Process Google Calendar-private extended property "iCalUid" (#126) * Added uid initialization Signed-off-by: Arnau Mora * Added extended properties fetch Signed-off-by: Arnau Mora Gras * Added empty check Signed-off-by: Arnau Mora Gras * Fixed extended properties check Signed-off-by: Arnau Mora Gras * Fixed iCalUid insertion Signed-off-by: Arnau Mora Gras * Cleaned up extended properties fetch Signed-off-by: Arnau Mora Gras * Using `populateExtended` Signed-off-by: Arnau Mora Gras * Added extended properties utils Signed-off-by: Arnau Mora Gras * Removed duplicate Signed-off-by: Arnau Mora Gras * Renamed external properties constants Signed-off-by: Arnau Mora Gras * Removed duplicate Signed-off-by: Arnau Mora Gras * Typo Signed-off-by: Arnau Mora Gras * Added default function to `valuesBuilder` and cleanup Signed-off-by: Arnau Mora Gras * Added test for UID_2445 / iCalUid precedence * Implemented iCalUid removal Signed-off-by: Arnau Mora Gras * Delete iCalUid rows on update; don't load extended properties to extendedProperties --------- Signed-off-by: Arnau Mora Signed-off-by: Arnau Mora Gras Co-authored-by: Ricki Hirner --- .../bitfire/ical4android/AndroidEventTest.kt | 160 +++++++++++------- .../at/bitfire/ical4android/AndroidEvent.kt | 51 ++++-- 2 files changed, 134 insertions(+), 77 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index 9b8755b2..ee56a7e3 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -17,6 +17,7 @@ import android.provider.CalendarContract.Calendars import android.provider.CalendarContract.Events import android.provider.CalendarContract.ExtendedProperties import android.provider.CalendarContract.Reminders +import androidx.core.content.contentValuesOf import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestCalendar @@ -562,7 +563,7 @@ class AndroidEventTest { buildEvent(true) { url = URI("https://example.com") }.let { result -> - assertEquals("https://example.com", firstExtendedProperty(result, AndroidEvent.MIMETYPE_URL)) + assertEquals("https://example.com", firstExtendedProperty(result, AndroidEvent.EXTNAME_URL)) } } @@ -1448,13 +1449,14 @@ class AndroidEventTest { } - private fun populateEvent( - automaticDates: Boolean, - destinationCalendar: TestCalendar = calendar, - asSyncAdapter: Boolean = false, - insertCallback: (id: Long) -> Unit = {}, - valuesBuilder: ContentValues.() -> Unit - ): Event { + private fun populateAndroidEvent( + automaticDates: Boolean, + destinationCalendar: TestCalendar = calendar, + asSyncAdapter: Boolean = false, + insertCallback: (id: Long) -> Unit = {}, + extendedProperties: Map = emptyMap(), + valuesBuilder: ContentValues.() -> Unit = {} + ): AndroidEvent { val values = ContentValues() values.put(Events.CALENDAR_ID, destinationCalendar.id) if (automaticDates) { @@ -1466,18 +1468,45 @@ class AndroidEventTest { valuesBuilder(values) Ical4Android.log.info("Inserting test event: $values") val uri = provider.insert( - if (asSyncAdapter) - Events.CONTENT_URI.asSyncAdapter(testAccount) - else - Events.CONTENT_URI, - values)!! + if (asSyncAdapter) + Events.CONTENT_URI.asSyncAdapter(testAccount) + else + Events.CONTENT_URI, + values)!! val id = ContentUris.parseId(uri) // insert additional rows etc. insertCallback(id) - val androidEvent = destinationCalendar.findById(id) - return androidEvent.event!! + // insert extended properties + for ((name, value) in extendedProperties) { + val extendedValues = contentValuesOf( + ExtendedProperties.EVENT_ID to id, + ExtendedProperties.NAME to name, + ExtendedProperties.VALUE to value + ) + provider.insert(ExtendedProperties.CONTENT_URI.asSyncAdapter(testAccount), extendedValues) + } + + return destinationCalendar.findById(id) + } + + private fun populateEvent( + automaticDates: Boolean, + destinationCalendar: TestCalendar = calendar, + asSyncAdapter: Boolean = false, + insertCallback: (id: Long) -> Unit = {}, + extendedProperties: Map = emptyMap(), + valuesBuilder: ContentValues.() -> Unit = {} + ): Event { + return populateAndroidEvent( + automaticDates, + destinationCalendar, + asSyncAdapter, + insertCallback, + extendedProperties, + valuesBuilder + ).event!! } @Test @@ -1650,14 +1679,10 @@ class AndroidEventTest { } @Test - fun textPopulateEvent_Url() { - populateEvent(true, insertCallback = { id -> - val urlValues = ContentValues() - urlValues.put(ExtendedProperties.EVENT_ID, id) - urlValues.put(ExtendedProperties.NAME, AndroidEvent.MIMETYPE_URL) - urlValues.put(ExtendedProperties.VALUE, "https://example.com") - provider.insert(ExtendedProperties.CONTENT_URI.asSyncAdapter(testAccount), urlValues) - }, valuesBuilder = {}).let { result -> + fun testPopulateEvent_Url() { + populateEvent(true, + extendedProperties = mapOf(AndroidEvent.EXTNAME_URL to "https://example.com") + ).let { result -> assertEquals(URI("https://example.com"), result.url) } } @@ -1710,10 +1735,7 @@ class AndroidEventTest { @Test fun testPopulateEvent_Status_None() { - populateEvent(true) { - }.let { result -> - assertNull(result.status) - } + assertNull(populateEvent(true).status) } @Test @@ -1745,10 +1767,7 @@ class AndroidEventTest { @Test fun testPopulateEvent_Organizer_NotGroupScheduled() { - populateEvent(true) { - }.let { result -> - assertNull(result.organizer) - } + assertNull(populateEvent(true).organizer) } @Test @@ -1762,15 +1781,15 @@ class AndroidEventTest { @Test fun testPopulateEvent_Organizer_GroupScheduled() { - populateEvent(true, valuesBuilder = { - put(Events.ORGANIZER, "organizer@example.com") - }, insertCallback = { id -> + populateEvent(true, insertCallback = { id -> provider.insert(Attendees.CONTENT_URI.asSyncAdapter(testAccount), ContentValues().apply { put(Attendees.EVENT_ID, id) put(Attendees.ATTENDEE_EMAIL, "organizer@example.com") put(Attendees.ATTENDEE_TYPE, Attendees.RELATIONSHIP_ORGANIZER) }) - }).let { result -> + }) { + put(Events.ORGANIZER, "organizer@example.com") + }.let { result -> assertEquals("mailto:organizer@example.com", result.organizer?.value) } } @@ -1804,15 +1823,11 @@ class AndroidEventTest { @Test fun testPopulateEvent_Classification_Confidential_Retained() { - populateEvent(true, valuesBuilder = { + populateEvent(true, + extendedProperties = mapOf(UnknownProperty.CONTENT_ITEM_TYPE to UnknownProperty.toJsonString(Clazz.CONFIDENTIAL)) + ) { put(Events.ACCESS_LEVEL, Events.ACCESS_DEFAULT) - }, insertCallback = { id -> - provider.insert(ExtendedProperties.CONTENT_URI.asSyncAdapter(testAccount), ContentValues().apply { - put(ExtendedProperties.EVENT_ID, id) - put(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE) - put(ExtendedProperties.VALUE, UnknownProperty.toJsonString(Clazz.CONFIDENTIAL)) - }) - }).let { result -> + }.let { result -> assertEquals(Clazz.CONFIDENTIAL, result.classification) } } @@ -1828,15 +1843,15 @@ class AndroidEventTest { @Test fun testPopulateEvent_Classification_Custom() { - populateEvent(true, valuesBuilder = { - put(Events.ACCESS_LEVEL, Events.ACCESS_DEFAULT) - }, insertCallback = { id -> - provider.insert(ExtendedProperties.CONTENT_URI.asSyncAdapter(testAccount), ContentValues().apply { - put(ExtendedProperties.EVENT_ID, id) - put(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE) - put(ExtendedProperties.VALUE, UnknownProperty.toJsonString(Clazz("TOP-SECRET"))) - }) - }).let { result -> + populateEvent( + true, + valuesBuilder = { + put(Events.ACCESS_LEVEL, Events.ACCESS_DEFAULT) + }, + extendedProperties = mapOf( + UnknownProperty.CONTENT_ITEM_TYPE to UnknownProperty.toJsonString(Clazz("TOP-SECRET")) + ) + ).let { result -> assertEquals(Clazz("TOP-SECRET"), result.classification) } } @@ -1849,6 +1864,18 @@ class AndroidEventTest { } } + @Test + fun testPopulateEvent_Uid_iCalUid() { + populateEvent( + true, + extendedProperties = mapOf( + AndroidEvent.EXTNAME_ICAL_UID to "event1@example.com" + ) + ).let { result -> + assertEquals("event1@example.com", result.uid) + } + } + @Test fun testPopulateEvent_Uid_UID_2445() { populateEvent(true) { @@ -1858,9 +1885,21 @@ class AndroidEventTest { } } + @Test + fun testPopulateEvent_Uid_UID_2445_and_iCalUid() { + populateEvent(true, + extendedProperties = mapOf( + AndroidEvent.EXTNAME_ICAL_UID to "event1@example.com" + )) { + put(Events.UID_2445, "event2@example.com") + }.let { result -> + assertEquals("event2@example.com", result.uid) + } + } + private fun populateReminder(destinationCalendar: TestCalendar = calendar, builder: ContentValues.() -> Unit): VAlarm? { - populateEvent(true, destinationCalendar = destinationCalendar, valuesBuilder = {}, insertCallback = { id -> + populateEvent(true, destinationCalendar = destinationCalendar, insertCallback = { id -> val reminderValues = ContentValues() reminderValues.put(Reminders.EVENT_ID, id) builder(reminderValues) @@ -1937,7 +1976,7 @@ class AndroidEventTest { private fun populateAttendee(builder: ContentValues.() -> Unit): Attendee? { - populateEvent(true, valuesBuilder = {}, insertCallback = { id -> + populateEvent(true, insertCallback = { id -> val attendeeValues = ContentValues() attendeeValues.put(Attendees.EVENT_ID, id) builder(attendeeValues) @@ -2176,13 +2215,12 @@ class AndroidEventTest { val params = ParameterList() params.add(Language("en")) val unknownProperty = XProperty("X-NAME", params, "Custom Value") - val result = populateEvent(true, valuesBuilder = {}, insertCallback = { id -> - val values = ContentValues() - values.put(ExtendedProperties.EVENT_ID, id) - values.put(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE) - values.put(ExtendedProperties.VALUE, UnknownProperty.toJsonString(unknownProperty)) - provider.insert(ExtendedProperties.CONTENT_URI.asSyncAdapter(testAccount), values) - }).unknownProperties.first + val result = populateEvent( + true, + extendedProperties = mapOf( + UnknownProperty.CONTENT_ITEM_TYPE to UnknownProperty.toJsonString(unknownProperty) + ) + ).unknownProperties.first assertEquals("X-NAME", result.name) assertEquals("en", result.getParameter(Parameter.LANGUAGE).value) assertEquals("Custom Value", result.value) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 6cc0d535..ad74ea8b 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -96,14 +96,22 @@ abstract class AndroidEvent( * * Example: `Cat1\Cat2` */ - const val MIMETYPE_CATEGORIES = "categories" + const val EXTNAME_CATEGORIES = "categories" const val CATEGORIES_SEPARATOR = '\\' + /** + * Google Calendar uses an extended property called `iCalUid` for storing the event's UID, instead of the + * standard [Events.UID_2445]. + * + * @see GitHub Issue + */ + const val EXTNAME_ICAL_UID = "iCalUid" + /** * VEVENT URL is stored as an extended property with this [ExtendedProperties.NAME]. * The URL is directly put into [ExtendedProperties.VALUE]. */ - const val MIMETYPE_URL = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.ical4android.url" + const val EXTNAME_URL = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.ical4android.url" } var id: Long? = null @@ -452,23 +460,28 @@ abstract class AndroidEvent( } protected open fun populateExtended(row: ContentValues) { - val mimeType = row.getAsString(ExtendedProperties.NAME) + val name = row.getAsString(ExtendedProperties.NAME) val rawValue = row.getAsString(ExtendedProperties.VALUE) - Ical4Android.log.log(Level.FINE, "Read extended property from calender provider", arrayOf(mimeType, rawValue)) + Ical4Android.log.log(Level.FINE, "Read extended property from calender provider", arrayOf(name, rawValue)) val event = requireNotNull(event) try { - when (mimeType) { - MIMETYPE_CATEGORIES -> + when (name) { + EXTNAME_CATEGORIES -> event.categories += rawValue.split(CATEGORIES_SEPARATOR) - MIMETYPE_URL -> + EXTNAME_URL -> try { event.url = URI(rawValue) } catch(e: URISyntaxException) { Ical4Android.log.warning("Won't process invalid local URL: $rawValue") } + EXTNAME_ICAL_UID -> + // only consider iCalUid when there's no uid + if (event.uid == null) + event.uid = rawValue + UnknownProperty.CONTENT_ITEM_TYPE -> event.unknownProperties += UnknownProperty.fromJsonString(rawValue) } @@ -587,7 +600,7 @@ abstract class AndroidEvent( retainClassification() // URL event.url?.let { url -> - insertExtendedProperty(batch, idxEvent, MIMETYPE_URL, url.toString()) + insertExtendedProperty(batch, idxEvent, EXTNAME_URL, url.toString()) } // unknown properties event.unknownProperties.forEach { @@ -696,8 +709,14 @@ abstract class AndroidEvent( .enqueue(CpoBuilder .newDelete(ExtendedProperties.CONTENT_URI.asSyncAdapter(calendar.account)) .withSelection( - "${ExtendedProperties.EVENT_ID}=? AND ${ExtendedProperties.NAME} IN (?,?,?)", - arrayOf(existingId.toString(), MIMETYPE_CATEGORIES, MIMETYPE_URL, UnknownProperty.CONTENT_ITEM_TYPE) + "${ExtendedProperties.EVENT_ID}=? AND ${ExtendedProperties.NAME} IN (?,?,?,?)", + arrayOf( + existingId.toString(), + EXTNAME_CATEGORIES, + EXTNAME_ICAL_UID, // UID is stored in UID_2445, don't leave iCalUid rows in events that we have written + EXTNAME_URL, + UnknownProperty.CONTENT_ITEM_TYPE + ) )) addOrUpdateRows(batch) @@ -1011,12 +1030,12 @@ abstract class AndroidEvent( batch.enqueue(builder) } - protected open fun insertExtendedProperty(batch: BatchOperation, idxEvent: Int?, mimeType: String, value: String) { + protected open fun insertExtendedProperty(batch: BatchOperation, idxEvent: Int?, name: String, value: String) { val builder = CpoBuilder - .newInsert(ExtendedProperties.CONTENT_URI.asSyncAdapter(calendar.account)) - .withEventId(ExtendedProperties.EVENT_ID, idxEvent) - .withValue(ExtendedProperties.NAME, mimeType) - .withValue(ExtendedProperties.VALUE, value) + .newInsert(ExtendedProperties.CONTENT_URI.asSyncAdapter(calendar.account)) + .withEventId(ExtendedProperties.EVENT_ID, idxEvent) + .withValue(ExtendedProperties.NAME, name) + .withValue(ExtendedProperties.VALUE, value) batch.enqueue(builder) } @@ -1026,7 +1045,7 @@ abstract class AndroidEvent( // drop occurrences of CATEGORIES_SEPARATOR in category names category.filter { it != CATEGORIES_SEPARATOR } } - insertExtendedProperty(batch, idxEvent, MIMETYPE_CATEGORIES, rawCategories) + insertExtendedProperty(batch, idxEvent, EXTNAME_CATEGORIES, rawCategories) } protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int?, property: Property) { From 821ab3fe40f060060cd8ff858fe5b42372191dd4 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sat, 3 Feb 2024 03:05:23 -0800 Subject: [PATCH 37/92] Use gradle version catalog for dependencies (#133) * Migrated plugins and dependencies for library Signed-off-by: Arnau Mora Gras * Suppressed warnings Signed-off-by: Arnau Mora Gras * Manual build Signed-off-by: Arnau Mora Gras * Update action versions * Update versions --------- Signed-off-by: Arnau Mora Gras Co-authored-by: Ricki Hirner --- .github/workflows/codeql.yml | 14 +++++----- build.gradle.kts | 6 ++--- gradle/libs.versions.toml | 39 +++++++++++++++++++++++++++ lib/build.gradle.kts | 51 +++++++++++++++++------------------- 4 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6b9d70d8..11f7edec 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -44,23 +44,23 @@ jobs: with: distribution: 'temurin' java-version: 17 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/actions/setup-gradle@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # - name: Autobuild + # uses: github/codeql-action/autobuild@v2 - # - name: Build - # run: ./gradlew assemble + - name: Build + run: ./gradlew --no-daemon assemble - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/build.gradle.kts b/build.gradle.kts index 4c7ee0d4..ecb26b39 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,9 @@ **************************************************************************************************/ plugins { - id("com.android.library") version "8.2.1" apply false - id("org.jetbrains.kotlin.android") version "1.9.22" apply false - id("org.jetbrains.dokka") version "1.9.10" apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.dokka) apply false } group = "at.bitfire" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..97fcaf73 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,39 @@ +[versions] +agp = "8.2.2" +android-desugar = "2.0.4" +androidx-core = "1.12.0" +androidx-test-core = "1.5.0" +androidx-test-rules = "1.5.0" +androidx-test-runner = "1.5.2" +# noinspection GradleDependency +commons-collections = "4.2" +# noinspection GradleDependency +commons-lang = "3.8.1" +# noinspection GradleDependency +commons-io = "2.6" +dokka ="1.9.10" +ical4j = "3.2.14" +junit = "4.13.2" +kotlin = "1.9.22" +mockk = "1.13.9" +slf4j = "2.0.11" + +[libraries] +android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } +androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +commons-collections = { module = "org.apache.commons:commons-collections4", version.ref = "commons-collections" } +commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" } +commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } +ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } +junit = { module = "junit:junit", version.ref = "junit" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +slf4j = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" } + +[plugins] +android-library = { id = "com.android.library", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 16ff89e9..b8a597a2 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -3,14 +3,12 @@ **************************************************************************************************/ plugins { - id("com.android.library") - id("kotlin-android") - id("maven-publish") - id("org.jetbrains.dokka") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.dokka) + `maven-publish` } -val version_ical4j = "3.2.14" - android { compileSdk = 34 @@ -21,7 +19,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - buildConfigField("String", "version_ical4j", "\"${version_ical4j}\"") + buildConfigField("String", "version_ical4j", "\"${libs.versions.ical4j.get()}\"") aarMetadata { minCompileSdk = 29 @@ -94,33 +92,32 @@ configurations.forEach { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib") - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + implementation(libs.kotlin.stdlib) + coreLibraryDesugaring(libs.android.desugaring) - implementation("androidx.core:core-ktx:1.12.0") - api("org.mnode.ical4j:ical4j:${version_ical4j}") - implementation("org.slf4j:slf4j-jdk14:2.0.7") // ical4j logging over java.util.Logger + implementation(libs.androidx.core) + api(libs.ical4j) + implementation(libs.slf4j) // ical4j logging over java.util.Logger // ical4j requires newer Apache Commons libraries, which require Java8. Force latest Java7 versions. - // noinspection GradleDependency - api("org.apache.commons:commons-collections4") { + @Suppress("RedundantSuppression") + api(libs.commons.collections) { version { - strictly("4.2") + strictly(libs.versions.commons.collections.get()) } } - // noinspection GradleDependency - api("org.apache.commons:commons-lang3") { + @Suppress("RedundantSuppression") + api(libs.commons.lang3) { version { - strictly("3.8.1") + strictly(libs.versions.commons.lang.get()) } } - // noinspection GradleDependency - @Suppress("GradleDependency") - implementation("commons-io:commons-io:2.6") - - androidTestImplementation("androidx.test:core:1.5.0") - androidTestImplementation("androidx.test:runner:1.5.2") - androidTestImplementation("androidx.test:rules:1.5.0") - androidTestImplementation("io.mockk:mockk-android:1.13.8") - testImplementation("junit:junit:4.13.2") + @Suppress("RedundantSuppression") + implementation(libs.commons.io) + + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.mockk.android) + testImplementation(libs.junit) } From 1ca03ea0e9a1f875fe2f4bfdf5220753c3e83b23 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 17 Feb 2024 16:54:28 +0100 Subject: [PATCH 38/92] Rename AndroidTask to DmfsTask (#137) --- ...droidTaskListTest.kt => DmfsTaskListTest.kt} | 6 +++--- .../{AndroidTaskTest.kt => DmfsTaskTest.kt} | 4 ++-- .../at/bitfire/ical4android/impl/TestTask.kt | 14 +++++++------- .../bitfire/ical4android/impl/TestTaskList.kt | 10 +++++----- .../{AndroidTask.kt => DmfsTask.kt} | 11 +++++------ ...AndroidTaskFactory.kt => DmfsTaskFactory.kt} | 4 ++-- .../{AndroidTaskList.kt => DmfsTaskList.kt} | 17 ++++++++--------- ...askListFactory.kt => DmfsTaskListFactory.kt} | 2 +- 8 files changed, 33 insertions(+), 35 deletions(-) rename lib/src/androidTest/kotlin/at/bitfire/ical4android/{AndroidTaskListTest.kt => DmfsTaskListTest.kt} (94%) rename lib/src/androidTest/kotlin/at/bitfire/ical4android/{AndroidTaskTest.kt => DmfsTaskTest.kt} (99%) rename lib/src/main/kotlin/at/bitfire/ical4android/{AndroidTask.kt => DmfsTask.kt} (98%) rename lib/src/main/kotlin/at/bitfire/ical4android/{AndroidTaskFactory.kt => DmfsTaskFactory.kt} (72%) rename lib/src/main/kotlin/at/bitfire/ical4android/{AndroidTaskList.kt => DmfsTaskList.kt} (93%) rename lib/src/main/kotlin/at/bitfire/ical4android/{AndroidTaskListFactory.kt => DmfsTaskListFactory.kt} (85%) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt similarity index 94% rename from lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskListTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt index b89f6df5..7c20de5b 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt @@ -20,7 +20,7 @@ import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test -class AndroidTaskListTest(providerName: TaskProvider.ProviderName): +class DmfsTaskListTest(providerName: TaskProvider.ProviderName): AbstractTasksTest(providerName) { private val testAccount = Account("AndroidTaskListTest", TaskContract.LOCAL_ACCOUNT_TYPE) @@ -34,10 +34,10 @@ class AndroidTaskListTest(providerName: TaskProvider.ProviderName): info.put(TaskContract.TaskLists.SYNC_ENABLED, 1) info.put(TaskContract.TaskLists.VISIBLE, 1) - val uri = AndroidTaskList.create(testAccount, provider, info) + val uri = DmfsTaskList.create(testAccount, provider, info) assertNotNull(uri) - return AndroidTaskList.findByID(testAccount, provider, TestTaskList.Factory, ContentUris.parseId(uri)) + return DmfsTaskList.findByID(testAccount, provider, TestTaskList.Factory, ContentUris.parseId(uri)) } diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt similarity index 99% rename from lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index ffc90943..30f7dba5 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -29,7 +29,7 @@ import org.junit.Before import org.junit.Test import java.time.ZoneId -class AndroidTaskTest( +class DmfsTaskTest( providerName: TaskProvider.ProviderName ): AbstractTasksTest(providerName) { @@ -583,7 +583,7 @@ class AndroidTaskTest( }.let { result -> val taskId = result.getAsLong(Tasks._ID) val unknownProperty = firstProperty(taskId, UnknownProperty.CONTENT_ITEM_TYPE)!! - assertEquals(xProperty, UnknownProperty.fromJsonString(unknownProperty.getAsString(AndroidTask.UNKNOWN_PROPERTY_DATA))) + assertEquals(xProperty, UnknownProperty.fromJsonString(unknownProperty.getAsString(DmfsTask.UNKNOWN_PROPERTY_DATA))) } } diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt index 6c03dabd..3aa287a6 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt @@ -6,21 +6,21 @@ package at.bitfire.ical4android.impl import android.content.ContentValues -import at.bitfire.ical4android.AndroidTask -import at.bitfire.ical4android.AndroidTaskFactory -import at.bitfire.ical4android.AndroidTaskList +import at.bitfire.ical4android.DmfsTask +import at.bitfire.ical4android.DmfsTaskFactory +import at.bitfire.ical4android.DmfsTaskList import at.bitfire.ical4android.Task -class TestTask: AndroidTask { +class TestTask: DmfsTask { - constructor(taskList: AndroidTaskList, values: ContentValues) + constructor(taskList: DmfsTaskList, values: ContentValues) : super(taskList, values) constructor(taskList: TestTaskList, task: Task) : super(taskList, task) - object Factory: AndroidTaskFactory { - override fun fromProvider(taskList: AndroidTaskList, values: ContentValues) = + object Factory: DmfsTaskFactory { + override fun fromProvider(taskList: DmfsTaskList, values: ContentValues) = TestTask(taskList, values) } diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt index 0f4ef1e5..0ed9edf7 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt @@ -7,8 +7,8 @@ package at.bitfire.ical4android.impl import android.accounts.Account import android.content.ContentUris import android.content.ContentValues -import at.bitfire.ical4android.AndroidTaskList -import at.bitfire.ical4android.AndroidTaskListFactory +import at.bitfire.ical4android.DmfsTaskList +import at.bitfire.ical4android.DmfsTaskListFactory import at.bitfire.ical4android.TaskProvider import org.dmfs.tasks.contract.TaskContract @@ -16,7 +16,7 @@ class TestTaskList( account: Account, provider: TaskProvider, id: Long -): AndroidTaskList(account, provider, TestTask.Factory, id) { +): DmfsTaskList(account, provider, TestTask.Factory, id) { companion object { @@ -26,7 +26,7 @@ class TestTaskList( values.put(TaskContract.TaskListColumns.LIST_COLOR, 0xffff0000) values.put(TaskContract.TaskListColumns.SYNC_ENABLED, 1) values.put(TaskContract.TaskListColumns.VISIBLE, 1) - val uri = AndroidTaskList.create(account, provider, values) + val uri = DmfsTaskList.create(account, provider, values) return TestTaskList(account, provider, ContentUris.parseId(uri)) } @@ -34,7 +34,7 @@ class TestTaskList( } - object Factory: AndroidTaskListFactory { + object Factory: DmfsTaskListFactory { override fun newInstance(account: Account, provider: TaskProvider, id: Long) = TestTaskList(account, provider, id) } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt similarity index 98% rename from lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 67b4d2de..57891097 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -32,16 +32,15 @@ import java.util.logging.Level /** * Stores and retrieves VTODO iCalendar objects (represented as [Task]s) to/from the - * OpenTasks provider. + * tasks.org-content provider (currently tasks.org and OpenTasks). * * Extend this class to process specific fields of the task. * * The SEQUENCE field is stored in [Tasks.SYNC_VERSION], so don't use [Tasks.SYNC_VERSION] * for anything else. - * */ -abstract class AndroidTask( - val taskList: AndroidTaskList +abstract class DmfsTask( + val taskList: DmfsTaskList ) { companion object { @@ -53,11 +52,11 @@ abstract class AndroidTask( var id: Long? = null - constructor(taskList: AndroidTaskList, values: ContentValues): this(taskList) { + constructor(taskList: DmfsTaskList, values: ContentValues): this(taskList) { id = values.getAsLong(Tasks._ID) } - constructor(taskList: AndroidTaskList, task: Task): this(taskList) { + constructor(taskList: DmfsTaskList, task: Task): this(taskList) { this.task = task } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt similarity index 72% rename from lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt index 41d3f70a..42ea06c9 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt @@ -6,8 +6,8 @@ package at.bitfire.ical4android import android.content.ContentValues -interface AndroidTaskFactory { +interface DmfsTaskFactory { - fun fromProvider(taskList: AndroidTaskList, values: ContentValues): T + fun fromProvider(taskList: DmfsTaskList, values: ContentValues): T } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt similarity index 93% rename from lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index 69a8adc9..4c77dadb 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -20,14 +20,13 @@ import java.util.logging.Level /** - * Represents a locally stored task list, containing AndroidTasks (whose data objects are Tasks). - * Communicates with third-party content providers to store the tasks. - * Currently, only the OpenTasks tasks provider (org.dmfs.provider.tasks) is supported. + * Represents a locally stored task list, containing [DmfsTask]s (tasks). + * Communicates with tasks.org-compatible content providers (currently tasks.org and OpenTasks) to store the tasks. */ -abstract class AndroidTaskList( +abstract class DmfsTaskList( val account: Account, val provider: TaskProvider, - val taskFactory: AndroidTaskFactory, + val taskFactory: DmfsTaskFactory, val id: Long ) { @@ -42,10 +41,10 @@ abstract class AndroidTaskList( ?: throw CalendarStorageException("Couldn't create task list (empty result from provider)") } - fun > findByID( + fun > findByID( account: Account, provider: TaskProvider, - factory: AndroidTaskListFactory, + factory: DmfsTaskListFactory, id: Long ): T { provider.client.query( @@ -64,10 +63,10 @@ abstract class AndroidTaskList( throw FileNotFoundException() } - fun > find( + fun > find( account: Account, provider: TaskProvider, - factory: AndroidTaskListFactory, + factory: DmfsTaskListFactory, where: String?, whereArgs: Array? ): List { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskListFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt similarity index 85% rename from lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskListFactory.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt index f7f33bda..3627c319 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidTaskListFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt @@ -6,7 +6,7 @@ package at.bitfire.ical4android import android.accounts.Account -interface AndroidTaskListFactory> { +interface DmfsTaskListFactory> { fun newInstance(account: Account, provider: TaskProvider, id: Long): T From 998f6b6042f0e6d25196f305f0f526d97cd72972 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 21 Feb 2024 15:47:38 +0100 Subject: [PATCH 39/92] Remove recurrence rules in exceptions of recurring events (#138) * Add testRemoveRrulesOfRruleExceptions * Implement removeRRulesOfExceptions * Minor changes --------- Co-authored-by: Ricki Hirner --- .../validation/EventValidatorTest.kt | 37 +++++++++++++++++++ .../ical4android/validation/EventValidator.kt | 14 +++++++ 2 files changed, 51 insertions(+) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index 47375c5f..984c7ea6 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -311,6 +311,43 @@ class EventValidatorTest { } + @Test + fun testRemoveRrulesOfRruleExceptions() { + val calendar = Event.eventsFromReader(StringReader( + "BEGIN:VCALENDAR\n" + + "BEGIN:VEVENT\n" + + "DTSTAMP:20240215T102755Z\n" + + "SUMMARY:recurring event\n" + + "DTSTART;TZID=Europe/Paris:20240219T130000\n" + + "DTEND;TZID=Europe/Paris:20240219T140000\n" + + "RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5\n" + // Should keep this RRULE + "UID:76c08fb1-99a3-41cf-b482-2d3b06648814\n" + + "END:VEVENT\n" + + + // Exception for the recurring event above + "BEGIN:VEVENT\n" + + "DTSTAMP:20240215T102908Z\n" + + "RECURRENCE-ID;TZID=Europe/Paris:20240221T130000\n" + + "SUMMARY:exception of recurring event\n" + + "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // but remove this one + "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // and this one + "DTSTART;TZID=Europe/Paris:20240221T110000\n" + + "DTEND;TZID=Europe/Paris:20240221T120000\n" + + "UID:76c08fb1-99a3-41cf-b482-2d3b06648814\n" + + "END:VEVENT\n" + + "END:VCALENDAR" + )) + assertEquals("FREQ=DAILY;COUNT=5;INTERVAL=1", calendar.first().rRules.joinToString()) + assertEquals( + "FREQ=DAILY;COUNT=6;INTERVAL=2\nFREQ=DAILY;COUNT=6;INTERVAL=2", + calendar.first().exceptions.first.rRules.joinToString() + ) + EventValidator.removeRRulesOfExceptions(calendar.first().exceptions) + assertEquals("FREQ=DAILY;COUNT=5;INTERVAL=1", calendar.first().rRules.joinToString()) + assertTrue(calendar.first().exceptions.first.rRules.isEmpty()) + } + + @Test fun testRemoveRRulesWithUntilBeforeDtStart() { val dtStart = DtStart(DateTime("20220531T125304")) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt index 5696eef0..a210cf03 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt @@ -31,6 +31,8 @@ class EventValidator(val e: Event) { val dtStart = correctStartAndEndTime(e) sameTypeForDtStartAndRruleUntil(dtStart, e.rRules) removeRRulesWithUntilBeforeDtStart(dtStart, e.rRules) + + removeRRulesOfExceptions(e.exceptions) } companion object { @@ -130,6 +132,18 @@ class EventValidator(val e: Event) { throw InvalidCalendarException("Event with invalid DTSTART value") } + + /** + * Removes RRULEs of exceptions of (potentially recurring) events + * + * @param exceptions exceptions of an event + */ + internal fun removeRRulesOfExceptions(exceptions: List) = + exceptions.forEach { exception -> + exception.rRules.clear() // Drop all RRULEs for the exception + } + + /** * Will remove the RRULES of an event where UNTIL lies before DTSTART */ From a847d0c0dba7b0aec9f9840f4d5b504cf3685ba2 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 23 Feb 2024 10:20:20 +0100 Subject: [PATCH 40/92] Add KDoc, update slf4j --- gradle/libs.versions.toml | 2 +- .../bitfire/ical4android/AndroidCalendar.kt | 21 ++++++++++++++----- .../at/bitfire/ical4android/DmfsTaskList.kt | 11 ++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97fcaf73..c394bfb2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ ical4j = "3.2.14" junit = "4.13.2" kotlin = "1.9.22" mockk = "1.13.9" -slf4j = "2.0.11" +slf4j = "2.0.12" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt index cc135889..527efc3a 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt @@ -10,6 +10,7 @@ import android.content.ContentUris import android.content.ContentValues import android.net.Uri import android.provider.CalendarContract.* +import androidx.annotation.CallSuper import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.toValues import java.io.FileNotFoundException @@ -22,12 +23,12 @@ import java.util.logging.Level * database to store the events. */ abstract class AndroidCalendar( - val account: Account, - val provider: ContentProviderClient, - val eventFactory: AndroidEventFactory, + val account: Account, + val provider: ContentProviderClient, + val eventFactory: AndroidEventFactory, - /** the calendar ID ([Calendars._ID]) **/ - val id: Long + /** the calendar ID ([Calendars._ID]) **/ + val id: Long ) { companion object { @@ -155,6 +156,16 @@ abstract class AndroidCalendar( var ownerAccount: String? = null + /** + * Sets the calendar properties ([name], [displayName] etc.) from the passed argument, + * which is usually directly taken from the Calendar Provider. + * + * Called when an instance is created from a Calendar Provider data row, for example + * using [find]. + * + * @param info values from Calendar Provider + */ + @CallSuper protected open fun populate(info: ContentValues) { name = info.getAsString(Calendars.NAME) displayName = info.getAsString(Calendars.CALENDAR_DISPLAY_NAME) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index 4c77dadb..05e3855a 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -8,6 +8,7 @@ import android.accounts.Account import android.content.ContentUris import android.content.ContentValues import android.net.Uri +import androidx.annotation.CallSuper import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.toValues import org.dmfs.tasks.contract.TaskContract @@ -98,6 +99,16 @@ abstract class DmfsTaskList( var isVisible = false + /** + * Sets the task list properties ([syncId], [name] etc.) from the passed argument, + * which is usually directly taken from the tasks provider. + * + * Called when an instance is created from a tasks provider data row, for example + * using [find]. + * + * @param info values from tasks provider + */ + @CallSuper protected fun populate(values: ContentValues) { syncId = values.getAsString(TaskLists._SYNC_ID) name = values.getAsString(TaskLists.LIST_NAME) From 31650d161521b36353bccf3b42fb6e66749af40b Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 23 Feb 2024 10:56:38 +0100 Subject: [PATCH 41/92] DmfsTaskList: make populate() overrideable --- lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index 05e3855a..6d8efd67 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -109,7 +109,7 @@ abstract class DmfsTaskList( * @param info values from tasks provider */ @CallSuper - protected fun populate(values: ContentValues) { + protected open fun populate(values: ContentValues) { syncId = values.getAsString(TaskLists._SYNC_ID) name = values.getAsString(TaskLists.LIST_NAME) color = values.getAsInteger(TaskLists.LIST_COLOR) From e32cc40ce05581e1fd853e05975aa95372461e2c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 1 Mar 2024 21:51:33 +0100 Subject: [PATCH 42/92] [CI] Update Github actions --- .github/workflows/build-kdoc.yml | 12 ++++++------ .github/workflows/codeql.yml | 6 +++--- .github/workflows/test-dev.yml | 27 ++++++++++++++------------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index c2b3d654..3dc61edd 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -13,17 +13,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - distribution: 'temurin' + distribution: temurin java-version: 17 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/actions/setup-gradle@v3 - name: Build KDoc run: ./gradlew --no-daemon --no-configuration-cache ical4android:dokkaHtml - - uses: actions/upload-pages-artifact@v1 + - uses: actions/upload-pages-artifact@v3 with: path: lib/build/dokka/html @@ -36,4 +36,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 11f7edec..354c6656 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v4 with: - distribution: 'temurin' + distribution: temurin java-version: 17 - uses: gradle/actions/setup-gradle@v3 diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index 2da6a807..f74c2dc7 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -5,22 +5,23 @@ jobs: name: Tests without emulator runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - distribution: 'temurin' + distribution: temurin java-version: 17 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/actions/setup-gradle@v3 - name: Check run: ./gradlew --no-daemon check - name: Archive results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: test-results path: | build/outputs/lint* build/reports + overwrite: true test_on_emulator: name: Tests with emulator @@ -29,14 +30,14 @@ jobs: matrix: api-level: [ 31 ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v4 with: - distribution: 'temurin' + distribution: temurin java-version: 17 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/actions/setup-gradle@v3 - name: Enable KVM group perms run: | @@ -45,7 +46,7 @@ jobs: sudo udevadm trigger --name-match=kvm - name: Cache AVD and APKs - uses: actions/cache@v3 + uses: actions/cache@v4 id: avd-cache with: path: | @@ -87,8 +88,8 @@ jobs: - name: Archive results if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: test-results - path: | - app/build/reports + path: app/build/reports + overwrite: true From 195d0c9d381fec3895f97442473e8af02c6a3cfb Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 1 Mar 2024 22:13:33 +0100 Subject: [PATCH 43/92] Amend all-day RDATE/EXDATE with time from DTSTART, if applicable (#142) --- .../ical4android/util/AndroidTimeUtilsTest.kt | 28 +++++++---- .../at/bitfire/ical4android/AndroidEvent.kt | 4 +- .../ical4android/util/AndroidTimeUtils.kt | 48 +++++++++++++++---- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt index ef186ea1..50142951 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt @@ -322,29 +322,39 @@ class AndroidTimeUtilsTest { @Test fun testRecurrenceSetsToAndroidString_Date() { - // DATEs (without time) have to be converted to T000000Z for Android + // DATEs (without time) have to be converted to THHmmssZ for Android val list = ArrayList(1) list.add(RDate(DateList("20150101,20150702", Value.DATE, tzDefault))) - val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, true) + val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, Date("20150101")) // We ignore the timezone assertEquals("20150101T000000Z,20150702T000000Z", androidTimeString.substringAfter(';')) } + @Test + fun testRecurrenceSetsToAndroidString_Date_AlthoughDtStartIsDateTime() { + // DATEs (without time) have to be converted to THHmmssZ for Android + val list = ArrayList(1) + list.add(RDate(DateList("20150101,20150702", Value.DATE, tzDefault))) + val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("20150101T043210", tzBerlin)) + // We ignore the timezone + assertEquals("20150101T033210Z,20150702T023210Z", androidTimeString.substringAfter(';')) + } + @Test fun testRecurrenceSetsToAndroidString_Period() { // PERIODs are not supported yet — should be implemented later val list = listOf( RDate(PeriodList("19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H")) ) - assertEquals("", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false)) + assertEquals("", AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("19960403T020000Z"))) } @Test - fun testRecurrenceSetsToAndroidString_TimeAlthoughAllDay() { + fun testRecurrenceSetsToAndroidString_Time_AlthoughDtStartIsAllDay() { // DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to T000000Z for Android val list = ArrayList(1) list.add(RDate(DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME, tzDefault))) - val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, true) + val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, Date("20150101")) // We ignore the timezone assertEquals("20150101T000000Z,20150702T000000Z", androidTimeString.substringAfter(';')) } @@ -355,7 +365,7 @@ class AndroidTimeUtilsTest { val list = ArrayList(2) list.add(RDate(DateList("20150103T113030", Value.DATE_TIME, tzToronto))) list.add(RDate(DateList("20150704T113040", Value.DATE_TIME, tzToronto))) - assertEquals("America/Toronto;20150103T113030,20150704T113040", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false)) + assertEquals("America/Toronto;20150103T113030,20150704T113040", AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("20150103T113030", tzToronto))) } @Test @@ -366,7 +376,7 @@ class AndroidTimeUtilsTest { val list = ArrayList(2) list.add(RDate(DateList("20150103T113030", Value.DATE_TIME, tzToronto))) list.add(RDate(DateList("20150704T113040", Value.DATE_TIME, tzBerlin))) - assertEquals("America/Toronto;20150103T113030,20150704T053040", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false)) + assertEquals("America/Toronto;20150103T113030,20150704T053040", AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("20150103T113030", tzToronto))) } @Test @@ -377,14 +387,14 @@ class AndroidTimeUtilsTest { val list = ArrayList(2) list.add(RDate(DateList("20150103T113030Z", Value.DATE_TIME))) list.add(RDate(DateList("20150704T113040", Value.DATE_TIME, tzBerlin))) - assertEquals("20150103T113030Z,20150704T093040Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false)) + assertEquals("20150103T113030Z,20150704T093040Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("20150103T113030Z"))) } @Test fun testRecurrenceSetsToAndroidString_UtcTime() { val list = ArrayList(1) list.add(RDate(DateList("20150101T103010Z,20150102T103020Z", Value.DATE_TIME))) - assertEquals("20150101T103010Z,20150102T103020Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false)) + assertEquals("20150101T103010Z,20150102T103020Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("20150101T103010ZZ"))) } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index ad74ea8b..041a14ff 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -855,7 +855,7 @@ abstract class AndroidEvent( listWithDtStart.add(dtStart.date) event.rDates.addFirst(RDate(listWithDtStart)) - builder.withValue(Events.RDATE, AndroidTimeUtils.recurrenceSetsToAndroidString(event.rDates, allDay)) + builder.withValue(Events.RDATE, AndroidTimeUtils.recurrenceSetsToAndroidString(event.rDates, dtStart.date)) } } else builder.withValue(Events.RDATE, null) @@ -868,7 +868,7 @@ abstract class AndroidEvent( if (event.exDates.isNotEmpty()) { for (exDate in event.exDates) AndroidTimeUtils.androidifyTimeZone(exDate) - builder.withValue(Events.EXDATE, AndroidTimeUtils.recurrenceSetsToAndroidString(event.exDates, allDay)) + builder.withValue(Events.EXDATE, AndroidTimeUtils.recurrenceSetsToAndroidString(event.exDates, dtStart.date)) } else builder.withValue(Events.EXDATE, null) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt index 2bf3671e..521f836e 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -8,6 +8,8 @@ package at.bitfire.ical4android.util import android.text.format.Time import at.bitfire.ical4android.Ical4Android +import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate +import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.TimeZone @@ -21,6 +23,8 @@ import java.text.ParseException import java.text.SimpleDateFormat import java.time.Duration import java.time.Period +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAmount import java.util.* @@ -131,18 +135,25 @@ object AndroidTimeUtils { * TZID is given) or "yyyymmddThhmmssZ". We don't use the TZID format here because then we're limited * to one time-zone, while an iCalendar may contain multiple EXDATE/RDATE lines with different time zones. * - * @param dates one more more lists of RDATE or EXDATE - * @param allDay whether the event is an all-day event or not + * This method converts the values to the type of [dtStart], if necessary: + * + * - DTSTART (DATE-TIME) and RDATE/EXDATE (DATE) → method converts RDATE/EXDATE to DATE-TIME with same time as DTSTART + * - DTSTART (DATE) and RDATE/EXDATE (DATE-TIME) → method converts RDATE/EXDATE to DATE (just drops time) + * + * @param dates one more more lists of RDATE or EXDATE + * @param dtStart used to determine whether the event is an all-day event or not; also used to + * generate the date-time if the event is not all-day but the exception is * * @return formatted string for Android calendar provider */ - fun recurrenceSetsToAndroidString(dates: List, allDay: Boolean): String { + fun recurrenceSetsToAndroidString(dates: List, dtStart: Date): String { /* rdate/exdate: DATE DATE_TIME all-day store as ...T000000Z cut off time and store as ...T000000Z event with time (undefined) store as ...ThhmmssZ */ val dateFormatUtcMidnight = SimpleDateFormat("yyyyMMdd'T'000000'Z'", Locale.ROOT) val strDates = LinkedList() + val allDay = dtStart !is DateTime // use time zone of first entry for the whole set; null for UTC val tz = @@ -156,26 +167,45 @@ object AndroidTimeUtils { } when (dateListProp.dates.type) { - Value.DATE_TIME -> { + Value.DATE_TIME -> { // RDATE/EXDATE is DATE-TIME if (tz == null && !dateListProp.dates.isUtc) dateListProp.setUtc(true) else if (tz != null && dateListProp.timeZone != tz) dateListProp.timeZone = tz if (allDay) + // DTSTART is DATE dateListProp.dates.mapTo(strDates) { dateFormatUtcMidnight.format(it) } else + // DTSTART is DATE-TIME strDates.add(dateListProp.value) } - Value.DATE -> - // DATE values have to be converted to DATE-TIME T000000Z for Android - dateListProp.dates.mapTo(strDates) { - dateFormatUtcMidnight.format(it) + Value.DATE -> // RDATE/EXDATE is DATE + if (allDay) { + // DTSTART is DATE; DATE values have to be returned as T000000Z for Android + dateListProp.dates.mapTo(strDates) { date -> + dateFormatUtcMidnight.format(date) + } + } else { + // DTSTART is DATE-TIME; amend DATE-TIME with clock time from dtStart + dateListProp.dates.mapTo(strDates) { date -> + // take time (including time zone) from dtStart and date from date + val dtStartTime = (dtStart as DateTime).toZonedDateTime() + val localDate = date.toLocalDate() + val dtStartTimeUtc = dtStartTime + .withDayOfMonth(localDate.dayOfMonth) + .withMonth(localDate.monthValue) + .withYear(localDate.year) + .withZoneSameInstant(ZoneOffset.UTC) + + val dateFormatUtc = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.ROOT) + dtStartTimeUtc.format(dateFormatUtc) + } } } } - // format: [tzid;]value1,value2,... + // format expected by Android: [tzid;]value1,value2,... val result = StringBuilder() if (tz != null) result.append(tz.id).append(RECURRENCE_LIST_TZID_SEPARATOR) From 9e7fb66efd5ac5593ee619277c4fb864d0499af4 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 19 Mar 2024 14:35:28 +0100 Subject: [PATCH 44/92] Update ical4j to 3.2.17 (#140) * Update ical4j to `3.2.15` Signed-off-by: Arnau Mora Gras * Update ical4j to `3.2.16` Signed-off-by: Arnau Mora Gras * Update ical4j to `3.2.17` Signed-off-by: Arnau Mora Gras --------- Signed-off-by: Arnau Mora Gras --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c394bfb2..861bc14c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ commons-lang = "3.8.1" # noinspection GradleDependency commons-io = "2.6" dokka ="1.9.10" -ical4j = "3.2.14" +ical4j = "3.2.17" junit = "4.13.2" kotlin = "1.9.22" mockk = "1.13.9" From 17fcf65d122f269b7416212828c5ecb95b2ab986 Mon Sep 17 00:00:00 2001 From: Wojciech Matuszewski Date: Thu, 21 Mar 2024 11:45:53 +0100 Subject: [PATCH 45/92] Add an option to ignore invalid event when parsing iCalendar (#145) * Add an option to ignore invalid event when parsing iCalendar * Add test for ignoreInvalidEvents parameter --- .../at/bitfire/ical4android/EventTest.kt | 22 ++++++- .../events/multiple-with-invalid.ics | 66 +++++++++++++++++++ .../kotlin/at/bitfire/ical4android/Event.kt | 63 +++++++++++------- 3 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 lib/src/androidTest/resources/events/multiple-with-invalid.ics diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt index 83e77474..73cfa774 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import android.util.Log import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime @@ -20,6 +21,7 @@ import net.fortuna.ical4j.model.property.RecurrenceId import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Test import java.io.ByteArrayOutputStream @@ -102,6 +104,19 @@ class EventTest { assertTrue("Event 2 Exception 2" == e.exceptions.first.summary || "Event 2 Exception 2" == e.exceptions[1].summary) } + @Test + fun testInvalid() { + assertThrows(InvalidCalendarException::class.java) { + parseCalendar("multiple-with-invalid.ics", ignoreInvalidEvents = false) + } + } + + @Test + fun testIgnoreInvalidEvents() { + val events = parseCalendar("multiple-with-invalid.ics", ignoreInvalidEvents = true) + assertEquals(3, events.size) + } + @Test fun testParse() { val event = parseCalendar("utf8.ics").first() @@ -334,9 +349,12 @@ class EventTest { throw FileNotFoundException() } - private fun parseCalendar(fname: String, charset: Charset = Charsets.UTF_8): List = + private fun parseCalendar( + fname: String, + charset: Charset = Charsets.UTF_8, + ignoreInvalidEvents: Boolean = false): List = javaClass.classLoader!!.getResourceAsStream("events/$fname").use { stream -> - return Event.eventsFromReader(InputStreamReader(stream, charset)) + return Event.eventsFromReader(InputStreamReader(stream, charset), ignoreInvalidEvents = ignoreInvalidEvents) } } diff --git a/lib/src/androidTest/resources/events/multiple-with-invalid.ics b/lib/src/androidTest/resources/events/multiple-with-invalid.ics new file mode 100644 index 00000000..788118ce --- /dev/null +++ b/lib/src/androidTest/resources/events/multiple-with-invalid.ics @@ -0,0 +1,66 @@ +BEGIN:VCALENDAR +VERSION:2.0 +X-WR-CALNAME:Test-Kalender +BEGIN:VEVENT +UID:multiple-0@ical4android.EventTest +DTSTAMP:20150826T132300Z +SUMMARY:Event 0 +DTSTART:20130910T170000T +DTEND:20130910T180000T +END:VEVENT +BEGIN:VEVENT +UID:multiple-1@ical4android.EventTest +DTSTAMP:20150826T132300Z +SUMMARY:Event 1 +RRULE:FREQ=DAILY;COUNT=10 +DTSTART;VALUE=DATE:20131009 +DTEND;VALUE=DATE:20131010 +END:VEVENT +BEGIN:VEVENT +UID:multiple-1@ical4android.EventTest +RECURRENCE-ID;VALUE=DATE:202131012 +SUMMARY:Event 1 Exception +DTSTART:20131012T170000T +DTEND:20131012T180000T +END:VEVENT +BEGIN:VEVENT +UID:multiple-2@ical4android.EventTest +DTSTAMP:20150826T132300Z +SUMMARY:Event 2 +DTSTART;VALUE=DATE:20131009 +DTEND;VALUE=DATE:20131010 +RRULE:FREQ=DAILY;COUNT=10 +END:VEVENT +BEGIN:VEVENT +UID:multiple-3@ical4android.EventTest +DTSTAMP:20150826T132300Z +SUMMARY:Event 3 Missing DTSTART +DTEND;VALUE=DATE:20131010 +RRULE:FREQ=DAILY;COUNT=10 +END:VEVENT +BEGIN:VEVENT +UID:multiple-2@ical4android.EventTest +RECURRENCE-ID:20131014 +SEQUENCE:1 +DTSTAMP:20150826T132300Z +SUMMARY:Event 2 Updated Exception 1 +DTSTART:20131010T170000T +DTEND:20131010T180000T +END:VEVENT +BEGIN:VEVENT +UID:multiple-2@ical4android.EventTest +RECURRENCE-ID:20131014 +DTSTAMP:20150826T132300Z +SUMMARY:Event 2 Original Exception 1 +DTSTART:20131010T170000T +DTEND:20131010T180000T +END:VEVENT +BEGIN:VEVENT +UID:multiple-2@ical4android.EventTest +RECURRENCE-ID:20131015 +DTSTAMP:20150826T132300Z +SUMMARY:Event 2 Exception 2 +DTSTART:20131010T170000T +DTEND:20131010T180000T +END:VEVENT +END:VCALENDAR diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index 5724d645..a841a304 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -21,8 +21,9 @@ import java.io.OutputStream import java.io.Reader import java.net.URI import java.util.* +import java.util.logging.Level -class Event: ICalendar() { +class Event : ICalendar() { // uid and sequence are inherited from iCalendar var recurrenceId: RecurrenceId? = null @@ -66,6 +67,7 @@ class Event: ICalendar() { * * @param reader where the iCalendar is taken from * @param properties Known iCalendar properties (like [CALENDAR_NAME]) will be put into this map. Key: property name; value: property value + * @param ignoreInvalidEvents If true, events that can't be parsed will be dropped. Otherwise, parsing will fail if an invalid event is encountered. * * @return array of filled [Event] data objects (may have size 0) * @@ -75,7 +77,11 @@ class Event: ICalendar() { * @throws InvalidCalendarException on parsing exceptions */ @UsesThreadContextClassLoader - fun eventsFromReader(reader: Reader, properties: MutableMap? = null): List { + fun eventsFromReader( + reader: Reader, + properties: MutableMap? = null, + ignoreInvalidEvents: Boolean = false + ): List { val ical = fromReader(reader, properties) // process VEVENTs @@ -90,8 +96,8 @@ class Event: ICalendar() { } Ical4Android.log.fine("Assigning exceptions to main events") - val mainEvents = mutableMapOf() - val exceptions = mutableMapOf>() + val mainEvents = mutableMapOf() + val exceptions = mutableMapOf>() for (vEvent in vEvents) { val uid = vEvent.uid.value val sequence = vEvent.sequence?.sequenceNo ?: 0 @@ -127,17 +133,25 @@ class Event: ICalendar() { val events = mutableListOf() for ((uid, vEvent) in mainEvents) { - val event = fromVEvent(vEvent) + try { + val event = fromVEvent(vEvent) - // assign exceptions to main event and then remove them from exceptions array - exceptions.remove(uid)?.let { eventExceptions -> - event.exceptions.addAll(eventExceptions.values.map { fromVEvent(it) }) - } + // assign exceptions to main event and then remove them from exceptions array + exceptions.remove(uid)?.let { eventExceptions -> + event.exceptions.addAll(eventExceptions.values.map { fromVEvent(it) }) + } + + // make sure that exceptions have at least a SUMMARY + event.exceptions.forEach { it.summary = it.summary ?: event.summary } - // make sure that exceptions have at least a SUMMARY - event.exceptions.forEach { it.summary = it.summary ?: event.summary } + events += event + } catch (e: InvalidCalendarException) { + Ical4Android.log.log(Level.WARNING, "Invalid VEvent: $vEvent", e) - events += event + if (!ignoreInvalidEvents) { + throw e + } + } } for ((uid, onlyExceptions) in exceptions) { @@ -172,6 +186,7 @@ class Event: ICalendar() { is Categories -> for (category in prop.categories) e.categories += category + is Color -> e.color = Css3Color.fromString(prop.value) is DtStart -> e.dtStart = prop is DtEnd -> e.dtEnd = prop @@ -186,7 +201,9 @@ class Event: ICalendar() { is Organizer -> e.organizer = prop is Attendee -> e.attendees += prop is LastModified -> e.lastModified = prop - is ProdId, is DtStamp -> { /* don't save these as unknown properties */ } + is ProdId, is DtStamp -> { /* don't save these as unknown properties */ + } + else -> e.unknownProperties += prop } @@ -321,16 +338,16 @@ class Event: ICalendar() { val organizerEmail: String? - get() { - var email: String? = null - organizer?.let { organizer -> - val uri = organizer.calAddress - email = if (uri.scheme.equals("mailto", true)) - uri.schemeSpecificPart - else - organizer.getParameter(Parameter.EMAIL)?.value + get() { + var email: String? = null + organizer?.let { organizer -> + val uri = organizer.calAddress + email = if (uri.scheme.equals("mailto", true)) + uri.schemeSpecificPart + else + organizer.getParameter(Parameter.EMAIL)?.value + } + return email } - return email - } } From fa0c197fa01324a2bd97bc0561551e3796c4920d Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 28 Mar 2024 10:46:24 +0100 Subject: [PATCH 46/92] Update libs --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 861bc14c..a962d801 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,8 +14,8 @@ commons-io = "2.6" dokka ="1.9.10" ical4j = "3.2.17" junit = "4.13.2" -kotlin = "1.9.22" -mockk = "1.13.9" +kotlin = "1.9.23" +mockk = "1.13.10" slf4j = "2.0.12" [libraries] From f10bd57dacf0326d514f3d2382dd341f4dd0f304 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 28 Mar 2024 12:23:37 +0100 Subject: [PATCH 47/92] recurrenceSetsToAndroidString: correctly assemble ZonedDateTime from LocalDate and LocalTime instead of modifying the original value on the fly (#148) --- .../bitfire/ical4android/util/AndroidTimeUtilsTest.kt | 10 ++++++++++ .../at/bitfire/ical4android/util/AndroidTimeUtils.kt | 11 ++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt index 50142951..5805f2a3 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt @@ -340,6 +340,16 @@ class AndroidTimeUtilsTest { assertEquals("20150101T033210Z,20150702T023210Z", androidTimeString.substringAfter(';')) } + @Test + fun testRecurrenceSetsToAndroidString_Date_AlthoughDtStartIsDateTime_MonthWithLessDays() { + // DATEs (without time) have to be converted to THHmmssZ for Android + val list = ArrayList(1) + list.add(ExDate(DateList("20240531", Value.DATE, tzDefault))) + val androidTimeString = AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("20240401T114500", tzBerlin)) + // We ignore the timezone + assertEquals("20240531T094500Z", androidTimeString.substringAfter(';')) + } + @Test fun testRecurrenceSetsToAndroidString_Period() { // PERIODs are not supported yet — should be implemented later diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt index 521f836e..e3686878 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -24,6 +24,7 @@ import java.text.SimpleDateFormat import java.time.Duration import java.time.Period import java.time.ZoneOffset +import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAmount import java.util.* @@ -192,11 +193,11 @@ object AndroidTimeUtils { // take time (including time zone) from dtStart and date from date val dtStartTime = (dtStart as DateTime).toZonedDateTime() val localDate = date.toLocalDate() - val dtStartTimeUtc = dtStartTime - .withDayOfMonth(localDate.dayOfMonth) - .withMonth(localDate.monthValue) - .withYear(localDate.year) - .withZoneSameInstant(ZoneOffset.UTC) + val dtStartTimeUtc = ZonedDateTime.of( + localDate, + dtStartTime.toLocalTime(), + dtStartTime.zone + ).withZoneSameInstant(ZoneOffset.UTC) val dateFormatUtc = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.ROOT) dtStartTimeUtc.format(dateFormatUtc) From dc8f8e347a30d3c4c35afb782070eee4124cdc02 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 3 Apr 2024 12:58:06 +0200 Subject: [PATCH 48/92] [CI] Use normal runners instead of large runners for emulator tests See https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ --- .github/workflows/test-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index f74c2dc7..d6bcfdc4 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -25,7 +25,7 @@ jobs: test_on_emulator: name: Tests with emulator - runs-on: ubuntu-latest-4-cores + runs-on: ubuntu-latest strategy: matrix: api-level: [ 31 ] From 4a466b248579f27f8fc6b0a1a7f81dd9ea57032a Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 7 May 2024 19:55:38 +0200 Subject: [PATCH 49/92] Update dependencies (#150) * Update dependencies * [CI] Update task app APKs --- .github/workflows/test-dev.yml | 4 ++-- gradle/libs.versions.toml | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- .../kotlin/at/bitfire/ical4android/BatchOperation.kt | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index d6bcfdc4..dc86c7c9 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -69,8 +69,8 @@ jobs: - name: Install task apps and run tests uses: reactivecircus/android-emulator-runner@v2 env: - version_at_techbee_jtx: v2.7.0 - version_org_tasks: 130501 + version_at_techbee_jtx: v2.7.6 + version_org_tasks: 130804 version_org_dmfs_tasks: 82200 with: api-level: ${{ matrix.api-level }} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a962d801..055b455d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -agp = "8.2.2" +agp = "8.4.0" android-desugar = "2.0.4" -androidx-core = "1.12.0" +androidx-core = "1.13.1" androidx-test-core = "1.5.0" androidx-test-rules = "1.5.0" androidx-test-runner = "1.5.2" @@ -11,12 +11,12 @@ commons-collections = "4.2" commons-lang = "3.8.1" # noinspection GradleDependency commons-io = "2.6" -dokka ="1.9.10" +dokka ="1.9.20" ical4j = "3.2.17" junit = "4.13.2" -kotlin = "1.9.23" +kotlin = "1.9.24" mockk = "1.13.10" -slf4j = "2.0.12" +slf4j = "2.0.13" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d0d403e2..48c0a02c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt index b57c1819..8cefe93e 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt @@ -69,8 +69,10 @@ class BatchOperation( /** * Runs a subset of the operations in [queue] using [providerClient] in a transaction. * Catches [TransactionTooLargeException] and splits the operations accordingly. + * * @param start index of first operation which will be run (inclusive) * @param end index of last operation which will be run (exclusive!) + * * @throws RemoteException on calendar provider errors * @throws OperationApplicationException when the batch can't be processed * @throws CalendarStorageException if the transaction is too large From a3e886c738a9b87f0ea8e7a22e900316eacaf3ec Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 7 May 2024 20:36:02 +0200 Subject: [PATCH 50/92] Don't use CalendarStorageException to wrap RemoteException / as all-catch (#151) * Don't use CalendarStorageException to wrap RemoteException / as all-catch * BatchOperation: wrap RuntimeException --- .../at/bitfire/ical4android/BatchOperation.kt | 67 ++++++++++++------- .../ical4android/CalendarStorageException.kt | 5 ++ .../at/bitfire/ical4android/DmfsTask.kt | 33 ++++++--- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt index 8cefe93e..b0f7ef93 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt @@ -34,30 +34,35 @@ class BatchOperation( * Commits all operations from [queue] and then empties the queue. * * @return number of affected rows + * + * @throws RemoteException on calendar provider errors. In case of [android.os.DeadObjectException], + * the provider has probably been killed/crashed or the calling process is cached and thus IPC is frozen (Android 14+). + * + * @throws CalendarStorageException if + * + * - the transaction is too large and can't be split (wrapped [TransactionTooLargeException]) + * - the batch can't be processed (wrapped [OperationApplicationException]) + * - the content provider throws a [RuntimeException] (will be wrapped) */ fun commit(): Int { var affected = 0 - if (!queue.isEmpty()) - try { - if (Ical4Android.log.isLoggable(Level.FINE)) { - Ical4Android.log.log(Level.FINE, "Committing ${queue.size} operations:") - for ((idx, op) in queue.withIndex()) - Ical4Android.log.log(Level.FINE, "#$idx: ${op.build()}") - } - - results = arrayOfNulls(queue.size) - runBatch(0, queue.size) + if (!queue.isEmpty()) { + if (Ical4Android.log.isLoggable(Level.FINE)) { + Ical4Android.log.log(Level.FINE, "Committing ${queue.size} operations:") + for ((idx, op) in queue.withIndex()) + Ical4Android.log.log(Level.FINE, "#$idx: ${op.build()}") + } - for (result in results.filterNotNull()) - when { - result.count != null -> affected += result.count ?: 0 - result.uri != null -> affected += 1 - } - Ical4Android.log.fine("… $affected record(s) affected") + results = arrayOfNulls(queue.size) + runBatch(0, queue.size) - } catch(e: Exception) { - throw CalendarStorageException("Couldn't apply batch operation", e) - } + for (result in results.filterNotNull()) + when { + result.count != null -> affected += result.count ?: 0 + result.uri != null -> affected += 1 + } + Ical4Android.log.fine("… $affected record(s) affected") + } queue.clear() return affected @@ -68,14 +73,19 @@ class BatchOperation( /** * Runs a subset of the operations in [queue] using [providerClient] in a transaction. - * Catches [TransactionTooLargeException] and splits the operations accordingly. + * Catches [TransactionTooLargeException] and splits the operations accordingly (if possible). * * @param start index of first operation which will be run (inclusive) * @param end index of last operation which will be run (exclusive!) * - * @throws RemoteException on calendar provider errors - * @throws OperationApplicationException when the batch can't be processed - * @throws CalendarStorageException if the transaction is too large + * @throws RemoteException on calendar provider errors. In case of [android.os.DeadObjectException], + * the provider has probably been killed/crashed or the calling process is cached and thus IPC is frozen (Android 14+). + * + * @throws CalendarStorageException if + * + * - the transaction is too large and can't be split (wrapped [TransactionTooLargeException]) + * - the batch can't be processed (wrapped [OperationApplicationException]) + * - the content provider throws a [RuntimeException] (will be wrapped) */ private fun runBatch(start: Int, end: Int) { if (end == start) @@ -83,7 +93,7 @@ class BatchOperation( try { val ops = toCPO(start, end) - Ical4Android.log.fine("Running ${ops.size} operations ($start .. ${end-1})") + Ical4Android.log.fine("Running ${ops.size} operations ($start .. ${end - 1})") val partResults = providerClient.applyBatch(ops) val n = end - start @@ -91,10 +101,17 @@ class BatchOperation( Ical4Android.log.warning("Batch operation returned only ${partResults.size} instead of $n results") System.arraycopy(partResults, 0, results, start, partResults.size) + + } catch (e: OperationApplicationException) { + throw CalendarStorageException("Couldn't apply batch operation", e) + + } catch (e: RuntimeException) { + throw CalendarStorageException("Content provider threw a runtime exception", e) + } catch(e: TransactionTooLargeException) { if (end <= start + 1) // only one operation, can't be split - throw CalendarStorageException("Can't transfer data to content provider (data row too large)") + throw CalendarStorageException("Can't transfer data to content provider (too large data row can't be split)", e) Ical4Android.log.warning("Transaction too large, splitting (losing atomicity)") val mid = start + (end - start)/2 diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt b/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt index 85f463b0..70106fa4 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt @@ -4,6 +4,11 @@ package at.bitfire.ical4android +/** + * Indicates a problem with a calendar storage operation, like when a row can't be inserted or updated. + * + * Should not be used to wrap [android.os.RemoteException]. + */ class CalendarStorageException: Exception { constructor(message: String): super(message) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 57891097..7afe59aa 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -14,15 +14,36 @@ import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.util.MiscUtils.toValues -import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Completed +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.Geo +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RelatedTo +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Trigger import net.fortuna.ical4j.util.TimeZones import org.dmfs.tasks.contract.TaskContract.Properties -import org.dmfs.tasks.contract.TaskContract.Property.* +import org.dmfs.tasks.contract.TaskContract.Property.Alarm +import org.dmfs.tasks.contract.TaskContract.Property.Category +import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.Tasks import java.io.FileNotFoundException import java.net.URISyntaxException @@ -435,11 +456,7 @@ abstract class DmfsTask( } fun delete(): Int { - try { - return taskList.provider.client.delete(taskSyncURI(), null, null) - } catch(e: RemoteException) { - throw CalendarStorageException("Couldn't delete event", e) - } + return taskList.provider.client.delete(taskSyncURI(), null, null) } @CallSuper From 00244a9344ca4899ce69779fe721a1f58e884542 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Thu, 30 May 2024 11:21:07 +0200 Subject: [PATCH 51/92] Support COMMENT for tasks (#152) * Add test * Update existing test * Retrieve and create comment if not null * Add test for an empty comment * Remove redundant qualifier name * Optimize imports * Introduce import alias * Linting: Add clarifying parentheses * Save comment as string * Linting * Don't trim * Preserve property order --------- Co-authored-by: Ricki Hirner --- .../at/bitfire/ical4android/DmfsTaskTest.kt | 69 +++++++++++++++++-- .../at/bitfire/ical4android/DmfsTask.kt | 16 ++++- .../kotlin/at/bitfire/ical4android/Task.kt | 41 +++++++++-- 3 files changed, 115 insertions(+), 11 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index 30f7dba5..03b5b0ed 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -16,15 +16,37 @@ import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.parameter.* +import net.fortuna.ical4j.model.parameter.Email +import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.TzId -import net.fortuna.ical4j.model.property.* -import org.dmfs.tasks.contract.TaskContract -import org.dmfs.tasks.contract.TaskContract.* +import net.fortuna.ical4j.model.parameter.Value +import net.fortuna.ical4j.model.parameter.XParameter +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Completed +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.Geo +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RelatedTo +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.XProperty +import org.dmfs.tasks.contract.TaskContract.LOCAL_ACCOUNT_TYPE +import org.dmfs.tasks.contract.TaskContract.Properties +import org.dmfs.tasks.contract.TaskContract.Property import org.dmfs.tasks.contract.TaskContract.Property.Category import org.dmfs.tasks.contract.TaskContract.Property.Relation +import org.dmfs.tasks.contract.TaskContract.PropertyColumns +import org.dmfs.tasks.contract.TaskContract.Tasks import org.junit.After -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.time.ZoneId @@ -37,7 +59,7 @@ class DmfsTaskTest( private val tzChicago = DateUtils.ical4jTimeZone("America/Chicago")!! private val tzDefault = DateUtils.ical4jTimeZone(ZoneId.systemDefault().id)!! - private val testAccount = Account("AndroidTaskTest", TaskContract.LOCAL_ACCOUNT_TYPE) + private val testAccount = Account("AndroidTaskTest", LOCAL_ACCOUNT_TYPE) private lateinit var taskListUri: Uri private var taskList: TestTaskList? = null @@ -485,6 +507,39 @@ class DmfsTaskTest( assertTrue(hasCat2) } + @Test + fun testBuildTask_Comment() { + var hasComment = false + buildTask { + comment = "Comment value" + }.let { result -> + val id = result.getAsLong(Tasks._ID) + val uri = taskList!!.tasksPropertiesSyncUri() + provider.client.query(uri, arrayOf(Property.Comment.COMMENT), "${Properties.MIMETYPE}=? AND ${PropertyColumns.TASK_ID}=?", + arrayOf(Property.Comment.CONTENT_ITEM_TYPE, id.toString()), null)!!.use { cursor -> + if (cursor.moveToNext()) + hasComment = cursor.getString(0) == "Comment value" + } + } + assertTrue(hasComment) + } + + @Test + fun testBuildTask_Comment_empty() { + var hasComment: Boolean + buildTask { + comment = null + }.let { result -> + val id = result.getAsLong(Tasks._ID) + val uri = taskList!!.tasksPropertiesSyncUri() + provider.client.query(uri, arrayOf(Property.Comment.COMMENT), "${Properties.MIMETYPE}=? AND ${PropertyColumns.TASK_ID}=?", + arrayOf(Property.Comment.CONTENT_ITEM_TYPE, id.toString()), null)!!.use { cursor -> + hasComment = cursor.count > 0 + } + } + assertFalse(hasComment) + } + private fun firstProperty(taskId: Long, mimeType: String): ContentValues? { val uri = taskList!!.tasksPropertiesSyncUri() provider.client.query(uri, null, "${Properties.MIMETYPE}=? AND ${PropertyColumns.TASK_ID}=?", @@ -604,6 +659,7 @@ class DmfsTaskTest( // extended properties task.categories.addAll(arrayOf("Cat1", "Cat2")) + task.comment = "A comment" val sibling = RelatedTo("most-fields2@example.com") sibling.parameters.add(RelType.SIBLING) @@ -629,6 +685,7 @@ class DmfsTaskTest( assertEquals(task.dtStart, task2.dtStart) assertEquals(task.categories, task2.categories) + assertEquals(task.comment, task2.comment) assertEquals(task.relatedTo, task2.relatedTo) assertEquals(task.unknownProperties, task2.unknownProperties) } finally { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 7afe59aa..34d380a3 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -43,6 +43,7 @@ import net.fortuna.ical4j.util.TimeZones import org.dmfs.tasks.contract.TaskContract.Properties import org.dmfs.tasks.contract.TaskContract.Property.Alarm import org.dmfs.tasks.contract.TaskContract.Property.Category +import org.dmfs.tasks.contract.TaskContract.Property.Comment import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.Tasks import java.io.FileNotFoundException @@ -200,7 +201,7 @@ abstract class DmfsTask( else -> Status.VTODO_NEEDS_ACTION } - val allDay = values.getAsInteger(Tasks.IS_ALLDAY) ?: 0 != 0 + val allDay = (values.getAsInteger(Tasks.IS_ALLDAY) ?: 0) != 0 val tzID = values.getAsString(Tasks.TZ) val tz = tzID?.let { DateUtils.ical4jTimeZone(it) } @@ -264,6 +265,8 @@ abstract class DmfsTask( populateAlarm(row) Category.CONTENT_ITEM_TYPE -> task.categories += row.getAsString(Category.CATEGORY_NAME) + Comment.CONTENT_ITEM_TYPE -> + task.comment = row.getAsString(Comment.COMMENT) Relation.CONTENT_ITEM_TYPE -> populateRelatedTo(row) UnknownProperty.CONTENT_ITEM_TYPE -> @@ -368,6 +371,7 @@ abstract class DmfsTask( protected open fun insertProperties(batch: BatchOperation, idxTask: Int?) { insertAlarms(batch, idxTask) insertCategories(batch, idxTask) + insertComment(batch, idxTask) insertRelatedTo(batch, idxTask) insertUnknownProperties(batch, idxTask) } @@ -419,6 +423,16 @@ abstract class DmfsTask( } } + protected open fun insertComment(batch: BatchOperation, idxTask: Int?) { + val comment = requireNotNull(task).comment ?: return + val builder = CpoBuilder.newInsert(taskList.tasksPropertiesSyncUri()) + .withTaskId(Comment.TASK_ID, idxTask) + .withValue(Comment.MIMETYPE, Comment.CONTENT_ITEM_TYPE) + .withValue(Comment.COMMENT, comment) + Ical4Android.log.log(Level.FINE, "Inserting comment", builder.build()) + batch.enqueue(builder) + } + protected open fun insertRelatedTo(batch: BatchOperation, idxTask: Int?) { for (relatedTo in requireNotNull(task).relatedTo) { val relType = when ((relatedTo.getParameter(Parameter.RELTYPE) as RelType?)) { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 432f1499..45ef249c 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -8,18 +8,48 @@ import androidx.annotation.IntRange import at.bitfire.ical4android.util.DateUtils 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.Component +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.TextList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VToDo -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.property.Categories +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Color +import net.fortuna.ical4j.model.property.Comment +import net.fortuna.ical4j.model.property.Completed +import net.fortuna.ical4j.model.property.Created +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtStamp +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.Geo +import net.fortuna.ical4j.model.property.LastModified +import net.fortuna.ical4j.model.property.Location +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.PercentComplete +import net.fortuna.ical4j.model.property.Priority +import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RelatedTo +import net.fortuna.ical4j.model.property.Sequence +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Summary +import net.fortuna.ical4j.model.property.Uid +import net.fortuna.ical4j.model.property.Url +import net.fortuna.ical4j.model.property.Version import java.io.IOException import java.io.OutputStream import java.io.Reader import java.net.URI import java.net.URISyntaxException -import java.util.* +import java.util.LinkedList import java.util.logging.Level class Task: ICalendar() { @@ -54,6 +84,7 @@ class Task: ICalendar() { val exDates = LinkedList() val categories = LinkedList() + var comment: String? = null var relatedTo = LinkedList() val unknownProperties = LinkedList() @@ -62,7 +93,7 @@ class Task: ICalendar() { companion object { /** - * Parses an iCalendar resource, applies [ICalPreprocessor] to increase compatibility + * Parses an iCalendar resource, applies [at.bitfire.ical4android.validation.ICalPreprocessor] to increase compatibility * and extracts the VTODOs. * * @param reader where the iCalendar is taken from @@ -119,6 +150,7 @@ class Task: ICalendar() { is Categories -> for (category in prop.categories) t.categories += category + is Comment -> t.comment = prop.value is RelatedTo -> t.relatedTo.add(prop) is Uid, is ProdId, is DtStamp -> { /* don't save these as unknown properties */ } else -> t.unknownProperties += prop @@ -203,6 +235,7 @@ class Task: ICalendar() { if (categories.isNotEmpty()) props += Categories(TextList(categories.toTypedArray())) + comment?.let { props += Comment(it) } props.addAll(relatedTo) props.addAll(unknownProperties) From 9d8077e9a5519d4e02a05811324fc9ac22ec7ceb Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 30 May 2024 11:22:44 +0200 Subject: [PATCH 52/92] Update dependencies --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 055b455d..0219e1b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.4.0" +agp = "8.4.1" android-desugar = "2.0.4" androidx-core = "1.13.1" androidx-test-core = "1.5.0" @@ -14,8 +14,8 @@ commons-io = "2.6" dokka ="1.9.20" ical4j = "3.2.17" junit = "4.13.2" -kotlin = "1.9.24" -mockk = "1.13.10" +kotlin = "2.0.0" +mockk = "1.13.11" slf4j = "2.0.13" [libraries] From 377ef7bab21a27ccd039617aa9919292334f640f Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 30 May 2024 12:54:04 +0200 Subject: [PATCH 53/92] Enable skipped EventValidatorTest --- .../at/bitfire/ical4android/validation/EventValidatorTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index 984c7ea6..5564b3b4 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -196,7 +196,6 @@ class EventValidatorTest { EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals("FREQ=MONTHLY;UNTIL=20211214T001100Z", event.rRules.joinToString()) - Assume.assumeTrue(TimeZone.getDefault().id == "Europe/Vienna") val event2 = Event.eventsFromReader( StringReader( "BEGIN:VCALENDAR\n" + From ba5a013d69969839dcf05e48a7652ba82454d6b7 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Fri, 31 May 2024 07:49:52 +0200 Subject: [PATCH 54/92] Upgrade ical4j to 3.2.18 (#149) * Upgrade ical4j to 3.2.18 Signed-off-by: Arnau Mora Gras * Event: add alarms to containing list of components, not to (now dynamically generated) list of alarms * Adapt minifyTimeZone, VToDo.alarms - minifyTimeZone: construct new VTimeZone instead of modifying copy of old one (which is not possible anymore with the changes of ical4j 3.2.18) - VTodo: also add alarms to (read-write) components instead of (dynamically generated read-only) alarm list --------- Signed-off-by: Arnau Mora Gras Co-authored-by: Ricki Hirner --- .gitignore | 3 +- gradle/libs.versions.toml | 2 +- .../at/bitfire/ical4android/EventTest.kt | 2 +- .../at/bitfire/ical4android/ICalendarTest.kt | 8 ++-- .../at/bitfire/ical4android/TaskTest.kt | 2 +- .../kotlin/at/bitfire/ical4android/Event.kt | 2 +- .../at/bitfire/ical4android/ICalendar.kt | 45 +++++++++---------- .../kotlin/at/bitfire/ical4android/Task.kt | 2 +- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 291ae9be..bdcd7df9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,9 @@ # Files for the Dalvik VM *.dex -# Java class files +# Java/Kotlin files *.class +.kotlin/ # Generated files bin/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0219e1b9..37fc6e95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ commons-lang = "3.8.1" # noinspection GradleDependency commons-io = "2.6" dokka ="1.9.20" -ical4j = "3.2.17" +ical4j = "3.2.18" junit = "4.13.2" kotlin = "2.0.0" mockk = "1.13.11" diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt index 73cfa774..2cf5c030 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt @@ -259,7 +259,7 @@ class EventTest { fun testWrite() { val e = Event() e.uid = "SAMPLEUID" - e.dtStart = DtStart("20190101T100000", TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone("Europe/Berlin")) + e.dtStart = DtStart("20190101T100000", DateUtils.ical4jTimeZone("Europe/Berlin")) e.alarms += VAlarm(Duration.ofHours(-1)) val os = ByteArrayOutputStream() diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt index 9136d25f..a8658fdb 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -111,8 +111,8 @@ class ICalendarTest { ICalendar.minifyVTimeZone(vtzVienna, Date("20200101")).let { minified -> assertEquals(2, minified.observances.size) // now earliest observance for DAYLIGHT/STANDARD is 1981/1996 - assertEquals(DateTime("19810329T020000"), minified.observances[0].startDate.date) - assertEquals(DateTime("19961027T030000"), minified.observances[1].startDate.date) + assertEquals(DateTime("19961027T030000"), minified.observances[0].startDate.date) + assertEquals(DateTime("19810329T020000"), minified.observances[1].startDate.date) } } @@ -132,8 +132,8 @@ class ICalendarTest { // Keep future observances. ICalendar.minifyVTimeZone(vtzVienna, Date("19751001")).let { minified -> assertEquals(4, minified.observances.size) - assertEquals(DateTime("19160430T230000"), minified.observances[2].startDate.date) - assertEquals(DateTime("19161001T010000"), minified.observances[3].startDate.date) + assertEquals(DateTime("19161001T010000"), minified.observances[2].startDate.date) + assertEquals(DateTime("19160430T230000"), minified.observances[3].startDate.date) } ICalendar.minifyVTimeZone(vtzKarachi, Date("19611001")).let { minified -> assertEquals(4, minified.observances.size) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt index 1056b246..4c9015c9 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt @@ -198,7 +198,7 @@ class TaskTest { fun testWrite() { val t = Task() t.uid = "SAMPLEUID" - t.dtStart = DtStart("20190101T100000", TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone("Europe/Berlin")) + t.dtStart = DtStart("20190101T100000", DateUtils.ical4jTimeZone("Europe/Berlin")) val alarm = VAlarm(java.time.Duration.ofHours(-1) /*Dur(0, -1, 0, 0)*/) alarm.properties += Action.AUDIO diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index a841a304..510ee85c 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -331,7 +331,7 @@ class Event : ICalendar() { lastModified?.let { props += it } - event.alarms.addAll(alarms) + event.components.addAll(alarms) return event } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index 71c3b25a..372ab9ba 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -8,9 +8,11 @@ import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.validation.ICalPreprocessor import net.fortuna.ical4j.data.* import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.Daylight import net.fortuna.ical4j.model.component.Observance @@ -24,8 +26,6 @@ import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.TzUrl -import net.fortuna.ical4j.model.property.XProperty import net.fortuna.ical4j.validate.ValidationException import java.io.Reader import java.io.StringReader @@ -150,14 +150,14 @@ open class ICalendar { * @return minified time zone definition */ fun minifyVTimeZone(originalTz: VTimeZone, start: Date?): VTimeZone { - val newTz = originalTz.copy() as VTimeZone + var newTz: VTimeZone? = null val keep = mutableSetOf() if (start != null) { // find latest matching STANDARD/DAYLIGHT observances var latestDaylight: Pair? = null var latestStandard: Pair? = null - for (observance in newTz.observances) { + for (observance in originalTz.observances) { val latest = observance.getLatestOnset(start) if (latest == null) // observance begins after "start", keep in any case @@ -209,28 +209,27 @@ open class ICalendar { } } - // remove all observances that shall not be kept - val iterator = newTz.observances.iterator() as MutableIterator - while (iterator.hasNext()) { - val entry = iterator.next() - if (!keep.contains(entry)) - iterator.remove() + // construct minified time zone that only contains the ID and relevant observances + val relevantProperties = PropertyList().apply { + add(originalTz.timeZoneId) + } + val relevantObservances = ComponentList().apply { + addAll(keep) + } + newTz = VTimeZone(relevantProperties, relevantObservances) + + // validate minified timezone + try { + newTz.validate() + } catch (e: ValidationException) { + // This should never happen! + Ical4Android.log.log(Level.WARNING, "Minified timezone is invalid, using original one", e) + newTz = null } } - // remove unnecessary properties - newTz.properties.removeAll { it is TzUrl || it is XProperty } - - // validate minified timezone - try { - newTz.validate() - } catch (e: ValidationException) { - // This should never happen! - Ical4Android.log.log(Level.WARNING, "Minified timezone is invalid, using original one", e) - return originalTz - } - - return newTz + // use original time zone if we couldn't calculate a minified one + return newTz ?: originalTz } /** diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 45ef249c..c4de6968 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -257,7 +257,7 @@ class Task: ICalendar() { percentComplete?.let { props += PercentComplete(it) } if (alarms.isNotEmpty()) - vTodo.alarms.addAll(alarms) + vTodo.components.addAll(alarms) // determine earliest referenced date val earliest = arrayOf( From 78ccbf0741dec876c5a06293f81159aea2e3c6aa Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 25 Jun 2024 13:06:33 +0200 Subject: [PATCH 55/92] Fix removal of recurrence rules in exceptions of recurring events (#154) * Alter test so it tests eventsFromReader as well * Repair events directly before returning them in eventsFromReader * Fix test in CI * Apply validator only after reading the whole iCalendar * Make EventValidator an object; add @VisibleForTesting; move test * Don't throw exceptions in EventValidator * Events without DTSTART don't cause an exception anymore --------- Co-authored-by: Ricki Hirner --- .../at/bitfire/ical4android/EventTest.kt | 24 +- .../events/multiple-with-invalid.ics | 66 ---- .../kotlin/at/bitfire/ical4android/Event.kt | 49 ++- .../ical4android/validation/EventValidator.kt | 285 ++++++++++-------- .../validation/EventValidatorTest.kt | 263 ++++++++++------ 5 files changed, 355 insertions(+), 332 deletions(-) delete mode 100644 lib/src/androidTest/resources/events/multiple-with-invalid.ics rename lib/src/{androidTest => test}/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt (60%) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt index 2cf5c030..643ecfb3 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt @@ -4,12 +4,10 @@ package at.bitfire.ical4android -import android.util.Log import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.property.Attendee @@ -21,7 +19,6 @@ import net.fortuna.ical4j.model.property.RecurrenceId import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull -import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Test import java.io.ByteArrayOutputStream @@ -104,18 +101,6 @@ class EventTest { assertTrue("Event 2 Exception 2" == e.exceptions.first.summary || "Event 2 Exception 2" == e.exceptions[1].summary) } - @Test - fun testInvalid() { - assertThrows(InvalidCalendarException::class.java) { - parseCalendar("multiple-with-invalid.ics", ignoreInvalidEvents = false) - } - } - - @Test - fun testIgnoreInvalidEvents() { - val events = parseCalendar("multiple-with-invalid.ics", ignoreInvalidEvents = true) - assertEquals(3, events.size) - } @Test fun testParse() { @@ -349,12 +334,9 @@ class EventTest { throw FileNotFoundException() } - private fun parseCalendar( - fname: String, - charset: Charset = Charsets.UTF_8, - ignoreInvalidEvents: Boolean = false): List = + private fun parseCalendar(fname: String, charset: Charset = Charsets.UTF_8): List = javaClass.classLoader!!.getResourceAsStream("events/$fname").use { stream -> - return Event.eventsFromReader(InputStreamReader(stream, charset), ignoreInvalidEvents = ignoreInvalidEvents) + return Event.eventsFromReader(InputStreamReader(stream, charset)) } -} +} \ No newline at end of file diff --git a/lib/src/androidTest/resources/events/multiple-with-invalid.ics b/lib/src/androidTest/resources/events/multiple-with-invalid.ics deleted file mode 100644 index 788118ce..00000000 --- a/lib/src/androidTest/resources/events/multiple-with-invalid.ics +++ /dev/null @@ -1,66 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -X-WR-CALNAME:Test-Kalender -BEGIN:VEVENT -UID:multiple-0@ical4android.EventTest -DTSTAMP:20150826T132300Z -SUMMARY:Event 0 -DTSTART:20130910T170000T -DTEND:20130910T180000T -END:VEVENT -BEGIN:VEVENT -UID:multiple-1@ical4android.EventTest -DTSTAMP:20150826T132300Z -SUMMARY:Event 1 -RRULE:FREQ=DAILY;COUNT=10 -DTSTART;VALUE=DATE:20131009 -DTEND;VALUE=DATE:20131010 -END:VEVENT -BEGIN:VEVENT -UID:multiple-1@ical4android.EventTest -RECURRENCE-ID;VALUE=DATE:202131012 -SUMMARY:Event 1 Exception -DTSTART:20131012T170000T -DTEND:20131012T180000T -END:VEVENT -BEGIN:VEVENT -UID:multiple-2@ical4android.EventTest -DTSTAMP:20150826T132300Z -SUMMARY:Event 2 -DTSTART;VALUE=DATE:20131009 -DTEND;VALUE=DATE:20131010 -RRULE:FREQ=DAILY;COUNT=10 -END:VEVENT -BEGIN:VEVENT -UID:multiple-3@ical4android.EventTest -DTSTAMP:20150826T132300Z -SUMMARY:Event 3 Missing DTSTART -DTEND;VALUE=DATE:20131010 -RRULE:FREQ=DAILY;COUNT=10 -END:VEVENT -BEGIN:VEVENT -UID:multiple-2@ical4android.EventTest -RECURRENCE-ID:20131014 -SEQUENCE:1 -DTSTAMP:20150826T132300Z -SUMMARY:Event 2 Updated Exception 1 -DTSTART:20131010T170000T -DTEND:20131010T180000T -END:VEVENT -BEGIN:VEVENT -UID:multiple-2@ical4android.EventTest -RECURRENCE-ID:20131014 -DTSTAMP:20150826T132300Z -SUMMARY:Event 2 Original Exception 1 -DTSTART:20131010T170000T -DTEND:20131010T180000T -END:VEVENT -BEGIN:VEVENT -UID:multiple-2@ical4android.EventTest -RECURRENCE-ID:20131015 -DTSTAMP:20150826T132300Z -SUMMARY:Event 2 Exception 2 -DTSTART:20131010T170000T -DTEND:20131010T180000T -END:VEVENT -END:VCALENDAR diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index 510ee85c..8f1b73de 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -21,7 +21,6 @@ import java.io.OutputStream import java.io.Reader import java.net.URI import java.util.* -import java.util.logging.Level class Event : ICalendar() { @@ -62,25 +61,21 @@ class Event : ICalendar() { companion object { /** - * Parses an iCalendar resource, applies [ICalPreprocessor] to increase compatibility - * and extracts the VEVENTs. + * Parses an iCalendar resource, applies [at.bitfire.ical4android.validation.ICalPreprocessor] + * and [EventValidator] to increase compatibility and extracts the VEVENTs. * * @param reader where the iCalendar is taken from * @param properties Known iCalendar properties (like [CALENDAR_NAME]) will be put into this map. Key: property name; value: property value - * @param ignoreInvalidEvents If true, events that can't be parsed will be dropped. Otherwise, parsing will fail if an invalid event is encountered. * * @return array of filled [Event] 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 - * @throws InvalidCalendarException on parsing exceptions + * @throws ParserException when the iCalendar can't be parsed */ @UsesThreadContextClassLoader fun eventsFromReader( reader: Reader, - properties: MutableMap? = null, - ignoreInvalidEvents: Boolean = false + properties: MutableMap? = null ): List { val ical = fromReader(reader, properties) @@ -133,25 +128,17 @@ class Event : ICalendar() { val events = mutableListOf() for ((uid, vEvent) in mainEvents) { - try { - val event = fromVEvent(vEvent) + val event = fromVEvent(vEvent) - // assign exceptions to main event and then remove them from exceptions array - exceptions.remove(uid)?.let { eventExceptions -> - event.exceptions.addAll(eventExceptions.values.map { fromVEvent(it) }) - } - - // make sure that exceptions have at least a SUMMARY - event.exceptions.forEach { it.summary = it.summary ?: event.summary } + // assign exceptions to main event and then remove them from exceptions array + exceptions.remove(uid)?.let { eventExceptions -> + event.exceptions.addAll(eventExceptions.values.map { fromVEvent(it) }) + } - events += event - } catch (e: InvalidCalendarException) { - Ical4Android.log.log(Level.WARNING, "Invalid VEvent: $vEvent", e) + // make sure that exceptions have at least a SUMMARY + event.exceptions.forEach { it.summary = it.summary ?: event.summary } - if (!ignoreInvalidEvents) { - throw e - } - } + events += event } for ((uid, onlyExceptions) in exceptions) { @@ -164,6 +151,10 @@ class Event : ICalendar() { events += fakeEvent } + // Try to repair all events after reading the whole iCalendar + for (event in events) + EventValidator.repair(event) + return events } @@ -201,17 +192,13 @@ class Event : ICalendar() { is Organizer -> e.organizer = prop is Attendee -> e.attendees += prop is LastModified -> e.lastModified = prop - is ProdId, is DtStamp -> { /* don't save these as unknown properties */ - } + is ProdId, is DtStamp -> { /* don't save these as unknown properties */ } else -> e.unknownProperties += prop } e.alarms.addAll(event.alarms) - // validate and repair - EventValidator(e).repair() - return e } } @@ -227,7 +214,7 @@ class Event : ICalendar() { val dtStart = dtStart ?: throw InvalidCalendarException("Won't generate event without start time") - EventValidator(this).repair() // validate and repair this event before creating VEVENT + EventValidator.repair(this) // repair this event before creating the VEVENT // "main event" (without exceptions) val components = ical.components diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt index a210cf03..0a97d2e2 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt @@ -4,9 +4,9 @@ package at.bitfire.ical4android.validation +import androidx.annotation.VisibleForTesting import at.bitfire.ical4android.Event import at.bitfire.ical4android.Ical4Android -import at.bitfire.ical4android.InvalidCalendarException import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate @@ -22,148 +22,183 @@ import java.util.Calendar import java.util.TimeZone /** - * Sometimes CalendarStorage or servers respond with invalid event definitions. Here we try to - * validate, repair and assume whatever seems appropriate before denying the whole event. + * Validates events and tries to repair broken events, since sometimes CalendarStorage or servers + * respond with invalid event definitions. + * + * This class should not throw exceptions, but try to repair as much as possible instead. + * + * This class is applied + * + * - once to every event after completely reading an iCalendar, and + * - to every event when writing an iCalendar. */ -class EventValidator(val e: Event) { - - fun repair() { - val dtStart = correctStartAndEndTime(e) - sameTypeForDtStartAndRruleUntil(dtStart, e.rRules) - removeRRulesWithUntilBeforeDtStart(dtStart, e.rRules) - - removeRRulesOfExceptions(e.exceptions) +object EventValidator { + + /** + * Searches for some invalid conditions and fixes them. + * + * @param event event to repair (including its exceptions) – may be modified! + */ + fun repair(event: Event) { + val dtStart = correctStartAndEndTime(event) + sameTypeForDtStartAndRruleUntil(dtStart, event.rRules) + removeRRulesWithUntilBeforeDtStart(dtStart, event.rRules) + removeRRulesOfExceptions(event.exceptions) } - companion object { - /** - * Ensure proper start and end time - */ - internal fun correctStartAndEndTime(e: Event): DtStart { - val dtStart = e.dtStart ?: throw InvalidCalendarException("Event without start time") - e.dtEnd?.let { dtEnd -> - if (dtStart.date > dtEnd.date) { - Ical4Android.log.warning("DTSTART after DTEND; removing DTEND") - e.dtEnd = null - } + + /** + * Makes sure that event has a start time and that it's before the end time. + * If the event doesn't have start time, + * + * 1. the end time is used as start time, if available, + * 2. otherwise the current time is used as start time. + * + * If the event has an end time and it's before the start time, the end time is removed. + * + * @return the (potentially corrected) start time + */ + @VisibleForTesting + internal fun correctStartAndEndTime(e: Event): DtStart { + // make sure that event has a start time + var dtStart: DtStart? = e.dtStart + if (dtStart == null) { + dtStart = + e.dtEnd?.let { + DtStart(it.date) + } ?: DtStart(DateTime(/* current time */)) + e.dtStart = dtStart + } + + // validate end time + e.dtEnd?.let { dtEnd -> + if (dtStart.date > dtEnd.date) { + Ical4Android.log.warning("DTSTART after DTEND; removing DTEND") + e.dtEnd = null } - return dtStart } - /** - * Tries to make the value type of UNTIL and DTSTART the same (both DATE or DATETIME). - */ - internal fun sameTypeForDtStartAndRruleUntil(dtStart: DtStart, rRules: MutableList) { - if (DateUtils.isDate(dtStart)) { - // DTSTART is a DATE - val newRRules = mutableListOf() - val rRuleIterator = rRules.iterator() - while (rRuleIterator.hasNext()) { - val rRule = rRuleIterator.next() - rRule.recur.until?.let { until -> - if (until is DateTime) { - Ical4Android.log.warning("DTSTART has DATE, but UNTIL has DATETIME; making UNTIL have DATE only") - - val newUntil = until.toLocalDate().toIcal4jDate() - - // remove current RRULE and remember new one to be added - val newRRule = RRule(Recur.Builder(rRule.recur) - .until(newUntil) - .build()) - Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") - newRRules += newRRule - rRuleIterator.remove() - } + return dtStart + } + + /** + * Tries to make the value type of UNTIL and DTSTART the same (both DATE or DATETIME). + */ + @VisibleForTesting + internal fun sameTypeForDtStartAndRruleUntil(dtStart: DtStart, rRules: MutableList) { + if (DateUtils.isDate(dtStart)) { + // DTSTART is a DATE + val newRRules = mutableListOf() + val rRuleIterator = rRules.iterator() + while (rRuleIterator.hasNext()) { + val rRule = rRuleIterator.next() + rRule.recur.until?.let { until -> + if (until is DateTime) { + Ical4Android.log.warning("DTSTART has DATE, but UNTIL has DATETIME; making UNTIL have DATE only") + + val newUntil = until.toLocalDate().toIcal4jDate() + + // remove current RRULE and remember new one to be added + val newRRule = RRule(Recur.Builder(rRule.recur) + .until(newUntil) + .build()) + Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") + newRRules += newRRule + rRuleIterator.remove() } } - // add repaired RRULEs - rRules += newRRules - - } else if (DateUtils.isDateTime(dtStart)) { - // DTSTART is a DATE-TIME - val newRRules = mutableListOf() - val rRuleIterator = rRules.iterator() - while (rRuleIterator.hasNext()) { - val rRule = rRuleIterator.next() - rRule.recur.until?.let { until -> - if (until !is DateTime) { - Ical4Android.log.warning("DTSTART has DATETIME, but UNTIL has DATE; copying time from DTSTART to UNTIL") - val dtStartTimeZone = if (dtStart.timeZone != null) - dtStart.timeZone - else if (dtStart.isUtc) - TimeZones.getUtcTimeZone() - else /* floating time */ - TimeZone.getDefault() - - val dtStartCal = Calendar.getInstance(dtStartTimeZone).apply { - time = dtStart.date - } - val dtStartTime = LocalTime.of( - dtStartCal.get(Calendar.HOUR_OF_DAY), - dtStartCal.get(Calendar.MINUTE), - dtStartCal.get(Calendar.SECOND) - ) - - val newUntil = ZonedDateTime.of( - until.toLocalDate(), // date from until - dtStartTime, // time from dtStart - dtStartTimeZone.toZoneIdCompat() - ) - - // Android requires UNTIL in UTC as defined in RFC 2445. - // https://android.googlesource.com/platform/frameworks/opt/calendar/+/refs/tags/android-12.1.0_r27/src/com/android/calendarcommon2/RecurrenceProcessor.java#93 - val newUntilUTC = DateTime(true).apply { - time = newUntil.toInstant().toEpochMilli() - } - - // remove current RRULE and remember new one to be added - val newRRule = RRule(Recur.Builder(rRule.recur) - .until(newUntilUTC) - .build()) - Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") - newRRules += newRRule - rRuleIterator.remove() + } + // add repaired RRULEs + rRules += newRRules + + } else if (DateUtils.isDateTime(dtStart)) { + // DTSTART is a DATE-TIME + val newRRules = mutableListOf() + val rRuleIterator = rRules.iterator() + while (rRuleIterator.hasNext()) { + val rRule = rRuleIterator.next() + rRule.recur.until?.let { until -> + if (until !is DateTime) { + Ical4Android.log.warning("DTSTART has DATETIME, but UNTIL has DATE; copying time from DTSTART to UNTIL") + val dtStartTimeZone = if (dtStart.timeZone != null) + dtStart.timeZone + else if (dtStart.isUtc) + TimeZones.getUtcTimeZone() + else /* floating time */ + TimeZone.getDefault() + + val dtStartCal = Calendar.getInstance(dtStartTimeZone).apply { + time = dtStart.date + } + val dtStartTime = LocalTime.of( + dtStartCal.get(Calendar.HOUR_OF_DAY), + dtStartCal.get(Calendar.MINUTE), + dtStartCal.get(Calendar.SECOND) + ) + + val newUntil = ZonedDateTime.of( + until.toLocalDate(), // date from until + dtStartTime, // time from dtStart + dtStartTimeZone.toZoneIdCompat() + ) + + // Android requires UNTIL in UTC as defined in RFC 2445. + // https://android.googlesource.com/platform/frameworks/opt/calendar/+/refs/tags/android-12.1.0_r27/src/com/android/calendarcommon2/RecurrenceProcessor.java#93 + val newUntilUTC = DateTime(true).apply { + time = newUntil.toInstant().toEpochMilli() } + + // remove current RRULE and remember new one to be added + val newRRule = RRule(Recur.Builder(rRule.recur) + .until(newUntilUTC) + .build()) + Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") + newRRules += newRRule + rRuleIterator.remove() } } - // add repaired RRULEs - rRules += newRRules - } else - throw InvalidCalendarException("Event with invalid DTSTART value") + } + // add repaired RRULEs + rRules += newRRules } + } - /** - * Removes RRULEs of exceptions of (potentially recurring) events - * - * @param exceptions exceptions of an event - */ - internal fun removeRRulesOfExceptions(exceptions: List) = - exceptions.forEach { exception -> - exception.rRules.clear() // Drop all RRULEs for the exception - } + /** + * Removes RRULEs of exceptions of (potentially recurring) events + * Note: This repair step needs to be applied after all exceptions have been found + * + * @param exceptions exceptions of an event + */ + @VisibleForTesting + internal fun removeRRulesOfExceptions(exceptions: List) { + for (exception in exceptions) + exception.rRules.clear() // Drop all RRULEs for the exception + } - /** - * Will remove the RRULES of an event where UNTIL lies before DTSTART - */ - internal fun removeRRulesWithUntilBeforeDtStart(dtStart: DtStart, rRules: MutableList) { - val iter = rRules.iterator() - while (iter.hasNext()) { - val rRule = iter.next() + /** + * Will remove the RRULES of an event where UNTIL lies before DTSTART + */ + @VisibleForTesting + internal fun removeRRulesWithUntilBeforeDtStart(dtStart: DtStart, rRules: MutableList) { + val iter = rRules.iterator() + while (iter.hasNext()) { + val rRule = iter.next() - // drop invalid RRULEs - if (hasUntilBeforeDtStart(dtStart, rRule)) - iter.remove() - } + // drop invalid RRULEs + if (hasUntilBeforeDtStart(dtStart, rRule)) + iter.remove() } + } - /** - * Checks whether UNTIL of an RRULE lies before DTSTART - */ - internal fun hasUntilBeforeDtStart(dtStart: DtStart, rRule: RRule): Boolean { - val until = rRule.recur.until ?: return false - return until < dtStart.date - } + /** + * Checks whether UNTIL of an RRULE lies before DTSTART + */ + @VisibleForTesting + internal fun hasUntilBeforeDtStart(dtStart: DtStart, rRule: RRule): Boolean { + val until = rRule.recur.until ?: return false + return until < dtStart.date } + } \ No newline at end of file diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt similarity index 60% rename from lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt rename to lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index 5564b3b4..c75106f8 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -5,55 +5,66 @@ package at.bitfire.ical4android.validation import at.bitfire.ical4android.Event -import at.bitfire.ical4android.InvalidCalendarException +import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Recur -import net.fortuna.ical4j.model.TimeZone +import net.fortuna.ical4j.model.TimeZoneRegistry import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Assume import org.junit.Test import java.io.StringReader class EventValidatorTest { companion object { - val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() + val tzReg: TimeZoneRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() } // DTSTART and DTEND - @Test(expected = InvalidCalendarException::class) - fun testEnsureCorrectStartAndEndTime_noDtStart_DateTime() { + @Test + fun testCorrectStartAndEndTime_NoDtStart_EndDateTime() { val event = Event().apply { + // no dtStart dtEnd = DtEnd(DateTime("20000105T000000")) // DATETIME + } + EventValidator.correctStartAndEndTime(event) + assertEquals(event.dtEnd!!.date, event.dtStart!!.date) + } + + @Test + fun testCorrectStartAndEndTime_NoDtStart_EndDate() { + val event = Event().apply { // no dtStart + dtEnd = DtEnd(Date("20000105")) // DATE } EventValidator.correctStartAndEndTime(event) + assertEquals(event.dtEnd!!.date, event.dtStart!!.date) } - @Test(expected = InvalidCalendarException::class) - fun testEnsureCorrectStartAndEndTime_noDtStart_Date() { - Event.eventsFromReader(StringReader( - "BEGIN:VCALENDAR\n" + - "BEGIN:VEVENT\n" + - "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + - "DTEND;VALUE=DATE:20211116\n" + // DATE - "END:VEVENT\n" + - "END:VCALENDAR")).first() + @Test + fun testCorrectStartAndEndTime_NoDtStart_NoDtEnd() { + val event = Event(/* no dtStart, no dtEnd */) + + val time = System.currentTimeMillis() + EventValidator.correctStartAndEndTime(event) + + assertTrue(event.dtStart!!.date.time in (time-1000)..<(time+1000)) // within 2 seconds + assertNull(event.dtEnd) } @Test - fun testEnsureCorrectStartAndEndTime_dtEndBeforeDtStart() { + fun testCorrectStartAndEndTime_DtEndBeforeDtStart() { val event = Event().apply { dtStart = DtStart(DateTime("20000105T001100")) // DATETIME dtEnd = DtEnd(DateTime("20000105T000000")) // DATETIME @@ -201,13 +212,13 @@ class EventValidatorTest { "BEGIN:VCALENDAR\n" + "BEGIN:VEVENT\n" + "UID:381fb26b-2da5-4dd2-94d7-2e0874128aa7\n" + - "DTSTART;VALUE=DATETIME:20080214T001100\n" + // DATETIME (no timezone) - "RRULE:FREQ=YEARLY;UNTIL=20110214;BYMONTHDAY=15\n" + // DATE + "DTSTART;VALUE=DATETIME:20110605T001100Z\n" + // DATETIME (UTC) + "RRULE:FREQ=YEARLY;UNTIL=20211214;BYMONTHDAY=15\n" + // DATE "END:VEVENT\n" + "END:VCALENDAR" ) ).first() - assertEquals("FREQ=YEARLY;UNTIL=20110213T231100Z;BYMONTHDAY=15", event2.rRules.joinToString()) + assertEquals("FREQ=YEARLY;UNTIL=20211214T001100Z;BYMONTHDAY=15", event2.rRules.joinToString()) } @@ -217,7 +228,8 @@ class EventValidatorTest { fun testHasUntilBeforeDtStart_DtStartTime_RRuleNoUntil() { assertFalse( EventValidator.hasUntilBeforeDtStart( - DtStart(DateTime("20220531T010203")), RRule()) + DtStart(DateTime("20220531T010203")), RRule() + ) ) } @@ -225,46 +237,70 @@ class EventValidatorTest { @Test fun testHasUntilBeforeDtStart_DtStartDate_RRuleUntil_TimeBeforeDtStart_UTC() { assertTrue( - EventValidator.hasUntilBeforeDtStart(DtStart("20220912", tzReg.getTimeZone("UTC")), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220911T235959Z")) - .build()))) + EventValidator.hasUntilBeforeDtStart( + DtStart("20220912", tzReg.getTimeZone("UTC")), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220911T235959Z")) + .build() + ) + ) + ) } @Test fun testHasUntilBeforeDtStart_DtStartDate_RRuleUntil_TimeBeforeDtStart_noTimezone() { assertTrue( - EventValidator.hasUntilBeforeDtStart(DtStart("20220912"), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220911T235959")) - .build()))) + EventValidator.hasUntilBeforeDtStart( + DtStart("20220912"), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220911T235959")) + .build() + ) + ) + ) } @Test fun testHasUntilBeforeDtStart_DtStartDate_RRuleUntil_TimeBeforeDtStart_withTimezone() { assertTrue( - EventValidator.hasUntilBeforeDtStart(DtStart("20220912", tzReg.getTimeZone("America/New_York")), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220911T235959", tzReg.getTimeZone("America/New_York"))) - .build()))) + EventValidator.hasUntilBeforeDtStart( + DtStart("20220912", tzReg.getTimeZone("America/New_York")), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220911T235959", tzReg.getTimeZone("America/New_York"))) + .build() + ) + ) + ) } @Test fun testHasUntilBeforeDtStart_DtStartDate_RRuleUntil_DateBeforeDtStart() { assertTrue( - EventValidator.hasUntilBeforeDtStart(DtStart("20220531"), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220530T000000")) - .build()))) + EventValidator.hasUntilBeforeDtStart( + DtStart("20220531"), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220530T000000")) + .build() + ) + ) + ) } @Test fun testHasUntilBeforeDtStart_DtStartDate_RRuleUntil_TimeAfterDtStart() { assertFalse( - EventValidator.hasUntilBeforeDtStart(DtStart("20200912"), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220912T000001Z")) - .build())) + EventValidator.hasUntilBeforeDtStart( + DtStart("20200912"), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220912T000001Z")) + .build() + ) + ) ) } @@ -272,80 +308,129 @@ class EventValidatorTest { @Test fun testHasUntilBeforeDtStart_DtStartTime_RRuleUntil_DateBeforeDtStart() { assertTrue( - EventValidator.hasUntilBeforeDtStart(DtStart(DateTime("20220531T010203")), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(Date("20220530")) - .build())) + EventValidator.hasUntilBeforeDtStart( + DtStart(DateTime("20220531T010203")), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(Date("20220530")) + .build() + ) + ) ) } @Test fun testHasUntilBeforeDtStart_DtStartTime_RRuleUntil_TimeBeforeDtStart() { assertTrue( - EventValidator.hasUntilBeforeDtStart(DtStart(DateTime("20220531T010203")), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220531T010202")) - .build())) + EventValidator.hasUntilBeforeDtStart( + DtStart(DateTime("20220531T010203")), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220531T010202")) + .build() + ) + ) ) } @Test fun testHasUntilBeforeDtStart_DtStartTime_RRuleUntil_TimeAtDtStart() { assertFalse( - EventValidator.hasUntilBeforeDtStart(DtStart(DateTime("20220531T010203")), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220531T010203")) - .build())) + EventValidator.hasUntilBeforeDtStart( + DtStart(DateTime("20220531T010203")), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220531T010203")) + .build() + ) + ) ) } @Test fun testHasUntilBeforeDtStart_DtStartTime_RRuleUntil_TimeAfterDtStart() { assertFalse( - EventValidator.hasUntilBeforeDtStart(DtStart(DateTime("20220531T010203")), RRule(Recur.Builder() - .frequency(Recur.Frequency.DAILY) - .until(DateTime("20220531T010204")) - .build())) + EventValidator.hasUntilBeforeDtStart( + DtStart(DateTime("20220531T010203")), RRule( + Recur.Builder() + .frequency(Recur.Frequency.DAILY) + .until(DateTime("20220531T010204")) + .build() + ) + ) ) } - @Test fun testRemoveRrulesOfRruleExceptions() { - val calendar = Event.eventsFromReader(StringReader( + // Test manually created event + val tz = DateUtils.ical4jTimeZone("Europe/Paris") + val manualEvent = Event().apply { + dtStart = DtStart("20240219T130000", tz) + dtEnd = DtEnd("20240219T140000", tz) + summary = "recurring event" + rRules.add(RRule(Recur.Builder() // Should keep this RRULE + .frequency(Recur.Frequency.DAILY) + .interval(1) + .count(5) + .build())) + sequence = 0 + uid = "76c08fb1-99a3-41cf-b482-2d3b06648814" + exceptions.add(Event().apply { + dtStart = DtStart("20240221T110000", tz) + dtEnd = DtEnd("20240221T120000", tz) + recurrenceId = RecurrenceId("20240221T130000", tz) + sequence = 0 + summary = "exception of recurring event" + rRules.addAll(listOf( + RRule(Recur.Builder() // but remove this one + .frequency(Recur.Frequency.DAILY) + .count(6) + .interval(2) + .build()), + RRule(Recur.Builder() // and this one + .frequency(Recur.Frequency.DAILY) + .count(6) + .interval(2) + .build()) + )) + uid = "76c08fb1-99a3-41cf-b482-2d3b06648814" + }) + } + assertTrue(manualEvent.rRules.size == 1) + assertTrue(manualEvent.exceptions.first.rRules.size == 2) + EventValidator.removeRRulesOfExceptions(manualEvent.exceptions) // Repair the manually created event + assertTrue(manualEvent.rRules.size == 1) + assertTrue(manualEvent.exceptions.first.rRules.isEmpty()) + + // Test event from reader, the reader will repair the event itself + val eventFromReader = Event.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + - "BEGIN:VEVENT\n" + - "DTSTAMP:20240215T102755Z\n" + - "SUMMARY:recurring event\n" + - "DTSTART;TZID=Europe/Paris:20240219T130000\n" + - "DTEND;TZID=Europe/Paris:20240219T140000\n" + - "RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5\n" + // Should keep this RRULE - "UID:76c08fb1-99a3-41cf-b482-2d3b06648814\n" + - "END:VEVENT\n" + - - // Exception for the recurring event above - "BEGIN:VEVENT\n" + - "DTSTAMP:20240215T102908Z\n" + - "RECURRENCE-ID;TZID=Europe/Paris:20240221T130000\n" + - "SUMMARY:exception of recurring event\n" + - "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // but remove this one - "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // and this one - "DTSTART;TZID=Europe/Paris:20240221T110000\n" + - "DTEND;TZID=Europe/Paris:20240221T120000\n" + - "UID:76c08fb1-99a3-41cf-b482-2d3b06648814\n" + - "END:VEVENT\n" + - "END:VCALENDAR" - )) - assertEquals("FREQ=DAILY;COUNT=5;INTERVAL=1", calendar.first().rRules.joinToString()) - assertEquals( - "FREQ=DAILY;COUNT=6;INTERVAL=2\nFREQ=DAILY;COUNT=6;INTERVAL=2", - calendar.first().exceptions.first.rRules.joinToString() - ) - EventValidator.removeRRulesOfExceptions(calendar.first().exceptions) - assertEquals("FREQ=DAILY;COUNT=5;INTERVAL=1", calendar.first().rRules.joinToString()) - assertTrue(calendar.first().exceptions.first.rRules.isEmpty()) - } + "BEGIN:VEVENT\n" + + "DTSTAMP:20240215T102755Z\n" + + "SUMMARY:recurring event\n" + + "DTSTART;TZID=Europe/Paris:20240219T130000\n" + + "DTEND;TZID=Europe/Paris:20240219T140000\n" + + "RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5\n" + // Should keep this RRULE + "UID:76c08fb1-99a3-41cf-b482-2d3b06648814\n" + + "END:VEVENT\n" + + // Exception for the recurring event above + "BEGIN:VEVENT\n" + + "DTSTAMP:20240215T102908Z\n" + + "RECURRENCE-ID;TZID=Europe/Paris:20240221T130000\n" + + "SUMMARY:exception of recurring event\n" + + "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // but remove this one + "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // and this one + "DTSTART;TZID=Europe/Paris:20240221T110000\n" + + "DTEND;TZID=Europe/Paris:20240221T120000\n" + + "UID:76c08fb1-99a3-41cf-b482-2d3b06648814\n" + + "END:VEVENT\n" + + "END:VCALENDAR" + )).first() + assertTrue(eventFromReader.rRules.size == 1) + assertTrue(eventFromReader.exceptions.first.rRules.isEmpty()) + } @Test fun testRemoveRRulesWithUntilBeforeDtStart() { From 83cda23cebeaf886b10a8a0511e695348284b306 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 25 Jun 2024 13:08:57 +0200 Subject: [PATCH 56/92] Update dependencies, including ical4j --- gradle/libs.versions.toml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37fc6e95..aca2c2e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,8 @@ [versions] -agp = "8.4.1" +agp = "8.4.2" android-desugar = "2.0.4" androidx-core = "1.13.1" -androidx-test-core = "1.5.0" -androidx-test-rules = "1.5.0" -androidx-test-runner = "1.5.2" +androidx-test = "1.6.0" # noinspection GradleDependency commons-collections = "4.2" # noinspection GradleDependency @@ -12,7 +10,8 @@ commons-lang = "3.8.1" # noinspection GradleDependency commons-io = "2.6" dokka ="1.9.20" -ical4j = "3.2.18" +# noinspection GradleDependency +ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" kotlin = "2.0.0" mockk = "1.13.11" @@ -21,9 +20,9 @@ slf4j = "2.0.13" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } -androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } -androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } -androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" } commons-collections = { module = "org.apache.commons:commons-collections4", version.ref = "commons-collections" } commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" } commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } From 759d27fc88886880980f775062ad92f0edafaea3 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 15 Jul 2024 14:34:43 +0200 Subject: [PATCH 57/92] Remove @UsesThreadContextClassLoader because it was unclear what it does - make sure checkThreadContextClassLoader() is always called before ical4j uses the ServiceLoader --- .../main/kotlin/at/bitfire/ical4android/Event.kt | 2 -- .../kotlin/at/bitfire/ical4android/ICalendar.kt | 4 +--- .../kotlin/at/bitfire/ical4android/Ical4Android.kt | 3 ++- .../at/bitfire/ical4android/JtxICalObject.kt | 3 --- .../main/kotlin/at/bitfire/ical4android/Task.kt | 2 -- .../ical4android/UsesThreadContextClassLoader.kt | 13 ------------- .../at/bitfire/ical4android/util/DateUtils.kt | 14 +++++--------- 7 files changed, 8 insertions(+), 33 deletions(-) delete mode 100644 lib/src/main/kotlin/at/bitfire/ical4android/UsesThreadContextClassLoader.kt diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index 8f1b73de..9f072750 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -72,7 +72,6 @@ class Event : ICalendar() { * @throws IOException on I/O errors * @throws ParserException when the iCalendar can't be parsed */ - @UsesThreadContextClassLoader fun eventsFromReader( reader: Reader, properties: MutableMap? = null @@ -204,7 +203,6 @@ class Event : ICalendar() { } - @UsesThreadContextClassLoader fun write(os: OutputStream) { Ical4Android.checkThreadContextClassLoader() diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index 372ab9ba..66e9f596 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -87,10 +87,9 @@ open class ICalendar { * @throws ParserException when the iCalendar can't be parsed * @throws IllegalArgumentException when the iCalendar resource contains an invalid value */ - @UsesThreadContextClassLoader fun fromReader(reader: Reader, properties: MutableMap? = null): Calendar { - Ical4Android.log.fine("Parsing iCalendar stream") Ical4Android.checkThreadContextClassLoader() + Ical4Android.log.fine("Parsing iCalendar stream") // preprocess stream to work around some problems that can't be fixed later val preprocessed = ICalPreprocessor.preprocessStream(reader) @@ -237,7 +236,6 @@ open class ICalendar { * @param timezoneDef time zone definition (VCALENDAR with VTIMEZONE component) * @return time zone id (TZID) if VTIMEZONE contains a TZID, null otherwise */ - @UsesThreadContextClassLoader fun timezoneDefToTzId(timezoneDef: String): String? { Ical4Android.checkThreadContextClassLoader() try { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt index 806e9810..952127a0 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt @@ -7,6 +7,7 @@ package at.bitfire.ical4android import java.util.logging.Level import java.util.logging.Logger +@Suppress("unused") object Ical4Android { val log: Logger = Logger.getLogger("ical4android") @@ -21,7 +22,7 @@ object Ical4Android { fun checkThreadContextClassLoader() { if (Thread.currentThread().contextClassLoader == null) - throw IllegalStateException("Thread.currentThread().contextClassLoader must be set") + throw IllegalStateException("Thread.currentThread().contextClassLoader must be set for java.util.ServiceLoader (used by ical4j)") } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt index dd072132..64ba0f9e 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt @@ -217,7 +217,6 @@ open class JtxICalObject( * @throws IllegalArgumentException when the iCalendar resource contains an invalid value * @throws IOException on I/O errors */ - @UsesThreadContextClassLoader fun fromReader( reader: Reader, collection: JtxCollection @@ -563,7 +562,6 @@ open class JtxICalObject( * Takes the current JtxICalObject and transforms it to a Calendar (ical4j) * @return The current JtxICalObject transformed into a ical4j Calendar */ - @UsesThreadContextClassLoader fun getICalendarFormat(): Calendar? { Ical4Android.checkThreadContextClassLoader() @@ -668,7 +666,6 @@ open class JtxICalObject( * 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() CalendarOutputter(false).output(this.getICalendarFormat(), os) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index c4de6968..94ed207f 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -104,7 +104,6 @@ class Task: ICalendar() { * @throws IllegalArgumentException when the iCalendar resource contains an invalid value * @throws IOException on I/O errors */ - @UsesThreadContextClassLoader fun tasksFromReader(reader: Reader): List { val ical = fromReader(reader) val vToDos = ical.getComponents(Component.VTODO) @@ -189,7 +188,6 @@ class Task: ICalendar() { } - @UsesThreadContextClassLoader fun write(os: OutputStream) { Ical4Android.checkThreadContextClassLoader() diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/UsesThreadContextClassLoader.kt b/lib/src/main/kotlin/at/bitfire/ical4android/UsesThreadContextClassLoader.kt deleted file mode 100644 index 8bc96baf..00000000 --- a/lib/src/main/kotlin/at/bitfire/ical4android/UsesThreadContextClassLoader.kt +++ /dev/null @@ -1,13 +0,0 @@ -/*************************************************************************************************** - * 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) -@Retention(AnnotationRetention.SOURCE) -@MustBeDocumented -/** - * Requires the current thread's [Thread.getContextClassLoader] to be set (not null). - */ -annotation class UsesThreadContextClassLoader \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index 2962cd3d..2eb21f5d 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -5,7 +5,6 @@ package at.bitfire.ical4android.util import at.bitfire.ical4android.Ical4Android -import at.bitfire.ical4android.UsesThreadContextClassLoader import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime @@ -24,17 +23,16 @@ import java.time.ZoneId */ object DateUtils { + init { + Ical4Android.checkThreadContextClassLoader() + } + /** * Global ical4j time zone registry used for event/task processing. Do not * modify this registry or its entries! */ - @UsesThreadContextClassLoader private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - init { - Ical4Android.checkThreadContextClassLoader() - } - // time zones @@ -93,8 +91,6 @@ object DateUtils { } } - @Suppress("DEPRECATION") - @UsesThreadContextClassLoader /** * Loads a time zone from the ical4j time zone registry (which contains the * VTIMEZONE definitions). @@ -128,9 +124,9 @@ object DateUtils { * @return parsed VTimeZone * @throws IllegalArgumentException when the timezone definition can't be parsed */ - @UsesThreadContextClassLoader fun parseVTimeZone(timezoneDef: String): VTimeZone { Ical4Android.checkThreadContextClassLoader() + val builder = CalendarBuilder(tzRegistry) try { val cal = builder.build(StringReader(timezoneDef)) From 7ebab70eb67b097d48da1f327674a6603ba6f3b2 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 15 Jul 2024 14:40:28 +0200 Subject: [PATCH 58/92] Mark Ical4android.log as deprecated; update gradle, AGP, dependencies --- gradle/libs.versions.toml | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- .../main/kotlin/at/bitfire/ical4android/Ical4Android.kt | 9 ++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aca2c2e5..575dfb32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -agp = "8.4.2" +agp = "8.5.1" android-desugar = "2.0.4" androidx-core = "1.13.1" -androidx-test = "1.6.0" +androidx-test = "1.6.1" # noinspection GradleDependency commons-collections = "4.2" # noinspection GradleDependency diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48c0a02c..0d184210 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt index 952127a0..7df7e8b2 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt @@ -4,21 +4,16 @@ package at.bitfire.ical4android -import java.util.logging.Level import java.util.logging.Logger @Suppress("unused") object Ical4Android { - val log: Logger = Logger.getLogger("ical4android") - const val ical4jVersion = BuildConfig.version_ical4j + @Deprecated("Use java.util.Logger.getLogger(javaClass.name) instead", ReplaceWith("Logger.getLogger(javaClass.name)", "java.util.logging.Logger")) + val log: Logger = Logger.getLogger("at.bitfire.ical4android") - init { - if (BuildConfig.DEBUG) - log.level = Level.ALL - } fun checkThreadContextClassLoader() { if (Thread.currentThread().contextClassLoader == null) From 505f74771e886af236f62bf0d6e6c5aaa87fb1eb Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sat, 20 Jul 2024 13:38:52 +0200 Subject: [PATCH 59/92] Get rid of Apache Commons (#156) * Got rid of commons dependencies Signed-off-by: Arnau Mora * Migrated from commons Signed-off-by: Arnau Mora * Removed commons import Signed-off-by: Arnau Mora * Added comment Signed-off-by: Arnau Mora * Got rid of `reflectionToString` Signed-off-by: Arnau Mora * Converted into data classes Signed-off-by: Arnau Mora * Import cleanup Signed-off-by: Arnau Mora * Override `uid`, `sequence` and `userAgents` Signed-off-by: Arnau Mora Gras * Using full expression Signed-off-by: Arnau Mora * Added backing field _event Signed-off-by: Arnau Mora * Changed getters Signed-off-by: Arnau Mora * Fixed `_event` calls Signed-off-by: Arnau Mora * Added private setter for event and replaced usages Signed-off-by: Arnau Mora Gras --------- Signed-off-by: Arnau Mora Signed-off-by: Arnau Mora Gras --- gradle/libs.versions.toml | 3 - lib/build.gradle.kts | 16 ---- .../ical4android/BatchOperationTest.kt | 7 +- .../at/bitfire/ical4android/AndroidEvent.kt | 33 ++++--- .../at/bitfire/ical4android/DmfsTask.kt | 3 - .../kotlin/at/bitfire/ical4android/Event.kt | 88 +++++++++++++------ .../at/bitfire/ical4android/ICalendar.kt | 9 +- .../kotlin/at/bitfire/ical4android/Task.kt | 56 ++++++------ .../at/bitfire/ical4android/util/MiscUtils.kt | 32 ------- .../validation/StreamPreprocessor.kt | 5 +- .../at/bitfire/ical4android/MiscUtilsTest.kt | 43 --------- 11 files changed, 119 insertions(+), 176 deletions(-) delete mode 100644 lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 575dfb32..fcd99910 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,9 +23,6 @@ androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-cor androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" } -commons-collections = { module = "org.apache.commons:commons-collections4", version.ref = "commons-collections" } -commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" } -commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index b8a597a2..508f4669 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -99,22 +99,6 @@ dependencies { api(libs.ical4j) implementation(libs.slf4j) // ical4j logging over java.util.Logger - // ical4j requires newer Apache Commons libraries, which require Java8. Force latest Java7 versions. - @Suppress("RedundantSuppression") - api(libs.commons.collections) { - version { - strictly(libs.versions.commons.collections.get()) - } - } - @Suppress("RedundantSuppression") - api(libs.commons.lang3) { - version { - strictly(libs.versions.commons.lang.get()) - } - } - @Suppress("RedundantSuppression") - implementation(libs.commons.io) - androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.runner) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt index 3a2d426e..4e5c24d1 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt @@ -19,9 +19,14 @@ import at.bitfire.ical4android.util.MiscUtils.closeCompat import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart -import org.junit.* +import org.junit.After +import org.junit.AfterClass import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test import java.net.URI import java.util.Arrays diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 041a14ff..2d1bb376 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -21,7 +21,6 @@ import androidx.annotation.CallSuper import at.bitfire.ical4android.BatchOperation.CpoBuilder import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils -import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.removeBlankStrings import at.bitfire.ical4android.util.MiscUtils.toValues @@ -135,17 +134,22 @@ abstract class AndroidEvent( this.event = event } - var event: Event? = null - /** - * This getter returns the full event data, either from [event] or, if [event] is null, by reading event - * number [id] from the Android calendar storage - * @throws IllegalArgumentException if event has not been saved yet - * @throws FileNotFoundException if there's no event with [id] in the calendar storage - * @throws RemoteException on calendar provider errors - */ + private var _event: Event? = null + + /** + * Returns the full event data, either from [event] or, if [event] is null, by reading event + * number [id] from the Android calendar storage + * @throws IllegalArgumentException if event has not been saved yet + * @throws FileNotFoundException if there's no event with [id] in the calendar storage + * @throws RemoteException on calendar provider errors + */ + var event: Event? + private set(value) { + _event = value + } get() { - if (field != null) - return field + if (_event != null) + return _event val id = requireNotNull(id) var iterEvents: EntityIterator? = null @@ -162,7 +166,7 @@ abstract class AndroidEvent( // create new Event which will be populated val newEvent = Event() - field = newEvent + _event = newEvent // calculate some scheduling properties val groupScheduled = e.subValues.any { it.uri == Attendees.CONTENT_URI } @@ -186,7 +190,7 @@ abstract class AndroidEvent( /* Populating event has been interrupted by an exception, so we reset the event to avoid an inconsistent state. This also ensures that the exception will be thrown again on the next get() call. */ - field = null + _event = null throw e } finally { iterEvents?.close() @@ -1094,6 +1098,7 @@ abstract class AndroidEvent( return ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(calendar.account) } - override fun toString() = MiscUtils.reflectionToString(this) + @CallSuper + override fun toString(): String = "AndroidEvent(calendar=$calendar, id=$id, event=$_event)" } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 34d380a3..58dc7bd4 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -12,7 +12,6 @@ import androidx.annotation.CallSuper import at.bitfire.ical4android.BatchOperation.CpoBuilder import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils -import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.util.MiscUtils.toValues import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime @@ -591,6 +590,4 @@ abstract class DmfsTask( return ContentUris.withAppendedId(taskList.tasksSyncUri(loadProperties), id) } - override fun toString() = MiscUtils.reflectionToString(this) - } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index 9f072750..b326e907 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -9,55 +9,89 @@ import at.bitfire.ical4android.util.DateUtils.isDateTime import at.bitfire.ical4android.validation.EventValidator 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.Component +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.TextList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.parameter.Email -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.property.Attendee +import net.fortuna.ical4j.model.property.Categories +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Color +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStamp +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.ExRule +import net.fortuna.ical4j.model.property.LastModified +import net.fortuna.ical4j.model.property.Location +import net.fortuna.ical4j.model.property.Organizer +import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.Sequence +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Summary +import net.fortuna.ical4j.model.property.Transp +import net.fortuna.ical4j.model.property.Uid +import net.fortuna.ical4j.model.property.Url +import net.fortuna.ical4j.model.property.Version import java.io.IOException import java.io.OutputStream import java.io.Reader import java.net.URI -import java.util.* +import java.util.LinkedList +import java.util.UUID -class Event : ICalendar() { +data class Event( + override var uid: String? = null, + override var sequence: Int? = null, + + /** list of Calendar User Agents which have edited the event since last sync */ + override var userAgents: LinkedList = LinkedList(), // uid and sequence are inherited from iCalendar - var recurrenceId: RecurrenceId? = null + var recurrenceId: RecurrenceId? = null, - var summary: String? = null - var location: String? = null - var url: URI? = null - var description: String? = null - var color: Css3Color? = null + var summary: String? = null, + var location: String? = null, + var url: URI? = null, + var description: String? = null, + var color: Css3Color? = null, - var dtStart: DtStart? = null - var dtEnd: DtEnd? = null + var dtStart: DtStart? = null, + var dtEnd: DtEnd? = null, - var duration: Duration? = null - val rRules = LinkedList() - val exRules = LinkedList() - val rDates = LinkedList() - val exDates = LinkedList() + var duration: Duration? = null, + val rRules: LinkedList = LinkedList(), + val exRules: LinkedList = LinkedList(), + val rDates: LinkedList = LinkedList(), + val exDates: LinkedList = LinkedList(), - val exceptions = LinkedList() + val exceptions: LinkedList = LinkedList(), - var classification: Clazz? = null - var status: Status? = null + var classification: Clazz? = null, + var status: Status? = null, - var opaque = true + var opaque: Boolean = true, - var organizer: Organizer? = null - val attendees = LinkedList() + var organizer: Organizer? = null, + val attendees: LinkedList = LinkedList(), - val alarms = LinkedList() + val alarms: LinkedList = LinkedList(), - var lastModified: LastModified? = null + var lastModified: LastModified? = null, - val categories = LinkedList() - val unknownProperties = LinkedList() + val categories: LinkedList = LinkedList(), + val unknownProperties: LinkedList = LinkedList() +) : ICalendar() { companion object { /** diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index 66e9f596..2ca93788 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -4,7 +4,6 @@ package at.bitfire.ical4android -import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.validation.ICalPreprocessor import net.fortuna.ical4j.data.* import net.fortuna.ical4j.model.Calendar @@ -38,11 +37,11 @@ import java.util.logging.Logger open class ICalendar { - var uid: String? = null - var sequence: Int? = null + open var uid: String? = null + open var sequence: Int? = null /** list of CUAs which have edited the event since last sync */ - var userAgents = LinkedList() + open var userAgents = LinkedList() companion object { @@ -381,6 +380,4 @@ open class ICalendar { fun prodId(): ProdId = prodId(userAgents) - override fun toString() = MiscUtils.reflectionToString(this) - } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 94ed207f..0bb70c27 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -52,43 +52,43 @@ import java.net.URISyntaxException import java.util.LinkedList import java.util.logging.Level -class Task: ICalendar() { - - var createdAt: Long? = null - var lastModified: Long? = null - - var summary: String? = null - var location: String? = null - var geoPosition: Geo? = null - var description: String? = null - var color: Int? = null - var url: String? = null - var organizer: Organizer? = null +data class Task( + var createdAt: Long? = null, + var lastModified: Long? = null, + + var summary: String? = null, + var location: String? = null, + var geoPosition: Geo? = null, + var description: String? = null, + var color: Int? = null, + var url: String? = null, + var organizer: Organizer? = null, @IntRange(from = 0, to = 9) - var priority: Int = Priority.UNDEFINED.level + var priority: Int = Priority.UNDEFINED.level, - var classification: Clazz? = null - var status: Status? = null + var classification: Clazz? = null, + var status: Status? = null, - var dtStart: DtStart? = null - var due: Due? = null - var duration: Duration? = null - var completedAt: Completed? = null + var dtStart: DtStart? = null, + var due: Due? = null, + var duration: Duration? = null, + var completedAt: Completed? = null, @IntRange(from = 0, to = 100) - var percentComplete: Int? = null + var percentComplete: Int? = null, - var rRule: RRule? = null - val rDates = LinkedList() - val exDates = LinkedList() + var rRule: RRule? = null, + val rDates: LinkedList = LinkedList(), + val exDates: LinkedList = LinkedList(), - val categories = LinkedList() - var comment: String? = null - var relatedTo = LinkedList() - val unknownProperties = LinkedList() + val categories: LinkedList = LinkedList(), + var comment: String? = null, + var relatedTo: LinkedList = LinkedList(), + val unknownProperties: LinkedList = LinkedList(), - val alarms = LinkedList() + val alarms: LinkedList = LinkedList(), +) : ICalendar() { companion object { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt index 950075a1..d3bbd5b7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt @@ -12,41 +12,9 @@ import android.database.DatabaseUtils import android.net.Uri import android.os.Build import android.provider.CalendarContract -import at.bitfire.ical4android.Ical4Android -import org.apache.commons.lang3.StringUtils -import java.lang.reflect.Modifier -import java.util.* -import kotlin.ConcurrentModificationException object MiscUtils { - private const val TOSTRING_MAXCHARS = 10000 - - /** - * Generates useful toString info (fields and values) from [obj] by reflection. - * - * @param obj object to inspect - * @return string containing properties and non-static declared fields - */ - fun reflectionToString(obj: Any): String { - val s = LinkedList() - var clazz: Class? = obj.javaClass - while (clazz != null) { - for (prop in clazz.declaredFields.filterNot { Modifier.isStatic(it.modifiers) }) { - prop.isAccessible = true - val valueStr = try { - StringUtils.abbreviate(prop.get(obj)?.toString(), TOSTRING_MAXCHARS) - } catch(e: OutOfMemoryError) { - "![$e]" - } - s += "${prop.name}=" + valueStr - } - clazz = clazz.superclass - } - return "${obj.javaClass.simpleName}=[${s.joinToString(", ")}]" - } - - // various extension methods fun ContentProviderClient.closeCompat() { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt index 02579c20..7bae3678 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt @@ -4,7 +4,6 @@ package at.bitfire.ical4android.validation -import org.apache.commons.io.IOUtils import java.io.IOException import java.io.Reader import java.io.StringReader @@ -31,11 +30,11 @@ abstract class StreamPreprocessor { // reset is supported, no need to copy the whole stream to another String (unless we have to fix the TZOFFSET) if (regex == null || Scanner(reader).findWithinHorizon(regex.toPattern(), 0) != null) { reader.reset() - result = fixString(IOUtils.toString(reader)) + result = fixString(reader.readText()) } } else // reset not supported, always generate a new String that will be returned - result = fixString(IOUtils.toString(reader)) + result = fixString(reader.readText()) if (result != null) // modified or reset not supported, return new stream diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt deleted file mode 100644 index 313e31c5..00000000 --- a/lib/src/test/kotlin/at/bitfire/ical4android/MiscUtilsTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ - -package at.bitfire.ical4android - -import at.bitfire.ical4android.util.MiscUtils -import org.junit.Assert.assertTrue -import org.junit.Test - -class MiscUtilsTest { - - @Test - fun testReflectionToString() { - val s = MiscUtils.reflectionToString(TestClass()) - assertTrue(s.startsWith("TestClass=[")) - assertTrue(s.contains("i=2")) - assertTrue(s.contains("large=null")) - assertTrue(s.contains("s=test")) - } - - @Test - fun testReflectionToString_OOM() { - val t = TestClass() - t.large = object: Any() { - override fun toString(): String { - throw OutOfMemoryError("toString() causes OOM") - } - } - val s = MiscUtils.reflectionToString(t) - assertTrue(s.startsWith("TestClass=[")) - assertTrue(s.contains("large=![java.lang.OutOfMemoryError")) - } - - - @Suppress("unused") - private class TestClass { - val i = 2 - var large: Any? = null - private val s = "test" - } - -} \ No newline at end of file From 71a329ba12f58714af0d0255dd317fc6d3895b49 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 28 Jul 2024 12:01:40 +0200 Subject: [PATCH 60/92] [CI] Update test workflow (#158) * Update build kdoc, use CodeQL default setup (remove CodeQL workflow) * Test workflow: activate caching, submit dependency graph, update tasks apps --- .github/workflows/build-kdoc.yml | 4 +- .github/workflows/codeql.yml | 66 -------------------------------- .github/workflows/test-dev.yml | 56 ++++++++++++++++----------- 3 files changed, 35 insertions(+), 91 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index 3dc61edd..14924e86 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -21,7 +21,7 @@ jobs: - uses: gradle/actions/setup-gradle@v3 - name: Build KDoc - run: ./gradlew --no-daemon --no-configuration-cache ical4android:dokkaHtml + run: ./gradlew ical4android:dokkaHtml - uses: actions/upload-pages-artifact@v3 with: @@ -36,4 +36,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 354c6656..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,66 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '39 11 * * 0' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - # - name: Autobuild - # uses: github/codeql-action/autobuild@v2 - - - name: Build - run: ./gradlew --no-daemon assemble - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index dc86c7c9..c1984e7c 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -1,7 +1,28 @@ name: Development tests on: push jobs: + compile: + name: Compile and cache + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + # See https://community.gradle.org/github-actions/docs/setup-gradle/ for more information + - uses: gradle/actions/setup-gradle@v3 # creates build cache when on main branch + with: + cache-encryption-key: ${{ secrets.gradle_encryption_key }} + gradle-home-cache-cleanup: true # clean up unused files + dependency-graph: generate-and-submit # submit Github Dependency Graph info + dependency-graph-continue-on-failure: false + + - run: ./gradlew --build-cache --configuration-cache assembleDebug + test: + needs: compile name: Tests without emulator runs-on: ubuntu-latest steps: @@ -11,19 +32,15 @@ jobs: distribution: temurin java-version: 17 - uses: gradle/actions/setup-gradle@v3 - - - name: Check - run: ./gradlew --no-daemon check - - name: Archive results - uses: actions/upload-artifact@v4 with: - name: test-results - path: | - build/outputs/lint* - build/reports - overwrite: true + cache-encryption-key: ${{ secrets.gradle_encryption_key }} + cache-read-only: true + + - name: Run lint and unit tests + run: ./gradlew --build-cache --configuration-cache lintDebug testDebugUnitTest test_on_emulator: + needs: compile name: Tests with emulator runs-on: ubuntu-latest strategy: @@ -31,13 +48,14 @@ jobs: api-level: [ 31 ] steps: - uses: actions/checkout@v4 - with: - submodules: true - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - uses: gradle/actions/setup-gradle@v3 + with: + cache-encryption-key: ${{ secrets.gradle_encryption_key }} + cache-read-only: true - name: Enable KVM group perms run: | @@ -69,8 +87,8 @@ jobs: - name: Install task apps and run tests uses: reactivecircus/android-emulator-runner@v2 env: - version_at_techbee_jtx: v2.7.6 - version_org_tasks: 130804 + version_at_techbee_jtx: v2.9.0 + version_org_tasks: 131104 version_org_dmfs_tasks: 82200 with: api-level: ${{ matrix.api-level }} @@ -84,12 +102,4 @@ jobs: (wget -cq -O org.tasks.apk https://f-droid.org/repo/org.tasks_${{ env.version_org_tasks }}.apk || wget -cq -O org.tasks.apk https://f-droid.org/archive/org.tasks_${{ env.version_org_tasks }}.apk) && adb install org.tasks.apk (wget -cq -O at.techbee.jtx.apk https://github.com/TechbeeAT/jtxBoard/releases/download/${{ env.version_at_techbee_jtx }}/jtxBoard-${{ env.version_at_techbee_jtx }}.apk) && adb install at.techbee.jtx.apk cd .. - ./gradlew --no-daemon connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest - - - name: Archive results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: app/build/reports - overwrite: true + ./gradlew --build-cache --configuration-cache connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest From c9c528ccb6ff9f9db2397974711e1f356975f7d8 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 28 Jul 2024 12:08:55 +0200 Subject: [PATCH 61/92] Fix dokka task --- .github/workflows/build-kdoc.yml | 4 ++-- gradle/libs.versions.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index 14924e86..c3de0cda 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -21,7 +21,7 @@ jobs: - uses: gradle/actions/setup-gradle@v3 - name: Build KDoc - run: ./gradlew ical4android:dokkaHtml + run: ./gradlew --no-configuration-cache dokkaHtml - uses: actions/upload-pages-artifact@v3 with: @@ -36,4 +36,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fcd99910..8b4ec30e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ dokka ="1.9.20" ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" kotlin = "2.0.0" -mockk = "1.13.11" +mockk = "1.13.12" slf4j = "2.0.13" [libraries] From a36ee480aa4008042a0c2471fedf81d34bd04988 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 28 Jul 2024 15:56:40 +0200 Subject: [PATCH 62/92] Replace deprecated logger calls (#159) --- .../bitfire/ical4android/AbstractTasksTest.kt | 3 +- .../bitfire/ical4android/AndroidEventTest.kt | 9 ++- .../bitfire/ical4android/AndroidCalendar.kt | 28 +++++--- .../AndroidCompatTimeZoneRegistry.kt | 6 +- .../at/bitfire/ical4android/AndroidEvent.kt | 48 +++++++------- .../at/bitfire/ical4android/BatchOperation.kt | 19 +++--- .../at/bitfire/ical4android/Css3Color.kt | 8 ++- .../at/bitfire/ical4android/DmfsTask.kt | 31 +++++---- .../at/bitfire/ical4android/DmfsTaskList.kt | 12 ++-- .../kotlin/at/bitfire/ical4android/Event.kt | 17 +++-- .../at/bitfire/ical4android/ICalendar.kt | 33 ++++------ .../at/bitfire/ical4android/Ical4Android.kt | 7 +- .../at/bitfire/ical4android/JtxCollection.kt | 22 ++++--- .../at/bitfire/ical4android/JtxICalObject.kt | 66 ++++++++++++++----- .../kotlin/at/bitfire/ical4android/Task.kt | 16 +++-- .../at/bitfire/ical4android/TaskProvider.kt | 10 ++- .../ical4android/util/AndroidTimeUtils.kt | 10 ++- .../at/bitfire/ical4android/util/DateUtils.kt | 6 +- .../ical4android/validation/EventValidator.kt | 14 ++-- .../FixInvalidUtcOffsetPreprocessor.kt | 7 +- .../validation/ICalPreprocessor.kt | 5 +- .../main/kotlin/at/techbee/jtx/JtxContract.kt | 9 ++- 22 files changed, 237 insertions(+), 149 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt index e77b7f90..cf21bda8 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt @@ -12,6 +12,7 @@ import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith import org.junit.runners.Parameterized +import java.util.logging.Logger @RunWith(Parameterized::class) @@ -38,7 +39,7 @@ abstract class AbstractTasksTest( Assume.assumeNotNull(providerOrNull) // will halt here if providerOrNull is null provider = providerOrNull!! - Ical4Android.log.fine("Using task provider: $provider") + Logger.getLogger(javaClass.name).fine("Using task provider: $provider") } @After diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index ee56a7e3..dfd8c060 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -60,6 +60,7 @@ import org.junit.Test import java.net.URI import java.time.Duration import java.time.Period +import java.util.logging.Logger class AndroidEventTest { @@ -88,6 +89,8 @@ class AndroidEventTest { } + private val logger = Logger.getLogger(javaClass.name) + private val testAccount = Account("ical4android@example.com", ACCOUNT_TYPE_LOCAL) private val tzVienna = DateUtils.ical4jTimeZone("Europe/Vienna")!! @@ -1466,7 +1469,7 @@ class AndroidEventTest { values.put(Events.EVENT_END_TIMEZONE, "Europe/Berlin") } valuesBuilder(values) - Ical4Android.log.info("Inserting test event: $values") + logger.info("Inserting test event: $values") val uri = provider.insert( if (asSyncAdapter) Events.CONTENT_URI.asSyncAdapter(testAccount) @@ -1903,7 +1906,7 @@ class AndroidEventTest { val reminderValues = ContentValues() reminderValues.put(Reminders.EVENT_ID, id) builder(reminderValues) - Ical4Android.log.info("Inserting test reminder: $reminderValues") + logger.info("Inserting test reminder: $reminderValues") provider.insert(Reminders.CONTENT_URI.asSyncAdapter(testAccount), reminderValues) }).let { result -> return result.alarms.firstOrNull() @@ -1980,7 +1983,7 @@ class AndroidEventTest { val attendeeValues = ContentValues() attendeeValues.put(Attendees.EVENT_ID, id) builder(attendeeValues) - Ical4Android.log.info("Inserting test attendee: $attendeeValues") + logger.info("Inserting test attendee: $attendeeValues") provider.insert(Attendees.CONTENT_URI.asSyncAdapter(testAccount), attendeeValues) }).let { result -> return result.attendees.firstOrNull() diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt index 527efc3a..b68dcbea 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt @@ -9,13 +9,19 @@ import android.content.ContentProviderClient import android.content.ContentUris import android.content.ContentValues import android.net.Uri -import android.provider.CalendarContract.* +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.CalendarEntity +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Colors +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.Reminders import androidx.annotation.CallSuper import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.toValues import java.io.FileNotFoundException import java.util.LinkedList import java.util.logging.Level +import java.util.logging.Logger /** * Represents a locally stored calendar, containing [AndroidEvent]s (whose data objects are [Event]s). @@ -32,7 +38,10 @@ abstract class AndroidCalendar( ) { companion object { - + + private val logger + get() = Logger.getLogger(AndroidCalendar::class.java.name) + /** * Recommended initial values when creating Android [Calendars]. */ @@ -59,7 +68,7 @@ abstract class AndroidCalendar( info.putAll(calendarBaseValues) - Ical4Android.log.log(Level.FINE, "Creating local calendar", info) + logger.log(Level.FINE, "Creating local calendar", info) return provider.insert(Calendars.CONTENT_URI.asSyncAdapter(account), info) ?: throw Exception("Couldn't create calendar: provider returned null") } @@ -71,7 +80,7 @@ abstract class AndroidCalendar( return } - Ical4Android.log.info("Inserting event colors for account $account") + logger.info("Inserting event colors for account $account") val values = ContentValues(5) values.put(Colors.ACCOUNT_NAME, account.name) values.put(Colors.ACCOUNT_TYPE, account.type) @@ -82,13 +91,13 @@ abstract class AndroidCalendar( try { provider.insert(Colors.CONTENT_URI.asSyncAdapter(account), values) } catch(e: Exception) { - Ical4Android.log.log(Level.WARNING, "Couldn't insert event color: ${color.name}", e) + logger.log(Level.WARNING, "Couldn't insert event color: ${color.name}", e) } } } fun removeColors(provider: ContentProviderClient, account: Account) { - Ical4Android.log.info("Removing event colors from account $account") + logger.info("Removing event colors from account $account") // unassign colors from events /* ANDROID STRANGENESS: @@ -147,6 +156,7 @@ abstract class AndroidCalendar( } + var name: String? = null var displayName: String? = null var color: Int? = null @@ -180,12 +190,12 @@ abstract class AndroidCalendar( fun update(info: ContentValues): Int { - Ical4Android.log.log(Level.FINE, "Updating local calendar (#$id)", info) + logger.log(Level.FINE, "Updating local calendar (#$id)", info) return provider.update(calendarSyncURI(), info, null, null) } fun delete(): Int { - Ical4Android.log.log(Level.FINE, "Deleting local calendar (#$id)") + logger.log(Level.FINE, "Deleting local calendar (#$id)") return provider.delete(calendarSyncURI(), null, null) } @@ -215,4 +225,4 @@ abstract class AndroidCalendar( fun calendarSyncURI() = ContentUris.withAppendedId(Calendars.CONTENT_URI, id).asSyncAdapter(account) -} +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt index f22ad7c2..1f4dac92 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt @@ -9,6 +9,7 @@ import net.fortuna.ical4j.model.TimeZoneRegistryImpl import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.TzId import java.time.ZoneId +import java.util.logging.Logger /** * Wrapper around default [TimeZoneRegistry] that uses the Android name if a time zone has a @@ -26,6 +27,9 @@ class AndroidCompatTimeZoneRegistry( private val base: TimeZoneRegistry ): TimeZoneRegistry by base { + private val logger + get() = Logger.getLogger(javaClass.name) + /** * Gets the time zone for a given ID. * @@ -63,7 +67,7 @@ class AndroidCompatTimeZoneRegistry( but most Android devices don't now Europe/Kyiv yet. */ if (tz.id != androidTzId) { - Ical4Android.log.warning("Using Android TZID $androidTzId instead of ical4j ${tz.id}") + logger.warning("Using Android TZID $androidTzId instead of ical4j ${tz.id}") // create a copy of the VTIMEZONE so that we don't modify the original registry values (which are not immutable) val vTimeZone = tz.vTimeZone diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 2d1bb376..9905cc13 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -68,6 +68,7 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.util.Locale import java.util.logging.Level +import java.util.logging.Logger /** * Stores and retrieves VEVENT iCalendar objects (represented as [Event]s) to/from the @@ -111,8 +112,11 @@ abstract class AndroidEvent( * The URL is directly put into [ExtendedProperties.VALUE]. */ const val EXTNAME_URL = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.ical4android.url" + } + protected val logger: Logger by lazy { Logger.getLogger(AndroidEvent::class.java.name) } + var id: Long? = null protected set @@ -206,7 +210,7 @@ abstract class AndroidEvent( @Suppress("UNUSED_VALUE") @CallSuper protected open fun populateEvent(row: ContentValues, groupScheduled: Boolean) { - Ical4Android.log.log(Level.FINE, "Read event entity from calender provider", row) + logger.log(Level.FINE, "Read event entity from calender provider", row) val event = requireNotNull(event) row.getAsString(Events.MUTATORS)?.let { strPackages -> @@ -244,10 +248,10 @@ abstract class AndroidEvent( if (tsEnd != null) { when { tsEnd < tsStart -> - Ical4Android.log.warning("dtEnd $tsEnd (allDay) < dtStart $tsStart (allDay), ignoring") + logger.warning("dtEnd $tsEnd (allDay) < dtStart $tsStart (allDay), ignoring") tsEnd == tsStart -> - Ical4Android.log.fine("dtEnd $tsEnd (allDay) = dtStart, won't generate DTEND property") + logger.fine("dtEnd $tsEnd (allDay) = dtStart, won't generate DTEND property") else /* tsEnd > tsStart */ -> event.dtEnd = DtEnd(Date(tsEnd)) @@ -284,9 +288,9 @@ abstract class AndroidEvent( if (tsEnd != null) { if (tsEnd < tsStart) - Ical4Android.log.warning("dtEnd $tsEnd < dtStart $tsStart, ignoring") + logger.warning("dtEnd $tsEnd < dtStart $tsStart, ignoring") /*else if (tsEnd == tsStart) // iCloud sends 404 when it receives an iCalendar with DTSTART but without DTEND - Ical4Android.log.fine("dtEnd $tsEnd == dtStart, won't generate DTEND property")*/ + logger.fine("dtEnd $tsEnd == dtStart, won't generate DTEND property")*/ else /* tsEnd > tsStart */ { val endTz = row.getAsString(Events.EVENT_END_TIMEZONE)?.let { tzId -> DateUtils.ical4jTimeZone(tzId) @@ -324,7 +328,7 @@ abstract class AndroidEvent( event.exDates += exDate } } catch (e: Exception) { - Ical4Android.log.log(Level.WARNING, "Couldn't parse recurrence rules, ignoring", e) + logger.log(Level.WARNING, "Couldn't parse recurrence rules, ignoring", e) } event.uid = row.getAsString(Events.UID_2445) @@ -336,7 +340,7 @@ abstract class AndroidEvent( try { event.color = Css3Color.valueOf(name) } catch (e: IllegalArgumentException) { - Ical4Android.log.warning("Ignoring unknown color $name from Calendar Provider") + logger.warning("Ignoring unknown color $name from Calendar Provider") } } @@ -357,7 +361,7 @@ abstract class AndroidEvent( try { event.organizer = Organizer(URI("mailto", row.getAsString(Events.ORGANIZER), null)) } catch (e: URISyntaxException) { - Ical4Android.log.log(Level.WARNING, "Error when creating ORGANIZER mailto URI, ignoring", e) + logger.log(Level.WARNING, "Error when creating ORGANIZER mailto URI, ignoring", e) } } @@ -389,7 +393,7 @@ abstract class AndroidEvent( } protected open fun populateAttendee(row: ContentValues, isOrganizer: Boolean) { - Ical4Android.log.log(Level.FINE, "Read event attendee from calender provider", row) + logger.log(Level.FINE, "Read event attendee from calender provider", row) try { val attendee: Attendee @@ -425,12 +429,12 @@ abstract class AndroidEvent( event!!.attendees.add(attendee) } catch (e: URISyntaxException) { - Ical4Android.log.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e) + logger.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e) } } protected open fun populateReminder(row: ContentValues) { - Ical4Android.log.log(Level.FINE, "Read event reminder from calender provider", row) + logger.log(Level.FINE, "Read event reminder from calender provider", row) val event = requireNotNull(event) val alarm = VAlarm(Duration.ofMinutes(-row.getAsLong(Reminders.MINUTES))) @@ -448,7 +452,7 @@ abstract class AndroidEvent( // account name (should be account owner's email address) props += Attendee(URI("mailto", calendar.account.name, null)) } else { - Ical4Android.log.warning("Account name is not an email address; changing EMAIL reminder to DISPLAY") + logger.warning("Account name is not an email address; changing EMAIL reminder to DISPLAY") props += Action.DISPLAY props += Description(event.summary) } @@ -466,7 +470,7 @@ abstract class AndroidEvent( protected open fun populateExtended(row: ContentValues) { val name = row.getAsString(ExtendedProperties.NAME) val rawValue = row.getAsString(ExtendedProperties.VALUE) - Ical4Android.log.log(Level.FINE, "Read extended property from calender provider", arrayOf(name, rawValue)) + logger.log(Level.FINE, "Read extended property from calender provider", arrayOf(name, rawValue)) val event = requireNotNull(event) try { @@ -478,7 +482,7 @@ abstract class AndroidEvent( try { event.url = URI(rawValue) } catch(e: URISyntaxException) { - Ical4Android.log.warning("Won't process invalid local URL: $rawValue") + logger.warning("Won't process invalid local URL: $rawValue") } EXTNAME_ICAL_UID -> @@ -490,7 +494,7 @@ abstract class AndroidEvent( event.unknownProperties += UnknownProperty.fromJsonString(rawValue) } } catch (e: Exception) { - Ical4Android.log.log(Level.WARNING, "Couldn't parse extended property", e) + logger.log(Level.WARNING, "Couldn't parse extended property", e) } } @@ -532,7 +536,7 @@ abstract class AndroidEvent( event.exceptions += exceptionEvent } } catch (e: Exception) { - Ical4Android.log.log(Level.WARNING, "Couldn't find exception details", e) + logger.log(Level.WARNING, "Couldn't find exception details", e) } } } @@ -630,7 +634,7 @@ abstract class AndroidEvent( val recurrenceId = exception.recurrenceId if (recurrenceId == null) { - Ical4Android.log.warning("Ignoring exception of event ${event.uid} without recurrenceId") + logger.warning("Ignoring exception of event ${event.uid} without recurrenceId") continue } @@ -849,7 +853,7 @@ abstract class AndroidEvent( } if (infiniteRrule) - Ical4Android.log.warning("Android can't handle infinite RRULE + RDATE [https://issuetracker.google.com/issues/216374004]; ignoring RDATE(s)") + logger.warning("Android can't handle infinite RRULE + RDATE [https://issuetracker.google.com/issues/216374004]; ignoring RDATE(s)") else { for (rDate in event.rDates) AndroidTimeUtils.androidifyTimeZone(rDate) @@ -932,7 +936,7 @@ abstract class AndroidEvent( if (cursor.moveToNext()) return@let colorName else - Ical4Android.log.fine("Ignoring event color: $colorName (not available for this account)") + logger.fine("Ignoring event color: $colorName (not available for this account)") } null }) @@ -949,7 +953,7 @@ abstract class AndroidEvent( organizer.getParameter(Parameter.EMAIL)?.value if (email != null) return@let email - Ical4Android.log.warning("Ignoring ORGANIZER without email address (not supported by Android)") + logger.warning("Ignoring ORGANIZER without email address (not supported by Android)") null } ?: calendar.ownerAccount) @@ -1054,11 +1058,11 @@ abstract class AndroidEvent( protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int?, property: Property) { if (property.value == null) { - Ical4Android.log.warning("Ignoring unknown property with null value") + logger.warning("Ignoring unknown property with null value") return } if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) { - Ical4Android.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)") + logger.warning("Ignoring unknown property with ${property.value.length} octets (too long)") return } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt index b0f7ef93..a8c983f9 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt @@ -14,10 +14,13 @@ import android.os.RemoteException import android.os.TransactionTooLargeException import java.util.LinkedList import java.util.logging.Level +import java.util.logging.Logger class BatchOperation( - private val providerClient: ContentProviderClient + private val providerClient: ContentProviderClient ) { + + private val logger = Logger.getLogger(javaClass.name) private val queue = LinkedList() private var results = arrayOfNulls(0) @@ -47,10 +50,10 @@ class BatchOperation( fun commit(): Int { var affected = 0 if (!queue.isEmpty()) { - if (Ical4Android.log.isLoggable(Level.FINE)) { - Ical4Android.log.log(Level.FINE, "Committing ${queue.size} operations:") + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Committing ${queue.size} operations:") for ((idx, op) in queue.withIndex()) - Ical4Android.log.log(Level.FINE, "#$idx: ${op.build()}") + logger.log(Level.FINE, "#$idx: ${op.build()}") } results = arrayOfNulls(queue.size) @@ -61,7 +64,7 @@ class BatchOperation( result.count != null -> affected += result.count ?: 0 result.uri != null -> affected += 1 } - Ical4Android.log.fine("… $affected record(s) affected") + logger.fine("… $affected record(s) affected") } queue.clear() @@ -93,12 +96,12 @@ class BatchOperation( try { val ops = toCPO(start, end) - Ical4Android.log.fine("Running ${ops.size} operations ($start .. ${end - 1})") + logger.fine("Running ${ops.size} operations ($start .. ${end - 1})") val partResults = providerClient.applyBatch(ops) val n = end - start if (partResults.size != n) - Ical4Android.log.warning("Batch operation returned only ${partResults.size} instead of $n results") + logger.warning("Batch operation returned only ${partResults.size} instead of $n results") System.arraycopy(partResults, 0, results, start, partResults.size) @@ -113,7 +116,7 @@ class BatchOperation( // only one operation, can't be split throw CalendarStorageException("Can't transfer data to content provider (too large data row can't be split)", e) - Ical4Android.log.warning("Transaction too large, splitting (losing atomicity)") + logger.warning("Transaction too large, splitting (losing atomicity)") val mid = start + (end - start)/2 runBatch(start, mid) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt index 8a5443f0..a69db983 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt @@ -5,6 +5,7 @@ package at.bitfire.ical4android import android.graphics.Color +import java.util.logging.Logger import kotlin.math.sqrt /** @@ -166,6 +167,9 @@ enum class Css3Color(val argb: Int) { companion object { + private val logger + get() = Logger.getLogger(Css3Color::javaClass.name) + /** * Parses the given color either as CSS3 color name or as (A)RGB hex value. * @@ -177,7 +181,7 @@ enum class Css3Color(val argb: Int) { try { Color.parseColor(color) } catch(e: Exception) { - Ical4Android.log.warning("Invalid color value: $color") + logger.warning("Invalid color value: $color") null } @@ -191,7 +195,7 @@ enum class Css3Color(val argb: Int) { try { valueOf(name.lowercase()) } catch (e: IllegalArgumentException) { - Ical4Android.log.warning("Invalid color name: $name") + logger.warning("Invalid color name: $name") null } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 58dc7bd4..42b1e9e8 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -50,6 +50,7 @@ import java.net.URISyntaxException import java.time.ZoneId import java.util.Locale import java.util.logging.Level +import java.util.logging.Logger /** * Stores and retrieves VTODO iCalendar objects (represented as [Task]s) to/from the @@ -70,6 +71,8 @@ abstract class DmfsTask( val utcTimeZone by lazy { DateUtils.ical4jTimeZone(TimeZones.UTC_ID) } } + protected val logger = Logger.getLogger(javaClass.name) + var id: Long? = null @@ -104,7 +107,7 @@ abstract class DmfsTask( field = newTask val values = cursor.toValues(true) - Ical4Android.log.log(Level.FINER, "Found task", values) + logger.log(Level.FINER, "Found task", values) populateTask(values) if (values.containsKey(Properties.PROPERTY_ID)) { @@ -165,7 +168,7 @@ abstract class DmfsTask( try { task.geoPosition = Geo(lat.toBigDecimal(), lng.toBigDecimal()) } catch (e: NumberFormatException) { - Ical4Android.log.warning("Invalid GEO value: $geo") + logger.warning("Invalid GEO value: $geo") } } @@ -177,7 +180,7 @@ abstract class DmfsTask( try { task.organizer = Organizer("mailto:$it") } catch(e: URISyntaxException) { - Ical4Android.log.log(Level.WARNING, "Invalid ORGANIZER email", e) + logger.log(Level.WARNING, "Invalid ORGANIZER email", e) } } @@ -256,7 +259,7 @@ abstract class DmfsTask( } protected open fun populateProperty(row: ContentValues) { - Ical4Android.log.log(Level.FINER, "Found property", row) + logger.log(Level.FINER, "Found property", row) val task = requireNotNull(task) when (val type = row.getAsString(Properties.MIMETYPE)) { @@ -271,7 +274,7 @@ abstract class DmfsTask( UnknownProperty.CONTENT_ITEM_TYPE -> task.unknownProperties += UnknownProperty.fromJsonString(row.getAsString(UNKNOWN_PROPERTY_DATA)) else -> - Ical4Android.log.warning("Found unknown property of type $type") + logger.warning("Found unknown property of type $type") } } @@ -306,7 +309,7 @@ abstract class DmfsTask( protected open fun populateRelatedTo(row: ContentValues) { val uid = row.getAsString(Relation.RELATED_UID) if (uid == null) { - Ical4Android.log.warning("Task relation doesn't refer to same task list; can't be synchronized") + logger.warning("Task relation doesn't refer to same task list; can't be synchronized") return } @@ -406,7 +409,7 @@ abstract class DmfsTask( .withValue(Alarm.MESSAGE, alarm.description?.value ?: alarm.summary) .withValue(Alarm.ALARM_TYPE, alarmType) - Ical4Android.log.log(Level.FINE, "Inserting alarm", builder.build()) + logger.log(Level.FINE, "Inserting alarm", builder.build()) batch.enqueue(builder) } } @@ -417,7 +420,7 @@ abstract class DmfsTask( .withTaskId(Category.TASK_ID, idxTask) .withValue(Category.MIMETYPE, Category.CONTENT_ITEM_TYPE) .withValue(Category.CATEGORY_NAME, category) - Ical4Android.log.log(Level.FINE, "Inserting category", builder.build()) + logger.log(Level.FINE, "Inserting category", builder.build()) batch.enqueue(builder) } } @@ -428,7 +431,7 @@ abstract class DmfsTask( .withTaskId(Comment.TASK_ID, idxTask) .withValue(Comment.MIMETYPE, Comment.CONTENT_ITEM_TYPE) .withValue(Comment.COMMENT, comment) - Ical4Android.log.log(Level.FINE, "Inserting comment", builder.build()) + logger.log(Level.FINE, "Inserting comment", builder.build()) batch.enqueue(builder) } @@ -447,7 +450,7 @@ abstract class DmfsTask( .withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE) .withValue(Relation.RELATED_UID, relatedTo.value) .withValue(Relation.RELATED_TYPE, relType) - Ical4Android.log.log(Level.FINE, "Inserting relation", builder.build()) + logger.log(Level.FINE, "Inserting relation", builder.build()) batch.enqueue(builder) } } @@ -455,7 +458,7 @@ abstract class DmfsTask( protected open fun insertUnknownProperties(batch: BatchOperation, idxTask: Int?) { for (property in requireNotNull(task).unknownProperties) { if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) { - Ical4Android.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)") + logger.warning("Ignoring unknown property with ${property.value.length} octets (too long)") return } @@ -463,7 +466,7 @@ abstract class DmfsTask( .withTaskId(Properties.TASK_ID, idxTask) .withValue(Properties.MIMETYPE, UnknownProperty.CONTENT_ITEM_TYPE) .withValue(UNKNOWN_PROPERTY_DATA, UnknownProperty.toJsonString(property)) - Ical4Android.log.log(Level.FINE, "Inserting unknown property", builder.build()) + logger.log(Level.FINE, "Inserting unknown property", builder.build()) batch.enqueue(builder) } } @@ -500,7 +503,7 @@ abstract class DmfsTask( if (email != null) builder.withValue(Tasks.ORGANIZER, email) else - Ical4Android.log.warning("Ignoring ORGANIZER without email address (not supported by Android)") + logger.warning("Ignoring ORGANIZER without email address (not supported by Android)") } builder .withValue(Tasks.PRIORITY, task.priority) @@ -554,7 +557,7 @@ abstract class DmfsTask( null else AndroidTimeUtils.recurrenceSetsToOpenTasksString(task.exDates, if (allDay) null else getTimeZone())) - Ical4Android.log.log(Level.FINE, "Built task object", builder.build()) + logger.log(Level.FINE, "Built task object", builder.build()) } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index 6d8efd67..bd13f96c 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -18,6 +18,7 @@ import org.dmfs.tasks.contract.TaskContract.Tasks import java.io.FileNotFoundException import java.util.LinkedList import java.util.logging.Level +import java.util.logging.Logger /** @@ -33,11 +34,14 @@ abstract class DmfsTaskList( companion object { + private val logger + get() = Logger.getLogger(DmfsTaskList::class.java.name) + fun create(account: Account, provider: TaskProvider, info: ContentValues): Uri { info.put(TaskContract.ACCOUNT_NAME, account.name) info.put(TaskContract.ACCOUNT_TYPE, account.type) - Ical4Android.log.log(Level.FINE, "Creating ${provider.name.authority} task list", info) + logger.log(Level.FINE, "Creating ${provider.name.authority} task list", info) return provider.client.insert(provider.taskListsUri().asSyncAdapter(account), info) ?: throw CalendarStorageException("Couldn't create task list (empty result from provider)") } @@ -118,12 +122,12 @@ abstract class DmfsTaskList( } fun update(info: ContentValues): Int { - Ical4Android.log.log(Level.FINE, "Updating ${provider.name.authority} task list (#$id)", info) + logger.log(Level.FINE, "Updating ${provider.name.authority} task list (#$id)", info) return provider.client.update(taskListSyncUri(), info, null, null) } fun delete(): Int { - Ical4Android.log.log(Level.FINE, "Deleting ${provider.name.authority} task list (#$id)") + logger.log(Level.FINE, "Deleting ${provider.name.authority} task list (#$id)") return provider.client.delete(taskListSyncUri(), null, null) } @@ -148,7 +152,7 @@ abstract class DmfsTaskList( * @return number of touched [Relation] rows */ fun touchRelations(): Int { - Ical4Android.log.fine("Touching relations to set parent_id") + logger.fine("Touching relations to set parent_id") val batchOperation = BatchOperation(provider.client) provider.client.query( tasksSyncUri(true), null, diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index b326e907..6e97f02e 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -49,6 +49,7 @@ import java.io.Reader import java.net.URI import java.util.LinkedList import java.util.UUID +import java.util.logging.Logger data class Event( override var uid: String? = null, @@ -94,6 +95,10 @@ data class Event( ) : ICalendar() { companion object { + + private val logger + get() = Logger.getLogger(Event::class.java.name) + /** * Parses an iCalendar resource, applies [at.bitfire.ical4android.validation.ICalPreprocessor] * and [EventValidator] to increase compatibility and extracts the VEVENTs. @@ -119,11 +124,11 @@ data class Event( for (vEvent in vEvents) if (vEvent.uid == null) { val uid = Uid(UUID.randomUUID().toString()) - Ical4Android.log.warning("Found VEVENT without UID, using a random one: ${uid.value}") + logger.warning("Found VEVENT without UID, using a random one: ${uid.value}") vEvent.properties += uid } - Ical4Android.log.fine("Assigning exceptions to main events") + logger.fine("Assigning exceptions to main events") val mainEvents = mutableMapOf() val exceptions = mutableMapOf>() for (vEvent in vEvents) { @@ -175,7 +180,7 @@ data class Event( } for ((uid, onlyExceptions) in exceptions) { - Ical4Android.log.info("UID $uid doesn't have a main event but only exceptions: $onlyExceptions") + logger.info("UID $uid doesn't have a main event but only exceptions: $onlyExceptions") // create a fake main event from the first exception val fakeEvent = fromVEvent(onlyExceptions.values.first()) @@ -265,7 +270,7 @@ data class Event( val recurrenceId = exception.recurrenceId if (recurrenceId == null) { - Ical4Android.log.warning("Ignoring exception without recurrenceId") + logger.warning("Ignoring exception without recurrenceId") continue } @@ -274,13 +279,13 @@ data class Event( strict in what we send (and servers may reject such a case). */ if (isDateTime(recurrenceId) != isDateTime(dtStart)) { - Ical4Android.log.warning("Ignoring exception $recurrenceId with other date type than dtStart: $dtStart") + logger.warning("Ignoring exception $recurrenceId with other date type than dtStart: $dtStart") continue } // for simplicity and compatibility, rewrite date-time exceptions to the same time zone as DTSTART if (isDateTime(recurrenceId) && recurrenceId.timeZone != dtStart.timeZone) { - Ical4Android.log.fine("Changing timezone of $recurrenceId to same time zone as dtStart: $dtStart") + logger.fine("Changing timezone of $recurrenceId to same time zone as dtStart: $dtStart") recurrenceId.timeZone = dtStart.timeZone } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index 2ca93788..6d1772e7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -5,7 +5,10 @@ package at.bitfire.ical4android import at.bitfire.ical4android.validation.ICalPreprocessor -import net.fortuna.ical4j.data.* +import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.data.CalendarParserFactory +import net.fortuna.ical4j.data.ContentHandlerContext +import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Date @@ -45,18 +48,8 @@ open class ICalendar { companion object { - // static ical4j initialization - init { - // reduce verbosity of various ical4j loggers - org.slf4j.LoggerFactory.getLogger(net.fortuna.ical4j.data.CalendarParserImpl::class.java) - Logger.getLogger(net.fortuna.ical4j.data.CalendarParserImpl::class.java.name).level = Level.CONFIG - - org.slf4j.LoggerFactory.getLogger(net.fortuna.ical4j.model.Recur::class.java) - Logger.getLogger(net.fortuna.ical4j.model.Recur::class.java.name).level = Level.CONFIG - - org.slf4j.LoggerFactory.getLogger(net.fortuna.ical4j.data.FoldingWriter::class.java) - Logger.getLogger(net.fortuna.ical4j.data.FoldingWriter::class.java.name).level = Level.CONFIG - } + private val logger + get() = Logger.getLogger(ICalendar::class.java.name) // known iCalendar properties const val CALENDAR_NAME = "X-WR-CALNAME" @@ -88,7 +81,7 @@ open class ICalendar { */ fun fromReader(reader: Reader, properties: MutableMap? = null): Calendar { Ical4Android.checkThreadContextClassLoader() - Ical4Android.log.fine("Parsing iCalendar stream") + logger.fine("Parsing iCalendar stream") // preprocess stream to work around some problems that can't be fixed later val preprocessed = ICalPreprocessor.preprocessStream(reader) @@ -111,7 +104,7 @@ open class ICalendar { try { ICalPreprocessor.preprocessCalendar(calendar) } catch (e: Exception) { - Ical4Android.log.log(Level.WARNING, "Couldn't pre-process iCalendar", e) + logger.log(Level.WARNING, "Couldn't pre-process iCalendar", e) } // fill calendar properties @@ -221,7 +214,7 @@ open class ICalendar { newTz.validate() } catch (e: ValidationException) { // This should never happen! - Ical4Android.log.log(Level.WARNING, "Minified timezone is invalid, using original one", e) + logger.log(Level.WARNING, "Minified timezone is invalid, using original one", e) newTz = null } } @@ -243,7 +236,7 @@ open class ICalendar { val timezone = cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone? timezone?.timeZoneId?.let { return it.value } } catch (e: ParserException) { - Ical4Android.log.log(Level.SEVERE, "Can't understand time zone definition", e) + logger.log(Level.SEVERE, "Can't understand time zone definition", e) } return null } @@ -266,7 +259,7 @@ open class ICalendar { // debug build, re-throw ValidationException throw e else - Ical4Android.log.log(Level.WARNING, "iCalendar validation failed - This is only a warning!", e) + logger.log(Level.WARNING, "iCalendar validation failed - This is only a warning!", e) } } @@ -349,7 +342,7 @@ open class ICalendar { if (related == Related.END && !allowRelEnd) { if (duration == null) { - Ical4Android.log.warning("Event/task without duration; can't calculate END-related alarm") + logger.warning("Event/task without duration; can't calculate END-related alarm") return null } // move alarm towards end @@ -364,7 +357,7 @@ open class ICalendar { minutes = Duration.between(triggerTime.toInstant(), start.toInstant()).toMinutes().toInt() } else { - Ical4Android.log.log(Level.WARNING, "VALARM TRIGGER type is not DURATION or DATE-TIME (requires event DTSTART for Android), ignoring alarm", alarm) + logger.log(Level.WARNING, "VALARM TRIGGER type is not DURATION or DATE-TIME (requires event DTSTART for Android), ignoring alarm", alarm) return null } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt index 7df7e8b2..fdd8c695 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt @@ -4,20 +4,15 @@ package at.bitfire.ical4android -import java.util.logging.Logger - @Suppress("unused") object Ical4Android { const val ical4jVersion = BuildConfig.version_ical4j - @Deprecated("Use java.util.Logger.getLogger(javaClass.name) instead", ReplaceWith("Logger.getLogger(javaClass.name)", "java.util.logging.Logger")) - val log: Logger = Logger.getLogger("at.bitfire.ical4android") - fun checkThreadContextClassLoader() { if (Thread.currentThread().contextClassLoader == null) throw IllegalStateException("Thread.currentThread().contextClassLoader must be set for java.util.ServiceLoader (used by ical4j)") } -} +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index 3df4f26b..75a839e9 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -19,6 +19,7 @@ import net.fortuna.ical4j.model.component.VToDo import net.fortuna.ical4j.model.property.Version import java.util.LinkedList import java.util.logging.Level +import java.util.logging.Logger open class JtxCollection(val account: Account, val client: ContentProviderClient, @@ -27,8 +28,11 @@ open class JtxCollection(val account: Account, companion object { + private val logger + get() = Logger.getLogger(javaClass.name) + fun create(account: Account, client: ContentProviderClient, values: ContentValues): Uri { - Ical4Android.log.log(Level.FINE, "Creating jtx Board collection", values) + logger.log(Level.FINE, "Creating jtx Board collection", values) return client.insert(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), values) ?: throw CalendarStorageException("Couldn't create JTX Collection") } @@ -60,12 +64,12 @@ open class JtxCollection(val account: Account, fun delete() { - Ical4Android.log.log(Level.FINE, "Deleting jtx Board collection (#$id)") + logger.log(Level.FINE, "Deleting jtx Board collection (#$id)") client.delete(ContentUris.withAppendedId(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), id), null, null) } fun update(values: ContentValues) { - Ical4Android.log.log(Level.FINE, "Updating jtx Board collection (#$id)", values) + logger.log(Level.FINE, "Updating jtx Board collection (#$id)", values) client.update(ContentUris.withAppendedId(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), id), values, null, null) } @@ -108,7 +112,7 @@ open class JtxCollection(val account: Account, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DELETED} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NULL", arrayOf(id.toString(), "1"), null ).use { cursor -> - Ical4Android.log.fine("findDeleted: found ${cursor?.count} deleted records in ${account.name}") + logger.fine("findDeleted: found ${cursor?.count} deleted records in ${account.name}") while (cursor?.moveToNext() == true) { values.add(cursor.toValues()) } @@ -128,7 +132,7 @@ open class JtxCollection(val account: Account, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.DIRTY} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NULL", arrayOf(id.toString(), "1"), null ).use { cursor -> - Ical4Android.log.fine("findDirty: found ${cursor?.count} dirty records in ${account.name}") + logger.fine("findDirty: found ${cursor?.count} dirty records in ${account.name}") while (cursor?.moveToNext() == true) { values.add(cursor.toValues()) } @@ -147,7 +151,7 @@ open class JtxCollection(val account: Account, "${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ? AND ${JtxContract.JtxICalObject.FILENAME} = ? AND ${JtxContract.JtxICalObject.RECURID} IS NULL", arrayOf(id.toString(), filename), null ).use { cursor -> - Ical4Android.log.fine("queryByFilename: found ${cursor?.count} records in ${account.name}") + logger.fine("queryByFilename: found ${cursor?.count} records in ${account.name}") if (cursor?.count != 1) return null cursor.moveToFirst() @@ -162,7 +166,7 @@ open class JtxCollection(val account: Account, */ fun queryByUID(uid: String): ContentValues? { client.query(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, "${JtxContract.JtxICalObject.UID} = ?", arrayOf(uid), null).use { cursor -> - Ical4Android.log.fine("queryByUID: found ${cursor?.count} records in ${account.name}") + logger.fine("queryByUID: found ${cursor?.count} records in ${account.name}") if (cursor?.count != 1) return null cursor.moveToFirst() @@ -184,7 +188,7 @@ open class JtxCollection(val account: Account, arrayOf(uid, recurid, dtstart.toString()), null ).use { cursor -> - Ical4Android.log.fine("queryByUID: found ${cursor?.count} records in ${account.name}") + logger.fine("queryByUID: found ${cursor?.count} records in ${account.name}") if (cursor?.count != 1) return null cursor.moveToFirst() @@ -241,7 +245,7 @@ open class JtxCollection(val account: Account, arrayOf(id.toString(), "0"), null ).use { cursor -> - Ical4Android.log.fine("getICSForCollection: found ${cursor?.count} records in ${account.name}") + logger.fine("getICSForCollection: found ${cursor?.count} records in ${account.name}") val ical = Calendar() ical.properties += Version.VERSION_2_0 diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt index 64ba0f9e..6b2ef777 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt @@ -16,11 +16,37 @@ 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.ComponentList +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterList +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.TextList +import net.fortuna.ical4j.model.TimeZoneRegistryFactory 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.parameter.AltRep +import net.fortuna.ical4j.model.parameter.Cn +import net.fortuna.ical4j.model.parameter.CuType +import net.fortuna.ical4j.model.parameter.DelegatedFrom +import net.fortuna.ical4j.model.parameter.DelegatedTo +import net.fortuna.ical4j.model.parameter.Dir +import net.fortuna.ical4j.model.parameter.FmtType +import net.fortuna.ical4j.model.parameter.Language +import net.fortuna.ical4j.model.parameter.Member +import net.fortuna.ical4j.model.parameter.PartStat +import net.fortuna.ical4j.model.parameter.RelType +import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.parameter.Role +import net.fortuna.ical4j.model.parameter.Rsvp +import net.fortuna.ical4j.model.parameter.SentBy +import net.fortuna.ical4j.model.parameter.Value +import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.* import java.io.FileNotFoundException import java.io.IOException @@ -32,6 +58,7 @@ import java.time.format.DateTimeParseException import java.util.TimeZone import java.util.UUID import java.util.logging.Level +import java.util.logging.Logger open class JtxICalObject( val collection: JtxCollection @@ -200,6 +227,9 @@ open class JtxICalObject( companion object { + private val logger + get() = Logger.getLogger(JtxICalObject::class.java.name) + const val X_PROP_COMPLETEDTIMEZONE = "X-COMPLETEDTIMEZONE" const val X_PARAM_ATTACH_LABEL = "X-LABEL" // used for filename in KOrganizer const val X_PARAM_FILENAME = "FILENAME" // used for filename in GNOME Evolution @@ -328,12 +358,12 @@ open class JtxICalObject( 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 DtEnd -> logger.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.") + logger.warning("The property Completed is only supported for VTODO, this value is rejected.") } is Due -> { @@ -346,7 +376,7 @@ open class JtxICalObject( 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.") + logger.warning("The property Due is only supported for VTODO, this value is rejected.") } is Duration -> iCalObject.duration = prop.value @@ -365,7 +395,7 @@ open class JtxICalObject( 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.") + logger.warning("The property PercentComplete is only supported for VTODO, this value is rejected.") } is RRule -> iCalObject.rrule = prop.value @@ -526,7 +556,7 @@ open class JtxICalObject( else -> when(prop.name) { X_PROP_COMPLETEDTIMEZONE -> iCalObject.completedTimezone = prop.value X_PROP_XSTATUS -> iCalObject.xstatus = prop.value - X_PROP_GEOFENCE_RADIUS -> iCalObject.geofenceRadius = try { prop.value.toInt() } catch (e: NumberFormatException) { Ical4Android.log.warning("Wrong format for geofenceRadius: ${prop.value}"); null } + X_PROP_GEOFENCE_RADIUS -> iCalObject.geofenceRadius = try { prop.value.toInt() } catch (e: NumberFormatException) { logger.warning("Wrong format for geofenceRadius: ${prop.value}"); null } else -> iCalObject.unknown.add(Unknown(value = UnknownProperty.toJsonString(prop))) // save the whole property for unknown properties } } @@ -539,20 +569,20 @@ open class JtxICalObject( 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") + logger.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") + logger.warning("DTSTART is DATE-TIME but DUE is DATE, rewriting DUE to DATE-TIME") iCalObject.dueTimezone = dtStartTZ } //previously due was dropped, now reduced to a warning, see also https://github.com/bitfireAT/ical4android/issues/70 if ( iCalObject.dtstart != null && iCalObject.due != null && iCalObject.due!! < iCalObject.dtstart!!) - Ical4Android.log.warning("Found invalid DUE < DTSTART") + logger.warning("Found invalid DUE < DTSTART") } if (iCalObject.duration != null && iCalObject.dtstart == null) { - Ical4Android.log.warning("Found DURATION without DTSTART; ignoring") + logger.warning("Found DURATION without DTSTART; ignoring") iCalObject.duration = null } } @@ -603,7 +633,7 @@ open class JtxICalObject( this.parameters.add(Related.END) } } catch (e: DateTimeParseException) { - Ical4Android.log.log(Level.WARNING, "Could not parse Trigger duration as Duration.", e) + logger.log(Level.WARNING, "Could not parse Trigger duration as Duration.", e) } }) @@ -626,7 +656,7 @@ open class JtxICalObject( } } } catch (e: ParseException) { - Ical4Android.log.log(Level.WARNING, "TriggerTime could not be parsed.", e) + logger.log(Level.WARNING, "TriggerTime could not be parsed.", e) }}) } alarm.summary?.let { add(Summary(it)) } @@ -636,7 +666,7 @@ open class JtxICalObject( 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) + logger.log(Level.WARNING, "Could not parse duration as Duration.", e) } }) } alarm.description?.let { add(Description(it)) } @@ -708,7 +738,7 @@ open class JtxICalObject( try { props += Url(URI(it)) } catch (e: URISyntaxException) { - Ical4Android.log.log(Level.WARNING, "Ignoring invalid task URL: $url", e) + logger.log(Level.WARNING, "Ignoring invalid task URL: $url", e) } } contact?.let { props += Contact(it) } @@ -861,11 +891,11 @@ open class JtxICalObject( } } } catch (e: FileNotFoundException) { - Ical4Android.log.log(Level.WARNING, "File not found at the given Uri: ${attachment.uri}", e) + logger.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) + logger.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) + logger.log(Level.WARNING, "Uri could not be parsed: ${attachment.uri}", e) } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 0bb70c27..2a1172ad 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -51,6 +51,7 @@ import java.net.URI import java.net.URISyntaxException import java.util.LinkedList import java.util.logging.Level +import java.util.logging.Logger data class Task( var createdAt: Long? = null, @@ -92,6 +93,9 @@ data class Task( companion object { + private val logger + get() = Logger.getLogger(Task::class.java.name) + /** * Parses an iCalendar resource, applies [at.bitfire.ical4android.validation.ICalPreprocessor] to increase compatibility * and extracts the VTODOs. @@ -116,7 +120,7 @@ data class Task( if (todo.uid != null) t.uid = todo.uid.value else { - Ical4Android.log.warning("Received VTODO without UID, generating new one") + logger.warning("Received VTODO without UID, generating new one") t.generateUID() } @@ -163,22 +167,22 @@ data class Task( if (dtStart != null && due != null) { if (DateUtils.isDate(dtStart) && DateUtils.isDateTime(due)) { - Ical4Android.log.warning("DTSTART is DATE but DUE is DATE-TIME, rewriting DTSTART to DATE-TIME") + logger.warning("DTSTART is DATE but DUE is DATE-TIME, rewriting DTSTART to DATE-TIME") t.dtStart = DtStart(DateTime(dtStart.value, due.timeZone)) } else if (DateUtils.isDateTime(dtStart) && DateUtils.isDate(due)) { - Ical4Android.log.warning("DTSTART is DATE-TIME but DUE is DATE, rewriting DUE to DATE-TIME") + logger.warning("DTSTART is DATE-TIME but DUE is DATE, rewriting DUE to DATE-TIME") t.due = Due(DateTime(due.value, dtStart.timeZone)) } if (due.date <= dtStart.date) { - Ical4Android.log.warning("Found invalid DUE <= DTSTART; dropping DTSTART") + logger.warning("Found invalid DUE <= DTSTART; dropping DTSTART") t.dtStart = null } } if (t.duration != null && t.dtStart == null) { - Ical4Android.log.warning("Found DURATION without DTSTART; ignoring") + logger.warning("Found DURATION without DTSTART; ignoring") t.duration = null } @@ -217,7 +221,7 @@ data class Task( try { props += Url(URI(it)) } catch (e: URISyntaxException) { - Ical4Android.log.log(Level.WARNING, "Ignoring invalid task URL: $url", e) + logger.log(Level.WARNING, "Ignoring invalid task URL: $url", e) } } organizer?.let { props += it } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt index 763ea7ff..287cae94 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt @@ -13,6 +13,7 @@ import at.bitfire.ical4android.util.MiscUtils.closeCompat import org.dmfs.tasks.contract.TaskContract import java.io.Closeable import java.util.logging.Level +import java.util.logging.Logger class TaskProvider private constructor( @@ -48,6 +49,9 @@ class TaskProvider private constructor( companion object { + private val logger + get() = Logger.getLogger(TaskProvider::javaClass.name) + val TASK_PROVIDERS = listOf( ProviderName.OpenTasks, ProviderName.TasksOrg, @@ -87,9 +91,9 @@ class TaskProvider private constructor( if (client != null) return TaskProvider(provider, client) } catch(e: SecurityException) { - Ical4Android.log.log(Level.WARNING, "Not allowed to access task provider", e) + logger.log(Level.WARNING, "Not allowed to access task provider", e) } catch(e: PackageManager.NameNotFoundException) { - Ical4Android.log.warning("Package ${provider.packageName} not installed") + logger.warning("Package ${provider.packageName} not installed") } return null } @@ -114,7 +118,7 @@ class TaskProvider private constructor( val installedVersionCode = PackageInfoCompat.getLongVersionCode(info) if (installedVersionCode < name.minVersionCode) { val exception = ProviderTooOldException(name, installedVersionCode, info.versionName) - Ical4Android.log.log(Level.WARNING, "Task provider too old", exception) + logger.log(Level.WARNING, "Task provider too old", exception) throw exception } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt index e3686878..653622a0 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -28,6 +28,7 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAmount import java.util.* +import java.util.logging.Logger object AndroidTimeUtils { @@ -45,6 +46,9 @@ object AndroidTimeUtils { */ const val RECURRENCE_RULE_SEPARATOR = "\n" + private val logger + get() = Logger.getLogger(javaClass.name) + /** * Ensures that a given [DateProperty] either @@ -163,7 +167,7 @@ object AndroidTimeUtils { for (dateListProp in dates) { if (dateListProp is RDate && dateListProp.periods.isNotEmpty()) { - Ical4Android.log.warning("RDATE PERIOD not supported, ignoring") + logger.warning("RDATE PERIOD not supported, ignoring") break } @@ -290,10 +294,10 @@ object AndroidTimeUtils { for (dateListProp in dates) { if (dateListProp is RDate) if (dateListProp.periods.isNotEmpty()) - Ical4Android.log.warning("RDATE PERIOD not supported, ignoring") + logger.warning("RDATE PERIOD not supported, ignoring") else if (dateListProp is ExDate) if (dateListProp.periods.isNotEmpty()) - Ical4Android.log.warning("EXDATE PERIOD not supported, ignoring") + logger.warning("EXDATE PERIOD not supported, ignoring") for (date in dateListProp.dates) { val dateToUse = diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index 2eb21f5d..ab7af8df 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -14,6 +14,7 @@ import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.DateProperty import java.io.StringReader import java.time.ZoneId +import java.util.logging.Logger /** * Date/time utilities @@ -27,6 +28,9 @@ object DateUtils { Ical4Android.checkThreadContextClassLoader() } + private val logger + get() = Logger.getLogger(javaClass.name) + /** * Global ical4j time zone registry used for event/task processing. Do not * modify this registry or its entries! @@ -64,7 +68,7 @@ object DateUtils { for (availableTZ in availableTZs) if (availableTZ.contains(tzID) || tzID.contains(availableTZ)) { result = availableTZ - Ical4Android.log.warning("Couldn't find system time zone \"$tzID\", assuming $result") + logger.warning("Couldn't find system time zone \"$tzID\", assuming $result") break } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt index 0a97d2e2..b0491161 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt @@ -20,6 +20,7 @@ import java.time.LocalTime import java.time.ZonedDateTime import java.util.Calendar import java.util.TimeZone +import java.util.logging.Logger /** * Validates events and tries to repair broken events, since sometimes CalendarStorage or servers @@ -34,6 +35,9 @@ import java.util.TimeZone */ object EventValidator { + private val logger + get() = Logger.getLogger(javaClass.name) + /** * Searches for some invalid conditions and fixes them. * @@ -73,7 +77,7 @@ object EventValidator { // validate end time e.dtEnd?.let { dtEnd -> if (dtStart.date > dtEnd.date) { - Ical4Android.log.warning("DTSTART after DTEND; removing DTEND") + logger.warning("DTSTART after DTEND; removing DTEND") e.dtEnd = null } } @@ -94,7 +98,7 @@ object EventValidator { val rRule = rRuleIterator.next() rRule.recur.until?.let { until -> if (until is DateTime) { - Ical4Android.log.warning("DTSTART has DATE, but UNTIL has DATETIME; making UNTIL have DATE only") + logger.warning("DTSTART has DATE, but UNTIL has DATETIME; making UNTIL have DATE only") val newUntil = until.toLocalDate().toIcal4jDate() @@ -102,7 +106,7 @@ object EventValidator { val newRRule = RRule(Recur.Builder(rRule.recur) .until(newUntil) .build()) - Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") + logger.info("New $newRRule (was ${rRule.toString().trim()})") newRRules += newRRule rRuleIterator.remove() } @@ -119,7 +123,7 @@ object EventValidator { val rRule = rRuleIterator.next() rRule.recur.until?.let { until -> if (until !is DateTime) { - Ical4Android.log.warning("DTSTART has DATETIME, but UNTIL has DATE; copying time from DTSTART to UNTIL") + logger.warning("DTSTART has DATETIME, but UNTIL has DATE; copying time from DTSTART to UNTIL") val dtStartTimeZone = if (dtStart.timeZone != null) dtStart.timeZone else if (dtStart.isUtc) @@ -152,7 +156,7 @@ object EventValidator { val newRRule = RRule(Recur.Builder(rRule.recur) .until(newUntilUTC) .build()) - Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") + logger.info("New $newRRule (was ${rRule.toString().trim()})") newRRules += newRRule rRuleIterator.remove() } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt index b9ef7c6e..6523b189 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt @@ -4,9 +4,9 @@ package at.bitfire.ical4android.validation -import at.bitfire.ical4android.Ical4Android import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor.TZOFFSET_REGEXP import java.util.logging.Level +import java.util.logging.Logger /** @@ -17,6 +17,9 @@ import java.util.logging.Level */ object FixInvalidUtcOffsetPreprocessor: StreamPreprocessor() { + private val logger + get() = Logger.getLogger(javaClass.name) + private val TZOFFSET_REGEXP = Regex("^(TZOFFSET(FROM|TO):[+\\-]?)((18|19|[2-6]\\d)\\d\\d)$", setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)) @@ -24,7 +27,7 @@ object FixInvalidUtcOffsetPreprocessor: StreamPreprocessor() { override fun fixString(original: String) = original.replace(TZOFFSET_REGEXP) { - Ical4Android.log.log(Level.FINE, "Applying Synology WebDAV fix to invalid utc-offset", it.value) + logger.log(Level.FINE, "Applying Synology WebDAV fix to invalid utc-offset", it.value) "${it.groupValues[1]}00${it.groupValues[3]}" } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt index e1731d24..aac93807 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt @@ -4,7 +4,6 @@ package at.bitfire.ical4android.validation -import at.bitfire.ical4android.Ical4Android import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.transform.rfc5545.CreatedPropertyRule @@ -12,8 +11,8 @@ import net.fortuna.ical4j.transform.rfc5545.DateListPropertyRule import net.fortuna.ical4j.transform.rfc5545.DatePropertyRule import net.fortuna.ical4j.transform.rfc5545.Rfc5545PropertyRule import java.io.Reader -import java.util.* import java.util.logging.Level +import java.util.logging.Logger /** * Applies some rules to increase compatibility of parsed (incoming) iCalendars: @@ -72,7 +71,7 @@ object ICalPreprocessor { (it as Rfc5545PropertyRule).applyTo(property) val afterStr = property.toString() if (beforeStr != afterStr) - Ical4Android.log.log(Level.FINER, "$beforeStr -> $afterStr") + Logger.getLogger(javaClass.name).log(Level.FINER, "$beforeStr -> $afterStr") } } diff --git a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt index d729ab8c..3707a005 100644 --- a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt +++ b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt @@ -11,7 +11,6 @@ package at.techbee.jtx import android.accounts.Account import android.net.Uri import android.provider.BaseColumns -import at.bitfire.ical4android.Ical4Android import net.fortuna.ical4j.model.ParameterList import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyList @@ -19,11 +18,15 @@ import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.XProperty import org.json.JSONObject import java.util.logging.Level +import java.util.logging.Logger @Suppress("unused") object JtxContract { + private val logger + get() = Logger.getLogger(javaClass.name) + /** * URI parameter to signal that the caller is a sync adapter. */ @@ -101,7 +104,7 @@ object JtxContract { } } } catch (e: NullPointerException) { - Ical4Android.log.log(Level.WARNING, "Error parsing x-property-list $string", e) + logger.log(Level.WARNING, "Error parsing x-property-list $string", e) } return propertyList } @@ -164,7 +167,7 @@ object JtxContract { try { longList.add(it.toLong()) } catch (e: NumberFormatException) { - Ical4Android.log.log(Level.WARNING, "String could not be cast to Long ($it)") + logger.log(Level.WARNING, "String could not be cast to Long ($it)") return@forEach } } From 86af14d6c833263761903c13b50f21d548a12a8f Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 1 Aug 2024 11:20:24 +0200 Subject: [PATCH 63/92] Collections: unified delete() return value --- .../kotlin/at/bitfire/ical4android/AndroidCalendar.kt | 9 +++++++-- .../main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt | 9 +++++++-- .../main/kotlin/at/bitfire/ical4android/JtxCollection.kt | 6 +++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt index b68dcbea..f7ec09ed 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt @@ -194,9 +194,14 @@ abstract class AndroidCalendar( return provider.update(calendarSyncURI(), info, null, null) } - fun delete(): Int { + /** + * Deletes this calendar from the local calendar provider. + * + * @return `true` if the calendar was deleted, `false` otherwise (like it was not there before the call) + */ + fun delete(): Boolean { logger.log(Level.FINE, "Deleting local calendar (#$id)") - return provider.delete(calendarSyncURI(), null, null) + return provider.delete(calendarSyncURI(), null, null) > 0 } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index bd13f96c..8d42347c 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -126,9 +126,14 @@ abstract class DmfsTaskList( return provider.client.update(taskListSyncUri(), info, null, null) } - fun delete(): Int { + /** + * Deletes this calendar from the local calendar provider. + * + * @return `true` if the calendar was deleted, `false` otherwise (like it was not there before the call) + */ + fun delete(): Boolean { logger.log(Level.FINE, "Deleting ${provider.name.authority} task list (#$id)") - return provider.client.delete(taskListSyncUri(), null, null) + return provider.client.delete(taskListSyncUri(), null, null) > 0 } /** diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index 75a839e9..7e926d7a 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -29,7 +29,7 @@ open class JtxCollection(val account: Account, companion object { private val logger - get() = Logger.getLogger(javaClass.name) + get() = Logger.getLogger(JtxCollection::class.java.name) fun create(account: Account, client: ContentProviderClient, values: ContentValues): Uri { logger.log(Level.FINE, "Creating jtx Board collection", values) @@ -63,9 +63,9 @@ open class JtxCollection(val account: Account, var context: Context? = null - fun delete() { + fun delete(): Boolean { logger.log(Level.FINE, "Deleting jtx Board collection (#$id)") - client.delete(ContentUris.withAppendedId(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), id), null, null) + return client.delete(ContentUris.withAppendedId(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(account), id), null, null) > 0 } fun update(values: ContentValues) { From 60e214f4d48d08c9d8d4564d4de06d317d8bb78a Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Thu, 1 Aug 2024 17:59:37 +0200 Subject: [PATCH 64/92] Expect boolean instead of integer (#160) --- .../kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt | 3 ++- .../kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt index d409f151..669de0c2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt @@ -23,6 +23,7 @@ import net.fortuna.ical4j.model.property.DtStart import org.junit.* import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue class AndroidCalendarTest { @@ -77,7 +78,7 @@ class AndroidCalendarTest { assertNotNull(calendar) // delete calendar - assertEquals(1, calendar.delete()) + assertTrue(calendar.delete()) } diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt index 7c20de5b..4532e5cb 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt @@ -56,7 +56,7 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): assertEquals(testAccount.name, taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME)) } finally { // delete task list - assertEquals(1, taskList.delete()) + assertTrue(taskList.delete()) } } From 8d09c89ec2d12d49e49d3d6221944179c94f4b9c Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Thu, 8 Aug 2024 10:22:23 +0200 Subject: [PATCH 65/92] Replaced usages (#166) Signed-off-by: Arnau Mora Gras --- .../at/bitfire/ical4android/AndroidEventTest.kt | 16 ++++++++-------- .../kotlin/at/bitfire/ical4android/EventTest.kt | 10 +++++----- .../kotlin/at/bitfire/ical4android/Ical4jTest.kt | 2 +- .../kotlin/at/bitfire/ical4android/TaskTest.kt | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index dfd8c060..4883b0bb 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -2218,12 +2218,12 @@ class AndroidEventTest { val params = ParameterList() params.add(Language("en")) val unknownProperty = XProperty("X-NAME", params, "Custom Value") - val result = populateEvent( + val (result) = populateEvent( true, extendedProperties = mapOf( UnknownProperty.CONTENT_ITEM_TYPE to UnknownProperty.toJsonString(unknownProperty) ) - ).unknownProperties.first + ).unknownProperties assertEquals("X-NAME", result.name) assertEquals("en", result.getParameter(Parameter.LANGUAGE).value) assertEquals("Custom Value", result.value) @@ -2258,8 +2258,8 @@ class AndroidEventTest { }).let { event -> assertEquals("Recurring non-all-day event with exception", event.summary) assertEquals(DtStart("20200706T193000", tzVienna), event.dtStart) - assertEquals("FREQ=DAILY;COUNT=10", event.rRules.first.value) - val exception = event.exceptions.first!! + assertEquals("FREQ=DAILY;COUNT=10", event.rRules.first().value) + val exception = event.exceptions.first() assertEquals(RecurrenceId("20200708T013000", tzShanghai), exception.recurrenceId) assertEquals(DtStart("20200706T203000", tzShanghai), exception.dtStart) assertEquals("Event moved to one hour later", exception.summary) @@ -2286,8 +2286,8 @@ class AndroidEventTest { }).let { event -> assertEquals("Recurring all-day event with exception", event.summary) assertEquals(DtStart(Date("20200706")), event.dtStart) - assertEquals("FREQ=WEEKLY;COUNT=3", event.rRules.first.value) - val exception = event.exceptions.first!! + assertEquals("FREQ=WEEKLY;COUNT=3", event.rRules.first().value) + val exception = event.exceptions.first() assertEquals(RecurrenceId(Date("20200707")), exception.recurrenceId) assertEquals(DtStart("20200706T183000", tzShanghai), exception.dtStart) assertEquals("Today not an all-day event", exception.summary) @@ -2314,8 +2314,8 @@ class AndroidEventTest { }).let { event -> assertEquals("Recurring all-day event with cancelled exception", event.summary) assertEquals(DtStart("20200706T193000", tzVienna), event.dtStart) - assertEquals("FREQ=DAILY;COUNT=10", event.rRules.first.value) - assertEquals(DateTime("20200708T013000", tzShanghai), event.exDates.first.dates.first()) + assertEquals("FREQ=DAILY;COUNT=10", event.rRules.first().value) + assertEquals(DateTime("20200708T013000", tzShanghai), event.exDates.first().dates.first()) assertTrue(event.exceptions.isEmpty()) } } diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt index 643ecfb3..ade4ec57 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt @@ -92,13 +92,13 @@ class EventTest { e = findEvent(events, "multiple-1@ical4android.EventTest") assertEquals("Event 1", e.summary) assertEquals(1, e.exceptions.size) - assertEquals("Event 1 Exception", e.exceptions.first.summary) + assertEquals("Event 1 Exception", e.exceptions.first().summary) e = findEvent(events, "multiple-2@ical4android.EventTest") assertEquals("Event 2", e.summary) assertEquals(2, e.exceptions.size) - assertTrue("Event 2 Updated Exception 1" == e.exceptions.first.summary || "Event 2 Updated Exception 1" == e.exceptions[1].summary) - assertTrue("Event 2 Exception 2" == e.exceptions.first.summary || "Event 2 Exception 2" == e.exceptions[1].summary) + assertTrue("Event 2 Updated Exception 1" == e.exceptions.first().summary || "Event 2 Updated Exception 1" == e.exceptions[1].summary) + assertTrue("Event 2 Exception 2" == e.exceptions.first().summary || "Event 2 Exception 2" == e.exceptions[1].summary) } @@ -110,9 +110,9 @@ class EventTest { assertEquals("Test Description", event.description) assertEquals("中华人民共和国", event.location) assertEquals(Css3Color.aliceblue, event.color) - assertEquals("cyrus@example.com", event.attendees.first.parameters.getParameter("EMAIL").value) + assertEquals("cyrus@example.com", event.attendees.first().parameters.getParameter("EMAIL").value) - val unknown = event.unknownProperties.first + val (unknown) = event.unknownProperties assertEquals("X-UNKNOWN-PROP", unknown.name) assertEquals("xxx", unknown.getParameter("param1").value) assertEquals("Unknown Value", unknown.value) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt index 6ddb6ed5..1d74c2a9 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt @@ -38,7 +38,7 @@ class Ical4jTest { "END:VCALENDAR" ) ).first() - assertEquals("attendee1@example.virtual", e.attendees.first.getParameter(Parameter.EMAIL).value) + assertEquals("attendee1@example.virtual", e.attendees.first().getParameter(Parameter.EMAIL).value) } @Test diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt index 4c9015c9..37c10930 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt @@ -174,11 +174,11 @@ class TaskTest { assertArrayEquals(arrayOf("Test","Sample"), t.categories.toArray()) - val sibling = t.relatedTo.first + val (sibling) = t.relatedTo assertEquals("most-fields2@example.com", sibling.value) assertEquals(RelType.SIBLING, (sibling.getParameter(Parameter.RELTYPE) as RelType)) - val unknown = t.unknownProperties.first + val (unknown) = t.unknownProperties assertEquals("X-UNKNOWN-PROP", unknown.name) assertEquals("xxx", unknown.getParameter("param1").value) assertEquals("Unknown Value", unknown.value) From 1d9c51096326fa4a9fe7140a364f5d64ad6d4ae6 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Mon, 12 Aug 2024 11:41:41 +0200 Subject: [PATCH 66/92] Take ContentProviderClient instead of TasksProvider (#163) * Take ContentProviderClient instead of TaskProvider in find method * Take ContentProviderClient instead of TaskProvider in create method * Take ContentProviderClient instead of TaskProvider in findById method --- .../bitfire/ical4android/DmfsTaskListTest.kt | 10 ++-- .../bitfire/ical4android/impl/TestTaskList.kt | 19 ++++--- .../at/bitfire/ical4android/DmfsTask.kt | 12 ++--- .../at/bitfire/ical4android/DmfsTaskList.kt | 51 ++++++++++--------- .../ical4android/DmfsTaskListFactory.kt | 3 +- 5 files changed, 53 insertions(+), 42 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt index 4532e5cb..c227ddf3 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt @@ -21,7 +21,7 @@ import org.junit.Assert.assertTrue import org.junit.Test class DmfsTaskListTest(providerName: TaskProvider.ProviderName): - AbstractTasksTest(providerName) { + AbstractTasksTest(providerName) { private val testAccount = Account("AndroidTaskListTest", TaskContract.LOCAL_ACCOUNT_TYPE) @@ -34,10 +34,10 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): info.put(TaskContract.TaskLists.SYNC_ENABLED, 1) info.put(TaskContract.TaskLists.VISIBLE, 1) - val uri = DmfsTaskList.create(testAccount, provider, info) + val uri = DmfsTaskList.create(testAccount, provider.client, providerName, info) assertNotNull(uri) - return DmfsTaskList.findByID(testAccount, provider, TestTaskList.Factory, ContentUris.parseId(uri)) + return DmfsTaskList.findByID(testAccount, provider.client, providerName, TestTaskList.Factory, ContentUris.parseId(uri)) } @@ -80,7 +80,7 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): val parentId = ContentUris.parseId(parentContentUri) // OpenTasks should provide the correct relation - taskList.provider.client.query(taskList.tasksPropertiesSyncUri(), null, + taskList.provider.query(taskList.tasksPropertiesSyncUri(), null, "${Properties.TASK_ID}=?", arrayOf(childId.toString()), null, null)!!.use { cursor -> assertEquals(1, cursor.count) @@ -99,7 +99,7 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): taskList.touchRelations() // now parent_id should bet set - taskList.provider.client.query(childContentUri, arrayOf(Tasks.PARENT_ID), + taskList.provider.query(childContentUri, arrayOf(Tasks.PARENT_ID), null, null, null)!!.use { cursor -> assertTrue(cursor.moveToNext()) assertEquals(parentId, cursor.getLong(0)) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt index 0ed9edf7..13711224 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt @@ -5,6 +5,7 @@ package at.bitfire.ical4android.impl import android.accounts.Account +import android.content.ContentProviderClient import android.content.ContentUris import android.content.ContentValues import at.bitfire.ical4android.DmfsTaskList @@ -14,29 +15,33 @@ import org.dmfs.tasks.contract.TaskContract class TestTaskList( account: Account, - provider: TaskProvider, + provider: ContentProviderClient, + providerName: TaskProvider.ProviderName, id: Long -): DmfsTaskList(account, provider, TestTask.Factory, id) { +): DmfsTaskList(account, provider, providerName, TestTask.Factory, id) { companion object { - fun create(account: Account, provider: TaskProvider): TestTaskList { + fun create( + account: Account, + provider: TaskProvider, + ): TestTaskList { val values = ContentValues(4) values.put(TaskContract.TaskListColumns.LIST_NAME, "Test Task List") values.put(TaskContract.TaskListColumns.LIST_COLOR, 0xffff0000) values.put(TaskContract.TaskListColumns.SYNC_ENABLED, 1) values.put(TaskContract.TaskListColumns.VISIBLE, 1) - val uri = DmfsTaskList.create(account, provider, values) + val uri = DmfsTaskList.create(account, provider.client, provider.name, values) - return TestTaskList(account, provider, ContentUris.parseId(uri)) + return TestTaskList(account, provider.client, provider.name, ContentUris.parseId(uri)) } } object Factory: DmfsTaskListFactory { - override fun newInstance(account: Account, provider: TaskProvider, id: Long) = - TestTaskList(account, provider, id) + override fun newInstance(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, id: Long) = + TestTaskList(account, provider, providerName, id) } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 42b1e9e8..25a3ef0b 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -99,7 +99,7 @@ abstract class DmfsTask( val id = requireNotNull(id) try { - val client = taskList.provider.client + val client = taskList.provider client.query(taskSyncURI(true), null, null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { // create new Task which will be populated @@ -161,7 +161,7 @@ abstract class DmfsTask( task.sequence = values.getAsInteger(Tasks.SYNC_VERSION) task.summary = values.getAsString(Tasks.TITLE) task.location = values.getAsString(Tasks.LOCATION) - task.userAgents += taskList.provider.name.packageName + task.userAgents += taskList.providerName.packageName values.getAsString(Tasks.GEO)?.let { geo -> val (lng, lat) = geo.split(',') @@ -330,7 +330,7 @@ abstract class DmfsTask( fun add(): Uri { - val batch = BatchOperation(taskList.provider.client) + val batch = BatchOperation(taskList.provider) val builder = CpoBuilder.newInsert(taskList.tasksSyncUri()) buildTask(builder, false) @@ -350,7 +350,7 @@ abstract class DmfsTask( this.task = task val existingId = requireNotNull(id) - val batch = BatchOperation(taskList.provider.client) + val batch = BatchOperation(taskList.provider) // remove associated rows which are added later again batch.enqueue(CpoBuilder @@ -367,7 +367,7 @@ abstract class DmfsTask( insertProperties(batch, null) batch.commit() - return ContentUris.withAppendedId(taskList.provider.tasksUri(), existingId) + return ContentUris.withAppendedId(Tasks.getContentUri(taskList.providerName.authority), existingId) } protected open fun insertProperties(batch: BatchOperation, idxTask: Int?) { @@ -472,7 +472,7 @@ abstract class DmfsTask( } fun delete(): Int { - return taskList.provider.client.delete(taskSyncURI(), null, null) + return taskList.provider.delete(taskSyncURI(), null, null) } @CallSuper diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index 8d42347c..0e82f116 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -5,6 +5,7 @@ package at.bitfire.ical4android import android.accounts.Account +import android.content.ContentProviderClient import android.content.ContentUris import android.content.ContentValues import android.net.Uri @@ -27,7 +28,8 @@ import java.util.logging.Logger */ abstract class DmfsTaskList( val account: Account, - val provider: TaskProvider, + val provider: ContentProviderClient, + val providerName: TaskProvider.ProviderName, val taskFactory: DmfsTaskFactory, val id: Long ) { @@ -37,30 +39,32 @@ abstract class DmfsTaskList( private val logger get() = Logger.getLogger(DmfsTaskList::class.java.name) - fun create(account: Account, provider: TaskProvider, info: ContentValues): Uri { + fun create(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, info: ContentValues): Uri { info.put(TaskContract.ACCOUNT_NAME, account.name) info.put(TaskContract.ACCOUNT_TYPE, account.type) - logger.log(Level.FINE, "Creating ${provider.name.authority} task list", info) - return provider.client.insert(provider.taskListsUri().asSyncAdapter(account), info) + val url = TaskLists.getContentUri(providerName.authority).asSyncAdapter(account) + logger.log(Level.FINE, "Creating ${providerName.authority} task list", info) + return provider.insert(url, info) ?: throw CalendarStorageException("Couldn't create task list (empty result from provider)") } fun > findByID( account: Account, - provider: TaskProvider, + provider: ContentProviderClient, + providerName: TaskProvider.ProviderName, factory: DmfsTaskListFactory, id: Long ): T { - provider.client.query( - ContentUris.withAppendedId(provider.taskListsUri(), id).asSyncAdapter(account), + provider.query( + ContentUris.withAppendedId(TaskLists.getContentUri(providerName.authority), id).asSyncAdapter(account), null, null, null, null )?.use { cursor -> if (cursor.moveToNext()) { - val taskList = factory.newInstance(account, provider, id) + val taskList = factory.newInstance(account, provider, providerName, id) taskList.populate(cursor.toValues()) return taskList } @@ -70,14 +74,15 @@ abstract class DmfsTaskList( fun > find( account: Account, - provider: TaskProvider, factory: DmfsTaskListFactory, + provider: ContentProviderClient, + providerName: TaskProvider.ProviderName, where: String?, whereArgs: Array? ): List { val taskLists = LinkedList() - provider.client.query( - provider.taskListsUri().asSyncAdapter(account), + provider.query( + TaskLists.getContentUri(providerName.authority).asSyncAdapter(account), null, where, whereArgs, @@ -86,7 +91,7 @@ abstract class DmfsTaskList( while (cursor.moveToNext()) { val values = cursor.toValues() val taskList = - factory.newInstance(account, provider, values.getAsLong(TaskLists._ID)) + factory.newInstance(account, provider, providerName, values.getAsLong(TaskLists._ID)) taskList.populate(values) taskLists += taskList } @@ -110,7 +115,7 @@ abstract class DmfsTaskList( * Called when an instance is created from a tasks provider data row, for example * using [find]. * - * @param info values from tasks provider + * @param values values from tasks provider */ @CallSuper protected open fun populate(values: ContentValues) { @@ -122,8 +127,8 @@ abstract class DmfsTaskList( } fun update(info: ContentValues): Int { - logger.log(Level.FINE, "Updating ${provider.name.authority} task list (#$id)", info) - return provider.client.update(taskListSyncUri(), info, null, null) + logger.log(Level.FINE, "Updating ${providerName.authority} task list (#$id)", info) + return provider.update(taskListSyncUri(), info, null, null) } /** @@ -132,8 +137,8 @@ abstract class DmfsTaskList( * @return `true` if the calendar was deleted, `false` otherwise (like it was not there before the call) */ fun delete(): Boolean { - logger.log(Level.FINE, "Deleting ${provider.name.authority} task list (#$id)") - return provider.client.delete(taskListSyncUri(), null, null) > 0 + logger.log(Level.FINE, "Deleting ${providerName.authority} task list (#$id)") + return provider.delete(taskListSyncUri(), null, null) > 0 } /** @@ -158,8 +163,8 @@ abstract class DmfsTaskList( */ fun touchRelations(): Int { logger.fine("Touching relations to set parent_id") - val batchOperation = BatchOperation(provider.client) - provider.client.query( + val batchOperation = BatchOperation(provider) + provider.query( tasksSyncUri(true), null, "${Tasks.LIST_ID}=? AND ${Tasks.PARENT_ID} IS NULL AND ${Relation.MIMETYPE}=? AND ${Relation.RELATED_ID} IS NOT NULL", arrayOf(id.toString(), Relation.CONTENT_ITEM_TYPE), @@ -194,7 +199,7 @@ abstract class DmfsTaskList( val whereArgs = (_whereArgs ?: arrayOf()) + id.toString() val tasks = LinkedList() - provider.client.query( + provider.query( tasksSyncUri(), null, where, whereArgs, null @@ -210,10 +215,10 @@ abstract class DmfsTaskList( fun taskListSyncUri() = - ContentUris.withAppendedId(provider.taskListsUri(), id).asSyncAdapter(account) + ContentUris.withAppendedId(TaskLists.getContentUri(providerName.authority), id).asSyncAdapter(account) fun tasksSyncUri(loadProperties: Boolean = false): Uri { - val uri = provider.tasksUri().asSyncAdapter(account) + val uri = Tasks.getContentUri(providerName.authority).asSyncAdapter(account) return if (loadProperties) uri.buildUpon() .appendQueryParameter(TaskContract.LOAD_PROPERTIES, "1") @@ -222,6 +227,6 @@ abstract class DmfsTaskList( uri } - fun tasksPropertiesSyncUri() = provider.propertiesUri().asSyncAdapter(account) + fun tasksPropertiesSyncUri() = TaskContract.Properties.getContentUri(providerName.authority).asSyncAdapter(account) } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt index 3627c319..7b1da9e2 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt @@ -5,9 +5,10 @@ package at.bitfire.ical4android import android.accounts.Account +import android.content.ContentProviderClient interface DmfsTaskListFactory> { - fun newInstance(account: Account, provider: TaskProvider, id: Long): T + fun newInstance(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, id: Long): T } \ No newline at end of file From 0f42348d4d1d6ca9b775e3d99c0f698de0ab322d Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Wed, 14 Aug 2024 12:27:47 +0200 Subject: [PATCH 67/92] Fixed missing `first` calls (#169) Signed-off-by: Arnau Mora Gras --- .../androidTest/kotlin/at/bitfire/ical4android/EventTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt index ade4ec57..87a8b992 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt @@ -162,7 +162,7 @@ class EventTest { assertTrue(DateUtils.isDate(event.dtStart)) assertEquals(1, event.exceptions.size) - val exception = event.exceptions.first + val (exception) = event.exceptions assertEquals("20150503", exception.recurrenceId!!.value) assertEquals("Another summary for the third day", exception.summary) } @@ -172,7 +172,7 @@ class EventTest { val event = parseCalendar("recurring-only-exception.ics").first() assertEquals(1, event.exceptions.size) - val exception = event.exceptions.first + val (exception) = event.exceptions assertEquals("20150503T010203Z", exception.recurrenceId!!.value) assertEquals("This is an exception", exception.summary) From 6a5df96af9f0d4e12da887e96b707ac49e7f5bf4 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sat, 17 Aug 2024 12:30:47 +0200 Subject: [PATCH 68/92] Forced dependency versions and excluded dependencies (#165) * Forced dependency versions and excluded dependencies Signed-off-by: Arnau Mora Gras * Forced dependency versions and excluded dependencies Signed-off-by: Arnau Mora Gras * Changed notation Signed-off-by: Arnau Mora Gras * Assume SDK level Signed-off-by: Arnau Mora Gras * Using `PackageInfoCompat` Signed-off-by: Arnau Mora Gras * Using `SdkSuppress` Signed-off-by: Arnau Mora Gras --------- Signed-off-by: Arnau Mora Gras --- lib/build.gradle.kts | 12 +++++++++++- .../at/bitfire/ical4android/ICalPreprocessorTest.kt | 7 +++++-- .../at/bitfire/ical4android/JtxICalObjectTest.kt | 5 +++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 508f4669..428ab71a 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -96,7 +96,17 @@ dependencies { coreLibraryDesugaring(libs.android.desugaring) implementation(libs.androidx.core) - api(libs.ical4j) + api(libs.ical4j) { + // Get rid of unnecessary transitive dependencies + exclude(group = "commons-validator", module = "commons-validator") + } + // Force latest version of commons libraries + implementation("org.apache.commons:commons-lang3") { + version { require("3.15.0") } + } + implementation("commons-codec:commons-codec") { + version { require("1.17.1") } + } implementation(libs.slf4j) // ical4j logging over java.util.Logger androidTestImplementation(libs.androidx.test.core) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt index 96b8e246..7f30e8a2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt @@ -4,23 +4,26 @@ package at.bitfire.ical4android +import androidx.test.filters.SdkSuppress import at.bitfire.ical4android.validation.FixInvalidDayOffsetPreprocessor import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor import at.bitfire.ical4android.validation.ICalPreprocessor import io.mockk.mockkObject import io.mockk.verify +import java.io.InputStreamReader +import java.io.StringReader import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Test -import java.io.InputStreamReader -import java.io.StringReader class ICalPreprocessorTest { @Test + @SdkSuppress(minSdkVersion = 28) fun testPreprocessStream_appliesStreamProcessors() { + // Can only run on API Level 28 or newer because mockkObject doesn't support Android < P mockkObject(FixInvalidDayOffsetPreprocessor, FixInvalidUtcOffsetPreprocessor) { ICalPreprocessor.preprocessStream(StringReader("")) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt index 8d7cf820..9d689b21 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -11,6 +11,7 @@ import android.content.ContentValues import android.content.Context import android.database.DatabaseUtils import android.os.ParcelFileDescriptor +import androidx.core.content.pm.PackageInfoCompat import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.impl.TestJtxCollection @@ -139,7 +140,7 @@ class JtxICalObjectTest { @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_XSTATUS() { - val jtxVersionCode = context.packageManager.getPackageInfo("at.techbee.jtx", 0).longVersionCode + val jtxVersionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo("at.techbee.jtx", 0)) Assume.assumeTrue(jtxVersionCode > 204020003) insertRetrieveAssertString(JtxICalObject.EXTENDED_STATUS, sample?.xstatus, Component.VJOURNAL.name) } @@ -152,7 +153,7 @@ class JtxICalObjectTest { @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_GEOFENCE_RADIUS() { - val jtxVersionCode = context.packageManager.getPackageInfo("at.techbee.jtx", 0).longVersionCode + val jtxVersionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo("at.techbee.jtx", 0)) Assume.assumeTrue(jtxVersionCode > 204020003) insertRetrieveAssertInt(JtxICalObject.GEOFENCE_RADIUS, sample?.geofenceRadius, Component.VJOURNAL.name) } From cb943433580ac9b970d01d80f0cf8507f40892db Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 20 Aug 2024 15:48:41 +0200 Subject: [PATCH 69/92] AndroidCompatTimeZoneRegistry logs inappropriate warning when constructing Europe/Copenhagen from Europe/Berlin (#170) * Removing all `TZURL` and `X-LIC-LOCATION` Signed-off-by: Arnau Mora Gras * Added test to make sure Berlin is not present Signed-off-by: Arnau Mora Gras * Changed log level Signed-off-by: Arnau Mora Gras * Changed log level Signed-off-by: Arnau Mora Gras * Initializing empty property list Signed-off-by: Arnau Mora Gras * Optimized imports Signed-off-by: Arnau Mora Gras --------- Signed-off-by: Arnau Mora Gras --- .../AndroidCompatTimeZoneRegistryTest.kt | 7 +++++++ .../ical4android/AndroidCompatTimeZoneRegistry.kt | 12 +++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt index cd8764db..426ab95a 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt @@ -77,6 +77,13 @@ class AndroidCompatTimeZoneRegistryTest { ) } + @Test + fun getTimeZone_Copenhagen_NoBerlin() { + val tz = registry.getTimeZone("Europe/Copenhagen")!! + assertEquals("Europe/Copenhagen", tz.id) + assertFalse(tz.vTimeZone.toString().contains("Berlin")) + } + @Test fun getTimeZone_NotExisting() { assertNull(registry.getTimeZone("Test/NotExisting")) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt index 1f4dac92..3063c5b8 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt @@ -1,6 +1,9 @@ package at.bitfire.ical4android +import java.time.ZoneId +import java.util.logging.Logger import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory +import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistry @@ -8,8 +11,6 @@ import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.TimeZoneRegistryImpl import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.TzId -import java.time.ZoneId -import java.util.logging.Logger /** * Wrapper around default [TimeZoneRegistry] that uses the Android name if a time zone has a @@ -67,14 +68,11 @@ class AndroidCompatTimeZoneRegistry( but most Android devices don't now Europe/Kyiv yet. */ if (tz.id != androidTzId) { - logger.warning("Using Android TZID $androidTzId instead of ical4j ${tz.id}") + logger.fine("Using ical4j timezone ${tz.id} data to construct Android timezone $androidTzId") // create a copy of the VTIMEZONE so that we don't modify the original registry values (which are not immutable) val vTimeZone = tz.vTimeZone - val newVTimeZoneProperties = PropertyList(vTimeZone.properties) - newVTimeZoneProperties.removeAll { property -> - property is TzId - } + val newVTimeZoneProperties = PropertyList() newVTimeZoneProperties += TzId(androidTzId) return TimeZone(VTimeZone( newVTimeZoneProperties, From b75f33972aec7c581218c6e3fab9dcd378e0c0f2 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 21 Aug 2024 11:56:23 +0200 Subject: [PATCH 70/92] [CI] Dependency graph: only compile source (not tests) --- .github/workflows/test-dev.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index c1984e7c..153c34f8 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -17,9 +17,8 @@ jobs: cache-encryption-key: ${{ secrets.gradle_encryption_key }} gradle-home-cache-cleanup: true # clean up unused files dependency-graph: generate-and-submit # submit Github Dependency Graph info - dependency-graph-continue-on-failure: false - - run: ./gradlew --build-cache --configuration-cache assembleDebug + - run: ./gradlew --build-cache --configuration-cache compileDebugSources test: needs: compile From 044c1cacb92a8dc99724c2a76a3ee6d74bd96034 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 17 Sep 2024 17:05:41 +0200 Subject: [PATCH 71/92] Upgrade dependencies (#173) * Upgrade dependencies Signed-off-by: Arnau Mora Gras * Fix versions Signed-off-by: Arnau Mora Gras --------- Signed-off-by: Arnau Mora Gras --- gradle/libs.versions.toml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b4ec30e..5b3a09fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,10 @@ [versions] -agp = "8.5.1" -android-desugar = "2.0.4" +agp = "8.5.2" +android-desugar = "2.1.2" androidx-core = "1.13.1" -androidx-test = "1.6.1" +androidx-test-core = "1.6.1" +androidx-test-rules = "1.6.1" +androidx-test-runner = "1.6.2" # noinspection GradleDependency commons-collections = "4.2" # noinspection GradleDependency @@ -13,16 +15,16 @@ dokka ="1.9.20" # noinspection GradleDependency ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" -kotlin = "2.0.0" +kotlin = "2.0.20" mockk = "1.13.12" slf4j = "2.0.13" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } -androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } -androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } -androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } From eb50f30122721c2314d0249b7b873b6cc0da95e0 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 14 Oct 2024 17:36:31 +0200 Subject: [PATCH 72/92] Update dependencies and gradle --- .github/workflows/test-dev.yml | 6 +++--- gradle/libs.versions.toml | 18 +++++++----------- gradle/wrapper/gradle-wrapper.properties | 2 +- jitpack.yml | 6 +++--- lib/build.gradle.kts | 14 +++----------- 5 files changed, 17 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index 153c34f8..1e4429b4 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 21 # See https://community.gradle.org/github-actions/docs/setup-gradle/ for more information - uses: gradle/actions/setup-gradle@v3 # creates build cache when on main branch @@ -29,7 +29,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 21 - uses: gradle/actions/setup-gradle@v3 with: cache-encryption-key: ${{ secrets.gradle_encryption_key }} @@ -50,7 +50,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 21 - uses: gradle/actions/setup-gradle@v3 with: cache-encryption-key: ${{ secrets.gradle_encryption_key }} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b3a09fc..74161f8b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,30 +1,26 @@ [versions] -agp = "8.5.2" +agp = "8.7.0" android-desugar = "2.1.2" androidx-core = "1.13.1" -androidx-test-core = "1.6.1" androidx-test-rules = "1.6.1" androidx-test-runner = "1.6.2" -# noinspection GradleDependency -commons-collections = "4.2" -# noinspection GradleDependency -commons-lang = "3.8.1" -# noinspection GradleDependency -commons-io = "2.6" +commons-codec = { strictly = "1.17.1" } +commons-lang = { strictly = "3.15.0" } dokka ="1.9.20" # noinspection GradleDependency ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" -kotlin = "2.0.20" -mockk = "1.13.12" +kotlin = "2.0.21" +mockk = "1.13.13" slf4j = "2.0.13" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } -androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec" } +commons-lang = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" } ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0d184210..1e2fbf0d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jitpack.yml b/jitpack.yml index f29a6676..d62dbac2 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,5 @@ jdk: - - openjdk17 + - openjdk21 before_install: - - sdk install java 17.0.1-open - - sdk use java 17.0.1-open \ No newline at end of file + - sdk install java 21.0.4-tem + - sdk use java 21.0.4-tem \ No newline at end of file diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 428ab71a..dfc88d93 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -29,12 +29,9 @@ android { compileOptions { // ical4j >= 3.x uses the Java 8 Time API isCoreLibraryDesugaringEnabled = true - - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 } kotlin { - jvmToolchain(17) + jvmToolchain(21) } buildFeatures.buildConfig = true @@ -101,15 +98,10 @@ dependencies { exclude(group = "commons-validator", module = "commons-validator") } // Force latest version of commons libraries - implementation("org.apache.commons:commons-lang3") { - version { require("3.15.0") } - } - implementation("commons-codec:commons-codec") { - version { require("1.17.1") } - } + implementation(libs.commons.codec) + implementation(libs.commons.lang) implementation(libs.slf4j) // ical4j logging over java.util.Logger - androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.mockk.android) From 46d17997d042c2f3105b96b3223a4adf3c4b01a6 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 22 Oct 2024 13:32:52 +0200 Subject: [PATCH 73/92] Upgrade AGP and increase compile SDK (#176) * Upgraded AGP to `8.7.1` Signed-off-by: Arnau Mora Gras * Increase compile sdk to 35 Signed-off-by: Arnau Mora Gras * Made `installedVersionName` nullable Signed-off-by: Arnau Mora Gras --------- Signed-off-by: Arnau Mora Gras --- gradle/libs.versions.toml | 2 +- lib/build.gradle.kts | 2 +- lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74161f8b..5cde9f22 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.0" +agp = "8.7.1" android-desugar = "2.1.2" androidx-core = "1.13.1" androidx-test-rules = "1.6.1" diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index dfc88d93..748c1ad3 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } android { - compileSdk = 34 + compileSdk = 35 namespace = "at.bitfire.ical4android" diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt index 287cae94..7a2c02a1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt @@ -10,10 +10,10 @@ import android.content.Context import android.content.pm.PackageManager import androidx.core.content.pm.PackageInfoCompat import at.bitfire.ical4android.util.MiscUtils.closeCompat -import org.dmfs.tasks.contract.TaskContract import java.io.Closeable import java.util.logging.Level import java.util.logging.Logger +import org.dmfs.tasks.contract.TaskContract class TaskProvider private constructor( @@ -143,7 +143,7 @@ class TaskProvider private constructor( class ProviderTooOldException( val provider: ProviderName, installedVersionCode: Long, - val installedVersionName: String + val installedVersionName: String? ): Exception("Package ${provider.packageName} has version $installedVersionName ($installedVersionCode), " + "required: ${provider.minVersionName} (${provider.minVersionCode})") From 59d272814d6ce9daaabd69e870b280661e92c631 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 30 Oct 2024 13:44:33 +0100 Subject: [PATCH 74/92] AndroidEvent: update without event color correctly resets color (#180) --- .../bitfire/ical4android/AndroidEventTest.kt | 36 +++++++++++++++++-- .../at/bitfire/ical4android/AndroidEvent.kt | 36 ++++++++++++------- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index 4883b0bb..6a293181 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -581,7 +581,6 @@ class AndroidEventTest { @Test fun testBuildEvent_Color_WhenNotAvailable() { - AndroidCalendar.removeColors(provider, testAccount) buildEvent(true) { color = Css3Color.darkseagreen }.let { result -> @@ -1700,7 +1699,7 @@ class AndroidEventTest { } @Test - fun testPopulateEvent_Color() { + fun testPopulateEvent_Color_FromIndex() { AndroidCalendar.insertColors(provider, testAccount) populateEvent(true) { put(Events.EVENT_COLOR_KEY, Css3Color.silver.name) @@ -1709,6 +1708,15 @@ class AndroidEventTest { } } + @Test + fun testPopulateEvent_Color_FromValue() { + populateEvent(true) { + put(Events.EVENT_COLOR, Css3Color.silver.argb) + }.let { result -> + assertEquals(Css3Color.silver, result.color) + } + } + @Test fun testPopulateEvent_Status_Confirmed() { populateEvent(true) { @@ -2357,6 +2365,30 @@ class AndroidEventTest { } } + @Test + fun testUpdateEvent_ResetColor() { + // add event with color + val event = Event().apply { + uid = "sample1@testAddEvent" + dtStart = DtStart(DateTime()) + color = Css3Color.silver + } + val uri = TestEvent(calendar, event).add() + val id = ContentUris.parseId(uri) + + // verify that it has color + val beforeUpdate = calendar.findById(id) + assertNotNull(beforeUpdate.event?.color) + + // update: reset color + event.color = null + beforeUpdate.update(event) + + // verify that it doesn't have color anymore + val afterUpdate = calendar.findById(id) + assertNull(afterUpdate.event!!.color) + } + @Test fun testUpdateEvent_UpdateStatusFromNull() { val event = Event() diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 9905cc13..263846e2 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -336,13 +336,19 @@ abstract class AndroidEvent( event.location = row.getAsString(Events.EVENT_LOCATION) event.description = row.getAsString(Events.DESCRIPTION) - row.getAsString(Events.EVENT_COLOR_KEY)?.let { name -> - try { - event.color = Css3Color.valueOf(name) - } catch (e: IllegalArgumentException) { - logger.warning("Ignoring unknown color $name from Calendar Provider") + // color can be specified as RGB value and/or as index key (CSS3 color of AndroidCalendar) + event.color = + row.getAsString(Events.EVENT_COLOR_KEY)?.let { name -> // try color key first + try { + Css3Color.valueOf(name) + } catch (_: IllegalArgumentException) { + logger.warning("Ignoring unknown color name \"$name\"") + null + } + } ?: + row.getAsInteger(Events.EVENT_COLOR)?.let { color -> // otherwise, try to find the color name from the value + Css3Color.entries.firstOrNull { it.argb == color } } - } // status when (row.getAsInteger(Events.STATUS)) { @@ -928,18 +934,22 @@ abstract class AndroidEvent( builder.withValue(Events.TITLE, event.summary) builder.withValue(Events.EVENT_LOCATION, event.location) builder.withValue(Events.DESCRIPTION, event.description) - builder.withValue(Events.EVENT_COLOR_KEY, event.color?.let { color -> - val colorName = color.name + + val color = event.color + if (color != null) { // set event color (if it's available for this account) calendar.provider.query(Colors.CONTENT_URI.asSyncAdapter(calendar.account), arrayOf(Colors.COLOR_KEY), - "${Colors.COLOR_KEY}=? AND ${Colors.COLOR_TYPE}=${Colors.TYPE_EVENT}", arrayOf(colorName), null)?.use { cursor -> + "${Colors.COLOR_KEY}=? AND ${Colors.COLOR_TYPE}=${Colors.TYPE_EVENT}", arrayOf(color.name), null)?.use { cursor -> if (cursor.moveToNext()) - return@let colorName + builder.withValue(Events.EVENT_COLOR_KEY, color.name) else - logger.fine("Ignoring event color: $colorName (not available for this account)") + logger.fine("Ignoring event color \"${color.name}\" (not available for this account)") } - null - }) + } else { + // reset color index and value + builder .withValue(Events.EVENT_COLOR_KEY, null) + .withValue(Events.EVENT_COLOR, null) + } // scheduling val groupScheduled = event.attendees.isNotEmpty() From d01dba06fcc57717cfc55d0ce6423538be37da7b Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 13 Nov 2024 15:30:17 +0100 Subject: [PATCH 75/92] FixInvalidDayOffsetPreprocessor: Allow DURATION as value (#182) * Add test * Allow duration as value * Restrict the regex to properties that can have DURATION as a value * Tighten regex further * Convert negative asserts (not equals) to positive asserts (test for what is expected) * Test KDoc * Use capturing group; Add comments * Use replaceRange --------- Co-authored-by: Ricki Hirner --- .../FixInvalidDayOffsetPreprocessor.kt | 24 +++---- .../validation/StreamPreprocessor.kt | 8 ++- .../FixInvalidDayOffsetPreprocessorTest.kt | 62 ++++++++++++++----- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt index 54559628..85d99bf8 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt @@ -14,25 +14,27 @@ object FixInvalidDayOffsetPreprocessor : StreamPreprocessor() { // Examples: // TRIGGER:-P2DT // TRIGGER:-PT2D - "^(DURATION|TRIGGER):-?P((T-?\\d+D)|(-?\\d+DT))\$", + // REFRESH-INTERVAL;VALUE=DURATION:-PT1D + "(?:^|^(?:DURATION|REFRESH-INTERVAL|RELATED-TO|TRIGGER);VALUE=)(?:DURATION|TRIGGER):(-?P((T-?\\d+D)|(-?\\d+DT)))$", setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE) ) override fun fixString(original: String): String { - var s: String = original + var iCal: String = original // Find all instances matching the defined expression - val found = regexpForProblem().findAll(s) + val found = regexpForProblem().findAll(iCal).toList() - // ..and repair them - for (match in found) { - val matchStr = match.value - val fixed = matchStr - .replace("PT", "P") - .replace("DT", "D") - s = s.replace(matchStr, fixed) + // ... and repair them. Use reversed order so that already replaced occurrences don't interfere with the following matches. + for (match in found.reversed()) { + match.groups[1]?.let { duration -> // first capturing group is the duration value, for instance: "-PT1D" + val fixed = duration.value // fixed is then for instance: "-P1D" + .replace("PT", "P") + .replace("DT", "D") + iCal = iCal.replaceRange(duration.range, fixed) + } } - return s + return iCal } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt index 7bae3678..9e69ba11 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt @@ -13,6 +13,12 @@ abstract class StreamPreprocessor { abstract fun regexpForProblem(): Regex? + /** + * Fixes an iCalendar string. + * + * @param original The complete iCalendar string + * @return The complete iCalendar string, but fixed + */ abstract fun fixString(original: String): String fun preprocess(reader: Reader): Reader { @@ -21,7 +27,7 @@ abstract class StreamPreprocessor { val resetSupported = try { reader.reset() true - } catch(e: IOException) { + } catch(_: IOException) { false } diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt index 9a167ecd..0bf87f12 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt @@ -12,16 +12,23 @@ import java.time.Duration class FixInvalidDayOffsetPreprocessorTest { - private fun fixAndAssert(expected: String, testValue: String) { - + /** + * Calls [FixInvalidDayOffsetPreprocessor.fixString] and asserts the result is equal to [expected]. + * + * @param expected The expected result + * @param testValue The value to test + * @param parseDuration If `true`, [Duration.parse] is called on the fixed value to make sure it's a valid duration + */ + private fun assertFixedEquals(expected: String, testValue: String, parseDuration: Boolean = true) { // Fix the duration string val fixed = FixInvalidDayOffsetPreprocessor.fixString(testValue) // Test the duration can now be parsed - for (line in fixed.split('\n')) { - val duration = line.substring(line.indexOf(':') + 1) - Duration.parse(duration) - } + if (parseDuration) + for (line in fixed.split('\n')) { + val duration = line.substring(line.indexOf(':') + 1) + Duration.parse(duration) + } // Assert assertEquals(expected, fixed) @@ -35,36 +42,57 @@ class FixInvalidDayOffsetPreprocessorTest { ) } + @Test + fun test_FixString_SucceedsAsValueOnCorrectProperties() { + // By RFC 5545 the only properties allowed to hold DURATION as a VALUE are: + // DURATION, REFRESH, RELATED, TRIGGER + assertFixedEquals("DURATION;VALUE=DURATION:P1D", "DURATION;VALUE=DURATION:PT1D") + assertFixedEquals("REFRESH-INTERVAL;VALUE=DURATION:P1D", "REFRESH-INTERVAL;VALUE=DURATION:PT1D") + assertFixedEquals("RELATED-TO;VALUE=DURATION:P1D", "RELATED-TO;VALUE=DURATION:PT1D") + assertFixedEquals("TRIGGER;VALUE=DURATION:P1D", "TRIGGER;VALUE=DURATION:PT1D") + } + + @Test + fun test_FixString_FailsAsValueOnWrongProperty() { + // The update from RFC 2445 to RFC 5545 disallows using DURATION as a VALUE in FREEBUSY + assertFixedEquals("FREEBUSY;VALUE=DURATION:PT1D", "FREEBUSY;VALUE=DURATION:PT1D", parseDuration = false) + } + + @Test + fun test_FixString_FailsIfNotAtStartOfLine() { + assertFixedEquals("xxDURATION;VALUE=DURATION:PT1D", "xxDURATION;VALUE=DURATION:PT1D", parseDuration = false) + } + @Test fun test_FixString_DayOffsetFrom_Invalid() { - fixAndAssert("DURATION:-P1D", "DURATION:-PT1D") - fixAndAssert("TRIGGER:-P2D", "TRIGGER:-PT2D") + assertFixedEquals("DURATION:-P1D", "DURATION:-PT1D") + assertFixedEquals("TRIGGER:-P2D", "TRIGGER:-PT2D") - fixAndAssert("DURATION:-P1D", "DURATION:-P1DT") - fixAndAssert("TRIGGER:-P2D", "TRIGGER:-P2DT") + assertFixedEquals("DURATION:-P1D", "DURATION:-P1DT") + assertFixedEquals("TRIGGER:-P2D", "TRIGGER:-P2DT") } @Test fun test_FixString_DayOffsetFrom_Valid() { - fixAndAssert("DURATION:-PT12H", "DURATION:-PT12H") - fixAndAssert("TRIGGER:-PT12H", "TRIGGER:-PT12H") + assertFixedEquals("DURATION:-PT12H", "DURATION:-PT12H") + assertFixedEquals("TRIGGER:-PT12H", "TRIGGER:-PT12H") } @Test fun test_FixString_DayOffsetFromMultiple_Invalid() { - fixAndAssert("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-PT1D\nTRIGGER:-PT2D") - fixAndAssert("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-P1DT\nTRIGGER:-P2DT") + assertFixedEquals("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-PT1D\nTRIGGER:-PT2D") + assertFixedEquals("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-P1DT\nTRIGGER:-P2DT") } @Test fun test_FixString_DayOffsetFromMultiple_Valid() { - fixAndAssert("DURATION:-PT12H\nTRIGGER:-PT12H", "DURATION:-PT12H\nTRIGGER:-PT12H") + assertFixedEquals("DURATION:-PT12H\nTRIGGER:-PT12H", "DURATION:-PT12H\nTRIGGER:-PT12H") } @Test fun test_FixString_DayOffsetFromMultiple_Mixed() { - fixAndAssert("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-PT1D\nDURATION:-PT12H\nTRIGGER:-PT2D") - fixAndAssert("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-P1DT\nDURATION:-PT12H\nTRIGGER:-P2DT") + assertFixedEquals("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-PT1D\nDURATION:-PT12H\nTRIGGER:-PT2D") + assertFixedEquals("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-P1DT\nDURATION:-PT12H\nTRIGGER:-P2DT") } @Test From 16a4f223dab3fe204adb2c11320fce4e1d8f5807 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 20 Nov 2024 15:40:59 +0100 Subject: [PATCH 76/92] Update AGP and dependencies --- gradle/libs.versions.toml | 8 ++++---- lib/build.gradle.kts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5cde9f22..f833c759 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -agp = "8.7.1" -android-desugar = "2.1.2" -androidx-core = "1.13.1" +agp = "8.7.2" +android-desugar = "2.1.3" +androidx-core = "1.15.0" androidx-test-rules = "1.6.1" androidx-test-runner = "1.6.2" commons-codec = { strictly = "1.17.1" } @@ -12,7 +12,7 @@ ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" kotlin = "2.0.21" mockk = "1.13.13" -slf4j = "2.0.13" +slf4j = "2.0.16" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 748c1ad3..8df4e0c2 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -106,4 +106,4 @@ dependencies { androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.mockk.android) testImplementation(libs.junit) -} +} \ No newline at end of file From 6bf1b14351c407decc4e1c80b029e960696292a1 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 20 Nov 2024 15:43:17 +0100 Subject: [PATCH 77/92] Events: map no CLASSification to Android ACCESS_DEFAULT instead of ACCESS_PUBLIC (#183) --- .../kotlin/at/bitfire/ical4android/AndroidEventTest.kt | 2 +- lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index 6a293181..fc28e4e2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -745,7 +745,7 @@ class AndroidEventTest { fun testBuildEvent_Classification_None() { buildEvent(true) { }.let { result -> - assertEquals(Events.ACCESS_PUBLIC, result.getAsInteger(Events.ACCESS_LEVEL)) + assertEquals(Events.ACCESS_DEFAULT, result.getAsInteger(Events.ACCESS_LEVEL)) assertNull(firstUnknownProperty(result)) } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 263846e2..48c023fe 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -983,7 +983,8 @@ abstract class AndroidEvent( builder .withValue(Events.AVAILABILITY, if (event.opaque) Events.AVAILABILITY_BUSY else Events.AVAILABILITY_FREE) .withValue(Events.ACCESS_LEVEL, when (event.classification) { - null, Clazz.PUBLIC -> Events.ACCESS_PUBLIC + null -> Events.ACCESS_DEFAULT + Clazz.PUBLIC -> Events.ACCESS_PUBLIC Clazz.CONFIDENTIAL -> Events.ACCESS_CONFIDENTIAL else /* including Events.ACCESS_PRIVATE */ -> Events.ACCESS_PRIVATE }) From aa59e9e5f8a82da8ce33167d8ed6db27bee7cfcc Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 20 Nov 2024 16:05:24 +0100 Subject: [PATCH 78/92] Update gradle wrapper --- .github/workflows/build-kdoc.yml | 2 +- .github/workflows/test-dev.yml | 6 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 + gradlew | 285 ++++++++++++++--------- gradlew.bat | 37 +-- 6 files changed, 203 insertions(+), 129 deletions(-) diff --git a/.github/workflows/build-kdoc.yml b/.github/workflows/build-kdoc.yml index c3de0cda..325b2dd2 100644 --- a/.github/workflows/build-kdoc.yml +++ b/.github/workflows/build-kdoc.yml @@ -18,7 +18,7 @@ jobs: with: distribution: temurin java-version: 17 - - uses: gradle/actions/setup-gradle@v3 + - uses: gradle/actions/setup-gradle@v4 - name: Build KDoc run: ./gradlew --no-configuration-cache dokkaHtml diff --git a/.github/workflows/test-dev.yml b/.github/workflows/test-dev.yml index 1e4429b4..2d0d6770 100644 --- a/.github/workflows/test-dev.yml +++ b/.github/workflows/test-dev.yml @@ -12,7 +12,7 @@ jobs: java-version: 21 # See https://community.gradle.org/github-actions/docs/setup-gradle/ for more information - - uses: gradle/actions/setup-gradle@v3 # creates build cache when on main branch + - uses: gradle/actions/setup-gradle@v4 # creates build cache when on main branch with: cache-encryption-key: ${{ secrets.gradle_encryption_key }} gradle-home-cache-cleanup: true # clean up unused files @@ -30,7 +30,7 @@ jobs: with: distribution: temurin java-version: 21 - - uses: gradle/actions/setup-gradle@v3 + - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.gradle_encryption_key }} cache-read-only: true @@ -51,7 +51,7 @@ jobs: with: distribution: temurin java-version: 21 - - uses: gradle/actions/setup-gradle@v3 + - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.gradle_encryption_key }} cache-read-only: true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 59203 zcma&O1CT9Y(k9%tZQHhO+qUh#ZQHhO+qmuS+qP|E@9xZO?0h@l{(r>DQ>P;GjjD{w zH}lENr;dU&FbEU?00aa80D$0M0RRB{U*7-#kbjS|qAG&4l5%47zyJ#WrfA#1$1Ctx zf&Z_d{GW=lf^w2#qRJ|CvSJUi(^E3iv~=^Z(zH}F)3Z%V3`@+rNB7gTVU{Bb~90p|f+0(v;nz01EG7yDMX9@S~__vVgv%rS$+?IH+oZ03D5zYrv|^ zC1J)SruYHmCki$jLBlTaE5&dFG9-kq3!^i>^UQL`%gn6)jz54$WDmeYdsBE9;PqZ_ zoGd=P4+|(-u4U1dbAVQrFWoNgNd;0nrghPFbQrJctO>nwDdI`Q^i0XJDUYm|T|RWc zZ3^Qgo_Qk$%Fvjj-G}1NB#ZJqIkh;kX%V{THPqOyiq)d)0+(r9o(qKlSp*hmK#iIY zA^)Vr$-Hz<#SF=0@tL@;dCQsm`V9s1vYNq}K1B)!XSK?=I1)tX+bUV52$YQu*0%fnWEukW>mxkz+%3-S!oguE8u#MGzST8_Dy^#U?fA@S#K$S@9msUiX!gd_ow>08w5)nX{-KxqMOo7d?k2&?Vf z&diGDtZr(0cwPe9z9FAUSD9KC)7(n^lMWuayCfxzy8EZsns%OEblHFSzP=cL6}?J| z0U$H!4S_TVjj<`6dy^2j`V`)mC;cB%* z8{>_%E1^FH!*{>4a7*C1v>~1*@TMcLK{7nEQ!_igZC}ikJ$*<$yHy>7)oy79A~#xE zWavoJOIOC$5b6*q*F_qN1>2#MY)AXVyr$6x4b=$x^*aqF*L?vmj>Mgv+|ITnw_BoW zO?jwHvNy^prH{9$rrik1#fhyU^MpFqF2fYEt(;4`Q&XWOGDH8k6M=%@fics4ajI;st# zCU^r1CK&|jzUhRMv;+W~6N;u<;#DI6cCw-otsc@IsN3MoSD^O`eNflIoR~l4*&-%RBYk@gb^|-JXs&~KuSEmMxB}xSb z@K76cXD=Y|=I&SNC2E+>Zg?R6E%DGCH5J1nU!A|@eX9oS(WPaMm==k2s_ueCqdZw| z&hqHp)47`c{BgwgvY2{xz%OIkY1xDwkw!<0veB#yF4ZKJyabhyyVS`gZepcFIk%e2 zTcrmt2@-8`7i-@5Nz>oQWFuMC_KlroCl(PLSodswHqJ3fn<;gxg9=}~3x_L3P`9Sn zChIf}8vCHvTriz~T2~FamRi?rh?>3bX1j}%bLH+uFX+p&+^aXbOK7clZxdU~6Uxgy z8R=obwO4dL%pmVo*Ktf=lH6hnlz_5k3cG;m8lgaPp~?eD!Yn2kf)tU6PF{kLyn|oI@eQ`F z3IF7~Blqg8-uwUuWZScRKn%c2_}dXB6Dx_&xR*n9M9LXasJhtZdr$vBY!rP{c@=)& z#!?L$2UrkvClwQO>U*fSMs67oSj2mxiJ$t;E|>q%Kh_GzzWWO&3;ufU%2z%ucBU8H z3WIwr$n)cfCXR&>tyB7BcSInK>=ByZA%;cVEJhcg<#6N{aZC4>K41XF>ZgjG`z_u& zGY?;Ad?-sgiOnI`oppF1o1Gurqbi*;#x2>+SSV6|1^G@ooVy@fg?wyf@0Y!UZ4!}nGuLeC^l)6pwkh|oRY`s1Pm$>zZ3u-83T|9 zGaKJIV3_x+u1>cRibsaJpJqhcm%?0-L;2 zitBrdRxNmb0OO2J%Y&Ym(6*`_P3&&5Bw157{o7LFguvxC$4&zTy#U=W*l&(Q2MNO} zfaUwYm{XtILD$3864IA_nn34oVa_g^FRuHL5wdUd)+W-p-iWCKe8m_cMHk+=? zeKX)M?Dt(|{r5t7IenkAXo%&EXIb-i^w+0CX0D=xApC=|Xy(`xy+QG^UyFe z+#J6h_&T5i#sV)hj3D4WN%z;2+jJcZxcI3*CHXGmOF3^)JD5j&wfX)e?-|V0GPuA+ zQFot%aEqGNJJHn$!_}#PaAvQ^{3-Ye7b}rWwrUmX53(|~i0v{}G_sI9uDch_brX&6 zWl5Ndj-AYg(W9CGfQf<6!YmY>Ey)+uYd_JNXH=>|`OH-CDCmcH(0%iD_aLlNHKH z7bcW-^5+QV$jK?R*)wZ>r9t}loM@XN&M-Pw=F#xn(;u3!(3SXXY^@=aoj70;_=QE9 zGghsG3ekq#N||u{4We_25U=y#T*S{4I{++Ku)> zQ!DZW;pVcn>b;&g2;YE#+V`v*Bl&Y-i@X6D*OpNA{G@JAXho&aOk(_j^weW{#3X5Y z%$q_wpb07EYPdmyH(1^09i$ca{O<}7) zRWncXdSPgBE%BM#by!E>tdnc$8RwUJg1*x($6$}ae$e9Knj8gvVZe#bLi!<+&BkFj zg@nOpDneyc+hU9P-;jmOSMN|*H#>^Ez#?;%C3hg_65leSUm;iz)UkW)jX#p)e&S&M z1|a?wDzV5NVnlhRBCd_;F87wp>6c<&nkgvC+!@KGiIqWY4l}=&1w7|r6{oBN8xyzh zG$b#2=RJp_iq6)#t5%yLkKx(0@D=C3w+oiXtSuaQ%I1WIb-eiE$d~!)b@|4XLy!CZ z9p=t=%3ad@Ep+<9003D2KZ5VyP~_n$=;~r&YUg5UZ0KVD&tR1DHy9x)qWtKJp#Kq# zP*8p#W(8JJ_*h_3W}FlvRam?<4Z+-H77^$Lvi+#vmhL9J zJ<1SV45xi;SrO2f=-OB(7#iNA5)x1uNC-yNxUw|!00vcW2PufRm>e~toH;M0Q85MQLWd?3O{i8H+5VkR@l9Dg-ma ze2fZ%>G(u5(k9EHj2L6!;(KZ8%8|*-1V|B#EagbF(rc+5iL_5;Eu)L4Z-V;0HfK4d z*{utLse_rvHZeQ>V5H=f78M3Ntg1BPxFCVD{HbNA6?9*^YIq;B-DJd{Ca2L#)qWP? zvX^NhFmX?CTWw&Ns}lgs;r3i+Bq@y}Ul+U%pzOS0Fcv9~aB(0!>GT0)NO?p=25LjN z2bh>6RhgqD7bQj#k-KOm@JLgMa6>%-ok1WpOe)FS^XOU{c?d5shG(lIn3GiVBxmg`u%-j=)^v&pX1JecJics3&jvPI)mDut52? z3jEA)DM%}BYbxxKrizVYwq?(P&19EXlwD9^-6J+4!}9{ywR9Gk42jjAURAF&EO|~N z)?s>$Da@ikI4|^z0e{r`J8zIs>SpM~Vn^{3fArRu;?+43>lD+^XtUcY1HidJwnR6+ z!;oG2=B6Z_=M%*{z-RaHc(n|1RTKQdNjjV!Pn9lFt^4w|AeN06*j}ZyhqZ^!-=cyGP_ShV1rGxkx8t zB;8`h!S{LD%ot``700d0@Grql(DTt4Awgmi+Yr0@#jbe=2#UkK%rv=OLqF)9D7D1j z!~McAwMYkeaL$~kI~90)5vBhBzWYc3Cj1WI0RS`z000R8-@ET0dA~*r(gSiCJmQMN&4%1D zyVNf0?}sBH8zNbBLn>~(W{d3%@kL_eQ6jEcR{l>C|JK z(R-fA!z|TTRG40|zv}7E@PqCAXP3n`;%|SCQ|ZS%ym$I{`}t3KPL&^l5`3>yah4*6 zifO#{VNz3)?ZL$be;NEaAk9b#{tV?V7 zP|wf5YA*1;s<)9A4~l3BHzG&HH`1xNr#%){4xZ!jq%o=7nN*wMuXlFV{HaiQLJ`5G zBhDi#D(m`Q1pLh@Tq+L;OwuC52RdW7b8}~60WCOK5iYMUad9}7aWBuILb({5=z~YF zt?*Jr5NG+WadM{mDL>GyiByCuR)hd zA=HM?J6l1Xv0Dl+LW@w$OTcEoOda^nFCw*Sy^I@$sSuneMl{4ys)|RY#9&NxW4S)9 zq|%83IpslTLoz~&vTo!Ga@?rj_kw{|k{nv+w&Ku?fyk4Ki4I?);M|5Axm)t+BaE)D zm(`AQ#k^DWrjbuXoJf2{Aj^KT zFb1zMSqxq|vceV+Mf-)$oPflsO$@*A0n0Z!R{&(xh8s}=;t(lIy zv$S8x>m;vQNHuRzoaOo?eiWFe{0;$s`Bc+Osz~}Van${u;g(su`3lJ^TEfo~nERfP z)?aFzpDgnLYiERsKPu|0tq4l2wT)Atr6Qb%m-AUn6HnCue*yWICp7TjW$@sO zm5rm4aTcPQ(rfi7a`xP7cKCFrJD}*&_~xgLyr^-bmsL}y;A5P|al8J3WUoBSjqu%v zxC;mK!g(7r6RRJ852Z~feoC&sD3(6}^5-uLK8o)9{8L_%%rItZK9C){UxB|;G>JbP zsRRtS4-3B*5c+K2kvmgZK8472%l>3cntWUOVHxB|{Ay~aOg5RN;{PJgeVD*H%ac+y!h#wi%o2bF2Ca8IyMyH{>4#{E_8u^@+l-+n=V}Sq?$O z{091@v%Bd*3pk0^2UtiF9Z+(a@wy6 zUdw8J*ze$K#=$48IBi1U%;hmhO>lu!uU;+RS}p&6@rQila7WftH->*A4=5W|Fmtze z)7E}jh@cbmr9iup^i%*(uF%LG&!+Fyl@LFA-}Ca#bxRfDJAiR2dt6644TaYw1Ma79 zt8&DYj31j^5WPNf5P&{)J?WlCe@<3u^78wnd(Ja4^a>{^Tw}W>|Cjt^If|7l^l)^Q zbz|7~CF(k_9~n|h;ysZ+jHzkXf(*O*@5m zLzUmbHp=x!Q|!9NVXyipZ3)^GuIG$k;D)EK!a5=8MFLI_lpf`HPKl=-Ww%z8H_0$j ztJ||IfFG1lE9nmQ0+jPQy zCBdKkjArH@K7jVcMNz);Q(Q^R{d5G?-kk;Uu_IXSyWB)~KGIizZL(^&qF;|1PI7!E zTP`%l)gpX|OFn&)M%txpQ2F!hdA~hX1Cm5)IrdljqzRg!f{mN%G~H1&oqe`5eJCIF zHdD7O;AX-{XEV(a`gBFJ9ews#CVS2y!&>Cm_dm3C8*n3MA*e67(WC?uP@8TXuMroq z{#w$%z@CBIkRM7?}Xib+>hRjy?%G!fiw8! z8(gB+8J~KOU}yO7UGm&1g_MDJ$IXS!`+*b*QW2x)9>K~Y*E&bYMnjl6h!{17_8d!%&9D`a7r&LKZjC<&XOvTRaKJ1 zUY@hl5^R&kZl3lU3njk`3dPzxj$2foOL26r(9zsVF3n_F#v)s5vv3@dgs|lP#eylq62{<-vczqP!RpVBTgI>@O6&sU>W|do17+#OzQ7o5A$ICH z?GqwqnK^n2%LR;$^oZM;)+>$X3s2n}2jZ7CdWIW0lnGK-b#EG01)P@aU`pg}th&J-TrU`tIpb5t((0eu|!u zQz+3ZiOQ^?RxxK4;zs=l8q!-n7X{@jSwK(iqNFiRColuEOg}!7cyZi`iBX4g1pNBj zAPzL?P^Ljhn;1$r8?bc=#n|Ed7wB&oHcw()&*k#SS#h}jO?ZB246EGItsz*;^&tzp zu^YJ0=lwsi`eP_pU8}6JA7MS;9pfD;DsSsLo~ogzMNP70@@;Fm8f0^;>$Z>~}GWRw!W5J3tNX*^2+1f3hz{~rIzJo z6W%J(H!g-eI_J1>0juX$X4Cl6i+3wbc~k146UIX&G22}WE>0ga#WLsn9tY(&29zBvH1$`iWtTe zG2jYl@P!P)eb<5DsR72BdI7-zP&cZNI{7q3e@?N8IKc4DE#UVr->|-ryuJXk^u^>4 z$3wE~=q390;XuOQP~TNoDR?#|NSPJ%sTMInA6*rJ%go|=YjGe!B>z6u$IhgQSwoV* zjy3F2#I>uK{42{&IqP59)Y(1*Z>>#W8rCf4_eVsH)`v!P#^;BgzKDR`ARGEZzkNX+ zJUQu=*-ol=Xqqt5=`=pA@BIn@6a9G8C{c&`i^(i+BxQO9?YZ3iu%$$da&Kb?2kCCo zo7t$UpSFWqmydXf@l3bVJ=%K?SSw)|?srhJ-1ZdFu*5QhL$~-IQS!K1s@XzAtv6*Y zl8@(5BlWYLt1yAWy?rMD&bwze8bC3-GfNH=p zynNFCdxyX?K&G(ZZ)afguQ2|r;XoV^=^(;Cku#qYn4Lus`UeKt6rAlFo_rU`|Rq z&G?~iWMBio<78of-2X(ZYHx~=U0Vz4btyXkctMKdc9UM!vYr~B-(>)(Hc|D zMzkN4!PBg%tZoh+=Gba!0++d193gbMk2&krfDgcbx0jI92cq?FFESVg0D$>F+bil} zY~$)|>1HZsX=5sAZ2WgPB5P=8X#TI+NQ(M~GqyVB53c6IdX=k>Wu@A0Svf5#?uHaF zsYn|koIi3$(%GZ2+G+7Fv^lHTb#5b8sAHSTnL^qWZLM<(1|9|QFw9pnRU{svj}_Al zL)b9>fN{QiA($8peNEJyy`(a{&uh-T4_kdZFIVsKKVM(?05}76EEz?#W za^fiZOAd14IJ4zLX-n7Lq0qlQ^lW8Cvz4UKkV9~P}>sq0?xD3vg+$4vLm~C(+ zM{-3Z#qnZ09bJ>}j?6ry^h+@PfaD7*jZxBEY4)UG&daWb??6)TP+|3#Z&?GL?1i+280CFsE|vIXQbm| zM}Pk!U`U5NsNbyKzkrul-DzwB{X?n3E6?TUHr{M&+R*2%yOiXdW-_2Yd6?38M9Vy^ z*lE%gA{wwoSR~vN0=no}tP2Ul5Gk5M(Xq`$nw#ndFk`tcpd5A=Idue`XZ!FS>Q zG^0w#>P4pPG+*NC9gLP4x2m=cKP}YuS!l^?sHSFftZy{4CoQrb_ z^20(NnG`wAhMI=eq)SsIE~&Gp9Ne0nD4%Xiu|0Fj1UFk?6avDqjdXz{O1nKao*46y zT8~iA%Exu=G#{x=KD;_C&M+Zx4+n`sHT>^>=-1YM;H<72k>$py1?F3#T1*ef9mLZw z5naLQr?n7K;2l+{_uIw*_1nsTn~I|kkCgrn;|G~##hM;9l7Jy$yJfmk+&}W@JeKcF zx@@Woiz8qdi|D%aH3XTx5*wDlbs?dC1_nrFpm^QbG@wM=i2?Zg;$VK!c^Dp8<}BTI zyRhAq@#%2pGV49*Y5_mV4+OICP|%I(dQ7x=6Ob}>EjnB_-_18*xrY?b%-yEDT(wrO z9RY2QT0`_OpGfMObKHV;QLVnrK%mc?$WAdIT`kJQT^n%GuzE7|9@k3ci5fYOh(287 zuIbg!GB3xLg$YN=n)^pHGB0jH+_iIiC=nUcD;G6LuJsjn2VI1cyZx=a?ShCsF==QK z;q~*m&}L<-cb+mDDXzvvrRsybcgQ;Vg21P(uLv5I+eGc7o7tc6`;OA9{soHFOz zT~2?>Ts}gprIX$wRBb4yE>ot<8+*Bv`qbSDv*VtRi|cyWS>)Fjs>fkNOH-+PX&4(~ z&)T8Zam2L6puQl?;5zg9h<}k4#|yH9czHw;1jw-pwBM*O2hUR6yvHATrI%^mvs9q_ z&ccT0>f#eDG<^WG^q@oVqlJrhxH)dcq2cty@l3~|5#UDdExyXUmLQ}f4#;6fI{f^t zDCsgIJ~0`af%YR%Ma5VQq-p21k`vaBu6WE?66+5=XUd%Ay%D$irN>5LhluRWt7 zov-=f>QbMk*G##&DTQyou$s7UqjjW@k6=!I@!k+S{pP8R(2=e@io;N8E`EOB;OGoI zw6Q+{X1_I{OO0HPpBz!X!@`5YQ2)t{+!?M_iH25X(d~-Zx~cXnS9z>u?+If|iNJbx zyFU2d1!ITX64D|lE0Z{dLRqL1Ajj=CCMfC4lD3&mYR_R_VZ>_7_~|<^o*%_&jevU+ zQ4|qzci=0}Jydw|LXLCrOl1_P6Xf@c0$ieK2^7@A9UbF{@V_0p%lqW|L?5k>bVM8|p5v&2g;~r>B8uo<4N+`B zH{J)h;SYiIVx@#jI&p-v3dwL5QNV1oxPr8J%ooezTnLW>i*3Isb49%5i!&ac_dEXv zvXmVUck^QHmyrF8>CGXijC_R-y(Qr{3Zt~EmW)-nC!tiH`wlw5D*W7Pip;T?&j%kX z6DkZX4&}iw>hE(boLyjOoupf6JpvBG8}jIh!!VhnD0>}KSMMo{1#uU6kiFcA04~|7 zVO8eI&x1`g4CZ<2cYUI(n#wz2MtVFHx47yE5eL~8bot~>EHbevSt}LLMQX?odD{Ux zJMnam{d)W4da{l7&y-JrgiU~qY3$~}_F#G7|MxT)e;G{U`In&?`j<5D->}cb{}{T(4DF0BOk-=1195KB-E*o@c?`>y#4=dMtYtSY=&L{!TAjFVcq0y@AH`vH! z$41+u!Ld&}F^COPgL(EE{0X7LY&%D7-(?!kjFF7=qw<;`V{nwWBq<)1QiGJgUc^Vz ztMUlq1bZqKn17|6x6iAHbWc~l1HcmAxr%$Puv!znW)!JiukwIrqQ00|H$Z)OmGG@= zv%A8*4cq}(?qn4rN6o`$Y))(MyXr8R<2S^J+v(wmFmtac!%VOfN?&(8Nr!T@kV`N; z*Q33V3t`^rN&aBiHet)18wy{*wi1=W!B%B-Q6}SCrUl$~Hl{@!95ydml@FK8P=u4s z4e*7gV2s=YxEvskw2Ju!2%{8h01rx-3`NCPc(O zH&J0VH5etNB2KY6k4R@2Wvl^Ck$MoR3=)|SEclT2ccJ!RI9Nuter7u9@;sWf-%um;GfI!=eEIQ2l2p_YWUd{|6EG ze{yO6;lMc>;2tPrsNdi@&1K6(1;|$xe8vLgiouj%QD%gYk`4p{Ktv9|j+!OF-P?@p z;}SV|oIK)iwlBs+`ROXkhd&NK zzo__r!B>tOXpBJMDcv!Mq54P+n4(@dijL^EpO1wdg~q+!DT3lB<>9AANSe!T1XgC=J^)IP0XEZ()_vpu!!3HQyJhwh?r`Ae%Yr~b% zO*NY9t9#qWa@GCPYOF9aron7thfWT`eujS4`t2uG6)~JRTI;f(ZuoRQwjZjp5Pg34 z)rp$)Kr?R+KdJ;IO;pM{$6|2y=k_siqvp%)2||cHTe|b5Ht8&A{wazGNca zX$Ol?H)E_R@SDi~4{d-|8nGFhZPW;Cts1;08TwUvLLv&_2$O6Vt=M)X;g%HUr$&06 zISZb(6)Q3%?;3r~*3~USIg=HcJhFtHhIV(siOwV&QkQe#J%H9&E21!C*d@ln3E@J* zVqRO^<)V^ky-R|%{(9`l-(JXq9J)1r$`uQ8a}$vr9E^nNiI*thK8=&UZ0dsFN_eSl z(q~lnD?EymWLsNa3|1{CRPW60>DSkY9YQ;$4o3W7Ms&@&lv9eH!tk~N&dhqX&>K@} zi1g~GqglxkZ5pEFkllJ)Ta1I^c&Bt6#r(QLQ02yHTaJB~- zCcE=5tmi`UA>@P=1LBfBiqk)HB4t8D?02;9eXj~kVPwv?m{5&!&TFYhu>3=_ zsGmYZ^mo*-j69-42y&Jj0cBLLEulNRZ9vXE)8~mt9C#;tZs;=#M=1*hebkS;7(aGf zcs7zH(I8Eui9UU4L--))yy`&d&$In&VA2?DAEss4LAPCLd>-$i?lpXvn!gu^JJ$(DoUlc6wE98VLZ*z`QGQov5l4Fm_h?V-;mHLYDVOwKz7>e4+%AzeO>P6v}ndPW| zM>m#6Tnp7K?0mbK=>gV}=@k*0Mr_PVAgGMu$j+pWxzq4MAa&jpCDU&-5eH27Iz>m^ zax1?*HhG%pJ((tkR(V(O(L%7v7L%!_X->IjS3H5kuXQT2!ow(;%FDE>16&3r){!ex zhf==oJ!}YU89C9@mfDq!P3S4yx$aGB?rbtVH?sHpg?J5C->!_FHM%Hl3#D4eplxzQ zRA+<@LD%LKSkTk2NyWCg7u=$%F#;SIL44~S_OGR}JqX}X+=bc@swpiClB`Zbz|f!4 z7Ysah7OkR8liXfI`}IIwtEoL}(URrGe;IM8%{>b1SsqXh)~w}P>yiFRaE>}rEnNkT z!HXZUtxUp1NmFm)Dm@-{FI^aRQqpSkz}ZSyKR%Y}YHNzBk)ZIp} zMtS=aMvkgWKm9&oTcU0?S|L~CDqA+sHpOxwnswF-fEG)cXCzUR?ps@tZa$=O)=L+5 zf%m58cq8g_o}3?Bhh+c!w4(7AjxwQ3>WnVi<{{38g7yFboo>q|+7qs<$8CPXUFAN< zG&}BHbbyQ5n|qqSr?U~GY{@GJ{(Jny{bMaOG{|IkUj7tj^9pa9|FB_<+KHLxSxR;@ zHpS$4V)PP+tx}22fWx(Ku9y+}Ap;VZqD0AZW4gCDTPCG=zgJmF{|x;(rvdM|2|9a}cex6xrMkERnkE;}jvU-kmzd%_J50$M`lIPCKf+^*zL=@LW`1SaEc%=m zQ+lT06Gw+wVwvQ9fZ~#qd430v2HndFsBa9WjD0P}K(rZYdAt^5WQIvb%D^Q|pkVE^ zte$&#~zmULFACGfS#g=2OLOnIf2Of-k!(BIHjs77nr!5Q1*I9 z1%?=~#Oss!rV~?-6Gm~BWJiA4mJ5TY&iPm_$)H1_rTltuU1F3I(qTQ^U$S>%$l z)Wx1}R?ij0idp@8w-p!Oz{&*W;v*IA;JFHA9%nUvVDy7Q8woheC#|8QuDZb-L_5@R zOqHwrh|mVL9b=+$nJxM`3eE{O$sCt$UK^2@L$R(r^-_+z?lOo+me-VW=Zw z-Bn>$4ovfWd%SPY`ab-u9{INc*k2h+yH%toDHIyqQ zO68=u`N}RIIs7lsn1D){)~%>ByF<>i@qFb<-axvu(Z+6t7v<^z&gm9McRB~BIaDn$ z#xSGT!rzgad8o>~kyj#h1?7g96tOcCJniQ+*#=b7wPio>|6a1Z?_(TS{)KrPe}(8j z!#&A=k(&Pj^F;r)CI=Z{LVu>uj!_W1q4b`N1}E(i%;BWjbEcnD=mv$FL$l?zS6bW!{$7j1GR5ocn94P2u{ z70tAAcpqtQo<@cXw~@i-@6B23;317|l~S>CB?hR5qJ%J3EFgyBdJd^fHZu7AzHF(BQ!tyAz^L0`X z23S4Fe{2X$W0$zu9gm%rg~A>ijaE#GlYlrF9$ds^QtaszE#4M(OLVP2O-;XdT(XIC zatwzF*)1c+t~c{L=fMG8Z=k5lv>U0;C{caN1NItnuSMp)6G3mbahu>E#sj&oy94KC zpH}8oEw{G@N3pvHhp{^-YaZeH;K+T_1AUv;IKD<=mv^&Ueegrb!yf`4VlRl$M?wsl zZyFol(2|_QM`e_2lYSABpKR{{NlxlDSYQNkS;J66aT#MSiTx~;tUmvs-b*CrR4w=f z8+0;*th6kfZ3|5!Icx3RV11sp=?`0Jy3Fs0N4GZQMN=8HmT6%x9@{Dza)k}UwL6JT zHRDh;%!XwXr6yuuy`4;Xsn0zlR$k%r%9abS1;_v?`HX_hI|+EibVnlyE@3aL5vhQq zlIG?tN^w@0(v9M*&L+{_+RQZw=o|&BRPGB>e5=ys7H`nc8nx)|-g;s7mRc7hg{GJC zAe^vCIJhajmm7C6g! zL&!WAQ~5d_5)00?w_*|*H>3$loHrvFbitw#WvLB!JASO?#5Ig5$Ys10n>e4|3d;tS zELJ0|R4n3Az(Fl3-r^QiV_C;)lQ1_CW{5bKS15U|E9?ZgLec@%kXr84>5jV2a5v=w z?pB1GPdxD$IQL4)G||B_lI+A=08MUFFR4MxfGOu07vfIm+j=z9tp~5i_6jb`tR>qV z$#`=BQ*jpCjm$F0+F)L%xRlnS%#&gro6PiRfu^l!EVan|r3y}AHJQOORGx4~ z&<)3=K-tx518DZyp%|!EqpU!+X3Et7n2AaC5(AtrkW>_57i}$eqs$rupubg0a1+WO zGHZKLN2L0D;ab%{_S1Plm|hx8R?O14*w*f&2&bB050n!R2by zw!@XOQx$SqZ5I<(Qu$V6g>o#A!JVwErWv#(Pjx=KeS0@hxr4?13zj#oWwPS(7Ro|v z>Mp@Kmxo79q|}!5qtX2-O@U&&@6s~!I&)1WQIl?lTnh6UdKT_1R640S4~f=_xoN3- zI+O)$R@RjV$F=>Ti7BlnG1-cFKCC(t|Qjm{SalS~V-tX#+2ekRhwmN zZr`8{QF6y~Z!D|{=1*2D-JUa<(1Z=;!Ei!KiRNH?o{p5o3crFF=_pX9O-YyJchr$~ zRC`+G+8kx~fD2k*ZIiiIGR<8r&M@3H?%JVOfE>)})7ScOd&?OjgAGT@WVNSCZ8N(p zuQG~76GE3%(%h1*vUXg$vH{ua0b`sQ4f0*y=u~lgyb^!#CcPJa2mkSEHGLsnO^kb$ zru5_l#nu=Y{rSMWiYx?nO{8I!gH+?wEj~UM?IrG}E|bRIBUM>UlY<`T1EHpRr36vv zBi&dG8oxS|J$!zoaq{+JpJy+O^W(nt*|#g32bd&K^w-t>!Vu9N!k9eA8r!Xc{utY> zg9aZ(D2E0gL#W0MdjwES-7~Wa8iubPrd?8-$C4BP?*wok&O8+ykOx{P=Izx+G~hM8 z*9?BYz!T8~dzcZr#ux8kS7u7r@A#DogBH8km8Ry4slyie^n|GrTbO|cLhpqgMdsjX zJ_LdmM#I&4LqqsOUIXK8gW;V0B(7^$y#h3h>J0k^WJfAMeYek%Y-Dcb_+0zPJez!GM zAmJ1u;*rK=FNM0Nf}Y!!P9c4)HIkMnq^b;JFd!S3?_Qi2G#LIQ)TF|iHl~WKK6JmK zbv7rPE6VkYr_%_BT}CK8h=?%pk@3cz(UrZ{@h40%XgThP*-Oeo`T0eq9 zA8BnWZKzCy5e&&_GEsU4*;_k}(8l_&al5K-V*BFM=O~;MgRkYsOs%9eOY6s6AtE*<7GQAR2ulC3RAJrG_P1iQK5Z~&B z&f8X<>yJV6)oDGIlS$Y*D^Rj(cszTy5c81a5IwBr`BtnC6_e`ArI8CaTX_%rx7;cn zR-0?J_LFg*?(#n~G8cXut(1nVF0Oka$A$1FGcERU<^ggx;p@CZc?3UB41RY+wLS`LWFNSs~YP zuw1@DNN3lTd|jDL7gjBsd9}wIw}4xT2+8dBQzI00m<@?c2L%>}QLfK5%r!a-iII`p zX@`VEUH)uj^$;7jVUYdADQ2k*!1O3WdfgF?OMtUXNpQ1}QINamBTKDuv19^{$`8A1 zeq%q*O0mi@(%sZU>Xdb0Ru96CFqk9-L3pzLVsMQ`Xpa~N6CR{9Rm2)A|CI21L(%GW zh&)Y$BNHa=FD+=mBw3{qTgw)j0b!Eahs!rZnpu)z!!E$*eXE~##yaXz`KE5(nQM`s zD!$vW9XH)iMxu9R>r$VlLk9oIR%HxpUiW=BK@4U)|1WNQ=mz9a z^!KkO=>GaJ!GBXm{KJj^;kh-MkUlEQ%lza`-G&}C5y1>La1sR6hT=d*NeCnuK%_LV zOXt$}iP6(YJKc9j-Fxq~*ItVUqljQ8?oaysB-EYtFQp9oxZ|5m0^Hq(qV!S+hq#g( z?|i*H2MIr^Kxgz+3vIljQ*Feejy6S4v~jKEPTF~Qhq!(ms5>NGtRgO5vfPPc4Z^AM zTj!`5xEreIN)vaNxa|q6qWdg>+T`Ol0Uz)ckXBXEGvPNEL3R8hB3=C5`@=SYgAju1 z!)UBr{2~=~xa{b8>x2@C7weRAEuatC)3pkRhT#pMPTpSbA|tan%U7NGMvzmF?c!V8 z=pEWxbdXbTAGtWTyI?Fml%lEr-^AE}w#l(<7OIw;ctw}imYax&vR4UYNJZK6P7ZOd zP87XfhnUHxCUHhM@b*NbTi#(-8|wcv%3BGNs#zRCVV(W?1Qj6^PPQa<{yaBwZ`+<`w|;rqUY_C z&AeyKwwf*q#OW-F()lir=T^<^wjK65Lif$puuU5+tk$;e_EJ;Lu+pH>=-8=PDhkBg z8cWt%@$Sc#C6F$Vd+0507;{OOyT7Hs%nKS88q-W!$f~9*WGBpHGgNp}=C*7!RiZ5s zn1L_DbKF@B8kwhDiLKRB@lsXVVLK|ph=w%_`#owlf@s@V(pa`GY$8h%;-#h@TsO|Y8V=n@*!Rog7<7Cid%apR|x zOjhHCyfbIt%+*PCveTEcuiDi%Wx;O;+K=W?OFUV%)%~6;gl?<0%)?snDDqIvkHF{ zyI02)+lI9ov42^hL>ZRrh*HhjF9B$A@=H94iaBESBF=eC_KT$8A@uB^6$~o?3Wm5t1OIaqF^~><2?4e3c&)@wKn9bD? zoeCs;H>b8DL^F&>Xw-xjZEUFFTv>JD^O#1E#)CMBaG4DX9bD(Wtc8Rzq}9soQ8`jf zeSnHOL}<+WVSKp4kkq&?SbETjq6yr@4%SAqOG=9E(3YeLG9dtV+8vmzq+6PFPk{L; z(&d++iu=^F%b+ea$i2UeTC{R*0Isk;vFK!no<;L+(`y`3&H-~VTdKROkdyowo1iqR zbVW(3`+(PQ2>TKY>N!jGmGo7oeoB8O|P_!Ic@ zZ^;3dnuXo;WJ?S+)%P>{Hcg!Jz#2SI(s&dY4QAy_vRlmOh)QHvs_7c&zkJCmJGVvV zX;Mtb>QE+xp`KyciG$Cn*0?AK%-a|=o!+7x&&yzHQOS>8=B*R=niSnta^Pxp1`=md z#;$pS$4WCT?mbiCYU?FcHGZ#)kHVJTTBt^%XE(Q};aaO=Zik0UgLcc0I(tUpt(>|& zcxB_|fxCF7>&~5eJ=Dpn&5Aj{A^cV^^}(7w#p;HG&Q)EaN~~EqrE1qKrMAc&WXIE;>@<&)5;gD2?={Xf@Mvn@OJKw=8Mgn z!JUFMwD+s==JpjhroT&d{$kQAy%+d`a*XxDEVxy3`NHzmITrE`o!;5ClXNPb4t*8P zzAivdr{j_v!=9!^?T3y?gzmqDWX6mkzhIzJ-3S{T5bcCFMr&RPDryMcdwbBuZbsgN zGrp@^i?rcfN7v0NKGzDPGE#4yszxu=I_`MI%Z|10nFjU-UjQXXA?k8Pk|OE<(?ae) zE%vG#eZAlj*E7_3dx#Zz4kMLj>H^;}33UAankJiDy5ZvEhrjr`!9eMD8COp}U*hP+ zF}KIYx@pkccIgyxFm#LNw~G&`;o&5)2`5aogs`1~7cMZQ7zj!%L4E`2yzlQN6REX20&O<9 zKV6fyr)TScJPPzNTC2gL+0x#=u>(({{D7j)c-%tvqls3#Y?Z1m zV5WUE)zdJ{$p>yX;^P!UcXP?UD~YM;IRa#Rs5~l+*$&nO(;Ers`G=0D!twR(0GF@c zHl9E5DQI}Oz74n zfKP>&$q0($T4y$6w(p=ERAFh+>n%iaeRA%!T%<^+pg?M)@ucY<&59$x9M#n+V&>}=nO9wCV{O~lg&v#+jcUj(tQ z`0u1YH)-`U$15a{pBkGyPL0THv1P|4e@pf@3IBZS4dVJPo#H>pWq%Lr0YS-SeWash z8R7=jb28KPMI|_lo#GEO|5B?N_e``H*23{~a!AmUJ+fb4HX-%QI@lSEUxKlGV7z7Q zSKw@-TR>@1RL%w{x}dW#k1NgW+q4yt2Xf1J62Bx*O^WG8OJ|FqI4&@d3_o8Id@*)4 zYrk=>@!wv~mh7YWv*bZhxqSmFh2Xq)o=m;%n$I?GSz49l1$xRpPu_^N(vZ>*>Z<04 z2+rP70oM=NDysd!@fQdM2OcyT?3T^Eb@lIC-UG=Bw{BjQ&P`KCv$AcJ;?`vdZ4){d z&gkoUK{$!$$K`3*O-jyM1~p-7T*qb)Ys>Myt^;#1&a%O@x8A+E>! zY8=eD`ZG)LVagDLBeHg>=atOG?Kr%h4B%E6m@J^C+U|y)XX@f z8oyJDW|9g=<#f<{JRr{y#~euMnv)`7j=%cHWLc}ngjq~7k**6%4u>Px&W%4D94(r* z+akunK}O0DC2A%Xo9jyF;DobX?!1I(7%}@7F>i%&nk*LMO)bMGg2N+1iqtg+r(70q zF5{Msgsm5GS7DT`kBsjMvOrkx&|EU!{{~gL4d2MWrAT=KBQ-^zQCUq{5PD1orxlIL zq;CvlWx#f1NWvh`hg011I%?T_s!e38l*lWVt|~z-PO4~~1g)SrJ|>*tXh=QfXT)%( z+ex+inPvD&O4Ur;JGz>$sUOnWdpSLcm1X%aQDw4{dB!cnj`^muI$CJ2%p&-kULVCE z>$eMR36kN$wCPR+OFDM3-U(VOrp9k3)lI&YVFqd;Kpz~K)@Fa&FRw}L(SoD z9B4a+hQzZT-BnVltst&=kq6Y(f^S4hIGNKYBgMxGJ^;2yrO}P3;r)(-I-CZ)26Y6? z&rzHI_1GCvGkgy-t1E;r^3Le30|%$ebDRu2+gdLG)r=A~Qz`}~&L@aGJ{}vVs_GE* zVUjFnzHiXfKQbpv&bR&}l2bzIjAooB)=-XNcYmrGmBh(&iu@o!^hn0^#}m2yZZUK8 zufVm7Gq0y`Mj;9b>`c?&PZkU0j4>IL=UL&-Lp3j&47B5pAW4JceG{!XCA)kT<%2nqCxj<)uy6XR_uws~>_MEKPOpAQ!H zkn>FKh)<9DwwS*|Y(q?$^N!6(51O0 z^JM~Ax{AI1Oj$fs-S5d4T7Z_i1?{%0SsIuQ&r8#(JA=2iLcTN+?>wOL532%&dMYkT z*T5xepC+V6zxhS@vNbMoi|i)=rpli@R9~P!39tWbSSb904ekv7D#quKbgFEMTb48P zuq(VJ+&L8aWU(_FCD$3^uD!YM%O^K(dvy~Wm2hUuh6bD|#(I39Xt>N1Y{ZqXL`Fg6 zKQ?T2htHN!(Bx;tV2bfTtIj7e)liN-29s1kew>v(D^@)#v;}C4-G=7x#;-dM4yRWm zyY`cS21ulzMK{PoaQ6xChEZ}o_#}X-o}<&0)$1#3we?+QeLt;aVCjeA)hn!}UaKt< zat1fHEx13y-rXNMvpUUmCVzocPmN~-Y4(YJvQ#db)4|%B!rBsgAe+*yor~}FrNH08 z3V!97S}D7d$zbSD{$z;@IYMxM6aHdypIuS*pr_U6;#Y!_?0i|&yU*@16l z*dcMqDQgfNBf}?quiu4e>H)yTVfsp#f+Du0@=Kc41QockXkCkvu>FBd6Q+@FL!(Yx z2`YuX#eMEiLEDhp+9uFqME_E^faV&~9qjBHJkIp~%$x^bN=N)K@kvSVEMdDuzA0sn z88CBG?`RX1@#hQNd`o^V{37)!w|nA)QfiYBE^m=yQKv-fQF+UCMcuEe1d4BH7$?>b zJl-r9@0^Ie=)guO1vOd=i$_4sz>y3x^R7n4ED!5oXL3@5**h(xr%Hv)_gILarO46q+MaDOF%ChaymKoI6JU5Pg;7#2n9-18|S1;AK+ zgsn6;k6-%!QD>D?cFy}8F;r@z8H9xN1jsOBw2vQONVqBVEbkiNUqgw~*!^##ht>w0 zUOykwH=$LwX2j&nLy=@{hr)2O&-wm-NyjW7n~Zs9UlH;P7iP3 zI}S(r0YFVYacnKH(+{*)Tbw)@;6>%=&Th=+Z6NHo_tR|JCI8TJiXv2N7ei7M^Q+RM z?9o`meH$5Yi;@9XaNR#jIK^&{N|DYNNbtdb)XW1Lv2k{E>;?F`#Pq|&_;gm~&~Zc9 zf+6ZE%{x4|{YdtE?a^gKyzr}dA>OxQv+pq|@IXL%WS0CiX!V zm$fCePA%lU{%pTKD7|5NJHeXg=I0jL@$tOF@K*MI$)f?om)D63K*M|r`gb9edD1~Y zc|w7N)Y%do7=0{RC|AziW7#am$)9jciRJ?IWl9PE{G3U+$%FcyKs_0Cgq`=K3@ttV z9g;M!3z~f_?P%y3-ph%vBMeS@p7P&Ea8M@97+%XEj*(1E6vHj==d zjsoviB>j^$_^OI_DEPvFkVo(BGRo%cJeD){6Uckei=~1}>sp299|IRjhXe)%?uP0I zF5+>?0#Ye}T^Y$u_rc4=lPcq4K^D(TZG-w30-YiEM=dcK+4#o*>lJ8&JLi+3UcpZk z!^?95S^C0ja^jwP`|{<+3cBVog$(mRdQmadS+Vh~z zS@|P}=|z3P6uS+&@QsMp0no9Od&27O&14zHXGAOEy zh~OKpymK5C%;LLb467@KgIiVwYbYd6wFxI{0-~MOGfTq$nBTB!{SrWmL9Hs}C&l&l#m?s*{tA?BHS4mVKHAVMqm63H<|c5n0~k)-kbg zXidai&9ZUy0~WFYYKT;oe~rytRk?)r8bptITsWj(@HLI;@=v5|XUnSls7$uaxFRL+ zRVMGuL3w}NbV1`^=Pw*0?>bm8+xfeY(1PikW*PB>>Tq(FR`91N0c2&>lL2sZo5=VD zQY{>7dh_TX98L2)n{2OV=T10~*YzX27i2Q7W86M4$?gZIXZaBq#sA*{PH8){|GUi;oM>e?ua7eF4WFuFYZSG| zze?srg|5Ti8Og{O zeFxuw9!U+zhyk?@w zjsA6(oKD=Ka;A>Ca)oPORxK+kxH#O@zhC!!XS4@=swnuMk>t+JmLmFiE^1aX3f<)D@`%K0FGK^gg1a1j>zi z2KhV>sjU7AX3F$SEqrXSC}fRx64GDoc%!u2Yag68Lw@w9v;xOONf@o)Lc|Uh3<21ctTYu-mFZuHk*+R{GjXHIGq3p)tFtQp%TYqD=j1&y)>@zxoxUJ!G@ zgI0XKmP6MNzw>nRxK$-Gbzs}dyfFzt>#5;f6oR27ql!%+{tr+(`(>%51|k`ML} zY4eE)Lxq|JMas(;JibNQds1bUB&r}ydMQXBY4x(^&fY_&LlQC)3hylc$~8&~|06-D z#T+%66rYbHX%^KuqJED_wuGB+=h`nWA!>1n0)3wZrBG3%`b^Ozv6__dNa@%V14|!D zQ?o$z5u0^8`giv%qE!BzZ!3j;BlDlJDk)h@9{nSQeEk!z9RGW) z${RSF3phEM*ce*>Xdp}585vj$|40=&S{S-GTiE?Op*vY&Lvr9}BO$XWy80IF+6@%n z5*2ueT_g@ofP#u5pxb7n*fv^Xtt7&?SRc{*2Ka-*!BuOpf}neHGCiHy$@Ka1^Dint z;DkmIL$-e)rj4o2WQV%Gy;Xg(_Bh#qeOsTM2f@KEe~4kJ8kNLQ+;(!j^bgJMcNhvklP5Z6I+9Fq@c&D~8Fb-4rmDT!MB5QC{Dsb;BharP*O;SF4& zc$wj-7Oep7#$WZN!1nznc@Vb<_Dn%ga-O#J(l=OGB`dy=Sy&$(5-n3zzu%d7E#^8`T@}V+5B;PP8J14#4cCPw-SQTdGa2gWL0*zKM z#DfSXs_iWOMt)0*+Y>Lkd=LlyoHjublNLefhKBv@JoC>P7N1_#> zv=mLWe96%EY;!ZGSQDbZWb#;tzqAGgx~uk+-$+2_8U`!ypbwXl z^2E-FkM1?lY@yt8=J3%QK+xaZ6ok=-y%=KXCD^0r!5vUneW>95PzCkOPO*t}p$;-> ze5j-BLT_;)cZQzR2CEsm@rU7GZfFtdp*a|g4wDr%8?2QkIGasRfDWT-Dvy*U{?IHT z*}wGnzdlSptl#ZF^sf)KT|BJs&kLG91^A6ls{CzFprZ6-Y!V0Xysh%9p%iMd7HLsS zN+^Un$tDV)T@i!v?3o0Fsx2qI(AX_$dDkBzQ@fRM%n zRXk6hb9Py#JXUs+7)w@eo;g%QQ95Yq!K_d=z{0dGS+pToEI6=Bo8+{k$7&Z zo4>PH(`ce8E-Ps&uv`NQ;U$%t;w~|@E3WVOCi~R4oj5wP?%<*1C%}Jq%a^q~T7u>K zML5AKfQDv6>PuT`{SrKHRAF+^&edg6+5R_#H?Lz3iGoWo#PCEd0DS;)2U({{X#zU^ zw_xv{4x7|t!S)>44J;KfA|DC?;uQ($l+5Vp7oeqf7{GBF9356nx|&B~gs+@N^gSdd zvb*>&W)|u#F{Z_b`f#GVtQ`pYv3#||N{xj1NgB<#=Odt6{eB%#9RLt5v zIi|0u70`#ai}9fJjKv7dE!9ZrOIX!3{$z_K5FBd-Kp-&e4(J$LD-)NMTp^_pB`RT; zftVVlK2g@+1Ahv2$D){@Y#cL#dUj9*&%#6 zd2m9{1NYp>)6=oAvqdCn5#cx{AJ%S8skUgMglu2*IAtd+z1>B&`MuEAS(D(<6X#Lj z?f4CFx$)M&$=7*>9v1ER4b6!SIz-m0e{o0BfkySREchp?WdVPpQCh!q$t>?rL!&Jg zd#heM;&~A}VEm8Dvy&P|J*eAV&w!&Nx6HFV&B8jJFVTmgLaswn!cx$&%JbTsloz!3 zMEz1d`k==`Ueub_JAy_&`!ogbwx27^ZXgFNAbx=g_I~5nO^r)}&myw~+yY*cJl4$I znNJ32M&K=0(2Dj_>@39`3=FX!v3nZHno_@q^!y}%(yw0PqOo=);6Y@&ylVe>nMOZ~ zd>j#QQSBn3oaWd;qy$&5(5H$Ayi)0haAYO6TH>FR?rhqHmNOO+(})NB zLI@B@v0)eq!ug`>G<@htRlp3n!EpU|n+G+AvXFrWSUsLMBfL*ZB`CRsIVHNTR&b?K zxBgsN0BjfB>UVcJ|x%=-zb%OV7lmZc& zxiupadZVF7)6QuhoY;;FK2b*qL0J-Rn-8!X4ZY$-ZSUXV5DFd7`T41c(#lAeLMoeT z4%g655v@7AqT!i@)Edt5JMbN(=Q-6{=L4iG8RA%}w;&pKmtWvI4?G9pVRp|RTw`g0 zD5c12B&A2&P6Ng~8WM2eIW=wxd?r7A*N+&!Be7PX3s|7~z=APxm=A?5 zt>xB4WG|*Td@VX{Rs)PV0|yK`oI3^xn(4c_j&vgxk_Y3o(-`_5o`V zRTghg6%l@(qodXN;dB#+OKJEEvhfcnc#BeO2|E(5df-!fKDZ!%9!^BJ_4)9P+9Dq5 zK1=(v?KmIp34r?z{NEWnLB3Px{XYwy-akun4F7xTRr2^zeYW{gcK9)>aJDdU5;w5@ zak=<+-PLH-|04pelTb%ULpuuuJC7DgyT@D|p{!V!0v3KpDnRjANN12q6SUR3mb9<- z>2r~IApQGhstZ!3*?5V z8#)hJ0TdZg0M-BK#nGFP>$i=qk82DO z7h;Ft!D5E15OgW)&%lej*?^1~2=*Z5$2VX>V{x8SC+{i10BbtUk9@I#Vi&hX)q
Q!LwySI{Bnv%Sm)yh{^sSVJ8&h_D-BJ_YZe5eCaAWU9b$O2c z$T|{vWVRtOL!xC0DTc(Qbe`ItNtt5hr<)VijD0{U;T#bUEp381_y`%ZIav?kuYG{iyYdEBPW=*xNSc;Rlt6~F4M`5G+VtOjc z*0qGzCb@gME5udTjJA-9O<&TWd~}ysBd(eVT1-H82-doyH9RST)|+Pb{o*;$j9Tjs zhU!IlsPsj8=(x3bAKJTopW3^6AKROHR^7wZ185wJGVhA~hEc|LP;k7NEz-@4p5o}F z`AD6naG3(n=NF9HTH81=F+Q|JOz$7wm9I<+#BSmB@o_cLt2GkW9|?7mM;r!JZp89l zbo!Hp8=n!XH1{GwaDU+k)pGp`C|cXkCU5%vcH)+v@0eK>%7gWxmuMu9YLlChA|_D@ zi#5zovN_!a-0?~pUV-Rj*1P)KwdU-LguR>YM&*Nen+ln8Q$?WFCJg%DY%K}2!!1FE zDv-A%Cbwo^p(lzac&_TZ-l#9kq`mhLcY3h9ZTUVCM(Ad&=EriQY5{jJv<5K&g|*Lk zgV%ILnf1%8V2B0E&;Sp4sYbYOvvMebLwYwzkRQ#F8GpTQq#uv=J`uaSJ34OWITeSGo6+-8Xw znCk*n{kdDEi)Hi&u^)~cs@iyCkFWB2SWZU|Uc%^43ZIZQ-vWNExCCtDWjqHs;;tWf$v{}0{p0Rvxkq``)*>+Akq%|Na zA`@~-Vfe|+(AIlqru+7Ceh4nsVmO9p9jc8}HX^W&ViBDXT+uXbT#R#idPn&L>+#b6 zflC-4C5-X;kUnR~L>PSLh*gvL68}RBsu#2l`s_9KjUWRhiqF`j)`y`2`YU(>3bdBj z?>iyjEhe-~$^I5!nn%B6Wh+I`FvLNvauve~eX<+Ipl&04 zT}};W&1a3%W?dJ2=N#0t?e+aK+%t}5q%jSLvp3jZ%?&F}nOOWr>+{GFIa%wO_2`et z=JzoRR~}iKuuR+azPI8;Gf9)z3kyA4EIOSl!sRR$DlW}0>&?GbgPojmjmnln;cTqCt=ADbE zZ8GAnoM+S1(5$i8^O4t`ue;vO4i}z0wz-QEIVe5_u03;}-!G1NyY8;h^}y;tzY}i5 zqQr#Ur3Fy8sSa$Q0ys+f`!`+>9WbvU_I`Sj;$4{S>O3?#inLHCrtLy~!s#WXV=oVP zeE93*Nc`PBi4q@%Ao$x4lw9vLHM!6mn3-b_cebF|n-2vt-zYVF_&sDE--J-P;2WHo z+@n2areE0o$LjvjlV2X7ZU@j+`{*8zq`JR3gKF#EW|#+{nMyo-a>nFFTg&vhyT=b} zDa8+v0(Dgx0yRL@ZXOYIlVSZ0|MFizy0VPW8;AfA5|pe!#j zX}Py^8fl5SyS4g1WSKKtnyP+_PoOwMMwu`(i@Z)diJp~U54*-miOchy7Z35eL>^M z4p<-aIxH4VUZgS783@H%M7P9hX>t{|RU7$n4T(brCG#h9e9p! z+o`i;EGGq3&pF;~5V~eBD}lC)>if$w%Vf}AFxGqO88|ApfHf&Bvu+xdG)@vuF}Yvk z)o;~k-%+0K0g+L`Wala!$=ZV|z$e%>f0%XoLib%)!R^RoS+{!#X?h-6uu zF&&KxORdZU&EwQFITIRLo(7TA3W}y6X{?Y%y2j0It!ekU#<)$qghZtpcS>L3uh`Uj z7GY;6f$9qKynP#oS3$$a{p^{D+0oJQ71`1?OAn_m8)UGZmj3l*ZI)`V-a>MKGGFG< z&^jg#Ok%(hhm>hSrZ5;Qga4u(?^i>GiW_j9%_7M>j(^|Om$#{k+^*ULnEgzW_1gCICtAD^WpC`A z{9&DXkG#01Xo)U$OC(L5Y$DQ|Q4C6CjUKk1UkPj$nXH##J{c8e#K|&{mA*;b$r0E4 zUNo0jthwA(c&N1l=PEe8Rw_8cEl|-eya9z&H3#n`B$t#+aJ03RFMzrV@gowbe8v(c zIFM60^0&lCFO10NU4w@|61xiZ4CVXeaKjd;d?sv52XM*lS8XiVjgWpRB;&U_C0g+`6B5V&w|O6B*_q zsATxL!M}+$He)1eOWECce#eS@2n^xhlB4<_Nn?yCVEQWDs(r`|@2GqLe<#(|&P0U? z$7V5IgpWf09uIf_RazRwC?qEqRaHyL?iiS05UiGesJy%^>-C{{ypTBI&B0-iUYhk> zIk<5xpsuV@g|z(AZD+C-;A!fTG=df1=<%nxy(a(IS+U{ME4ZbDEBtcD_3V=icT6*_ z)>|J?>&6%nvHhZERBtjK+s4xnut*@>GAmA5m*OTp$!^CHTr}vM4n(X1Q*;{e-Rd2BCF-u@1ZGm z!S8hJ6L=Gl4T_SDa7Xx|-{4mxveJg=ctf`BJ*fy!yF6Dz&?w(Q_6B}WQVtNI!BVBC zKfX<>7vd6C96}XAQmF-Jd?1Q4eTfRB3q7hCh0f!(JkdWT5<{iAE#dKy*Jxq&3a1@~ z8C||Dn2mFNyrUV|<-)C^_y7@8c2Fz+2jrae9deBDu;U}tJ{^xAdxCD248(k;dCJ%o z`y3sADe>U%suxwwv~8A1+R$VB=Q?%U?4joI$um;aH+eCrBqpn- z%79D_7rb;R-;-9RTrwi9dPlg8&@tfWhhZ(Vx&1PQ+6(huX`;M9x~LrW~~#3{j0Bh2kDU$}@!fFQej4VGkJv?M4rU^x!RU zEwhu$!CA_iDjFjrJa`aocySDX16?~;+wgav;}Zut6Mg%C4>}8FL?8)Kgwc(Qlj{@#2Pt0?G`$h7P#M+qoXtlV@d}%c&OzO+QYKK`kyXaK{U(O^2DyIXCZlNQjt0^8~8JzNGrIxhj}}M z&~QZlbx%t;MJ(Vux;2tgNKGlAqphLq%pd}JG9uoVHUo?|hN{pLQ6Em%r*+7t^<);X zm~6=qChlNAVXNN*Sow->*4;}T;l;D1I-5T{Bif@4_}=>l`tK;qqDdt5zvisCKhMAH z#r}`)7VW?LZqfdmXQ%zo5bJ00{Xb9^YKrk0Nf|oIW*K@(=`o2Vndz}ZDyk{!u}PVx zzd--+_WC*U{~DH3{?GI64IB+@On&@9X>EUAo&L+G{L^dozaI4C3G#2wr~hseW@K&g zKWs{uHu-9Je!3;4pE>eBltKUXb^*hG8I&413)$J&{D4N%7PcloU6bn%jPxJyQL?g* z9g+YFFEDiE`8rW^laCNzQmi7CTnPfwyg3VDHRAl>h=In6jeaVOP@!-CP60j3+#vpL zEYmh_oP0{-gTe7Or`L6x)6w?77QVi~jD8lWN@3RHcm80iV%M1A!+Y6iHM)05iC64tb$X2lV_%Txk@0l^hZqi^%Z?#- zE;LE0uFx)R08_S-#(wC=dS&}vj6P4>5ZWjhthP=*Hht&TdLtKDR;rXEX4*z0h74FA zMCINqrh3Vq;s%3MC1YL`{WjIAPkVL#3rj^9Pj9Ss7>7duy!9H0vYF%>1jh)EPqvlr6h%R%CxDsk| z!BACz7E%j?bm=pH6Eaw{+suniuY7C9Ut~1cWfOX9KW9=H><&kQlinPV3h9R>3nJvK z4L9(DRM=x;R&d#a@oFY7mB|m8h4692U5eYfcw|QKwqRsshN(q^v$4$)HgPpAJDJ`I zkqjq(8Cd!K!+wCd=d@w%~e$=gdUgD&wj$LQ1r>-E=O@c ze+Z$x{>6(JA-fNVr)X;*)40Eym1TtUZI1Pwwx1hUi+G1Jlk~vCYeXMNYtr)1?qwyg zsX_e*$h?380O00ou?0R@7-Fc59o$UvyVs4cUbujHUA>sH!}L54>`e` zHUx#Q+Hn&Og#YVOuo*niy*GU3rH;%f``nk#NN5-xrZ34NeH$l`4@t);4(+0|Z#I>Y z)~Kzs#exIAaf--65L0UHT_SvV8O2WYeD>Mq^Y6L!Xu8%vnpofG@w!}R7M28?i1*T&zp3X4^OMCY6(Dg<-! zXmcGQrRgHXGYre7GfTJ)rhl|rs%abKT_Nt24_Q``XH{88NVPW+`x4ZdrMuO0iZ0g` z%p}y};~T5gbb9SeL8BSc`SO#ixC$@QhXxZ=B}L`tP}&k?1oSPS=4%{UOHe0<_XWln zwbl5cn(j-qK`)vGHY5B5C|QZd5)W7c@{bNVXqJ!!n$^ufc?N9C-BF2QK1(kv++h!>$QbAjq)_b$$PcJdV+F7hz0Hu@ zqj+}m0qn{t^tD3DfBb~0B36|Q`bs*xs|$i^G4uNUEBl4g;op-;Wl~iThgga?+dL7s zUP(8lMO?g{GcYpDS{NM!UA8Hco?#}eNEioRBHy4`mq!Pd-9@-97|k$hpEX>xoX+dY zDr$wfm^P&}Wu{!%?)U_(%Mn79$(ywvu*kJ9r4u|MyYLI_67U7%6Gd_vb##Nerf@>& z8W11z$$~xEZt$dPG}+*IZky+os5Ju2eRi;1=rUEeIn>t-AzC_IGM-IXWK3^6QNU+2pe=MBn4I*R@A%-iLDCOHTE-O^wo$sL_h{dcPl=^muAQb`_BRm};=cy{qSkui;`WSsj9%c^+bIDQ z0`_?KX0<-=o!t{u(Ln)v>%VGL z0pC=GB7*AQ?N7N{ut*a%MH-tdtNmNC+Yf$|KS)BW(gQJ*z$d{+{j?(e&hgTy^2|AR9vx1Xre2fagGv0YXWqtNkg*v%40v?BJBt|f9wX5 z{QTlCM}b-0{mV?IG>TW_BdviUKhtosrBqdfq&Frdz>cF~yK{P@(w{Vr7z2qKFwLhc zQuogKO@~YwyS9%+d-zD7mJG~@?EFJLSn!a&mhE5$_4xBl&6QHMzL?CdzEnC~C3$X@ zvY!{_GR06ep5;<#cKCSJ%srxX=+pn?ywDwtJ2{TV;0DKBO2t++B(tIO4)Wh`rD13P z4fE$#%zkd=UzOB74gi=-*CuID&Z3zI^-`4U^S?dHxK8fP*;fE|a(KYMgMUo`THIS1f!*6dOI2 zFjC3O=-AL`6=9pp;`CYPTdVX z8(*?V&%QoipuH0>WKlL8A*zTKckD!paN@~hh zmXzm~qZhMGVdQGd=AG8&20HW0RGV8X{$9LldFZYm zE?}`Q3i?xJRz43S?VFMmqRyvWaS#(~Lempg9nTM$EFDP(Gzx#$r)W&lpFKqcAoJh-AxEw$-bjW>`_+gEi z2w`99#UbFZGiQjS8kj~@PGqpsPX`T{YOj`CaEqTFag;$jY z8_{Wzz>HXx&G*Dx<5skhpETxIdhKH?DtY@b9l8$l?UkM#J-Snmts7bd7xayKTFJ(u zyAT&@6cAYcs{PBfpqZa%sxhJ5nSZBPji?Zlf&}#L?t)vC4X5VLp%~fz2Sx<*oN<7` z?ge=k<=X7r<~F7Tvp9#HB{!mA!QWBOf%EiSJ6KIF8QZNjg&x~-%e*tflL(ji_S^sO ztmib1rp09uon}RcsFi#k)oLs@$?vs(i>5k3YN%$T(5Or(TZ5JW9mA6mIMD08=749$ z!d+l*iu{Il7^Yu}H;lgw=En1sJpCKPSqTCHy4(f&NPelr31^*l%KHq^QE>z>Ks_bH zjbD?({~8Din7IvZeJ>8Ey=e;I?thpzD=zE5UHeO|neioJwG;IyLk?xOz(yO&0DTU~ z^#)xcs|s>Flgmp;SmYJ4g(|HMu3v7#;c*Aa8iF#UZo7CvDq4>8#qLJ|YdZ!AsH%^_7N1IQjCro

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1e2fbf0d..df97d72b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,69 +15,104 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +122,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..9b42019c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 1accbfd42dd90b97f4d61425fe7515ee7dad9f1c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 Nov 2024 00:51:42 +0100 Subject: [PATCH 79/92] DateUtils.parseVTimeZone: return null instead of throwing a RuntimeException on parsing error (#185) --- .../ical4android/util/DateUtilsTest.kt | 19 +++++++++++++++++++ .../at/bitfire/ical4android/util/DateUtils.kt | 16 +++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt index 7e7195c2..3bf128e2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt @@ -63,4 +63,23 @@ class DateUtilsTest { assertFalse(DateUtils.isDateTime(null)) } + + @Test + fun testParseVTimeZone() { + val vtz = """ + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:DAVx5 + BEGIN:VTIMEZONE + TZID:Asia/Shanghai + END:VTIMEZONE + END:VCALENDAR""".trimIndent() + assertEquals("Asia/Shanghai", DateUtils.parseVTimeZone(vtz)?.timeZoneId?.value) + } + + @Test + fun testParseVTimeZone_Invalid() { + assertNull(DateUtils.parseVTimeZone("Invalid")) + } + } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index ab7af8df..a19a9c02 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -123,20 +123,22 @@ object DateUtils { fun isDateTime(date: DateProperty?) = date != null && date.date is DateTime /** - * Parses a VTIMEZONE definition to a VTimeZone object. - * @param timezoneDef VTIMEZONE definition - * @return parsed VTimeZone - * @throws IllegalArgumentException when the timezone definition can't be parsed + * Parses an iCalendar that only contains a `VTIMEZONE` definition to a VTimeZone object. + * + * @param timezoneDef iCalendar with only a `VTIMEZONE` definition + * + * @return parsed [VTimeZone], or `null` when the timezone definition can't be parsed */ - fun parseVTimeZone(timezoneDef: String): VTimeZone { + fun parseVTimeZone(timezoneDef: String): VTimeZone? { Ical4Android.checkThreadContextClassLoader() val builder = CalendarBuilder(tzRegistry) try { val cal = builder.build(StringReader(timezoneDef)) return cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone - } catch (e: Exception) { - throw IllegalArgumentException("Couldn't parse timezone definition") + } catch (_: Exception) { + // Couldn't parse timezone definition + return null } } From 8481c391e484f683f5bde9312ba17eef70623613 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 22 Nov 2024 16:44:58 +0100 Subject: [PATCH 80/92] BatchOperation: set yield point after every 500 operations (#188) * BatchOperation: set "yield allowed" after every 500 operations * Only use "max. operations per yield point" for tasks.org/OpenTasks operations --- ...sTest.kt => DmfsStyleProvidersTaskTest.kt} | 2 +- .../bitfire/ical4android/DmfsTaskListTest.kt | 2 +- .../at/bitfire/ical4android/DmfsTaskTest.kt | 25 +++++++--- .../at/bitfire/ical4android/BatchOperation.kt | 46 ++++++++++++++----- .../at/bitfire/ical4android/DmfsTask.kt | 4 +- .../at/bitfire/ical4android/DmfsTaskList.kt | 2 +- 6 files changed, 57 insertions(+), 24 deletions(-) rename lib/src/androidTest/kotlin/at/bitfire/ical4android/{AbstractTasksTest.kt => DmfsStyleProvidersTaskTest.kt} (97%) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt similarity index 97% rename from lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt index cf21bda8..eb22ba8b 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AbstractTasksTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt @@ -16,7 +16,7 @@ import java.util.logging.Logger @RunWith(Parameterized::class) -abstract class AbstractTasksTest( +abstract class DmfsStyleProvidersTaskTest( val providerName: TaskProvider.ProviderName ) { diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt index c227ddf3..6c8360a9 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt @@ -21,7 +21,7 @@ import org.junit.Assert.assertTrue import org.junit.Test class DmfsTaskListTest(providerName: TaskProvider.ProviderName): - AbstractTasksTest(providerName) { + DmfsStyleProvidersTaskTest(providerName) { private val testAccount = Account("AndroidTaskListTest", TaskContract.LOCAL_ACCOUNT_TYPE) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index 03b5b0ed..b3c123b1 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -9,13 +9,13 @@ import android.content.ContentUris import android.content.ContentValues import android.database.DatabaseUtils import android.net.Uri -import androidx.test.filters.MediumTest import at.bitfire.ical4android.impl.TestTask import at.bitfire.ical4android.impl.TestTaskList import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.TzId @@ -52,8 +52,8 @@ import org.junit.Test import java.time.ZoneId class DmfsTaskTest( - providerName: TaskProvider.ProviderName -): AbstractTasksTest(providerName) { + providerName: TaskProvider.ProviderName +): DmfsStyleProvidersTaskTest(providerName) { private val tzVienna = DateUtils.ical4jTimeZone("Europe/Vienna")!! private val tzChicago = DateUtils.ical4jTimeZone("America/Chicago")!! @@ -643,7 +643,6 @@ class DmfsTaskTest( } - @MediumTest @Test fun testAddTask() { // build and write event to calendar provider @@ -693,7 +692,6 @@ class DmfsTaskTest( } } - @MediumTest @Test(expected = CalendarStorageException::class) fun testAddTaskWithInvalidDue() { val task = Task() @@ -705,7 +703,21 @@ class DmfsTaskTest( TestTask(taskList!!, task).add() } - @MediumTest + @Test + fun testAddTaskWithManyAlarms() { + val task = Task() + task.uid = "TaskWithManyAlarms" + task.summary = "Task with many alarms" + task.dtStart = DtStart(Date("20150102")) + + for (i in 1..1050) + task.alarms += VAlarm(java.time.Duration.ofMinutes(i.toLong())) + + val uri = TestTask(taskList!!, task).add() + val task2 = taskList!!.findById(ContentUris.parseId(uri)) + assertEquals(1050, task2.task?.alarms?.size) + } + @Test fun testUpdateTask() { // add test event without reminder @@ -739,7 +751,6 @@ class DmfsTaskTest( } } - @MediumTest @Test fun testBuildAllDayTask() { // add all-day event to calendar provider diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt index a8c983f9..6b699dbe 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt @@ -17,9 +17,17 @@ import java.util.logging.Level import java.util.logging.Logger class BatchOperation( - private val providerClient: ContentProviderClient + private val providerClient: ContentProviderClient, + private val maxOperationsPerYieldPoint: Int? = null ) { - + + companion object { + + /** Maximum number of operations per yield point in task providers that are based on SQLiteContentProvider. */ + const val TASKS_OPERATIONS_PER_YIELD_POINT = 499 + + } + private val logger = Logger.getLogger(javaClass.name) private val queue = LinkedList() @@ -96,7 +104,6 @@ class BatchOperation( try { val ops = toCPO(start, end) - logger.fine("Running ${ops.size} operations ($start .. ${end - 1})") val partResults = providerClient.applyBatch(ops) val n = end - start @@ -133,6 +140,7 @@ class BatchOperation( * 2. If a back reference points to a row outside of start/end, * replace it by the actual result, which has already been calculated. */ + var currentIdx = 0 for (cpoBuilder in queue.subList(start, end)) { for ((backrefKey, backref) in cpoBuilder.valueBackrefs) { val originalIdx = backref.originalIndex @@ -148,6 +156,11 @@ class BatchOperation( backref.setIndex(originalIdx - start) } + // Set a possible yield point every MAX_OPERATIONS_PER_YIELD_POINT operations for SQLiteContentProvider + currentIdx += 1 + if (maxOperationsPerYieldPoint != null && currentIdx.mod(maxOperationsPerYieldPoint) == 0) + cpoBuilder.withYieldAllowed() + cpo += cpoBuilder.build() } return cpo @@ -155,10 +168,10 @@ class BatchOperation( class BackReference( - /** index of the referenced row in the original, nonsplitted transaction */ - val originalIndex: Int + /** index of the referenced row in the original, non-splitted transaction */ + val originalIndex: Int ) { - /** overriden index, i.e. index within the splitted transaction */ + /** overridden index, i.e. index within the splitted transaction */ private var index: Int? = null /** @@ -182,8 +195,8 @@ class BatchOperation( * value back references. */ class CpoBuilder private constructor( - val uri: Uri, - val type: Type + val uri: Uri, + val type: Type ) { enum class Type { INSERT, UPDATE, DELETE } @@ -197,11 +210,13 @@ class BatchOperation( } - var selection: String? = null - var selectionArguments: Array? = null + private var selection: String? = null + private var selectionArguments: Array? = null + + internal val values = mutableMapOf() + internal val valueBackrefs = mutableMapOf() - val values = mutableMapOf() - val valueBackrefs = mutableMapOf() + private var yieldAllowed = false fun withSelection(select: String, args: Array): CpoBuilder { @@ -226,6 +241,10 @@ class BatchOperation( return this } + fun withYieldAllowed() { + yieldAllowed = true + } + fun build(): ContentProviderOperation { val builder = when (type) { @@ -242,6 +261,9 @@ class BatchOperation( for ((key, backref) in valueBackrefs) builder.withValueBackReference(key, backref.getIndex()) + if (yieldAllowed) + builder.withYieldAllowed(true) + return builder.build() } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 25a3ef0b..41eca481 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -330,7 +330,7 @@ abstract class DmfsTask( fun add(): Uri { - val batch = BatchOperation(taskList.provider) + val batch = BatchOperation(taskList.provider, BatchOperation.TASKS_OPERATIONS_PER_YIELD_POINT) val builder = CpoBuilder.newInsert(taskList.tasksSyncUri()) buildTask(builder, false) @@ -350,7 +350,7 @@ abstract class DmfsTask( this.task = task val existingId = requireNotNull(id) - val batch = BatchOperation(taskList.provider) + val batch = BatchOperation(taskList.provider, BatchOperation.TASKS_OPERATIONS_PER_YIELD_POINT) // remove associated rows which are added later again batch.enqueue(CpoBuilder diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index 0e82f116..65020ad0 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -163,7 +163,7 @@ abstract class DmfsTaskList( */ fun touchRelations(): Int { logger.fine("Touching relations to set parent_id") - val batchOperation = BatchOperation(provider) + val batchOperation = BatchOperation(provider, BatchOperation.TASKS_OPERATIONS_PER_YIELD_POINT) provider.query( tasksSyncUri(true), null, "${Tasks.LIST_ID}=? AND ${Tasks.PARENT_ID} IS NULL AND ${Relation.MIMETYPE}=? AND ${Relation.RELATED_ID} IS NOT NULL", From c1ed0a0570ed555fb0dc263acaa842082e4c6981 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Sat, 30 Nov 2024 02:15:04 -0800 Subject: [PATCH 81/92] Filter `RDATE` and `EXDATE` from exceptions (#191) * Filtered RDATE and EXDATE from exceptions * Also test for empty RDATE and EXDATE --------- Co-authored-by: Ricki Hirner --- .../ical4android/validation/EventValidator.kt | 18 +++--- .../validation/EventValidatorTest.kt | 57 +++++++++++++++++-- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt index b0491161..6bee7881 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt @@ -6,7 +6,6 @@ package at.bitfire.ical4android.validation import androidx.annotation.VisibleForTesting import at.bitfire.ical4android.Event -import at.bitfire.ical4android.Ical4Android import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate @@ -47,7 +46,7 @@ object EventValidator { val dtStart = correctStartAndEndTime(event) sameTypeForDtStartAndRruleUntil(dtStart, event.rRules) removeRRulesWithUntilBeforeDtStart(dtStart, event.rRules) - removeRRulesOfExceptions(event.exceptions) + removeRecurrenceOfExceptions(event.exceptions) } @@ -169,15 +168,20 @@ object EventValidator { /** - * Removes RRULEs of exceptions of (potentially recurring) events - * Note: This repair step needs to be applied after all exceptions have been found + * Removes all recurrence information of exceptions of (potentially recurring) events. This is: + * `RRULE`, `RDATE` and `EXDATE`. + * Note: This repair step needs to be applied after all exceptions have been found. * * @param exceptions exceptions of an event */ @VisibleForTesting - internal fun removeRRulesOfExceptions(exceptions: List) { - for (exception in exceptions) - exception.rRules.clear() // Drop all RRULEs for the exception + internal fun removeRecurrenceOfExceptions(exceptions: List) { + for (exception in exceptions) { + // Drop all RRULEs, RDATEs, EXDATEs for the exception + exception.rRules.clear() + exception.rDates.clear() + exception.exDates.clear() + } } diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index c75106f8..6dadd08f 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -7,14 +7,23 @@ package at.bitfire.ical4android.validation import at.bitfire.ical4android.Event import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.Recur +import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistry import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.TzId import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -362,7 +371,7 @@ class EventValidatorTest { } @Test - fun testRemoveRrulesOfRruleExceptions() { + fun testRemoveRecurrencesOfRecurringWithExceptions() { // Test manually created event val tz = DateUtils.ical4jTimeZone("Europe/Paris") val manualEvent = Event().apply { @@ -394,14 +403,48 @@ class EventValidatorTest { .interval(2) .build()) )) + rDates.addAll(listOf( + RDate(DateList(Value("19970714T123000Z"))), + RDate( + DateList( + Value("19960403T020000Z"), + TimeZone( + VTimeZone( + PropertyList(1).apply { + add(TzId("US-EASTERN")) + } + ) + ) + ) + ) + )) + exDates.addAll(listOf( + ExDate(DateList(Value("19970714T123000Z"))), + ExDate( + DateList( + Value("19960403T020000Z"), + TimeZone( + VTimeZone( + PropertyList(1).apply { + add(TzId("US-EASTERN")) + } + ) + ) + ) + ) + )) uid = "76c08fb1-99a3-41cf-b482-2d3b06648814" }) } assertTrue(manualEvent.rRules.size == 1) - assertTrue(manualEvent.exceptions.first.rRules.size == 2) - EventValidator.removeRRulesOfExceptions(manualEvent.exceptions) // Repair the manually created event + assertTrue(manualEvent.exceptions.first().rRules.size == 2) + assertTrue(manualEvent.exceptions.first().rDates.size == 2) + assertTrue(manualEvent.exceptions.first().exDates.size == 2) + EventValidator.removeRecurrenceOfExceptions(manualEvent.exceptions) // Repair the manually created event assertTrue(manualEvent.rRules.size == 1) - assertTrue(manualEvent.exceptions.first.rRules.isEmpty()) + assertTrue(manualEvent.exceptions.first().rRules.isEmpty()) + assertTrue(manualEvent.exceptions.first().rDates.isEmpty()) + assertTrue(manualEvent.exceptions.first().exDates.isEmpty()) // Test event from reader, the reader will repair the event itself val eventFromReader = Event.eventsFromReader(StringReader( @@ -422,6 +465,8 @@ class EventValidatorTest { "SUMMARY:exception of recurring event\n" + "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // but remove this one "RRULE:FREQ=DAILY;COUNT=6;INTERVAL=2\n" + // and this one + "EXDATE;TZID=Europe/Paris:20240704T193000\n" + // also this + "RDATE;TZID=US-EASTERN:19970714T083000\n" + // and this "DTSTART;TZID=Europe/Paris:20240221T110000\n" + "DTEND;TZID=Europe/Paris:20240221T120000\n" + "UID:76c08fb1-99a3-41cf-b482-2d3b06648814\n" + @@ -429,7 +474,9 @@ class EventValidatorTest { "END:VCALENDAR" )).first() assertTrue(eventFromReader.rRules.size == 1) - assertTrue(eventFromReader.exceptions.first.rRules.isEmpty()) + assertTrue(eventFromReader.exceptions.first().rRules.isEmpty()) + assertTrue(eventFromReader.exceptions.first().rDates.isEmpty()) + assertTrue(eventFromReader.exceptions.first().exDates.isEmpty()) } @Test From 12df9bfddb3996dd337a521fede70b5d3f3ea5e5 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sun, 15 Dec 2024 19:50:33 +0100 Subject: [PATCH 82/92] Update dependencies --- gradle/libs.versions.toml | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f833c759..c662a884 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.2" +agp = "8.7.3" android-desugar = "2.1.3" androidx-core = "1.15.0" androidx-test-rules = "1.6.1" @@ -10,7 +10,7 @@ dokka ="1.9.20" # noinspection GradleDependency ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" -kotlin = "2.0.21" +kotlin = "2.1.0" mockk = "1.13.13" slf4j = "2.0.16" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b..e2847c82 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 883954c7ca500a30537974f0885512b7b1ad7e2a Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:13:26 +0100 Subject: [PATCH 83/92] Add _sync_id property (#187) * Added sync_id column in JtxContract * Raise minimum jtx Board version * AndroidCalendar, JtxCollection: add syncId column --------- Co-authored-by: Ricki Hirner --- .../at/bitfire/ical4android/AndroidCalendarTest.kt | 2 +- .../at/bitfire/ical4android/AndroidCalendar.kt | 4 ++++ .../at/bitfire/ical4android/JtxCollection.kt | 4 ++++ .../kotlin/at/bitfire/ical4android/TaskProvider.kt | 14 +++++++------- lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt | 8 +++++++- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt index 669de0c2..a9d5e1d8 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt @@ -125,4 +125,4 @@ class AndroidCalendarTest { } } -} +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt index f7ec09ed..1981dc9b 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt @@ -165,6 +165,8 @@ abstract class AndroidCalendar( var ownerAccount: String? = null + var syncId: String? = null + /** * Sets the calendar properties ([name], [displayName] etc.) from the passed argument, @@ -186,6 +188,8 @@ abstract class AndroidCalendar( isVisible = info.getAsInteger(Calendars.VISIBLE) != 0 ownerAccount = info.getAsString(Calendars.OWNER_ACCOUNT) + + syncId = info.getAsString(Calendars._SYNC_ID) } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index 7e926d7a..c87b5a3d 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -60,6 +60,8 @@ open class JtxCollection(val account: Account, var supportsVTODO = true var supportsVJOURNAL = true + var syncId: Long? = null + var context: Context? = null @@ -85,6 +87,8 @@ open class JtxCollection(val account: Account, supportsVJOURNAL = values.getAsString(JtxContract.JtxCollection.SUPPORTSVJOURNAL) == "1" || values.getAsString(JtxContract.JtxCollection.SUPPORTSVJOURNAL) == "true" + syncId = values.getAsLong(JtxContract.JtxCollection.SYNC_ID) + this.context = context } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt index 7a2c02a1..eb00267f 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt @@ -22,14 +22,14 @@ class TaskProvider private constructor( ): Closeable { enum class ProviderName( - val authority: String, - val packageName: String, - val minVersionCode: Long, - val minVersionName: String, - private val readPermission: String, - private val writePermission: String + val authority: String, + val packageName: String, + val minVersionCode: Long, + val minVersionName: String, + private val readPermission: String, + private val writePermission: String ) { - JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 207000001, "2.07.00", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), + JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 210000000, "2.10.00", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), TasksOrg("org.tasks.opentasks", "org.tasks", 100000, "10.0", PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE), OpenTasks("org.dmfs.tasks", "org.dmfs.tasks", 103, "1.1.8.2", PERMISSION_OPENTASKS_READ, PERMISSION_OPENTASKS_WRITE); diff --git a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt index 3707a005..31ea075f 100644 --- a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt +++ b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt @@ -46,7 +46,7 @@ object JtxContract { const val AUTHORITY = "at.techbee.jtx.provider" /** The version of this SyncContentProviderContract */ - const val VERSION = 7 + const val VERSION = 8 /** Constructs an Uri for the Jtx Sync Adapter with the given Account * @param [account] The account that should be appended to the Base Uri @@ -1215,6 +1215,12 @@ object JtxContract { */ const val LAST_SYNC = "lastsync" + /** + * Purpose: This column/property stores a sync_id for the given collection + * See https://github.com/TechbeeAT/jtxBoard/issues/1635 + * Type: [Long] + */ + const val SYNC_ID = "sync_id" } From afa7724bcb4230f7acee76806fd49b71e245a03d Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 15 Apr 2025 11:56:56 +0200 Subject: [PATCH 84/92] Query recur instance without dtstart (#195) * Add queryRecur test * Query without dtstart * Correct log message * Edit kdoc --- .../bitfire/ical4android/JtxCollectionTest.kt | 25 +++++++++++++++++++ .../at/bitfire/ical4android/JtxCollection.kt | 9 ++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt index ed38d0e5..25d03701 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt @@ -105,6 +105,31 @@ class JtxCollectionTest { assertEquals(1, icalobjects.size) } + @Test + fun queryRecur_test() { + val collectionUri = JtxCollection.create(testAccount, client, cv) + assertNotNull(collectionUri) + + val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) + val item = collections[0].queryRecur("abc1234", "xyz5678") + assertNull(item) + + val cv = ContentValues().apply { + put(JtxContract.JtxICalObject.UID, "abc1234") + put(JtxContract.JtxICalObject.RECURID, "xyz5678") + put(JtxContract.JtxICalObject.RECURID_TIMEZONE, "Europe/Vienna") + 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 contentValues = collections[0].queryRecur("abc1234", "xyz5678") + + assertEquals("abc1234", contentValues?.getAsString(JtxContract.JtxICalObject.UID)) + assertEquals("xyz5678", contentValues?.getAsString(JtxContract.JtxICalObject.RECURID)) + assertEquals("Europe/Vienna", contentValues?.getAsString(JtxContract.JtxICalObject.RECURID_TIMEZONE)) + } + @Test fun getICSForCollection_test() { val collectionUri = JtxCollection.create(testAccount, client, cv) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index c87b5a3d..ff14ecfa 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -181,18 +181,19 @@ open class JtxCollection(val account: Account, /** * @param [uid] of the entry that should be retrieved as content values + * @param [recurid] of the entry that should be retrieved as content values * @return Content Values of the found item with the given UID or null if the result was empty or more than 1 * The query checks for the [uid] within all collections of this account, not only the current collection. */ - fun queryRecur(uid: String, recurid: String, dtstart: Long): ContentValues? { + fun queryRecur(uid: String, recurid: String): ContentValues? { client.query( JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account), null, - "${JtxContract.JtxICalObject.UID} = ? AND ${JtxContract.JtxICalObject.RECURID} = ? AND ${JtxContract.JtxICalObject.DTSTART} = ?", - arrayOf(uid, recurid, dtstart.toString()), + "${JtxContract.JtxICalObject.UID} = ? AND ${JtxContract.JtxICalObject.RECURID} = ?", + arrayOf(uid, recurid), null ).use { cursor -> - logger.fine("queryByUID: found ${cursor?.count} records in ${account.name}") + logger.fine("queryRecur: found ${cursor?.count} records in ${account.name}") if (cursor?.count != 1) return null cursor.moveToFirst() From f71347d7991c7595c7defb12adf18f3a005307c9 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 17 Apr 2025 16:12:39 +0200 Subject: [PATCH 85/92] Allow transitive validator dependency from ical4j again --- lib/build.gradle.kts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 8df4e0c2..fca888fa 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -93,10 +93,7 @@ dependencies { coreLibraryDesugaring(libs.android.desugaring) implementation(libs.androidx.core) - api(libs.ical4j) { - // Get rid of unnecessary transitive dependencies - exclude(group = "commons-validator", module = "commons-validator") - } + api(libs.ical4j) // Force latest version of commons libraries implementation(libs.commons.codec) implementation(libs.commons.lang) From 0bf7641e1163daf1c948505255cb9dd7b5d4afbb Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 17 Apr 2025 16:15:12 +0200 Subject: [PATCH 86/92] Update Gradle wrapper to 8.13, bump Kotlin to 2.1.20, and MockK to 1.14.0; don't set strict version for commons anymore --- gradle/libs.versions.toml | 14 +++++--------- gradle/wrapper/gradle-wrapper.properties | 2 +- lib/build.gradle.kts | 3 --- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c662a884..858b48b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,15 @@ [versions] agp = "8.7.3" -android-desugar = "2.1.3" -androidx-core = "1.15.0" +android-desugar = "2.1.5" +androidx-core = "1.16.0" androidx-test-rules = "1.6.1" androidx-test-runner = "1.6.2" -commons-codec = { strictly = "1.17.1" } -commons-lang = { strictly = "3.15.0" } -dokka ="1.9.20" +dokka = "1.9.20" # noinspection GradleDependency ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" -kotlin = "2.1.0" -mockk = "1.13.13" +kotlin = "2.1.20" +mockk = "1.14.0" slf4j = "2.0.16" [libraries] @@ -19,8 +17,6 @@ android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.re androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } -commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec" } -commons-lang = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" } ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c82..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index fca888fa..f483cd9e 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -94,9 +94,6 @@ dependencies { implementation(libs.androidx.core) api(libs.ical4j) - // Force latest version of commons libraries - implementation(libs.commons.codec) - implementation(libs.commons.lang) implementation(libs.slf4j) // ical4j logging over java.util.Logger androidTestImplementation(libs.androidx.test.rules) From 504be069ba138be2b64beae007e712636b51c222 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 17 Apr 2025 18:57:18 +0200 Subject: [PATCH 87/92] Add module name to README --- README.md | 3 +++ settings.gradle | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 59aa56be..5666f075 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ by Google LLC. Android is a trademark of Google LLC._ } ``` +To view the available gradle tasks for the library: `./gradlew ical4android:tasks` +(the `ical4android` module is defined in `settings.gradle`). + ## Contact diff --git a/settings.gradle b/settings.gradle index 1c20da9e..c692f1f0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,7 +17,7 @@ dependencyResolutionManagement { } } -rootProject.name = "ical4android" -// include ':sample-app' +rootProject.name = "root" + include ':lib' -project(':lib').name = 'ical4android' \ No newline at end of file +project(':lib').name = 'ical4android' From ef907cf6b4202baf88b931bfaafae81e44cb42dd Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 17 Apr 2025 21:45:17 +0200 Subject: [PATCH 88/92] Update proguard rules and build configuration (#196) --- gradle/libs.versions.toml | 2 +- lib/build.gradle.kts | 12 ++++++++++++ lib/consumer-rules.pro | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 lib/consumer-rules.pro diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 858b48b5..43dd314c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.3" +agp = "8.9.1" android-desugar = "2.1.5" androidx-core = "1.16.0" androidx-test-rules = "1.6.1" diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index f483cd9e..0bd1a32e 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -24,6 +24,9 @@ android { aarMetadata { minCompileSdk = 29 } + + // These ProGuard/R8 rules will be included in the final APK. + consumerProguardFiles("consumer-rules.pro") } compileOptions { @@ -51,6 +54,15 @@ android { } } + buildTypes { + release { + // Android libraries shouldn't be minified: + // https://developer.android.com/studio/projects/android-library#Considerations + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + lint { disable += listOf("AllowBackup", "InvalidPackage") } diff --git a/lib/consumer-rules.pro b/lib/consumer-rules.pro new file mode 100644 index 00000000..58cabf1a --- /dev/null +++ b/lib/consumer-rules.pro @@ -0,0 +1,11 @@ + +# keep all iCalendar properties/parameters (used via reflection) +-keep class net.fortuna.ical4j.** { *; } + +# don't warn when these are missing +-dontwarn com.github.erosb.jsonsKema.** +-dontwarn groovy.** +-dontwarn java.beans.Transient +-dontwarn javax.cache.** +-dontwarn org.codehaus.groovy.** +-dontwarn org.jparsec.** From d0776024dd9a16a184d37f73fd6e9b7c977c5183 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 7 May 2025 10:26:30 +0200 Subject: [PATCH 89/92] Remove unnecssary `contextClassLoader` checks (#199) * Remove thread context class loader checks and remove Ical4Android class * Add test for ical4j ServiceLoader without per-thread context class loader --- .../kotlin/at/bitfire/ical4android/Event.kt | 2 - .../at/bitfire/ical4android/ICalendar.kt | 3 +- .../{Ical4Android.kt => Ical4jVersion.kt} | 15 ++----- .../at/bitfire/ical4android/JtxICalObject.kt | 40 +++++++++++++++++-- .../kotlin/at/bitfire/ical4android/Task.kt | 2 - .../ical4android/util/AndroidTimeUtils.kt | 10 +++-- .../at/bitfire/ical4android/util/DateUtils.kt | 7 ---- .../ical4android/Ical4jServiceLoaderTest.kt | 31 ++++++++++++++ 8 files changed, 79 insertions(+), 31 deletions(-) rename lib/src/main/kotlin/at/bitfire/ical4android/{Ical4Android.kt => Ical4jVersion.kt} (50%) create mode 100644 lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index 6e97f02e..8c0a8420 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -243,8 +243,6 @@ data class Event( fun write(os: OutputStream) { - Ical4Android.checkThreadContextClassLoader() - val ical = Calendar() ical.properties += Version.VERSION_2_0 ical.properties += prodId() diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index 6d1772e7..a450cf24 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME import at.bitfire.ical4android.validation.ICalPreprocessor import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.data.CalendarParserFactory @@ -80,7 +81,6 @@ open class ICalendar { * @throws IllegalArgumentException when the iCalendar resource contains an invalid value */ fun fromReader(reader: Reader, properties: MutableMap? = null): Calendar { - Ical4Android.checkThreadContextClassLoader() logger.fine("Parsing iCalendar stream") // preprocess stream to work around some problems that can't be fixed later @@ -229,7 +229,6 @@ open class ICalendar { * @return time zone id (TZID) if VTIMEZONE contains a TZID, null otherwise */ fun timezoneDefToTzId(timezoneDef: String): String? { - Ical4Android.checkThreadContextClassLoader() try { val builder = CalendarBuilder() val cal = builder.build(StringReader(timezoneDef)) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt similarity index 50% rename from lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt rename to lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt index fdd8c695..17ee850b 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4Android.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt @@ -4,15 +4,8 @@ package at.bitfire.ical4android +/** + * The used version of ical4j. + */ @Suppress("unused") -object Ical4Android { - - const val ical4jVersion = BuildConfig.version_ical4j - - - fun checkThreadContextClassLoader() { - if (Thread.currentThread().contextClassLoader == null) - throw IllegalStateException("Thread.currentThread().contextClassLoader must be set for java.util.ServiceLoader (used by ical4j)") - } - -} \ No newline at end of file +const val ical4jVersion = BuildConfig.version_ical4j diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt index 6b2ef777..6b9af310 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt @@ -47,7 +47,42 @@ import net.fortuna.ical4j.model.parameter.Rsvp import net.fortuna.ical4j.model.parameter.SentBy import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.parameter.XParameter -import net.fortuna.ical4j.model.property.* +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Attach +import net.fortuna.ical4j.model.property.Categories +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Color +import net.fortuna.ical4j.model.property.Comment +import net.fortuna.ical4j.model.property.Completed +import net.fortuna.ical4j.model.property.Contact +import net.fortuna.ical4j.model.property.Created +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStamp +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.Geo +import net.fortuna.ical4j.model.property.LastModified +import net.fortuna.ical4j.model.property.Location +import net.fortuna.ical4j.model.property.PercentComplete +import net.fortuna.ical4j.model.property.Priority +import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.RelatedTo +import net.fortuna.ical4j.model.property.Repeat +import net.fortuna.ical4j.model.property.Resources +import net.fortuna.ical4j.model.property.Sequence +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Summary +import net.fortuna.ical4j.model.property.Trigger +import net.fortuna.ical4j.model.property.Uid +import net.fortuna.ical4j.model.property.Url +import net.fortuna.ical4j.model.property.Version +import net.fortuna.ical4j.model.property.XProperty import java.io.FileNotFoundException import java.io.IOException import java.io.OutputStream @@ -593,8 +628,6 @@ open class JtxICalObject( * @return The current JtxICalObject transformed into a ical4j Calendar */ fun getICalendarFormat(): Calendar? { - Ical4Android.checkThreadContextClassLoader() - val ical = Calendar() ical.properties += Version.VERSION_2_0 ical.properties += ICalendar.prodId(listOf(TaskProvider.ProviderName.JtxBoard.packageName)) @@ -697,7 +730,6 @@ open class JtxICalObject( * @param [os] OutputStream where iCalendar should be written to */ fun write(os: OutputStream) { - Ical4Android.checkThreadContextClassLoader() CalendarOutputter(false).output(this.getICalendarFormat(), os) } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 2a1172ad..900a9be1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -193,8 +193,6 @@ data class Task( fun write(os: OutputStream) { - Ical4Android.checkThreadContextClassLoader() - val ical = Calendar() ical.properties += Version.VERSION_2_0 ical.properties += prodId() diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt index 653622a0..67ff8dc0 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -7,11 +7,14 @@ package at.bitfire.ical4android.util import android.text.format.Time -import at.bitfire.ical4android.Ical4Android +import at.bitfire.ical4android.util.AndroidTimeUtils.androidifyTimeZone +import at.bitfire.ical4android.util.AndroidTimeUtils.storageTzId import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime -import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.TemporalAmountAdapter import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.DateListProperty @@ -27,7 +30,8 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAmount -import java.util.* +import java.util.LinkedList +import java.util.Locale import java.util.logging.Logger object AndroidTimeUtils { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index a19a9c02..26793f12 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -4,7 +4,6 @@ package at.bitfire.ical4android.util -import at.bitfire.ical4android.Ical4Android import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime @@ -24,10 +23,6 @@ import java.util.logging.Logger */ object DateUtils { - init { - Ical4Android.checkThreadContextClassLoader() - } - private val logger get() = Logger.getLogger(javaClass.name) @@ -130,8 +125,6 @@ object DateUtils { * @return parsed [VTimeZone], or `null` when the timezone definition can't be parsed */ fun parseVTimeZone(timezoneDef: String): VTimeZone? { - Ical4Android.checkThreadContextClassLoader() - val builder = CalendarBuilder(tzRegistry) try { val cal = builder.build(StringReader(timezoneDef)) diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt new file mode 100644 index 00000000..37f947d0 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt @@ -0,0 +1,31 @@ +package at.bitfire.ical4android + +import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.component.VEvent +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.StringReader + +class Ical4jServiceLoaderTest { + + @Test + fun Ical4j_ServiceLoader_DoesntNeedContextClassLoader() { + Thread.currentThread().contextClassLoader = null + + val iCal = "BEGIN:VCALENDAR\n" + + "PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VEVENT\n" + + "UID:uid1@example.com\n" + + "DTSTART:19960918T143000Z\n" + + "DTEND:19960920T220000Z\n" + + "SUMMARY:Networld+Interop Conference\n" + + "END:VEVENT\n" + + "END:VCALENDAR\n" + val result = CalendarBuilder().build(StringReader(iCal)) + val vEvent = result.getComponent(Component.VEVENT) + assertEquals("Networld+Interop Conference", vEvent.summary.value) + } + +} \ No newline at end of file From 615eda890d13a878a443a14ca94b76c1f96d544b Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 7 May 2025 10:43:16 +0200 Subject: [PATCH 90/92] Update dependencies, clean up build config (#200) --- gradle/libs.versions.toml | 8 ++++---- lib/build.gradle.kts | 12 +----------- lib/consumer-rules.pro | 2 +- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43dd314c..3be8ef8d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.9.1" +agp = "8.9.2" android-desugar = "2.1.5" androidx-core = "1.16.0" androidx-test-rules = "1.6.1" @@ -9,8 +9,8 @@ dokka = "1.9.20" ical4j = "3.2.19" # final version; update to 4.x will require much work junit = "4.13.2" kotlin = "2.1.20" -mockk = "1.14.0" -slf4j = "2.0.16" +mockk = "1.14.2" +slf4j = "2.0.17" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } @@ -21,7 +21,7 @@ ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } -slf4j = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" } +slf4j-jdk = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 0bd1a32e..cb4cadfc 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -90,23 +90,13 @@ publishing { } } -configurations.forEach { - // exclude modules which are in conflict with system libraries - it.exclude("commons-logging") - it.exclude("org.json", "json") - - // exclude groovy because we don"t need it, and it needs API 26+ - it.exclude("org.codehaus.groovy", "groovy") - it.exclude("org.codehaus.groovy", "groovy-dateutil") -} - dependencies { implementation(libs.kotlin.stdlib) coreLibraryDesugaring(libs.android.desugaring) implementation(libs.androidx.core) api(libs.ical4j) - implementation(libs.slf4j) // ical4j logging over java.util.Logger + implementation(libs.slf4j.jdk) // ical4j uses slf4j, this module uses java.util.Logger androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.runner) diff --git a/lib/consumer-rules.pro b/lib/consumer-rules.pro index 58cabf1a..18a085fe 100644 --- a/lib/consumer-rules.pro +++ b/lib/consumer-rules.pro @@ -1,5 +1,5 @@ -# keep all iCalendar properties/parameters (used via reflection) +# keep all iCalendar properties/parameters (referenced over ServiceLoader) -keep class net.fortuna.ical4j.** { *; } # don't warn when these are missing From 21622fef298bc3b86cebe5597c9b5f104b5f9bd6 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 7 May 2025 11:15:13 +0200 Subject: [PATCH 91/92] Update copyright headers to GPLv3 for all files (#201) --- .idea/copyright/GPL.xml | 8 ++++++++ .idea/copyright/profiles_settings.xml | 7 +++++++ AUTHORS | 16 +++------------ .../ical4android/AndroidCalendarTest.kt | 14 +++++++++---- .../AndroidCompatTimeZoneRegistryTest.kt | 8 +++++--- .../bitfire/ical4android/AndroidEventTest.kt | 17 +++++++++++++--- .../ical4android/AndroidTimeZonesTest.kt | 8 +++++--- .../at/bitfire/ical4android/AospTest.kt | 8 +++++--- .../ical4android/AttendeeMappingsTest.kt | 8 +++++--- .../ical4android/BatchOperationTest.kt | 8 +++++--- .../at/bitfire/ical4android/Css3ColorTest.kt | 8 +++++--- .../DmfsStyleProvidersTaskTest.kt | 8 +++++--- .../bitfire/ical4android/DmfsTaskListTest.kt | 8 +++++--- .../at/bitfire/ical4android/DmfsTaskTest.kt | 8 +++++--- .../at/bitfire/ical4android/EventTest.kt | 8 +++++--- .../ical4android/ICalPreprocessorTest.kt | 12 ++++++----- .../at/bitfire/ical4android/ICalendarTest.kt | 8 +++++--- .../ical4android/Ical4jSettingsTest.kt | 8 +++++--- .../at/bitfire/ical4android/Ical4jTest.kt | 8 +++++--- .../bitfire/ical4android/JtxCollectionTest.kt | 20 ++++++++++++++----- .../bitfire/ical4android/JtxICalObjectTest.kt | 8 +++++--- .../LocaleNonWesternDigitsTest.kt | 8 +++++--- .../at/bitfire/ical4android/TaskTest.kt | 8 +++++--- .../ical4android/UnknownPropertyTest.kt | 8 +++++--- .../bitfire/ical4android/impl/TestCalendar.kt | 8 +++++--- .../at/bitfire/ical4android/impl/TestEvent.kt | 8 +++++--- .../ical4android/impl/TestJtxCollection.kt | 8 +++++--- .../ical4android/impl/TestJtxIcalObject.kt | 8 +++++--- .../at/bitfire/ical4android/impl/TestTask.kt | 8 +++++--- .../bitfire/ical4android/impl/TestTaskList.kt | 8 +++++--- .../ical4android/util/AndroidTimeUtilsTest.kt | 8 +++++--- .../ical4android/util/DateUtilsTest.kt | 8 +++++--- .../ical4android/util/MiscUtilsTest.kt | 8 +++++--- .../util/TimeApiExtensionsTest.kt | 8 +++++--- lib/src/main/AndroidManifest.xml | 12 ----------- .../bitfire/ical4android/AndroidCalendar.kt | 8 +++++--- .../ical4android/AndroidCalendarFactory.kt | 8 +++++--- .../AndroidCompatTimeZoneRegistry.kt | 6 ++++++ .../at/bitfire/ical4android/AndroidEvent.kt | 8 +++++--- .../ical4android/AndroidEventFactory.kt | 8 +++++--- .../bitfire/ical4android/AttendeeMappings.kt | 8 +++++--- .../at/bitfire/ical4android/BatchOperation.kt | 8 +++++--- .../ical4android/CalendarStorageException.kt | 8 +++++--- .../at/bitfire/ical4android/Css3Color.kt | 8 +++++--- .../at/bitfire/ical4android/DmfsTask.kt | 8 +++++--- .../bitfire/ical4android/DmfsTaskFactory.kt | 8 +++++--- .../at/bitfire/ical4android/DmfsTaskList.kt | 8 +++++--- .../ical4android/DmfsTaskListFactory.kt | 8 +++++--- .../kotlin/at/bitfire/ical4android/Event.kt | 8 +++++--- .../at/bitfire/ical4android/ICalendar.kt | 8 +++++--- .../at/bitfire/ical4android/Ical4jVersion.kt | 8 +++++--- .../ical4android/InvalidCalendarException.kt | 8 +++++--- .../at/bitfire/ical4android/JtxCollection.kt | 8 +++++--- .../ical4android/JtxCollectionFactory.kt | 8 +++++--- .../at/bitfire/ical4android/JtxICalObject.kt | 8 +++++--- .../ical4android/JtxICalObjectFactory.kt | 8 +++++--- .../kotlin/at/bitfire/ical4android/Task.kt | 8 +++++--- .../at/bitfire/ical4android/TaskProvider.kt | 8 +++++--- .../bitfire/ical4android/UnknownProperty.kt | 8 +++++--- .../ical4android/util/AndroidTimeUtils.kt | 8 +++++--- .../at/bitfire/ical4android/util/DateUtils.kt | 8 +++++--- .../at/bitfire/ical4android/util/MiscUtils.kt | 8 +++++--- .../ical4android/util/TimeApiExtensions.kt | 8 +++++--- .../ical4android/validation/EventValidator.kt | 8 +++++--- .../FixInvalidDayOffsetPreprocessor.kt | 8 +++++--- .../FixInvalidUtcOffsetPreprocessor.kt | 8 +++++--- .../validation/ICalPreprocessor.kt | 8 +++++--- .../validation/StreamPreprocessor.kt | 8 +++++--- .../ical4android/Ical4jServiceLoaderTest.kt | 6 ++++++ .../validation/EventValidatorTest.kt | 8 +++++--- .../FixInvalidDayOffsetPreprocessorTest.kt | 8 +++++--- .../FixInvalidUtcOffsetPreprocessorTest.kt | 8 +++++--- 72 files changed, 386 insertions(+), 228 deletions(-) create mode 100644 .idea/copyright/GPL.xml create mode 100644 .idea/copyright/profiles_settings.xml diff --git a/.idea/copyright/GPL.xml b/.idea/copyright/GPL.xml new file mode 100644 index 00000000..97e8cdf3 --- /dev/null +++ b/.idea/copyright/GPL.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..df54724c --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index 40c6495e..125cb86a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,14 +1,4 @@ -# This is the list of significant contributors to ical4android. -# -# This does not necessarily list everyone who has contributed work. -# To see the full list of contributors, see the revision history in -# source control. +You can view the list of people who have contributed to the code base in the version control history: +https://github.com/bitfireAT/ical4android/graphs/contributors -Initial contributor: - -* Ricki Hirner (bitfire.at) - - -Further contributor: - -* Patrick Lang (techbee.at): jtx Board integration +Every contribution is welcome. There are many other forms of contributing besides writing code! diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt index a9d5e1d8..d2b0256b 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android @@ -20,10 +22,14 @@ import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.closeCompat import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart -import org.junit.* +import org.junit.AfterClass import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test class AndroidCalendarTest { diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt index 426ab95a..4b27c749 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt index fc28e4e2..db3f431b 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidEventTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android import android.Manifest @@ -61,6 +63,15 @@ import java.net.URI import java.time.Duration import java.time.Period import java.util.logging.Logger +import kotlin.collections.Map +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.emptyMap +import kotlin.collections.first +import kotlin.collections.firstOrNull +import kotlin.collections.iterator +import kotlin.collections.mapOf +import kotlin.collections.plusAssign class AndroidEventTest { diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt index e4715f25..a94dff2b 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt index 2c7cb863..92ddc2c4 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AttendeeMappingsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AttendeeMappingsTest.kt index 69e08426..2854c4dc 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AttendeeMappingsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AttendeeMappingsTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt index 4e5c24d1..9b887144 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Css3ColorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Css3ColorTest.kt index 04080b40..5e9a61f2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Css3ColorTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Css3ColorTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt index eb22ba8b..3c626614 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt index 6c8360a9..a62bb1ca 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index b3c123b1..bfa77edc 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt index 87a8b992..27a0ca88 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt index 7f30e8a2..419f0382 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android @@ -10,13 +12,13 @@ import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor import at.bitfire.ical4android.validation.ICalPreprocessor import io.mockk.mockkObject import io.mockk.verify -import java.io.InputStreamReader -import java.io.StringReader import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Test +import java.io.InputStreamReader +import java.io.StringReader class ICalPreprocessorTest { diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt index a8658fdb..6530fe61 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jSettingsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jSettingsTest.kt index 7386ef47..ec86a357 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jSettingsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jSettingsTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt index 1d74c2a9..e3aa41dc 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt index 25d03701..06512c9a 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android @@ -13,8 +15,16 @@ import at.bitfire.ical4android.impl.TestJtxCollection import at.bitfire.ical4android.util.MiscUtils.closeCompat import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter -import junit.framework.TestCase.* -import org.junit.* +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import org.junit.After +import org.junit.AfterClass +import org.junit.Assume +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test class JtxCollectionTest { diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt index 9d689b21..95dcd30f 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt index d6b3ed9d..1e651d29 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt index 37c10930..3e373f69 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt index b4a8be5d..f0f4a5f7 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt index 19d37cbc..d900dbc3 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.impl diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestEvent.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestEvent.kt index e8a4a76d..ac9d8c2d 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestEvent.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestEvent.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.impl diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt index 86cb0df7..5adc42e3 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.impl diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxIcalObject.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxIcalObject.kt index a1397e97..8862eccb 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxIcalObject.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxIcalObject.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.impl diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt index 3aa287a6..86ed0c2e 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.impl diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt index 13711224..a1c5797f 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.impl diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt index 5805f2a3..04ec8236 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.util diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt index 3bf128e2..35a8e718 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.util diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt index 985a04ca..9a052ca5 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.util diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt index 676cc3ea..3e05f5f0 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.util diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml index f4ef8d93..385f58ba 100644 --- a/lib/src/main/AndroidManifest.xml +++ b/lib/src/main/AndroidManifest.xml @@ -1,15 +1,3 @@ - - diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt index 1981dc9b..6efe3e82 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendarFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendarFactory.kt index a0ba2b28..2b5602da 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendarFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendarFactory.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt index 3063c5b8..84b66715 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt @@ -1,3 +1,9 @@ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package at.bitfire.ical4android import java.time.ZoneId diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 48c023fe..4fa98750 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEventFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEventFactory.kt index 31369566..795a3f6b 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEventFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEventFactory.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AttendeeMappings.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AttendeeMappings.kt index 69d4bafc..61117ea5 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AttendeeMappings.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AttendeeMappings.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt index 6b699dbe..8fa8a78d 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/BatchOperation.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt b/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt index 70106fa4..3e2e07d7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt index a69db983..f546a01c 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index 41eca481..b1d35370 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt index 42ea06c9..c941ddea 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt index 65020ad0..67718ea8 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt index 7b1da9e2..12a38a8d 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt index 8c0a8420..0eaa9ad9 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Event.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index a450cf24..20523d31 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt index 17ee850b..38f5a200 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/InvalidCalendarException.kt b/lib/src/main/kotlin/at/bitfire/ical4android/InvalidCalendarException.kt index 6c0da588..af87d994 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/InvalidCalendarException.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/InvalidCalendarException.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index ff14ecfa..53bc18b4 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollectionFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollectionFactory.kt index 177b2d11..cf470236 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollectionFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollectionFactory.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt index 6b9af310..ecf5b1e5 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObjectFactory.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObjectFactory.kt index 8f6648cb..fa3242a2 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObjectFactory.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObjectFactory.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 900a9be1..6841ecc4 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt index eb00267f..e06058c2 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt b/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt index 65373d60..c1c443e0 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt index 67ff8dc0..6e52629e 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ @file:Suppress("DEPRECATION") diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index 26793f12..f9aac2b7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.util diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt index d3bbd5b7..00b7f941 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.util diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt index a4aaf443..88cc75c1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.util diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt index 6bee7881..ede64f39 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt index 85d99bf8..bcb4d0fb 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt index 6523b189..3012ce60 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt index aac93807..de905d48 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt index 9e69ba11..2b35e641 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt index 37f947d0..0c6df32f 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt @@ -1,3 +1,9 @@ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package at.bitfire.ical4android import net.fortuna.ical4j.data.CalendarBuilder diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt index 6dadd08f..b7f5eb54 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt index 0bf87f12..87f7dc8d 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt index 5bb8dec1..561ee375 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt @@ -1,6 +1,8 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ +/* + * This file is part of ical4android which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ package at.bitfire.ical4android.validation From 240f756bab36aa79f6f17e222de6cc1e39a1937e Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 13 May 2025 15:05:11 +0200 Subject: [PATCH 92/92] Fix cursor without next (#203) * Check cursor has next value * Use getColumnIndexOrThrow for clarity * Fix linting --- .../kotlin/at/bitfire/ical4android/AndroidEvent.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 4fa98750..d8b00029 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -365,7 +365,7 @@ abstract class AndroidEvent( // scheduling if (groupScheduled) { // ORGANIZER must only be set for group-scheduled events (= events with attendees) - if (row.containsKey(Events.ORGANIZER) && groupScheduled) + if (row.containsKey(Events.ORGANIZER)) try { event.organizer = Organizer(URI("mailto", row.getAsString(Events.ORGANIZER), null)) } catch (e: URISyntaxException) { @@ -489,7 +489,7 @@ abstract class AndroidEvent( EXTNAME_URL -> try { event.url = URI(rawValue) - } catch(e: URISyntaxException) { + } catch(_: URISyntaxException) { logger.warning("Won't process invalid local URL: $rawValue") } @@ -703,9 +703,11 @@ abstract class AndroidEvent( var rebuild = false if (event.status == null) calendar.provider.query(eventSyncURI(), arrayOf(Events.STATUS), null, null, null)?.use { cursor -> - cursor.moveToNext() - if (!cursor.isNull(0)) // Events.STATUS != null - rebuild = true + if (cursor.moveToNext()) { + val statusIndex = cursor.getColumnIndexOrThrow(Events.STATUS) + if (!cursor.isNull(statusIndex)) + rebuild = true + } } if (rebuild) { // delete whole event and insert updated event