Loading build.gradle +2 −2 Original line number Diff line number Diff line buildscript { ext.versions = [ kotlin: '1.3.60', kotlin: '1.3.61', dokka: '0.10.0', ical4j: '2.2.6' ] Loading @@ -12,7 +12,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.5.2' classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } Loading src/androidTest/java/foundation/e/ical4android/MiscUtilsTest.kt +0 −8 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ import foundation.e.ical4android.MiscUtils.TextListHelper.toList import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TextList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.DtStart Loading Loading @@ -114,13 +113,6 @@ class MiscUtilsTest { assertEquals("row1_val2", values.getAsString("col2")) } @Test @SmallTest fun testTextListToList() { assertEquals(listOf("str1", "str2"), TextList(arrayOf("str1", "str2")).toList()) assertEquals(emptyList<String>(), TextList(arrayOf()).toList()) } @Suppress("unused") private class TestClass { Loading src/main/java/foundation/e/ical4android/AndroidEvent.kt +35 −4 Original line number Diff line number Diff line Loading @@ -57,6 +57,18 @@ abstract class AndroidEvent( @Deprecated("New content item MIME type", ReplaceWith("UnknownProperty.CONTENT_ITEM_TYPE")) const val EXT_UNKNOWN_PROPERTY2 = "unknown-property.v2" /** * VEVENT CATEGORIES will be stored as an extended property with this [ExtendedProperties.NAME]. * * The [ExtendedProperties.VALUE] format is the same as used by the AOSP Exchange ActiveSync adapter: * the category values are stored as list, separated by [EXT_CATEGORIES_SEPARATOR]. (If a category * value contains [EXT_CATEGORIES_SEPARATOR], [EXT_CATEGORIES_SEPARATOR] will be dropped.) * * Example: `Cat1\Cat2` */ const val EXT_CATEGORIES = "categories" const val EXT_CATEGORIES_SEPARATOR = '\\' /** * EMAIL parameter name (as used for ORGANIZER). Not declared in ical4j Parameters class yet. */ Loading Loading @@ -242,9 +254,7 @@ abstract class AndroidEvent( // exceptions from recurring events row.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime -> var originalAllDay = false row.getAsInteger(Events.ORIGINAL_ALL_DAY)?.let { originalAllDay = it != 0 } val originalAllDay = (row.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0 val originalDate = if (originalAllDay) Date(originalInstanceTime) else DateTime(originalInstanceTime) Loading Loading @@ -340,11 +350,17 @@ abstract class AndroidEvent( } protected open fun populateExtended(row: ContentValues) { Constants.log.log(Level.FINE, "Read extended property from calender provider", row.getAsString(ExtendedProperties.NAME)) val name = row.getAsString(ExtendedProperties.NAME) Constants.log.log(Level.FINE, "Read extended property from calender provider (name=$name)") val event = requireNotNull(event) try { when (row.getAsString(ExtendedProperties.NAME)) { EXT_CATEGORIES -> { val rawCategories = row.getAsString(ExtendedProperties.VALUE) event.categories += rawCategories.split(EXT_CATEGORIES_SEPARATOR) } EXT_UNKNOWN_PROPERTY -> { // deserialize unknown property (deprecated format) val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP)) Loading Loading @@ -426,6 +442,8 @@ abstract class AndroidEvent( // add unknown properties retainClassification() if (event.categories.isNotEmpty()) insertCategories(batch, idxEvent) event.unknownProperties.forEach { insertUnknownProperty(batch, idxEvent, it) } // add exceptions Loading Loading @@ -711,6 +729,18 @@ abstract class AndroidEvent( batch.enqueue(BatchOperation.Operation(builder, Attendees.EVENT_ID, idxEvent)) } protected open fun insertCategories(batch: BatchOperation, idxEvent: Int) { val rawCategories = event!!.categories .map { it.filter { it != EXT_CATEGORIES_SEPARATOR } } // drop backslashes .joinToString(EXT_CATEGORIES_SEPARATOR.toString()) // concatenate, separate by backslash val builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(ExtendedProperties.CONTENT_URI)) .withValue(ExtendedProperties.NAME, EXT_CATEGORIES) .withValue(ExtendedProperties.VALUE, rawCategories) Constants.log.log(Level.FINE, "Built categories", builder.build()) batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent)) } protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int, property: Property) { if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) { Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)") Loading @@ -721,6 +751,7 @@ abstract class AndroidEvent( .withValue(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE) .withValue(ExtendedProperties.VALUE, UnknownProperty.toJsonString(property)) Constants.log.log(Level.FINE, "Built unknown property: ${property.name}") batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent)) } Loading src/main/java/foundation/e/ical4android/DateUtils.kt +9 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.DateListProperty import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.ExDate import net.fortuna.ical4j.model.property.RDate import java.io.StringReader Loading Loading @@ -74,6 +75,14 @@ object DateUtils { return deviceTZ } /** * Determines whether a given date represents a DATE-TIME value. * @param date date property to check * @return *true* if the date is a DATE-TIME value; *false* otherwise (for instance, when the * date is a DATE value or null) */ fun isDateTime(date: DateProperty?) = date != null && date.date is DateTime /** * Parses a VTIMEZONE definition to a VTimeZone object. * @param timezoneDef VTIMEZONE definition Loading src/main/java/foundation/e/ical4android/Event.kt +43 −17 Original line number Diff line number Diff line Loading @@ -8,11 +8,13 @@ package foundation.e.ical4android import foundation.e.ical4android.DateUtils.isDateTime import foundation.e.ical4android.ICalendar.Companion.CALENDAR_NAME import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VEvent Loading Loading @@ -55,6 +57,7 @@ class Event: ICalendar() { var lastModified: LastModified? = null val categories = LinkedList<String>() val unknownProperties = LinkedList<Property>() companion object { Loading Loading @@ -150,6 +153,9 @@ class Event: ICalendar() { is Summary -> e.summary = prop.value is Location -> e.location = prop.value is Description -> e.description = prop.value 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 Loading Loading @@ -202,17 +208,31 @@ class Event: ICalendar() { // recurrence exceptions for (exception in exceptions) { // make sure that // - exceptions have the same UID as the main event and // - RECURRENCE-IDs have the same timezone as the main event's DTSTART // exceptions must always have the same UID as the main event exception.uid = uid exception.recurrenceId?.let { recurrenceId -> if (recurrenceId.timeZone != dtStart.timeZone) { val recurrenceId = exception.recurrenceId if (recurrenceId == null) { Constants.log.warning("Ignoring exception without recurrenceId") continue } /* Exceptions must always have the same value type as DTSTART [RFC 5545 3.8.4.4]. If this is not the case, we don't add the exception to the event because we're strict in what we send (and servers may reject such a case). */ if (isDateTime(recurrenceId) != isDateTime(dtStart)) { Constants.log.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) { Constants.log.fine("Changing timezone of $recurrenceId to same time zone as dtStart: $dtStart") recurrenceId.timeZone = dtStart.timeZone exception.recurrenceId = recurrenceId } // create VEVENT for exception // create and add VEVENT for exception val vException = exception.toVEvent() components += vException Loading @@ -220,7 +240,6 @@ class Event: ICalendar() { exception.dtStart?.timeZone?.let(usedTimeZones::add) exception.dtEnd?.timeZone?.let(usedTimeZones::add) } } // add VTIMEZONE components usedTimeZones.forEach { Loading @@ -239,7 +258,7 @@ class Event: ICalendar() { * @return generated VEvent */ private fun toVEvent(): VEvent { val event = VEvent(true /* generates DTSTAMP */) val event = VEvent(/* generates DTSTAMP */) val props = event.properties props += Uid(uid) Loading @@ -254,7 +273,7 @@ class Event: ICalendar() { description?.let { props += Description(it) } color?.let { props += Color(null, it.name) } props += dtStart dtStart?.let { props += it } dtEnd?.let { props += it } duration?.let { props += it } Loading @@ -271,6 +290,8 @@ class Event: ICalendar() { organizer?.let { props += it } props.addAll(attendees) if (categories.isNotEmpty()) props += Categories(TextList(categories.toTypedArray())) props.addAll(unknownProperties) lastModified?.let { props += it } Loading @@ -283,6 +304,11 @@ class Event: ICalendar() { // helpers fun isAllDay() = !isDateTime(dtStart) /** * Determines whether this Event is an all-day event. * * @return *true* if [dtStart] is a DATE value; *false* otherwise ([dtStart] is a DATETIME value or *null*) */ fun isAllDay() = dtStart != null && !isDateTime(dtStart) } Loading
build.gradle +2 −2 Original line number Diff line number Diff line buildscript { ext.versions = [ kotlin: '1.3.60', kotlin: '1.3.61', dokka: '0.10.0', ical4j: '2.2.6' ] Loading @@ -12,7 +12,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.5.2' classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } Loading
src/androidTest/java/foundation/e/ical4android/MiscUtilsTest.kt +0 −8 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ import foundation.e.ical4android.MiscUtils.TextListHelper.toList import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TextList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.DtStart Loading Loading @@ -114,13 +113,6 @@ class MiscUtilsTest { assertEquals("row1_val2", values.getAsString("col2")) } @Test @SmallTest fun testTextListToList() { assertEquals(listOf("str1", "str2"), TextList(arrayOf("str1", "str2")).toList()) assertEquals(emptyList<String>(), TextList(arrayOf()).toList()) } @Suppress("unused") private class TestClass { Loading
src/main/java/foundation/e/ical4android/AndroidEvent.kt +35 −4 Original line number Diff line number Diff line Loading @@ -57,6 +57,18 @@ abstract class AndroidEvent( @Deprecated("New content item MIME type", ReplaceWith("UnknownProperty.CONTENT_ITEM_TYPE")) const val EXT_UNKNOWN_PROPERTY2 = "unknown-property.v2" /** * VEVENT CATEGORIES will be stored as an extended property with this [ExtendedProperties.NAME]. * * The [ExtendedProperties.VALUE] format is the same as used by the AOSP Exchange ActiveSync adapter: * the category values are stored as list, separated by [EXT_CATEGORIES_SEPARATOR]. (If a category * value contains [EXT_CATEGORIES_SEPARATOR], [EXT_CATEGORIES_SEPARATOR] will be dropped.) * * Example: `Cat1\Cat2` */ const val EXT_CATEGORIES = "categories" const val EXT_CATEGORIES_SEPARATOR = '\\' /** * EMAIL parameter name (as used for ORGANIZER). Not declared in ical4j Parameters class yet. */ Loading Loading @@ -242,9 +254,7 @@ abstract class AndroidEvent( // exceptions from recurring events row.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime -> var originalAllDay = false row.getAsInteger(Events.ORIGINAL_ALL_DAY)?.let { originalAllDay = it != 0 } val originalAllDay = (row.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0 val originalDate = if (originalAllDay) Date(originalInstanceTime) else DateTime(originalInstanceTime) Loading Loading @@ -340,11 +350,17 @@ abstract class AndroidEvent( } protected open fun populateExtended(row: ContentValues) { Constants.log.log(Level.FINE, "Read extended property from calender provider", row.getAsString(ExtendedProperties.NAME)) val name = row.getAsString(ExtendedProperties.NAME) Constants.log.log(Level.FINE, "Read extended property from calender provider (name=$name)") val event = requireNotNull(event) try { when (row.getAsString(ExtendedProperties.NAME)) { EXT_CATEGORIES -> { val rawCategories = row.getAsString(ExtendedProperties.VALUE) event.categories += rawCategories.split(EXT_CATEGORIES_SEPARATOR) } EXT_UNKNOWN_PROPERTY -> { // deserialize unknown property (deprecated format) val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP)) Loading Loading @@ -426,6 +442,8 @@ abstract class AndroidEvent( // add unknown properties retainClassification() if (event.categories.isNotEmpty()) insertCategories(batch, idxEvent) event.unknownProperties.forEach { insertUnknownProperty(batch, idxEvent, it) } // add exceptions Loading Loading @@ -711,6 +729,18 @@ abstract class AndroidEvent( batch.enqueue(BatchOperation.Operation(builder, Attendees.EVENT_ID, idxEvent)) } protected open fun insertCategories(batch: BatchOperation, idxEvent: Int) { val rawCategories = event!!.categories .map { it.filter { it != EXT_CATEGORIES_SEPARATOR } } // drop backslashes .joinToString(EXT_CATEGORIES_SEPARATOR.toString()) // concatenate, separate by backslash val builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(ExtendedProperties.CONTENT_URI)) .withValue(ExtendedProperties.NAME, EXT_CATEGORIES) .withValue(ExtendedProperties.VALUE, rawCategories) Constants.log.log(Level.FINE, "Built categories", builder.build()) batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent)) } protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int, property: Property) { if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) { Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)") Loading @@ -721,6 +751,7 @@ abstract class AndroidEvent( .withValue(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE) .withValue(ExtendedProperties.VALUE, UnknownProperty.toJsonString(property)) Constants.log.log(Level.FINE, "Built unknown property: ${property.name}") batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent)) } Loading
src/main/java/foundation/e/ical4android/DateUtils.kt +9 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.DateListProperty import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.ExDate import net.fortuna.ical4j.model.property.RDate import java.io.StringReader Loading Loading @@ -74,6 +75,14 @@ object DateUtils { return deviceTZ } /** * Determines whether a given date represents a DATE-TIME value. * @param date date property to check * @return *true* if the date is a DATE-TIME value; *false* otherwise (for instance, when the * date is a DATE value or null) */ fun isDateTime(date: DateProperty?) = date != null && date.date is DateTime /** * Parses a VTIMEZONE definition to a VTimeZone object. * @param timezoneDef VTIMEZONE definition Loading
src/main/java/foundation/e/ical4android/Event.kt +43 −17 Original line number Diff line number Diff line Loading @@ -8,11 +8,13 @@ package foundation.e.ical4android import foundation.e.ical4android.DateUtils.isDateTime import foundation.e.ical4android.ICalendar.Companion.CALENDAR_NAME import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VEvent Loading Loading @@ -55,6 +57,7 @@ class Event: ICalendar() { var lastModified: LastModified? = null val categories = LinkedList<String>() val unknownProperties = LinkedList<Property>() companion object { Loading Loading @@ -150,6 +153,9 @@ class Event: ICalendar() { is Summary -> e.summary = prop.value is Location -> e.location = prop.value is Description -> e.description = prop.value 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 Loading Loading @@ -202,17 +208,31 @@ class Event: ICalendar() { // recurrence exceptions for (exception in exceptions) { // make sure that // - exceptions have the same UID as the main event and // - RECURRENCE-IDs have the same timezone as the main event's DTSTART // exceptions must always have the same UID as the main event exception.uid = uid exception.recurrenceId?.let { recurrenceId -> if (recurrenceId.timeZone != dtStart.timeZone) { val recurrenceId = exception.recurrenceId if (recurrenceId == null) { Constants.log.warning("Ignoring exception without recurrenceId") continue } /* Exceptions must always have the same value type as DTSTART [RFC 5545 3.8.4.4]. If this is not the case, we don't add the exception to the event because we're strict in what we send (and servers may reject such a case). */ if (isDateTime(recurrenceId) != isDateTime(dtStart)) { Constants.log.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) { Constants.log.fine("Changing timezone of $recurrenceId to same time zone as dtStart: $dtStart") recurrenceId.timeZone = dtStart.timeZone exception.recurrenceId = recurrenceId } // create VEVENT for exception // create and add VEVENT for exception val vException = exception.toVEvent() components += vException Loading @@ -220,7 +240,6 @@ class Event: ICalendar() { exception.dtStart?.timeZone?.let(usedTimeZones::add) exception.dtEnd?.timeZone?.let(usedTimeZones::add) } } // add VTIMEZONE components usedTimeZones.forEach { Loading @@ -239,7 +258,7 @@ class Event: ICalendar() { * @return generated VEvent */ private fun toVEvent(): VEvent { val event = VEvent(true /* generates DTSTAMP */) val event = VEvent(/* generates DTSTAMP */) val props = event.properties props += Uid(uid) Loading @@ -254,7 +273,7 @@ class Event: ICalendar() { description?.let { props += Description(it) } color?.let { props += Color(null, it.name) } props += dtStart dtStart?.let { props += it } dtEnd?.let { props += it } duration?.let { props += it } Loading @@ -271,6 +290,8 @@ class Event: ICalendar() { organizer?.let { props += it } props.addAll(attendees) if (categories.isNotEmpty()) props += Categories(TextList(categories.toTypedArray())) props.addAll(unknownProperties) lastModified?.let { props += it } Loading @@ -283,6 +304,11 @@ class Event: ICalendar() { // helpers fun isAllDay() = !isDateTime(dtStart) /** * Determines whether this Event is an all-day event. * * @return *true* if [dtStart] is a DATE value; *false* otherwise ([dtStart] is a DATETIME value or *null*) */ fun isAllDay() = dtStart != null && !isDateTime(dtStart) }