Commit 6a95540b authored by bernhard's avatar bernhard

Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	build.gradle
#	gradle/wrapper/gradle-wrapper.properties
parents 49f14ed9 8f201fbf
image: registry.gitlab.com/bitfireat/davdroid:latest
image: registry.gitlab.com/bitfireat/davx5-ose:latest
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle; chmod +x gradlew
......@@ -9,10 +9,9 @@ cache:
test:
script:
# - (cd /sdk/emulator; ./emulator @test -no-audio -no-window & wait-for-emulator.sh)
# - adb install src/androidTest/resources/org.dmfs.tasks_6880.apk
# - ./gradlew check connectedCheck
- ./gradlew check
- (cd /sdk/emulator; ./emulator @test -no-audio -no-window & wait-for-emulator.sh)
- adb install src/androidTest/resources/org.dmfs.tasks_6880.apk
- ./gradlew check connectedCheck
artifacts:
paths:
- build/outputs/lint-results-debug.html
......
buildscript {
ext.kotlin_version = '1.3.21'
ext.dokka_version = '0.9.17'
ext.versions = [
kotlin: '1.3.30',
dokka: '0.9.17',
ical4j: '2.2.3'
]
repositories {
jcenter()
......@@ -10,8 +13,8 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.4.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${versions.dokka}"
}
}
......@@ -24,10 +27,6 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.dokka-android'
ext {
ical4j_version = '3.0.1'
}
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
......@@ -38,7 +37,7 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "version_ical4j", "\"$ical4j_version\""
buildConfigField "String", "version_ical4j", "\"${versions.ical4j}\""
}
lintOptions {
disable 'AllowBackup'
......@@ -55,11 +54,13 @@ android {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
api "org.mnode.ical4j:ical4j:$ical4j_version"
api "org.mnode.ical4j:ical4j:${versions.ical4j}"
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
implementation 'androidx.core:core-ktx:1.0.1'
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
......
......@@ -80,17 +80,17 @@ class AndroidEventTest {
fun testAddEvent() {
// build and write recurring event to calendar provider
val event = Event()
event.uid = ("sample1@testAddEvent")
event.summary = ("Sample event")
event.description = ("Sample event with date/time")
event.location = ("Sample location")
event.dtStart = (DtStart("20150501T120000", tzVienna))
event.dtEnd = (DtEnd("20150501T130000", tzVienna))
event.organizer = (Organizer(URI("mailto:organizer@example.com")))
event.rRule = (RRule("FREQ=DAILY;COUNT=10"))
event.classification = (Clazz.PRIVATE)
event.status = (Status.VEVENT_CONFIRMED)
event.color = (EventColor.aliceblue)
event.uid = "sample1@testAddEvent"
event.summary = "Sample event"
event.description = "Sample event with date/time"
event.location = "Sample location"
event.dtStart = DtStart("20150501T120000", tzVienna)
event.dtEnd = DtEnd("20150501T130000", tzVienna)
event.organizer = Organizer(URI("mailto:organizer@example.com"))
event.rRule = RRule("FREQ=DAILY;COUNT=10")
event.classification = Clazz.PRIVATE
event.status = Status.VEVENT_CONFIRMED
event.color = Css3Color.aliceblue
assertFalse(event.isAllDay())
// TODO test rDates, exDate, duration
......
......@@ -65,11 +65,11 @@ class MiscUtilsTest {
// DATE (without time)
assertEquals(TimeZones.UTC_ID, MiscUtils.getTzId(DtStart(Date("20150101"))))
// DATE-TIME without time zone (floating time): should be UTC (because net.fortuna.ical4j.timezone.date.floating=false)
assertEquals(TimeZones.UTC_ID, MiscUtils.getTzId(DtStart(DateTime("20150101T000000"))))
// DATE-TIME without time zone (floating time): should be local time zone (because Android doesn't support floating times)
assertEquals(java.util.TimeZone.getDefault().id, MiscUtils.getTzId(DtStart(DateTime("20150101T000000"))))
// DATE-TIME without time zone (UTC)
assertEquals(TimeZones.UTC_ID, MiscUtils.getTzId(DtStart(DateTime(1438607288000L))))
assertEquals(TimeZones.UTC_ID, MiscUtils.getTzId(DtStart(DateTime(1438607288000L), true)))
// DATE-TIME with time zone
assertEquals(tzVienna.id, MiscUtils.getTzId(DtStart(DateTime("20150101T000000", tzVienna))))
......
package at.bitfire.ical4android
import androidx.test.filters.SmallTest
import net.fortuna.ical4j.model.parameter.Rsvp
import net.fortuna.ical4j.model.parameter.XParameter
import net.fortuna.ical4j.model.property.Attendee
import net.fortuna.ical4j.model.property.Uid
import org.json.JSONException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class UnknownPropertyTest {
@Test
@SmallTest
fun testFromExtendedProperty() {
val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"UID\", \"PropValue\" ]")
assertTrue(prop is Uid)
assertEquals("UID", prop.name)
assertEquals("PropValue", prop.value)
}
@Test
@SmallTest
fun testFromExtendedPropertyWithParameters() {
val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"ATTENDEE\", \"PropValue\", { \"x-param1\": \"value1\", \"x-param2\": \"value2\" } ]")
assertTrue(prop is Attendee)
assertEquals("ATTENDEE", prop.name)
assertEquals("PropValue", prop.value)
assertEquals(2, prop.parameters.size())
assertEquals("value1", prop.parameters.getParameter("x-param1").value)
assertEquals("value2", prop.parameters.getParameter("x-param2").value)
}
@Test(expected = JSONException::class)
@SmallTest
fun testFromInvalidExtendedProperty() {
AndroidEvent.UnknownProperty.fromExtendedProperty("This isn't JSON")
}
@Test
@SmallTest
fun testToExtendedProperty() {
val attendee = Attendee("mailto:test@test.at")
assertEquals(
"ATTENDEE:mailto:test@test.at",
attendee.toString().trim()
)
attendee.parameters.add(Rsvp(true))
attendee.parameters.add(XParameter("X-My-Param", "SomeValue"))
assertEquals(
"ATTENDEE;RSVP=TRUE;X-My-Param=SomeValue:mailto:test@test.at",
attendee.toString().trim()
)
}
}
\ No newline at end of file
......@@ -18,6 +18,7 @@ import android.provider.CalendarContract
import android.provider.CalendarContract.*
import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
/**
* Represents a locally stored calendar, containing [AndroidEvent]s (whose data objects are [Event]s).
......@@ -35,6 +36,17 @@ abstract class AndroidCalendar<out T: AndroidEvent>(
companion object {
/**
* Creates a local (Android calendar provider) calendar.
*
* @param account account which the calendar should be assigned to
* @param provider client for Android calendar provider
* @param info initial calendar properties ([Calendars.CALENDAR_DISPLAY_NAME] etc.)
*
* @return [Uri] of the created calendar
*
* @throws Exception if the calendar couldn't be created
*/
fun create(account: Account, provider: ContentProviderClient, info: ContentValues): Uri {
info.put(Calendars.ACCOUNT_NAME, account.name)
info.put(Calendars.ACCOUNT_TYPE, account.type)
......@@ -50,8 +62,8 @@ abstract class AndroidCalendar<out T: AndroidEvent>(
fun insertColors(provider: ContentProviderClient, account: Account) {
provider.query(syncAdapterURI(Colors.CONTENT_URI, account), arrayOf(Colors.COLOR_KEY), null, null, null)?.use { cursor ->
if (cursor.count == EventColor.values().size)
// colors already inserted and up to date
if (cursor.count == Css3Color.values().size)
// colors already inserted and up to date
return
}
......@@ -60,10 +72,14 @@ abstract class AndroidCalendar<out T: AndroidEvent>(
values.put(CalendarContract.Colors.ACCOUNT_NAME, account.name)
values.put(CalendarContract.Colors.ACCOUNT_TYPE, account.type)
values.put(Colors.COLOR_TYPE, Colors.TYPE_EVENT)
for (color in EventColor.values()) {
for (color in Css3Color.values()) {
values.put(Colors.COLOR_KEY, color.name)
values.put(Colors.COLOR, color.rgba)
provider.insert(syncAdapterURI(Colors.CONTENT_URI, account), values)
values.put(Colors.COLOR, color.argb)
try {
provider.insert(syncAdapterURI(Colors.CONTENT_URI, account), values)
} catch(e: Exception) {
Constants.log.log(Level.WARNING, "Couldn't insert event color: ${color.name}", e)
}
}
}
......
......@@ -26,7 +26,11 @@ import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.*
import net.fortuna.ical4j.model.property.*
import net.fortuna.ical4j.util.TimeZones
import java.io.*
import org.json.JSONArray
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.FileNotFoundException
import java.io.ObjectInputStream
import java.net.URI
import java.net.URISyntaxException
import java.text.ParseException
......@@ -50,9 +54,14 @@ abstract class AndroidEvent(
companion object {
/** [ExtendedProperties.NAME] for unknown iCal properties */
@Deprecated("New serialization format", ReplaceWith("EXT_UNKNOWN_PROPERTY2"))
const val EXT_UNKNOWN_PROPERTY = "unknown-property"
const val EXT_UNKNOWN_PROPERTY2 = "unknown-property.v2"
const val MAX_UNKNOWN_PROPERTY_SIZE = 25000
// not declared in ical4j Parameters class yet
private const val PARAMETER_EMAIL = "EMAIL"
}
var id: Long? = null
......@@ -144,7 +153,7 @@ abstract class AndroidEvent(
row.getAsString(Events.EVENT_COLOR_KEY)?.let { name ->
try {
event.color = EventColor.valueOf(name)
event.color = Css3Color.valueOf(name)
} catch(e: IllegalArgumentException) {
Constants.log.warning("Ignoring unknown color $name from Calendar Provider")
}
......@@ -258,7 +267,7 @@ abstract class AndroidEvent(
if (idNS != null || id != null) {
// attendee identified by namespace and ID
attendee = Attendee(URI(idNS, id, null))
email?.let { attendee.parameters.add(ICalendar.Email(it)) }
email?.let { attendee.parameters.add(Email(it)) }
} else
// attendee identified by email address
attendee = Attendee(URI("mailto", email, null))
......@@ -322,17 +331,25 @@ 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 event = requireNotNull(event)
if (row.getAsString(ExtendedProperties.NAME) == EXT_UNKNOWN_PROPERTY) {
// de-serialize unknown property
val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP))
try {
ObjectInputStream(stream).use {
event!!.unknownProperties += it.readObject() as Property
try {
when (row.getAsString(ExtendedProperties.NAME)) {
EXT_UNKNOWN_PROPERTY -> {
// deserialize unknown property v1 (deprecated)
val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP))
ObjectInputStream(stream).use {
event.unknownProperties += it.readObject() as Property
}
}
EXT_UNKNOWN_PROPERTY2 -> {
// deserialize unknown property v2
event.unknownProperties += UnknownProperty.fromExtendedProperty(row.getAsString(ExtendedProperties.VALUE))
}
} catch(e: Exception) {
Constants.log.log(Level.WARNING, "Couldn't de-serialize unknown property", e)
}
} catch(e: Exception) {
Constants.log.log(Level.WARNING, "Couldn't parse extended property", e)
}
}
......@@ -581,7 +598,7 @@ abstract class AndroidEvent(
email = if (uri.scheme.equals("mailto", true))
uri.schemeSpecificPart
else {
val emailParam = organizer.getParameter(ICalendar.Email.PARAMETER_NAME) as ICalendar.Email?
val emailParam = organizer.getParameter(PARAMETER_EMAIL) as? Email
emailParam?.value
}
if (email != null)
......@@ -639,7 +656,7 @@ abstract class AndroidEvent(
// attendee identified by other URI
builder .withValue(Attendees.ATTENDEE_ID_NAMESPACE, member.scheme)
.withValue(Attendees.ATTENDEE_IDENTITY, member.schemeSpecificPart)
(attendee.getParameter(ICalendar.Email.PARAMETER_NAME) as ICalendar.Email?)?.let { email ->
(attendee.getParameter(PARAMETER_EMAIL) as? Email)?.let { email ->
builder.withValue(Attendees.ATTENDEE_EMAIL, email.value)
}
}
......@@ -649,13 +666,13 @@ abstract class AndroidEvent(
}
var type = Attendees.TYPE_NONE
val cutype = attendee.getParameter(Parameter.CUTYPE) as CuType?
val cutype = attendee.getParameter(Parameter.CUTYPE) as? CuType
if (cutype in arrayOf(CuType.RESOURCE, CuType.ROOM))
// "attendee" is a (physical) resource
type = Attendees.TYPE_RESOURCE
else {
// attendee is not a (physical) resource
val role = attendee.getParameter(Parameter.ROLE) as Role?
val role = attendee.getParameter(Parameter.ROLE) as? Role
val relationship: Int
if (role == Role.CHAIR)
relationship = Attendees.RELATIONSHIP_ORGANIZER
......@@ -669,7 +686,7 @@ abstract class AndroidEvent(
builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship)
}
val partStat = attendee.getParameter(Parameter.PARTSTAT) as PartStat?
val partStat = attendee.getParameter(Parameter.PARTSTAT) as? PartStat
val status = when(partStat) {
null,
PartStat.NEEDS_ACTION -> Attendees.ATTENDEE_STATUS_INVITED
......@@ -687,25 +704,16 @@ abstract class AndroidEvent(
}
protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int, property: Property) {
val baos = ByteArrayOutputStream()
try {
ObjectOutputStream(baos).use { oos ->
oos.writeObject(property)
if (baos.size() > MAX_UNKNOWN_PROPERTY_SIZE) {
Constants.log.warning("Ignoring unknown property with ${baos.size()} octets")
return
}
if (property.value.length > MAX_UNKNOWN_PROPERTY_SIZE) {
Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)")
return
}
val builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(ExtendedProperties.CONTENT_URI))
builder .withValue(ExtendedProperties.NAME, EXT_UNKNOWN_PROPERTY)
.withValue(ExtendedProperties.VALUE, Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP))
val builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(ExtendedProperties.CONTENT_URI))
builder .withValue(ExtendedProperties.NAME, EXT_UNKNOWN_PROPERTY2)
.withValue(ExtendedProperties.VALUE, UnknownProperty.toExtendedProperty(property))
batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent))
}
} catch(e: IOException) {
Constants.log.log(Level.WARNING, "Couldn't serialize unknown property", e)
}
batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent))
}
private fun useRetainedClassification() {
......@@ -734,7 +742,62 @@ abstract class AndroidEvent(
return calendar.syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))
}
override fun toString() = MiscUtils.reflectionToString(this)
/**
* Helpers to (de)serialize unknown properties as JSON to store it in an Android ExtendedProperty row.
*
* Format: `{ propertyName, propertyValue, { param1Name: param1Value, ... } }`, with the third
* array (parameters) being optional.
*/
object UnknownProperty {
/**
* Deserializes a JSON string from an ExtendedProperty value to an ical4j property.
*
* @param jsonString JSON representation of an ical4j property
* @return ical4j property, generated from [jsonString]
* @throws org.json.JSONException when the input value can't be parsed
*/
fun fromExtendedProperty(jsonString: String): Property {
val json = JSONArray(jsonString)
val name = json.getString(0)
val value = json.getString(1)
val params = ParameterList()
json.optJSONObject(2)?.let { jsonParams ->
for (paramName in jsonParams.keys())
params.add(ICalendar.parameterFactoryRegistry.createParameter(
paramName,
jsonParams.getString(paramName)
))
}
return ICalendar.propertyFactoryRegistry.createProperty(name, params, value)
}
/**
* Serializes an ical4j property to a JSON string that can be stored in an ExtendedProperty.
*
* @param prop property to serialize as JSON
* @return JSON representation of [prop]
*/
fun toExtendedProperty(prop: Property): String {
val json = JSONArray()
json.put(prop.name)
json.put(prop.value)
if (!prop.parameters.isEmpty) {
val jsonParams = JSONObject()
for (param in prop.parameters)
jsonParams.put(param.name, param.value)
json.put(jsonParams)
}
return json.toString()
}
}
}
......@@ -216,7 +216,7 @@ abstract class AndroidTask(
task.organizer?.let {
try {
val uri = URI(it.value)
if (uri.scheme == "mailto")
if (uri.scheme.equals("mailto", true))
organizer = uri.schemeSpecificPart
else
Constants.log.log(Level.WARNING, "Found non-mailto ORGANIZER URI, ignoring", uri)
......
......@@ -10,9 +10,11 @@ package at.bitfire.ical4android
/**
* Represents an RGBA COLOR value, as specified in https://tools.ietf.org/html/rfc7986#section-5.9
*
* @property argb ARGB color value (0xAARRGGBB), alpha is 0xFF for all values
*/
@Suppress("EnumEntryName")
enum class EventColor(val rgba: Int) {
@Suppress("EnumEntryName", "SpellCheckingInspection")
enum class Css3Color(val argb: Int) {
// values taken from https://www.w3.org/TR/2011/REC-css3-color-20110607/#svg-color
aliceblue(0xfff0f8ff.toInt()),
antiquewhite(0xfffaebd7.toInt()),
......@@ -166,13 +168,29 @@ enum class EventColor(val rgba: Int) {
companion object {
/**
* Finds the best matching [EventColor] for a given RGBA value using a weighted Euclidian
* distance formula for RGB (A is being ignored).
* Returns the CSS3 color property of the given name.
*
* @param name color name
* @return [Css3Color] object or null if no match was found
*/
fun nearestMatch(rgba: Int): EventColor {
val rgb = rgba and 0xFFFFFF
fun fromString(name: String) =
try {
Css3Color.valueOf(name)
} catch (e: IllegalArgumentException) {
Constants.log.warning("Unknown color: $name")
null
}
/**
* Finds the best matching [Css3Color] for a given RGBA value using a weighted Euclidian
* distance formula for RGB.
*
* @param argb (A)RGB color (A will be ignored)
*/
fun nearestMatch(argb: Int): Css3Color {
val rgb = argb and 0xFFFFFF
val distance = values().map {
val cssColor = it.rgba and 0xFFFFFF
val cssColor = it.argb and 0xFFFFFF
val r1 = rgb shr 16
val r2 = cssColor shr 16
val r = (r1 + r2)/2.0
......
......@@ -30,7 +30,7 @@ class Event: ICalendar() {
var summary: String? = null
var location: String? = null
var description: String? = null
var color: EventColor? = null
var color: Css3Color? = null
var dtStart: DtStart? = null
var dtEnd: DtEnd? = null
......@@ -77,7 +77,9 @@ class Event: ICalendar() {
try {
ical = calendarBuilder().build(reader)
} catch(e: ParserException) {
throw InvalidCalendarException("Couldn't parse iCalendar resource", e)
throw InvalidCalendarException("Couldn't parse iCalendar object", e)
} catch(e: IllegalArgumentException) {
throw InvalidCalendarException("iCalendar object contains invalid value", e)
}
// fill calendar properties
......@@ -98,8 +100,8 @@ class Event: ICalendar() {
vEvent.properties += uid
}
Constants.log.fine("Assigning exceptions to master events")
val masterEvents = mutableMapOf<String /* UID */,VEvent>()
Constants.log.fine("Assigning exceptions to main events")
val mainEvents = mutableMapOf<String /* UID */,VEvent>()
val exceptions = mutableMapOf<String /* UID */,MutableMap<String /* RECURRENCE-ID */,VEvent>>()
for (vEvent in vEvents) {
......@@ -107,13 +109,13 @@ class Event: ICalendar() {
val sequence = vEvent.sequence?.sequenceNo ?: 0
if (vEvent.recurrenceId == null) {
// master event (no RECURRENCE-ID)
// main event (no RECURRENCE-ID)
// If there are multiple entries, compare SEQUENCE and use the one with higher SEQUENCE.
// If the SEQUENCE is identical, use latest version.
val event = masterEvents[uid]
val event = mainEvents[uid]
if (event == null || (event.sequence != null && sequence >= event.sequence.sequenceNo))
masterEvents[uid] = vEvent
mainEvents[uid] = vEvent
} else {
// exception (RECURRENCE-ID)
......@@ -132,7 +134,7 @@ class Event: ICalendar() {
}
val events = mutableListOf<Event>()
for ((uid, vEvent) in masterEvents) {
for ((uid, vEvent) in mainEvents) {
val event = fromVEvent(vEvent)
exceptions[uid]?.let { eventExceptions ->
event.exceptions.addAll(eventExceptions.map { (_,it) -> fromVEvent(it) })
......@@ -162,7 +164,7 @@ class Event: ICalendar() {
is Summary -> e.summary = prop.value
is Location -> e.location = prop.value
is Description -> e.description = prop.value
is Color -> e.color = prop.value
is Color -> e.color = Css3Color.fromString(prop.value)
is DtStart -> e.dtStart = prop
is DtEnd -> e.dtEnd = prop
is Duration -> e.duration = prop
......@@ -200,25 +202,38 @@ class Event: ICalendar() {
ical.properties += Version.VERSION_2_0
ical.properties += prodId
// "master event" (without exceptions)
val dtStart = dtStart ?: throw InvalidCalendarException("Won't generate event without start time")
// "main event" (without exceptions)
val components = ical.components
val master = toVEvent(Uid(uid))
components += master
val mainEvent = toVEvent()
components += mainEvent
// remember used time zones
val usedTimeZones = mutableSetOf<TimeZone>()
dtStart?.timeZone?.let(usedTimeZones::add)
dtStart.timeZone?.let(usedTimeZones::add)
dtEnd?.timeZone?.let(usedTimeZones::add)
// recurrence exceptions
for (exception in exceptions) {
// create VEVENT for exception
val vException = exception.toVEvent(master.uid)
components += vException
// 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
exception.uid = uid
exception.recurrenceId?.let { recurrenceId ->
if (recurrenceId.timeZone != dtStart.timeZone) {
recurrenceId.timeZone = dtStart.timeZone
exception.recurrenceId = recurrenceId
}
// remember used time zones
exception.dtStart?.timeZone?.let(usedTimeZones::add)
exception.dtEnd?.timeZone?.let(usedTimeZones::add)
// create VEVENT for exception
val vException = exception.toVEvent()
components += vException
// remember used time zones
exception.dtStart?.timeZone?.let(usedTimeZones::add)
exception.dtEnd?.timeZone?.let(usedTimeZones::add)
}
}
// add VTIMEZONE components
......@@ -231,18 +246,23 @@ class Event: ICalendar() {
CalendarOutputter(false).output(ical, os)
}
private fun toVEvent(uid: Uid): VEvent {
/**
* Generates a VEvent representation of this event.
*
* @return generated VEvent
*/
private fun toVEvent(): VEvent {
val event = VEvent()
val props = event.properties
props += uid
props += Uid(uid)
recurrenceId?.let { props += it }
sequence?.let { if (it != 0) props += Sequence(it) }
summary?.let { props += Summary(it) }
location?.let { props += Location(it) }
description?.let { props += Description(it) }
color?.let { props += Color(it) }
color?.let { props += Color(null, it.name) }
props += dtStart
dtEnd?.let { props += it }
......
......@@ -17,7 +17,6 @@ import net.fortuna.ical4j.model.component.*
import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.model.property.ProdId
import net.fortuna.ical4j.model.property.TzUrl
import net.fortuna.ical4j.util.Strings
import java.io.StringReader
import java.util.*
import java.util.logging.Level
......@@ -39,16 +38,8 @@ open class ICalendar {
}
var prodId = ProdId("+//IDN bitfire.at//ical4android")
private val parameterFactoryRegistry = ParameterFactoryRegistry()
init {
parameterFactoryRegistry.register(Email.PARAMETER_NAME, Email.Factory)
}
private val propertyFactoryRegistry = PropertyFactoryRegistry()
init {
propertyFactoryRegistry.register(Color.PROPERTY_NAME, Color.Factory)
}
val propertyFactoryRegistry = PropertyFactoryRegistry()
val parameterFactoryRegistry = ParameterFactoryRegistry()
@JvmStatic
protected fun calendarBuilder() = CalendarBuilder(
......@@ -155,66 +146,4 @@ open class ICalendar {
override fun toString() = MiscUtils.reflectionToString(this)
// ical4j helpers and extensions
/** COLOR property for VEVENT components [RFC 7986 5.9 COLOR] */
class Color(
var value: EventColor? = null
): Property(PROPERTY_NAME, Factory) {
companion object {
const val PROPERTY_NAME = "COLOR"
}